@yemi33/minions 0.1.2312 → 0.1.2313
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/render-prs.js +24 -0
- package/engine/lifecycle.js +149 -0
- package/engine/shared.js +19 -0
- package/engine.js +14 -1
- package/package.json +1 -1
|
@@ -187,6 +187,30 @@ function prRow(pr) {
|
|
|
187
187
|
+ '⏸ paused: ' + escapeHtml(cause) + ' ▶</button>';
|
|
188
188
|
}).join('');
|
|
189
189
|
}
|
|
190
|
+
// #639 follow-up — build-fix-ineffective chip. Distinct from the generic
|
|
191
|
+
// no-op pausedCauses above: pr._buildFixIneffective is set when a
|
|
192
|
+
// BUILD_FAILURE fix reported "no branch change" AND a live CI re-poll
|
|
193
|
+
// confirmed the build is STILL failing on that same head (see
|
|
194
|
+
// engine/lifecycle.js recordBuildFixIneffective). This is a separate,
|
|
195
|
+
// headSha-keyed pause distinct from _noOpFixes, so it needs its own,
|
|
196
|
+
// clearly-labeled indicator — a still-red build after an unchanged fix
|
|
197
|
+
// attempt usually means the failure isn't fixable by another agent pass
|
|
198
|
+
// (flaky infra, external outage, etc.), hence "infra issue suspected".
|
|
199
|
+
// No resume action is wired here (clearing this record is an engine-side
|
|
200
|
+
// change out of scope for this UI-only follow-up) — informational only.
|
|
201
|
+
var buildFixIneffective = pr._buildFixIneffective;
|
|
202
|
+
if (buildFixIneffective && typeof buildFixIneffective === 'object' && buildFixIneffective.paused === true) {
|
|
203
|
+
var bfiTitle = 'Build-fix reported no branch change, and a live CI re-poll confirmed the build is still "'
|
|
204
|
+
+ (buildFixIneffective.liveBuildStatus || 'failing') + '" on the same commit ('
|
|
205
|
+
+ (buildFixIneffective.headSha || 'unknown head') + ') after '
|
|
206
|
+
+ (buildFixIneffective.count || '?') + ' ineffective attempt(s). Reason: '
|
|
207
|
+
+ (buildFixIneffective.reason || 'unknown')
|
|
208
|
+
+ '. A new push to this branch will reset this pause.';
|
|
209
|
+
pausedChips += ' <span class="pr-badge rejected pr-build-fix-ineffective-chip" '
|
|
210
|
+
+ 'style="font-size:var(--text-xs)" '
|
|
211
|
+
+ 'title="' + escapeHtml(bfiTitle) + '">'
|
|
212
|
+
+ '⏸ infra issue suspected ▶</span>';
|
|
213
|
+
}
|
|
190
214
|
const titleText = pr.title || 'Untitled';
|
|
191
215
|
// Polish (W-mqv5ccn7): flatten Markdown in the body so the preview shows
|
|
192
216
|
// plain text instead of raw `##` / `**…**` / backtick source.
|
package/engine/lifecycle.js
CHANGED
|
@@ -2647,6 +2647,104 @@ async function detectPrFixBranchChange(meta, config) {
|
|
|
2647
2647
|
return { changed: null, beforeHead, afterHead: remoteHead || '', reason: 'unable to prove branch head after fix' };
|
|
2648
2648
|
}
|
|
2649
2649
|
|
|
2650
|
+
// #639 — lazy requires for the host-specific live build/conflict checkers.
|
|
2651
|
+
// engine/github.js and engine/ado.js both lazy-require lifecycle.js (see
|
|
2652
|
+
// their `lifecycleModule()` helpers), so a top-level require here would form
|
|
2653
|
+
// an import cycle. Deferring to first call avoids that.
|
|
2654
|
+
let _githubPrModule = null;
|
|
2655
|
+
function githubPrModule() { if (!_githubPrModule) _githubPrModule = require('./github'); return _githubPrModule; }
|
|
2656
|
+
let _adoPrModule = null;
|
|
2657
|
+
function adoPrModule() { if (!_adoPrModule) _adoPrModule = require('./ado'); return _adoPrModule; }
|
|
2658
|
+
|
|
2659
|
+
// #639 — No-op fix detection previously only proved whether the agent pushed
|
|
2660
|
+
// a NEW commit; it never checked whether the build the fix was dispatched
|
|
2661
|
+
// against had actually turned green. That let a stuck failure get marked
|
|
2662
|
+
// "handled"/no-op forever: a fix commit lands and gets recorded as handled,
|
|
2663
|
+
// CI re-runs and fails again with the SAME signature against that exact
|
|
2664
|
+
// commit, and every subsequent build-fix dispatch finds "the fix is already
|
|
2665
|
+
// on the branch" (branch unchanged) and concludes no-op — without ever
|
|
2666
|
+
// re-checking that the build is still red. Call this right before accepting
|
|
2667
|
+
// a BUILD_FAILURE "no branch change" outcome as a clean no-op so the caller
|
|
2668
|
+
// can distinguish "genuinely nothing to do" from "fix was ineffective".
|
|
2669
|
+
async function checkBuildStillFailingLive(pr, project) {
|
|
2670
|
+
if (!pr || !project) return null;
|
|
2671
|
+
try {
|
|
2672
|
+
const checkFn = project.repoHost === 'github'
|
|
2673
|
+
? githubPrModule().checkLiveBuildAndConflict
|
|
2674
|
+
: adoPrModule().checkLiveBuildAndConflict;
|
|
2675
|
+
if (typeof checkFn !== 'function') return null;
|
|
2676
|
+
const live = await checkFn(pr, project);
|
|
2677
|
+
// A stale/errored live read carries no fresh evidence — don't let it
|
|
2678
|
+
// masquerade as a confirmed still-failing result.
|
|
2679
|
+
if (!live || live.buildStatusStale) return null;
|
|
2680
|
+
return live.buildStatus || null;
|
|
2681
|
+
} catch (err) {
|
|
2682
|
+
log('warn', `Live build re-check for ${pr.id || pr.url || '(unknown PR)'}: ${err.message}`);
|
|
2683
|
+
return null;
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
// #639 — Records a build-fix dispatch that reported "no branch change" AND
|
|
2688
|
+
// was verified (via live re-poll) to still be failing on the same head.
|
|
2689
|
+
// Tracked deliberately separately from `_noOpFixes[BUILD_FAILURE]` — see
|
|
2690
|
+
// `shared.isBuildFixIneffectivePaused` for why sharing that counter would
|
|
2691
|
+
// let a genuinely-broken build get stuck paused forever with no distinct
|
|
2692
|
+
// signal. The dispatch-eligibility gate for BUILD_FAILURE reads BOTH
|
|
2693
|
+
// `shared.isPrNoOpFixCausePaused` and `shared.isBuildFixIneffectivePaused`
|
|
2694
|
+
// (engine.js), and neither this record's `outcome` nor its pause state
|
|
2695
|
+
// suppresses the same-head skip guard tied to `outcome === 'noop'` — so the
|
|
2696
|
+
// engine keeps re-dispatching (subject to the normal cooldown) with a fresh
|
|
2697
|
+
// instruction to inspect the CURRENT failure rather than assume the prior
|
|
2698
|
+
// commit already fixed it, until either a real fix lands (head moves) or
|
|
2699
|
+
// the attempt cap is reached and a human is alerted.
|
|
2700
|
+
function recordBuildFixIneffective(target, dispatchItem, config, liveBuildStatus) {
|
|
2701
|
+
const headSha = getPrFixBaselineHead(target);
|
|
2702
|
+
const prior = target._buildFixIneffective && typeof target._buildFixIneffective === 'object'
|
|
2703
|
+
? target._buildFixIneffective : null;
|
|
2704
|
+
const sameHead = !!prior && !!headSha && prior.headSha === headSha;
|
|
2705
|
+
const count = sameHead ? (Number(prior.count) || 0) + 1 : 1;
|
|
2706
|
+
const pauseAfter = Number(config?.engine?.prNoOpFixPauseAttempts) || ENGINE_DEFAULTS.prNoOpFixPauseAttempts;
|
|
2707
|
+
const paused = count >= pauseAfter;
|
|
2708
|
+
const flippedToPaused = paused && !(sameHead && prior?.paused === true);
|
|
2709
|
+
const now = ts();
|
|
2710
|
+
const reason = `build-fix reported no branch change but live CI is still ${liveBuildStatus || 'failing'} on head ${String(headSha || '').slice(0, 12)}`;
|
|
2711
|
+
target._buildFixIneffective = {
|
|
2712
|
+
count,
|
|
2713
|
+
paused,
|
|
2714
|
+
headSha,
|
|
2715
|
+
liveBuildStatus: liveBuildStatus || 'failing',
|
|
2716
|
+
firstAt: sameHead ? (prior.firstAt || now) : now,
|
|
2717
|
+
lastAt: now,
|
|
2718
|
+
dispatchId: dispatchItem?.id || null,
|
|
2719
|
+
reason,
|
|
2720
|
+
};
|
|
2721
|
+
if (flippedToPaused) {
|
|
2722
|
+
try {
|
|
2723
|
+
const prNumber = target.prNumber || shared.getPrNumber(target) || target.id || 'unknown';
|
|
2724
|
+
const noteBody = `# Build fix ineffective: ${target.id || prNumber}\n\n`
|
|
2725
|
+
+ `**PR:** ${target.url || target.id || '(unknown)'}\n`
|
|
2726
|
+
+ `**Branch:** ${target.branch || '(unknown)'}\n`
|
|
2727
|
+
+ `**Head:** ${String(headSha || '').slice(0, 40) || '(unknown)'}\n`
|
|
2728
|
+
+ `**Attempt:** ${count}/${pauseAfter} (paused)\n\n`
|
|
2729
|
+
+ `A build-failure fix dispatch reported no branch change, but re-polling live CI showed the build is STILL \`${liveBuildStatus || 'failing'}\` against that exact commit. `
|
|
2730
|
+
+ `The engine reached \`engine.prNoOpFixPauseAttempts\` (${pauseAfter}) consecutive ineffective-fix attempts against the same head and has paused build-failure automation for this PR.\n\n`
|
|
2731
|
+
+ `**Recovery options:**\n`
|
|
2732
|
+
+ `1. A new head SHA on this PR auto-clears the pause (a real fix attempt landed).\n`
|
|
2733
|
+
+ `2. Manually inspect the live CI logs — the automated fixes may be addressing the wrong failure, or the failure may not be fixable by re-pushing (flaky infra, protected branch, etc).\n`;
|
|
2734
|
+
shared.writeToInbox(
|
|
2735
|
+
'engine',
|
|
2736
|
+
`build-fix-ineffective-${prNumber}`,
|
|
2737
|
+
noteBody,
|
|
2738
|
+
null,
|
|
2739
|
+
{ wi: dispatchItem?.meta?.item?.id || null, pr: target.id || null, cause: shared.PR_FIX_CAUSE.BUILD_FAILURE }
|
|
2740
|
+
);
|
|
2741
|
+
} catch (err) {
|
|
2742
|
+
log('warn', `build-fix-ineffective inbox alert for ${target.id || '(unknown)'}: ${err.message}`);
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
return target._buildFixIneffective;
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2650
2748
|
function recordPrNoOpFixAttempt(target, cause, source, dispatchItem, branchChange, config, noopReason) {
|
|
2651
2749
|
const evidenceFingerprint = shared.prFixEvidenceFingerprint(target, cause);
|
|
2652
2750
|
const prior = shared.getPrNoOpFixRecord(target, cause);
|
|
@@ -2992,6 +3090,22 @@ function updatePrAfterFix(pr, project, source, options = {}, legacyDispatchId =
|
|
|
2992
3090
|
return prs;
|
|
2993
3091
|
}
|
|
2994
3092
|
if (explicitlyChangedBranch && options.branchChange?.changed === false) {
|
|
3093
|
+
// #639 — a BUILD_FAILURE fix that reported "no branch change" is only a
|
|
3094
|
+
// genuine no-op if the build the fix was dispatched against actually
|
|
3095
|
+
// recovered. `options.buildStillFailing` (set by the caller from a live
|
|
3096
|
+
// re-poll of the current head commit) proves it did NOT — the same
|
|
3097
|
+
// failure persists against the exact commit the agent left in place.
|
|
3098
|
+
// Route through the distinct fixIneffective counter instead of the
|
|
3099
|
+
// shared no-op/pause counter so a stuck-red build gets its own signal
|
|
3100
|
+
// and its own (non-sticky-forever) pause, rather than silently
|
|
3101
|
+
// consuming the generic no-op pause budget with no way to tell "there
|
|
3102
|
+
// was truly nothing to do" apart from "the fix didn't work".
|
|
3103
|
+
if (cause === shared.PR_FIX_CAUSE.BUILD_FAILURE && options.buildStillFailing) {
|
|
3104
|
+
const record = recordBuildFixIneffective(target, options.dispatchItem, options.config, options.buildStillFailing);
|
|
3105
|
+
result = { noOp: true, cause, fixIneffective: true, paused: !!record.paused, count: record.count };
|
|
3106
|
+
log('warn', `Updated ${pr.id} → build-fix reported no branch change, but live CI is still ${options.buildStillFailing} on the same head; recorded fixIneffective attempt ${record.count}${record.paused ? ' (paused)' : ''}`);
|
|
3107
|
+
return prs;
|
|
3108
|
+
}
|
|
2995
3109
|
const record = recordPrNoOpFixAttempt(target, cause, source, options.dispatchItem, options.branchChange, options.config, options.noopReason);
|
|
2996
3110
|
result = { noOp: true, cause, paused: !!record.paused, count: record.count };
|
|
2997
3111
|
log('warn', `Updated ${pr.id} → recorded no-op ${cause} fix attempt ${record.count}${record.paused ? ' (paused)' : ''}; PR branch was unchanged`);
|
|
@@ -3022,6 +3136,10 @@ function updatePrAfterFix(pr, project, source, options = {}, legacyDispatchId =
|
|
|
3022
3136
|
target._buildFixPushedAt = ts();
|
|
3023
3137
|
delete target._buildFixPushFailedCount;
|
|
3024
3138
|
delete target._buildFixNeedsHumanRebase;
|
|
3139
|
+
// #639 — a genuine branch-changing fix landed; drop any stale
|
|
3140
|
+
// fixIneffective pause from a prior head so the next ineffective
|
|
3141
|
+
// detection (if any) starts a fresh count against the new commit.
|
|
3142
|
+
delete target._buildFixIneffective;
|
|
3025
3143
|
}
|
|
3026
3144
|
if (source === 'pr-human-feedback') {
|
|
3027
3145
|
const clearPendingFix = shouldClearHumanFeedbackPendingFix(target, pr, automationCauseKey);
|
|
@@ -5729,6 +5847,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5729
5847
|
// Archive is manual — user archives plans from the dashboard when ready
|
|
5730
5848
|
|
|
5731
5849
|
let prFixBranchChange = null;
|
|
5850
|
+
let prFixBuildStillFailing = null;
|
|
5732
5851
|
if (type === WORK_TYPE.FIX && effectiveSuccess && meta?.pr?.id) {
|
|
5733
5852
|
try {
|
|
5734
5853
|
prFixBranchChange = await detectPrFixBranchChange(meta, config);
|
|
@@ -5736,6 +5855,30 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5736
5855
|
log('warn', `PR fix no-op detection for ${meta.pr.id}: ${err.message}`);
|
|
5737
5856
|
prFixBranchChange = { changed: null, reason: err.message };
|
|
5738
5857
|
}
|
|
5858
|
+
// #639 — a BUILD_FAILURE fix that didn't change the branch is not
|
|
5859
|
+
// necessarily a genuine no-op: the agent may have found its own (or a
|
|
5860
|
+
// prior dispatch's) fix commit already present and concluded "nothing to
|
|
5861
|
+
// change" without checking whether CI actually turned green on that
|
|
5862
|
+
// commit. Re-poll live CI for the head before letting updatePrAfterFix
|
|
5863
|
+
// accept the no-op — if it's still failing, flag it so the no-op path
|
|
5864
|
+
// records a distinct fixIneffective signal instead of a clean no-op.
|
|
5865
|
+
if (prFixBranchChange?.changed === false) {
|
|
5866
|
+
const fixCause = shared.getPrFixAutomationCause({
|
|
5867
|
+
dispatchKey: dispatchItem?.meta?.dispatchKey,
|
|
5868
|
+
source: meta?.source,
|
|
5869
|
+
task: dispatchItem?.task,
|
|
5870
|
+
});
|
|
5871
|
+
if (fixCause === shared.PR_FIX_CAUSE.BUILD_FAILURE) {
|
|
5872
|
+
try {
|
|
5873
|
+
const liveStatus = await checkBuildStillFailingLive(meta.pr, meta.project);
|
|
5874
|
+
if (liveStatus === shared.BUILD_STATUS.FAILING) {
|
|
5875
|
+
prFixBuildStillFailing = liveStatus;
|
|
5876
|
+
}
|
|
5877
|
+
} catch (err) {
|
|
5878
|
+
log('warn', `Live build re-check for ${meta.pr.id}: ${err.message}`);
|
|
5879
|
+
}
|
|
5880
|
+
}
|
|
5881
|
+
}
|
|
5739
5882
|
}
|
|
5740
5883
|
|
|
5741
5884
|
// Scheduled task back-reference: update schedule-runs.json and write linked inbox note
|
|
@@ -5854,6 +5997,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5854
5997
|
automationCauseKey: meta?.automationCauseKey,
|
|
5855
5998
|
dispatchItem,
|
|
5856
5999
|
branchChange: prFixBranchChange,
|
|
6000
|
+
buildStillFailing: prFixBuildStillFailing,
|
|
5857
6001
|
config,
|
|
5858
6002
|
noopReason: noopRationale || meta?._noopReason || '',
|
|
5859
6003
|
});
|
|
@@ -6666,6 +6810,11 @@ module.exports = {
|
|
|
6666
6810
|
// Issue #2969 — exported for direct unit testing of the pause-flip alert
|
|
6667
6811
|
// path without going through updatePrAfterFix's many guards.
|
|
6668
6812
|
recordPrNoOpFixAttempt,
|
|
6813
|
+
// #639 — exported for direct unit testing of the build-fix-ineffective
|
|
6814
|
+
// detection path (no-op branch + still-failing live CI) without spinning
|
|
6815
|
+
// up the full ADO/GH poller stack.
|
|
6816
|
+
checkBuildStillFailingLive,
|
|
6817
|
+
recordBuildFixIneffective,
|
|
6669
6818
|
// W-mq5uzmc6001d708f — post-hoc PR enrollment for orphan `_pr` pointers.
|
|
6670
6819
|
enrollPrFromCanonicalId,
|
|
6671
6820
|
_setEnrollmentGhRunnerForTest,
|
package/engine/shared.js
CHANGED
|
@@ -8671,6 +8671,24 @@ function getPrPausedCauses(pr) {
|
|
|
8671
8671
|
return causes;
|
|
8672
8672
|
}
|
|
8673
8673
|
|
|
8674
|
+
// #639 — Distinct pause signal for "fix reported no branch change, but a live
|
|
8675
|
+
// re-poll proved the build is STILL failing on that exact (unchanged) head".
|
|
8676
|
+
// This is deliberately tracked separately from `_noOpFixes[BUILD_FAILURE]`:
|
|
8677
|
+
// that record's evidence fingerprint includes `headRefOid`, so when the head
|
|
8678
|
+
// never moves (the agent genuinely pushed nothing) the fingerprint never
|
|
8679
|
+
// changes either — a real "build never actually got fixed" case would get
|
|
8680
|
+
// stuck paused forever with no distinguishing signal from a benign "nothing
|
|
8681
|
+
// needed doing" no-op (see #639). `_buildFixIneffective` re-validates against
|
|
8682
|
+
// the live head SHA the same way `isPrNoOpFixCausePaused` re-validates the
|
|
8683
|
+
// fingerprint: a new head SHA (real fix attempt landed) always releases the
|
|
8684
|
+
// pause, even though the underlying counter never got a fresh evidence key.
|
|
8685
|
+
function isBuildFixIneffectivePaused(pr) {
|
|
8686
|
+
const record = pr?._buildFixIneffective;
|
|
8687
|
+
if (!record || typeof record !== 'object' || record.paused !== true) return false;
|
|
8688
|
+
const currentHead = _prHeadSha(pr);
|
|
8689
|
+
return !!currentHead && record.headSha === currentHead;
|
|
8690
|
+
}
|
|
8691
|
+
|
|
8674
8692
|
/**
|
|
8675
8693
|
* Recursively purge Windows reserved-name pseudo-files (NUL, CON, PRN, AUX, etc.)
|
|
8676
8694
|
* using the \\?\ extended path prefix that bypasses reserved-name interpretation.
|
|
@@ -9460,6 +9478,7 @@ module.exports = {
|
|
|
9460
9478
|
getPrNoOpFixRecord,
|
|
9461
9479
|
isPrNoOpFixCausePaused,
|
|
9462
9480
|
getPrPausedCauses,
|
|
9481
|
+
isBuildFixIneffectivePaused,
|
|
9463
9482
|
parseSkillFrontmatter,
|
|
9464
9483
|
sleepMs,
|
|
9465
9484
|
killGracefully,
|
package/engine.js
CHANGED
|
@@ -6619,6 +6619,18 @@ function isPrNoOpFixCauseSuppressed(pr, cause) {
|
|
|
6619
6619
|
return true;
|
|
6620
6620
|
}
|
|
6621
6621
|
|
|
6622
|
+
// #639 — distinct suppression check for the build-fix-ineffective pause
|
|
6623
|
+
// (see shared.isBuildFixIneffectivePaused / engine/lifecycle.js
|
|
6624
|
+
// recordBuildFixIneffective). Kept separate from isPrNoOpFixCauseSuppressed
|
|
6625
|
+
// so the log line is unambiguous about WHY build-fix automation stopped:
|
|
6626
|
+
// repeated attempts genuinely found nothing to change vs. repeated attempts
|
|
6627
|
+
// left the build still red on the same commit.
|
|
6628
|
+
function isBuildFixIneffectiveSuppressed(pr) {
|
|
6629
|
+
if (!shared.isBuildFixIneffectivePaused(pr)) return false;
|
|
6630
|
+
log('warn', `PR ${pr.id}: suppressing build-failure automation — live CI stayed failing across repeated fix attempts on the same head; waiting for a new commit or human resume`);
|
|
6631
|
+
return true;
|
|
6632
|
+
}
|
|
6633
|
+
|
|
6622
6634
|
const PR_PENDING_MISSING_BRANCH = shared.PR_PENDING_REASON.MISSING_BRANCH;
|
|
6623
6635
|
|
|
6624
6636
|
function normalizePrBranch(value) {
|
|
@@ -7330,7 +7342,8 @@ async function discoverFromPrs(config, project) {
|
|
|
7330
7342
|
}
|
|
7331
7343
|
if (pollEnabled && autoFixBuilds && pr.status === PR_STATUS.ACTIVE && pr.buildStatus === 'failing'
|
|
7332
7344
|
&& !fixDispatched
|
|
7333
|
-
&& !isPrNoOpFixCauseSuppressed(pr, shared.PR_FIX_CAUSE.BUILD_FAILURE)
|
|
7345
|
+
&& !isPrNoOpFixCauseSuppressed(pr, shared.PR_FIX_CAUSE.BUILD_FAILURE)
|
|
7346
|
+
&& !isBuildFixIneffectiveSuppressed(pr)) {
|
|
7334
7347
|
// W-mpritzcr0004afc5 (#2955): "don't fan out a parallel build-failure
|
|
7335
7348
|
// fix while the review-feedback path owns the PR" — implemented via
|
|
7336
7349
|
// the `!fixDispatched` gate above (mirrors the merge-conflict block
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2313",
|
|
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"
|