@yemi33/minions 0.1.1623 → 0.1.1625
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/CHANGELOG.md +5 -0
- package/docs/auto-discovery.md +3 -3
- package/docs/design-state-storage.md +2 -2
- package/docs/pr-review-fix-loop.md +1 -1
- package/engine/ado.js +77 -19
- package/engine/copilot-models.json +1 -1
- package/engine/dispatch.js +11 -0
- package/engine/runtimes/copilot.js +1 -4
- package/engine.js +37 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/docs/auto-discovery.md
CHANGED
|
@@ -11,8 +11,8 @@ tick()
|
|
|
11
11
|
1. checkTimeouts() Kill stale/hung agents (>heartbeatTimeout)
|
|
12
12
|
2. consolidateInbox() Merge learnings into notes.md (Haiku-powered)
|
|
13
13
|
2.5 runCleanup() Periodic cleanup (every 10 ticks ≈ 10min)
|
|
14
|
-
2.6 pollPrStatus() Poll ADO + GitHub for build, review, merge status (
|
|
15
|
-
2.7 pollPrHumanComments() Poll PR threads for human @minions comments (
|
|
14
|
+
2.6 pollPrStatus() Poll ADO + GitHub for build, review, merge status (wall-clock cadence from prPollStatusEvery × tickInterval, default ≈ 12min)
|
|
15
|
+
2.7 pollPrHumanComments() Poll PR threads for human @minions comments (wall-clock cadence from prPollCommentsEvery × tickInterval, default ≈ 12min)
|
|
16
16
|
3. discoverWork() Scan ALL linked projects for new tasks
|
|
17
17
|
4. updateSnapshot() Write identity/now.md
|
|
18
18
|
5. dispatch Spawn agents for pending items (up to maxConcurrent)
|
|
@@ -124,7 +124,7 @@ Both write to `work-items.json` and are picked up by Source 3 on the same or nex
|
|
|
124
124
|
|
|
125
125
|
## PR Status Polling (`pollPrStatus`)
|
|
126
126
|
|
|
127
|
-
**Runs:**
|
|
127
|
+
**Runs:** On a wall-clock cadence derived from `prPollStatusEvery × engine.tickInterval` (default 12 × 60s, ≈ 12 minutes), independently of work discovery or file-change wakeups. ADO polling lives in `engine/ado.js`; GitHub polling lives in `engine/github.js` — both run in parallel each cycle (`Promise.allSettled`) and write to the same per-project `pull-requests.json` schema. Replaces the retired agent-based `pr-sync`.
|
|
128
128
|
|
|
129
129
|
The engine directly polls the host REST API for **all** PR metadata: build/CI status, human review votes, and completion state. Two API calls per PR — no agent dispatch needed.
|
|
130
130
|
|
|
@@ -22,7 +22,7 @@ Minions persists all runtime state as flat JSON files guarded by file-lock-based
|
|
|
22
22
|
| `engine/metrics.json` | 5 KB | Per-agent stats | R/W on PR approval/merge | Low | Low |
|
|
23
23
|
| `engine/control.json` | 169 B | Single object | R/W on start/stop/heartbeat | Low | Low |
|
|
24
24
|
| `projects/*/work-items.json` | 370 KB | 180 items | R/W every 1-2 ticks; dashboard reads on-demand | **High** — engine + lifecycle + dashboard | High |
|
|
25
|
-
| `projects/*/pull-requests.json` | 241 KB | 128 PRs | R/W
|
|
25
|
+
| `projects/*/pull-requests.json` | 241 KB | 128 PRs | R/W on the `prPollStatusEvery × tickInterval` wall-clock cadence (default ≈12min); lifecycle writes | Medium | Medium |
|
|
26
26
|
| `engine/pipeline-runs.json` | 36 KB | Pipeline state | R/W on pipeline execution | Low | Low |
|
|
27
27
|
| `engine/schedule-runs.json` | 115 B | Last-run times | R every 10 ticks; W on schedule execution | Low | Low |
|
|
28
28
|
|
|
@@ -56,7 +56,7 @@ Key properties:
|
|
|
56
56
|
| Engine tick cycle | ~15 | ~3 | Heavy read, selective write |
|
|
57
57
|
| Dashboard (per page load) | ~8 | 0 | Read-only display |
|
|
58
58
|
| Dashboard (user action) | ~2 | ~2 | Read-modify-write |
|
|
59
|
-
| PR polling (every `prPollStatusEvery
|
|
59
|
+
| PR polling (every `prPollStatusEvery × tickInterval`, default ≈12min) | ~4 | ~2 | Batch read-modify-write |
|
|
60
60
|
| Consolidation (every 10 ticks) | ~3 | ~2 | Read inbox files, write notes.md |
|
|
61
61
|
|
|
62
62
|
**Read:write ratio is approximately 8:1.** This strongly favors a system that can serve reads without locking (e.g., WAL mode).
|
|
@@ -71,7 +71,7 @@ When multiple problems coexist, earlier triggers get the first chance to enqueue
|
|
|
71
71
|
|
|
72
72
|
## 6. Re-review cycle
|
|
73
73
|
|
|
74
|
-
- Poller (
|
|
74
|
+
- Poller (wall-clock cadence from `prPollStatusEvery × tickInterval`, default ~12min): detects new commit (`head.sha` changed) → sets `lastPushedAt`
|
|
75
75
|
- Platform review state drives next action:
|
|
76
76
|
- Reviewer approves → `approved` → done
|
|
77
77
|
- Reviewer re-requests changes → `changes-requested` → triggers another fix
|
package/engine/ado.js
CHANGED
|
@@ -27,6 +27,24 @@ const getAdoPrUrl = (project, prNumber) => {
|
|
|
27
27
|
return `https://dev.azure.com/${project.adoOrg}/${project.adoProject}/_git/${repoPath}/pullrequest/${prNumber}`;
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
+
function isGitHubProject(project) {
|
|
31
|
+
return String(project?.repoHost || '').toLowerCase() === 'github';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getAdoRepositoryId(project) {
|
|
35
|
+
const repositoryId = String(project?.repositoryId || '').trim();
|
|
36
|
+
if (repositoryId) return repositoryId;
|
|
37
|
+
return String(project?.repoName || '').trim();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getAdoProjectLabel(project) {
|
|
41
|
+
return project?.name || project?.repoName || `${project?.adoOrg || 'unknown-org'}/${project?.adoProject || 'unknown-project'}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function logMissingAdoRepository(project, purpose) {
|
|
45
|
+
log('error', `${purpose} disabled for project ${getAdoProjectLabel(project)}: missing project.repositoryId and project.repoName; configure one so Azure DevOps repository API calls can target the repo`);
|
|
46
|
+
}
|
|
47
|
+
|
|
30
48
|
// ── Build/Review Status Helpers ───────────────────────────────────────────────
|
|
31
49
|
|
|
32
50
|
/** Classify an array of ADO build records into a single status string. */
|
|
@@ -238,7 +256,7 @@ async function fetchAdoBuildErrorLog(orgBase, project, failedStatus, token, pr,
|
|
|
238
256
|
// ─── Shared PR Polling Loop ──────────────────────────────────────────────────
|
|
239
257
|
|
|
240
258
|
/**
|
|
241
|
-
* Iterate active PRs across all projects. Calls `callback(project, pr, prNum, orgBase)`
|
|
259
|
+
* Iterate active PRs across all projects. Calls `callback(project, pr, prNum, orgBase, adoRepositoryId)`
|
|
242
260
|
* for each active PR. If callback returns truthy, the PR file is saved after the project loop.
|
|
243
261
|
*/
|
|
244
262
|
async function forEachActivePr(config, token, callback) {
|
|
@@ -246,12 +264,19 @@ async function forEachActivePr(config, token, callback) {
|
|
|
246
264
|
let totalUpdated = 0;
|
|
247
265
|
|
|
248
266
|
for (const project of projects) {
|
|
249
|
-
if (
|
|
267
|
+
if (isGitHubProject(project)) continue;
|
|
268
|
+
if (!project.adoOrg || !project.adoProject) continue;
|
|
250
269
|
|
|
251
270
|
const prs = getPrs(project);
|
|
252
271
|
const activePrs = prs.filter(pr => shared.PR_POLLABLE_STATUSES.has(pr.status));
|
|
253
272
|
if (activePrs.length === 0) continue;
|
|
254
273
|
|
|
274
|
+
const adoRepositoryId = getAdoRepositoryId(project);
|
|
275
|
+
if (!adoRepositoryId) {
|
|
276
|
+
logMissingAdoRepository(project, 'ADO PR polling');
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
|
|
255
280
|
let projectUpdated = 0;
|
|
256
281
|
const orgBase = getAdoOrgBase(project);
|
|
257
282
|
|
|
@@ -262,7 +287,7 @@ async function forEachActivePr(config, token, callback) {
|
|
|
262
287
|
const results = await Promise.allSettled(batch.map(async (pr) => {
|
|
263
288
|
const prNum = shared.getPrNumber(pr);
|
|
264
289
|
if (!prNum) return false;
|
|
265
|
-
return callback(project, pr, prNum, orgBase);
|
|
290
|
+
return callback(project, pr, prNum, orgBase, adoRepositoryId);
|
|
266
291
|
}));
|
|
267
292
|
for (const r of results) {
|
|
268
293
|
if (r.status === 'fulfilled' && r.value) projectUpdated++;
|
|
@@ -275,7 +300,11 @@ async function forEachActivePr(config, token, callback) {
|
|
|
275
300
|
// Merge back updated PRs — preserve disk values that may have been changed
|
|
276
301
|
// by other writers between poll start and this write
|
|
277
302
|
for (const updatedPr of activePrs) {
|
|
278
|
-
const
|
|
303
|
+
const updatedPrNumber = shared.getPrNumber(updatedPr);
|
|
304
|
+
const idx = currentPrs.findIndex(p =>
|
|
305
|
+
p.id === updatedPr.id
|
|
306
|
+
|| (updatedPrNumber != null && shared.getPrNumber(p) === updatedPrNumber)
|
|
307
|
+
);
|
|
279
308
|
if (idx >= 0) {
|
|
280
309
|
// Never downgrade reviewStatus from 'approved' — it's a permanent terminal state
|
|
281
310
|
// The disk version may have been set to 'approved' by another writer after we read
|
|
@@ -317,9 +346,10 @@ async function pollPrStatus(config) {
|
|
|
317
346
|
return;
|
|
318
347
|
}
|
|
319
348
|
|
|
320
|
-
const totalUpdated = await forEachActivePr(config, token, async (project, pr, prNum, orgBase) => {
|
|
349
|
+
const totalUpdated = await forEachActivePr(config, token, async (project, pr, prNum, orgBase, adoRepositoryId) => {
|
|
321
350
|
try {
|
|
322
|
-
const
|
|
351
|
+
const encodedRepoId = encodeURIComponent(adoRepositoryId);
|
|
352
|
+
const repoBase = `${orgBase}/${project.adoProject}/_apis/git/repositories/${encodedRepoId}/pullrequests/${prNum}`;
|
|
323
353
|
let updated = false;
|
|
324
354
|
|
|
325
355
|
// Clear stale flag — we're attempting a fresh poll
|
|
@@ -453,7 +483,7 @@ async function pollPrStatus(config) {
|
|
|
453
483
|
if (prNumber && mergeCommitId) {
|
|
454
484
|
try {
|
|
455
485
|
const mergeRef = encodeURIComponent(`refs/pull/${prNumber}/merge`);
|
|
456
|
-
const buildsUrl = `${orgBase}/${project.adoProject}/_apis/build/builds?branchName=${mergeRef}&repositoryId=${
|
|
486
|
+
const buildsUrl = `${orgBase}/${project.adoProject}/_apis/build/builds?branchName=${mergeRef}&repositoryId=${encodedRepoId}&repositoryType=TfsGit&$top=25&api-version=7.1`;
|
|
457
487
|
const buildsData = await adoFetch(buildsUrl, token);
|
|
458
488
|
const allBuilds = buildsData?.value || [];
|
|
459
489
|
const prBuilds = allBuilds.filter(b => b.sourceVersion === mergeCommitId);
|
|
@@ -544,7 +574,7 @@ async function pollPrStatus(config) {
|
|
|
544
574
|
const mergeStrategy = config.engine?.prMergeMethod === 'merge' ? 1 : config.engine?.prMergeMethod === 'rebase' ? 2 : 3; // 3 = squash
|
|
545
575
|
const identityUrl = `${orgBase}/_apis/connectionData?api-version=7.1`;
|
|
546
576
|
const identity = await adoFetch(identityUrl, token).catch(() => null);
|
|
547
|
-
const autoCompleteUrl = `${orgBase}/${project.adoProject}/_apis/git/repositories/${
|
|
577
|
+
const autoCompleteUrl = `${orgBase}/${project.adoProject}/_apis/git/repositories/${encodedRepoId}/pullrequests/${prNum}?api-version=7.1`;
|
|
548
578
|
await adoFetch(autoCompleteUrl, token, {
|
|
549
579
|
method: 'PATCH',
|
|
550
580
|
body: JSON.stringify({
|
|
@@ -599,8 +629,8 @@ async function pollPrHumanComments(config) {
|
|
|
599
629
|
const token = await getAdoToken();
|
|
600
630
|
if (!token) return;
|
|
601
631
|
|
|
602
|
-
const totalUpdated = await forEachActivePr(config, token, async (project, pr, prNum, orgBase) => {
|
|
603
|
-
const threadsUrl = `${orgBase}/${project.adoProject}/_apis/git/repositories/${
|
|
632
|
+
const totalUpdated = await forEachActivePr(config, token, async (project, pr, prNum, orgBase, adoRepositoryId) => {
|
|
633
|
+
const threadsUrl = `${orgBase}/${project.adoProject}/_apis/git/repositories/${encodeURIComponent(adoRepositoryId)}/pullrequests/${prNum}/threads?api-version=7.1`;
|
|
604
634
|
const threadsData = await adoFetch(threadsUrl, token);
|
|
605
635
|
const threads = threadsData.value || [];
|
|
606
636
|
|
|
@@ -698,10 +728,16 @@ async function reconcilePrs(config) {
|
|
|
698
728
|
let totalAdded = 0;
|
|
699
729
|
|
|
700
730
|
for (const project of projects) {
|
|
701
|
-
if (
|
|
731
|
+
if (isGitHubProject(project)) continue;
|
|
732
|
+
if (!project.adoOrg || !project.adoProject) continue;
|
|
733
|
+
const adoRepositoryId = getAdoRepositoryId(project);
|
|
734
|
+
if (!adoRepositoryId) {
|
|
735
|
+
logMissingAdoRepository(project, 'ADO PR reconciliation');
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
702
738
|
|
|
703
739
|
const orgBase = shared.getAdoOrgBase(project);
|
|
704
|
-
const url = `${orgBase}/${project.adoProject}/_apis/git/repositories/${
|
|
740
|
+
const url = `${orgBase}/${project.adoProject}/_apis/git/repositories/${encodeURIComponent(adoRepositoryId)}/pullrequests?searchCriteria.status=active&api-version=7.1`;
|
|
705
741
|
|
|
706
742
|
let prData;
|
|
707
743
|
try {
|
|
@@ -829,7 +865,12 @@ async function checkLiveReviewStatus(pr, project) {
|
|
|
829
865
|
const orgBase = shared.getAdoOrgBase(project);
|
|
830
866
|
const prNum = shared.getPrNumber(pr);
|
|
831
867
|
if (!prNum) return null;
|
|
832
|
-
const
|
|
868
|
+
const adoRepositoryId = getAdoRepositoryId(project);
|
|
869
|
+
if (!adoRepositoryId) {
|
|
870
|
+
logMissingAdoRepository(project, 'ADO live review check');
|
|
871
|
+
return null;
|
|
872
|
+
}
|
|
873
|
+
const url = `${orgBase}/${project.adoProject}/_apis/git/repositories/${encodeURIComponent(adoRepositoryId)}/pullrequests/${prNum}?api-version=7.1`;
|
|
833
874
|
// SEC-02: use in-process adoFetch rather than a shell-out — keeps the bearer
|
|
834
875
|
// token out of the process argv list where any local process could read it.
|
|
835
876
|
// 4s timeout preserves the original request-cancellation semantics via AbortSignal.
|
|
@@ -868,7 +909,13 @@ async function checkLiveBuildAndConflict(pr, project) {
|
|
|
868
909
|
const orgBase = shared.getAdoOrgBase(project);
|
|
869
910
|
const prNum = shared.getPrNumber(pr);
|
|
870
911
|
if (!prNum) return null;
|
|
871
|
-
const
|
|
912
|
+
const adoRepositoryId = getAdoRepositoryId(project);
|
|
913
|
+
if (!adoRepositoryId) {
|
|
914
|
+
logMissingAdoRepository(project, 'ADO live build/conflict check');
|
|
915
|
+
return null;
|
|
916
|
+
}
|
|
917
|
+
const encodedRepoId = encodeURIComponent(adoRepositoryId);
|
|
918
|
+
const repoBase = `${orgBase}/${project.adoProject}/_apis/git/repositories/${encodedRepoId}`;
|
|
872
919
|
const prUrl = `${repoBase}/pullrequests/${prNum}?api-version=7.1`;
|
|
873
920
|
// 4s timeout — same budget as checkLiveReviewStatus. This is a pre-dispatch
|
|
874
921
|
// gate; we'd rather miss a freshness signal and fall back to cache than
|
|
@@ -889,7 +936,7 @@ async function checkLiveBuildAndConflict(pr, project) {
|
|
|
889
936
|
if (mergeCommitId) {
|
|
890
937
|
try {
|
|
891
938
|
const mergeRef = encodeURIComponent(`refs/pull/${prNum}/merge`);
|
|
892
|
-
const buildsUrl = `${orgBase}/${project.adoProject}/_apis/build/builds?branchName=${mergeRef}&repositoryId=${
|
|
939
|
+
const buildsUrl = `${orgBase}/${project.adoProject}/_apis/build/builds?branchName=${mergeRef}&repositoryId=${encodedRepoId}&repositoryType=TfsGit&$top=25&api-version=7.1`;
|
|
893
940
|
const buildsData = await adoFetch(buildsUrl, token, { timeout: 4000 });
|
|
894
941
|
const allBuilds = buildsData?.value || [];
|
|
895
942
|
const prBuilds = allBuilds.filter(b => b.sourceVersion === mergeCommitId);
|
|
@@ -940,9 +987,15 @@ async function fetchSinglePrBuildStatus(project, prNumber) {
|
|
|
940
987
|
if (!token) return null;
|
|
941
988
|
|
|
942
989
|
const orgBase = getAdoOrgBase(project);
|
|
943
|
-
const
|
|
990
|
+
const adoRepositoryId = getAdoRepositoryId(project);
|
|
991
|
+
if (!adoRepositoryId) {
|
|
992
|
+
logMissingAdoRepository(project, 'ADO single PR status fetch');
|
|
993
|
+
return null;
|
|
994
|
+
}
|
|
995
|
+
const encodedRepoId = encodeURIComponent(adoRepositoryId);
|
|
996
|
+
const repoBase = `${orgBase}/${project.adoProject}/_apis/git/repositories/${encodedRepoId}`;
|
|
944
997
|
const mergeRef = encodeURIComponent(`refs/pull/${prNumber}/merge`);
|
|
945
|
-
const buildsUrl = `${orgBase}/${project.adoProject}/_apis/build/builds?branchName=${mergeRef}&repositoryId=${
|
|
998
|
+
const buildsUrl = `${orgBase}/${project.adoProject}/_apis/build/builds?branchName=${mergeRef}&repositoryId=${encodedRepoId}&repositoryType=TfsGit&$top=25&api-version=7.1`;
|
|
946
999
|
|
|
947
1000
|
// Fetch PR metadata and builds in parallel
|
|
948
1001
|
const [prData, buildsData] = await Promise.all([
|
|
@@ -1011,7 +1064,12 @@ const getAdoThrottleState = () => _adoThrottle.getState();
|
|
|
1011
1064
|
* @returns {{ prNumber: number, url: string }|null}
|
|
1012
1065
|
*/
|
|
1013
1066
|
async function findOpenPrOnBranch(project, branch) {
|
|
1014
|
-
if (!project.adoOrg || !project.adoProject || !
|
|
1067
|
+
if (!project.adoOrg || !project.adoProject || !branch) return null;
|
|
1068
|
+
const adoRepositoryId = getAdoRepositoryId(project);
|
|
1069
|
+
if (!adoRepositoryId) {
|
|
1070
|
+
logMissingAdoRepository(project, 'ADO branch PR lookup');
|
|
1071
|
+
return null;
|
|
1072
|
+
}
|
|
1015
1073
|
if (isAdoThrottled()) {
|
|
1016
1074
|
log('debug', `[ado] Skipping branch PR lookup for ${project.name || project.repoName || 'unknown project'}:${branch} — throttled`);
|
|
1017
1075
|
return null;
|
|
@@ -1020,7 +1078,7 @@ async function findOpenPrOnBranch(project, branch) {
|
|
|
1020
1078
|
if (!token) return null;
|
|
1021
1079
|
const orgBase = shared.getAdoOrgBase(project);
|
|
1022
1080
|
const sourceRef = encodeURIComponent(`refs/heads/${branch}`);
|
|
1023
|
-
const url = `${orgBase}/${project.adoProject}/_apis/git/repositories/${
|
|
1081
|
+
const url = `${orgBase}/${project.adoProject}/_apis/git/repositories/${encodeURIComponent(adoRepositoryId)}/pullrequests?searchCriteria.status=active&searchCriteria.sourceRefName=${sourceRef}&api-version=7.1`;
|
|
1024
1082
|
const data = await adoFetch(url, token);
|
|
1025
1083
|
const pr = (data.value || [])[0];
|
|
1026
1084
|
if (!pr) return null;
|
package/engine/dispatch.js
CHANGED
|
@@ -111,6 +111,17 @@ function isRetryableFailureReason(reason = '', failureClass = '') {
|
|
|
111
111
|
'invalid file path',
|
|
112
112
|
'missing required',
|
|
113
113
|
'validation failed',
|
|
114
|
+
'auth failure',
|
|
115
|
+
'authentication failed',
|
|
116
|
+
'authentication failure',
|
|
117
|
+
'unauthorized',
|
|
118
|
+
'invalid api key',
|
|
119
|
+
'please log in',
|
|
120
|
+
'budget-exceeded',
|
|
121
|
+
'budget exceeded',
|
|
122
|
+
'budget cap exceeded',
|
|
123
|
+
'max-budget-usd',
|
|
124
|
+
'cost limit',
|
|
114
125
|
];
|
|
115
126
|
return !nonRetryable.some(s => r.includes(s));
|
|
116
127
|
}
|
|
@@ -552,12 +552,11 @@ function createStreamConsumer(ctx) {
|
|
|
552
552
|
// inspect...") is progress text only — terminal text comes from non-tool
|
|
553
553
|
// assistant messages or trailing deltas.
|
|
554
554
|
let copilotMessageBuffer = '';
|
|
555
|
-
let copilotTaskCompleteSeen = false;
|
|
556
555
|
|
|
557
556
|
function _captureTaskComplete(summary, success = true) {
|
|
558
557
|
if (typeof summary !== 'string' || !summary) return;
|
|
559
|
-
copilotTaskCompleteSeen = true;
|
|
560
558
|
copilotMessageBuffer = '';
|
|
559
|
+
ctx.pushText(summary);
|
|
561
560
|
ctx.notifyTaskComplete(summary, success !== false);
|
|
562
561
|
}
|
|
563
562
|
|
|
@@ -579,7 +578,6 @@ function createStreamConsumer(ctx) {
|
|
|
579
578
|
}
|
|
580
579
|
|
|
581
580
|
if (obj.type === 'assistant.message_delta' && typeof obj.data?.deltaContent === 'string') {
|
|
582
|
-
if (copilotTaskCompleteSeen) return;
|
|
583
581
|
copilotMessageBuffer += obj.data.deltaContent;
|
|
584
582
|
ctx.pushText(copilotMessageBuffer);
|
|
585
583
|
return;
|
|
@@ -629,7 +627,6 @@ function createStreamConsumer(ctx) {
|
|
|
629
627
|
|
|
630
628
|
function reset() {
|
|
631
629
|
copilotMessageBuffer = '';
|
|
632
|
-
copilotTaskCompleteSeen = false;
|
|
633
630
|
}
|
|
634
631
|
|
|
635
632
|
return { consume, reset };
|
package/engine.js
CHANGED
|
@@ -3337,11 +3337,30 @@ let tickCount = 0;
|
|
|
3337
3337
|
// In-memory cache of plan filenames confirmed completed — avoids redundant
|
|
3338
3338
|
// checkPlanCompletion calls. Cleared automatically on engine restart.
|
|
3339
3339
|
const completedPlanCache = new Set();
|
|
3340
|
+
let lastWatchCheckAt = 0;
|
|
3341
|
+
let lastPrStatusPollAt = 0;
|
|
3342
|
+
let lastPrCommentsPollAt = 0;
|
|
3340
3343
|
|
|
3341
3344
|
let tickRunning = false;
|
|
3342
3345
|
let _tickStartedAt = 0;
|
|
3343
3346
|
const TICK_TIMEOUT_MS = 300000; // 5 min — force-release tick lock if stuck
|
|
3344
3347
|
|
|
3348
|
+
function _pollIntervalMsFromTicks(ticks, tickIntervalMs) {
|
|
3349
|
+
const normalizedTicks = Math.max(1, Number(ticks) || 1);
|
|
3350
|
+
const normalizedTickInterval = Math.max(1, Number(tickIntervalMs) || ENGINE_DEFAULTS.tickInterval);
|
|
3351
|
+
return normalizedTicks * normalizedTickInterval;
|
|
3352
|
+
}
|
|
3353
|
+
|
|
3354
|
+
function _shouldRunPeriodicPhase(now, lastRunAt, intervalMs) {
|
|
3355
|
+
const current = Number(now);
|
|
3356
|
+
const previous = Number(lastRunAt) || 0;
|
|
3357
|
+
const interval = Math.max(1, Number(intervalMs) || 1);
|
|
3358
|
+
if (!Number.isFinite(current)) return false;
|
|
3359
|
+
if (!previous || !Number.isFinite(previous)) return true;
|
|
3360
|
+
if (current < previous) return true;
|
|
3361
|
+
return current - previous >= interval;
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3345
3364
|
async function tick() {
|
|
3346
3365
|
if (tickRunning) {
|
|
3347
3366
|
if (_tickStartedAt && Date.now() - _tickStartedAt > TICK_TIMEOUT_MS) {
|
|
@@ -3375,6 +3394,8 @@ async function tickInner() {
|
|
|
3375
3394
|
|
|
3376
3395
|
const config = getConfig();
|
|
3377
3396
|
tickCount++;
|
|
3397
|
+
const now = Date.now();
|
|
3398
|
+
const tickIntervalMs = Math.max(1, Number(config.engine?.tickInterval) || ENGINE_DEFAULTS.tickInterval);
|
|
3378
3399
|
_failedRefCache.clear(); // Reset per-tick failed-ref cache
|
|
3379
3400
|
|
|
3380
3401
|
// Helper: run a phase, log + continue on error
|
|
@@ -3402,8 +3423,10 @@ async function tickInner() {
|
|
|
3402
3423
|
safe('runCleanup', () => runCleanup(config));
|
|
3403
3424
|
}
|
|
3404
3425
|
|
|
3405
|
-
// 2.55. Check persistent watches (
|
|
3406
|
-
|
|
3426
|
+
// 2.55. Check persistent watches (3 tick-equivalents, default ~3 minutes)
|
|
3427
|
+
const watchPollIntervalMs = _pollIntervalMsFromTicks(3, tickIntervalMs);
|
|
3428
|
+
if (_shouldRunPeriodicPhase(now, lastWatchCheckAt, watchPollIntervalMs)) {
|
|
3429
|
+
lastWatchCheckAt = now;
|
|
3407
3430
|
safe('checkWatches', () => {
|
|
3408
3431
|
const { checkWatches } = require('./engine/watches');
|
|
3409
3432
|
const projects = getProjects(config);
|
|
@@ -3432,10 +3455,14 @@ async function tickInner() {
|
|
|
3432
3455
|
Number(config.engine?.prPollCommentsEvery ?? config.engine?.adoPollCommentsEvery) || ENGINE_DEFAULTS.prPollCommentsEvery
|
|
3433
3456
|
);
|
|
3434
3457
|
|
|
3435
|
-
// 2.6. Poll PR status: build, review, merge (every prPollStatusEvery
|
|
3458
|
+
// 2.6. Poll PR status: build, review, merge (every prPollStatusEvery tick-equivalents, default ~12 minutes)
|
|
3436
3459
|
// Awaited so PR state is consistent before discoverWork reads it
|
|
3437
3460
|
// Also re-polls early if previous tick had ADO auth failures (stale build status recovery)
|
|
3438
|
-
|
|
3461
|
+
const prPollStatusIntervalMs = _pollIntervalMsFromTicks(prPollStatusEvery, tickIntervalMs);
|
|
3462
|
+
const prStatusPollDue = _shouldRunPeriodicPhase(now, lastPrStatusPollAt, prPollStatusIntervalMs);
|
|
3463
|
+
const adoPollRetryDue = needsAdoPollRetry();
|
|
3464
|
+
if (prStatusPollDue || adoPollRetryDue) {
|
|
3465
|
+
lastPrStatusPollAt = now;
|
|
3439
3466
|
// Build promise array — enabled+unthrottled polls run concurrently via Promise.allSettled
|
|
3440
3467
|
const statusPolls = [];
|
|
3441
3468
|
if (adoPollEnabled && !isAdoThrottled()) {
|
|
@@ -3475,8 +3502,11 @@ async function tickInner() {
|
|
|
3475
3502
|
} catch (err) { log('warn', `Plan completion check error: ${err?.message || err}`); }
|
|
3476
3503
|
}
|
|
3477
3504
|
|
|
3478
|
-
|
|
3479
|
-
|
|
3505
|
+
const prPollCommentsIntervalMs = _pollIntervalMsFromTicks(prPollCommentsEvery, tickIntervalMs);
|
|
3506
|
+
|
|
3507
|
+
// 2.7. Poll PR threads for human comments (every prPollCommentsEvery tick-equivalents, default ~12 minutes)
|
|
3508
|
+
if (_shouldRunPeriodicPhase(now, lastPrCommentsPollAt, prPollCommentsIntervalMs)) {
|
|
3509
|
+
lastPrCommentsPollAt = now;
|
|
3480
3510
|
// Build promise array — enabled+unthrottled comment polls run concurrently via Promise.allSettled
|
|
3481
3511
|
const commentPolls = [];
|
|
3482
3512
|
if (adoPollEnabled && !isAdoThrottled()) {
|
|
@@ -3923,6 +3953,7 @@ module.exports = {
|
|
|
3923
3953
|
|
|
3924
3954
|
// Tick
|
|
3925
3955
|
tick,
|
|
3956
|
+
_pollIntervalMsFromTicks, _shouldRunPeriodicPhase, // exported for testing
|
|
3926
3957
|
};
|
|
3927
3958
|
|
|
3928
3959
|
// ─── Entrypoint ─────────────────────────────────────────────────────────────
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1625",
|
|
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"
|