@yemi33/minions 0.1.2075 → 0.1.2076
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/docs/deprecated.json +17 -0
- package/engine/pipeline.js +50 -15
- package/engine/shared.js +1 -0
- package/package.json +1 -1
package/docs/deprecated.json
CHANGED
|
@@ -56,6 +56,23 @@
|
|
|
56
56
|
"targetRemovalDate": null,
|
|
57
57
|
"notes": "Do NOT set targetRemovalDate — gating is signal-based. The function is silent on no-op (returns false without logging), so the meaningful telemetry signal is the absence of the promotion log line over the sweep window, NOT the absence of function invocations (cli.js calls it every boot regardless)."
|
|
58
58
|
},
|
|
59
|
+
{
|
|
60
|
+
"id": "sql-state-json-mirrors",
|
|
61
|
+
"description": "Phase X.5 follow-up to the SQL state migration (commits 62bd6a2c..1111cf54, phases 0–7). Every engine state file that previously used mutateJsonFileLocked now routes through a SQL store, but each store still writes a JSON dual-write mirror after every mutation because a handful of direct-readers (a few unit tests + a couple of inline safeJson calls) have not been migrated to the SQL read path. Once those readers are confirmed routed through the SQL store (or rewritten to use the store's read helper), the mirror writers can be deleted and the JSON files retired.",
|
|
62
|
+
"code": [
|
|
63
|
+
{ "file": "engine/dispatch-store.js", "note": "_mirrorJsonFromSql + _readDispatchJsonFallback — used when SQL is empty AND JSON has content (test seeding + first-time hydrate)" },
|
|
64
|
+
{ "file": "engine/work-items-store.js", "note": "_mirrorJsonFromSql + _readJsonArrayFallback (per scope) — same fallback contract as dispatch-store" },
|
|
65
|
+
{ "file": "engine/pull-requests-store.js", "note": "_mirrorJsonFromSql + _readJsonArrayFallback (per scope)" },
|
|
66
|
+
{ "file": "engine/logs-store.js", "note": "engine/log.json mirror written by shared._flushLogBuffer's byJsonPath loop — Phase 4.5 will retire" },
|
|
67
|
+
{ "file": "engine/metrics-store.js", "note": "_mirrorJsonFromSql + _readJsonObjectFallback" },
|
|
68
|
+
{ "file": "engine/watches-store.js", "note": "_mirrorJsonFromSql + _readJsonArrayFallback" },
|
|
69
|
+
{ "file": "engine/small-state-store.js", "note": "_mirrorScheduleRunsJson, _mirrorPipelineRunsJson, _mirrorManagedProcessesJson, _mirrorWorktreePoolJson + each store's _readJson fallback path" },
|
|
70
|
+
{ "file": "CLAUDE.md", "lines": "47-66, 240-265", "note": "State Files + Concurrency sections still describe JSON files as the source of truth; they describe a layered SQLite-then-mirror reality in places but the headline contract still reads as JSON-primary. Rewrite these sections to make SQL-as-source-of-truth the headline and the JSON mirrors a transitional compatibility detail." }
|
|
71
|
+
],
|
|
72
|
+
"removalGate": "All direct-readers of the mirror JSON files must be confirmed routed through their respective SQL store's read helper. Specifically: (a) grep the codebase for `safeJson`, `safeJsonArr`, `safeJsonObj`, `readFileSync(...work-items.json|pull-requests.json|metrics.json|watches.json|schedule-runs.json|pipeline-runs.json|managed-processes.json|worktree-pool.json|log.json|dispatch.json...)` and confirm every hit is either (i) a test fixture that can move to the SQL helper, or (ii) intentionally documented as bypassing SQL. (b) Run the full test suite with each store's _mirrorJsonFromSql temporarily neutered (returning early before safeWrite) and confirm 0 failures — that proves no production code path depends on the mirror. Once both conditions hold, removal deletes each store's _mirrorJsonFromSql call site in shared.js (mutateWorkItems/mutatePullRequests/etc.), the corresponding _readJsonArrayFallback paths, and the JSON file gitignore entries. CLAUDE.md update can ship independently as soon as someone has bandwidth.",
|
|
73
|
+
"targetRemovalDate": null,
|
|
74
|
+
"notes": "Do NOT set targetRemovalDate — gating is signal-based, not calendar-based. The mirror writes are cheap (a few KB per write, sub-ms) so there is no production cost to keeping them indefinitely; the only reason to remove them is to simplify the codebase and lock in SQL-as-the-single-source-of-truth. Order matters: when retiring a specific store's mirror, retire the corresponding CLAUDE.md mention in the same PR so the docs never claim SQL-only while a mirror still writes."
|
|
75
|
+
},
|
|
59
76
|
{
|
|
60
77
|
"id": "prune-default-claude-config",
|
|
61
78
|
"description": "pruneDefaultClaudeConfig: active sanitizer that strips generated `config.claude.{binary,outputFormat,allowedTools,permissionMode}` defaults from persisted config.json so the `deprecated-config-claude` warning stops tripping on stale defaults left by older `minions init` versions. Sub-cluster of `config-claude-binary-override` — the prune deliberately preserves non-default user overrides (binary/allowedTools), which is what keeps the override branch in engine/runtimes/claude.js load-bearing.",
|
package/engine/pipeline.js
CHANGED
|
@@ -357,7 +357,7 @@ async function executeStage(stage, run, pipeline, config) {
|
|
|
357
357
|
case STAGE_TYPE.PLAN:
|
|
358
358
|
return executePlanStage(resolved, stageState, run, config, pipeline);
|
|
359
359
|
case STAGE_TYPE.API:
|
|
360
|
-
return executeApiStage(resolved, stageState, run);
|
|
360
|
+
return await executeApiStage(resolved, stageState, run);
|
|
361
361
|
case STAGE_TYPE.MERGE_PRS:
|
|
362
362
|
return executeMergePrsStage(resolved, stageState, run, config);
|
|
363
363
|
case STAGE_TYPE.SCHEDULE:
|
|
@@ -708,14 +708,27 @@ async function executePlanStage(stage, stageState, run, config, pipeline = {}) {
|
|
|
708
708
|
};
|
|
709
709
|
}
|
|
710
710
|
|
|
711
|
-
|
|
711
|
+
// P-bfa1e-pipeline-state-machine-b — async + timeout-aware.
|
|
712
|
+
// Each call is awaited end-to-end so the stage result reflects the real
|
|
713
|
+
// outcome (previously the stage returned COMPLETED while requests were
|
|
714
|
+
// still in flight, silently hiding API failures). On per-attempt timeout
|
|
715
|
+
// the request is destroyed; the timeout flows through the existing retry
|
|
716
|
+
// path. After `pipelineApiRetries` attempts exhaust on any call — or any
|
|
717
|
+
// attempt times out past the retry budget — the stage returns FAILED with
|
|
718
|
+
// `<endpoint>: <reason>` so updateRunStage records a terminal failure.
|
|
719
|
+
async function executeApiStage(stage, stageState, run) {
|
|
712
720
|
const calls = stage.calls || [{ endpoint: stage.endpoint, method: stage.method || 'POST', body: stage.body }];
|
|
721
|
+
const maxAttempts = ENGINE_DEFAULTS.pipelineApiRetries;
|
|
722
|
+
const retryDelay = ENGINE_DEFAULTS.pipelineApiRetryDelay;
|
|
723
|
+
const timeoutMs = ENGINE_DEFAULTS.pipelineApiTimeoutMs;
|
|
724
|
+
|
|
713
725
|
for (const call of calls) {
|
|
714
726
|
const url = `http://localhost:${process.env.MINIONS_PORT || 7331}${call.endpoint}`;
|
|
715
727
|
const body = typeof call.body === 'string' ? call.body : JSON.stringify(call.body || {});
|
|
716
|
-
|
|
717
|
-
const
|
|
718
|
-
|
|
728
|
+
|
|
729
|
+
const attemptOnce = (attempt) => new Promise((resolve) => {
|
|
730
|
+
let settled = false;
|
|
731
|
+
const finish = (result) => { if (!settled) { settled = true; resolve(result); } };
|
|
719
732
|
try {
|
|
720
733
|
const parsed = new URL(url);
|
|
721
734
|
const req = http.request({
|
|
@@ -724,23 +737,45 @@ function executeApiStage(stage, stageState, run) {
|
|
|
724
737
|
headers: { 'Content-Type': 'application/json' },
|
|
725
738
|
}, (res) => {
|
|
726
739
|
res.resume(); // drain body to free socket
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
740
|
+
res.on('end', () => {
|
|
741
|
+
if (res.statusCode >= 400) {
|
|
742
|
+
log('warn', `Pipeline API call to ${call.endpoint} returned ${res.statusCode} (attempt ${attempt})`);
|
|
743
|
+
finish({ ok: false, reason: `HTTP ${res.statusCode}` });
|
|
744
|
+
} else {
|
|
745
|
+
finish({ ok: true });
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
res.on('error', (err) => finish({ ok: false, reason: err.message }));
|
|
749
|
+
});
|
|
750
|
+
req.setTimeout(timeoutMs, () => {
|
|
751
|
+
req.destroy(new Error('timeout'));
|
|
731
752
|
});
|
|
732
753
|
req.on('error', (err) => {
|
|
733
|
-
|
|
734
|
-
|
|
754
|
+
const reason = err && err.message ? err.message : (err && err.code) || 'request error';
|
|
755
|
+
log('warn', `Pipeline API call to ${call.endpoint} failed: ${reason} (attempt ${attempt})`);
|
|
756
|
+
finish({ ok: false, reason });
|
|
735
757
|
});
|
|
736
758
|
req.write(body);
|
|
737
759
|
req.end();
|
|
738
760
|
} catch (e) {
|
|
739
761
|
log('warn', `Pipeline API call to ${call.endpoint} threw: ${e.message} (attempt ${attempt})`);
|
|
740
|
-
|
|
762
|
+
finish({ ok: false, reason: e.message });
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
let lastReason = 'unknown';
|
|
767
|
+
let succeeded = false;
|
|
768
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
769
|
+
const result = await attemptOnce(attempt);
|
|
770
|
+
if (result.ok) { succeeded = true; break; }
|
|
771
|
+
lastReason = result.reason;
|
|
772
|
+
if (attempt < maxAttempts && retryDelay > 0) {
|
|
773
|
+
await new Promise((r) => setTimeout(r, retryDelay));
|
|
741
774
|
}
|
|
742
|
-
}
|
|
743
|
-
|
|
775
|
+
}
|
|
776
|
+
if (!succeeded) {
|
|
777
|
+
return { status: PIPELINE_STATUS.FAILED, error: `${call.endpoint}: ${lastReason}`, completedAt: ts() };
|
|
778
|
+
}
|
|
744
779
|
}
|
|
745
780
|
return { status: PIPELINE_STATUS.COMPLETED, completedAt: ts() };
|
|
746
781
|
}
|
|
@@ -1165,7 +1200,7 @@ module.exports = {
|
|
|
1165
1200
|
getPipelineRuns, getActiveRun, startRun, updateRunStage, completeRun,
|
|
1166
1201
|
discoverPipelineWork,
|
|
1167
1202
|
evaluateCondition, // exported for testing
|
|
1168
|
-
executeTaskStage, executePlanStage, executeScheduleStage, isStageComplete, resolveTemplate, // exported for testing
|
|
1203
|
+
executeTaskStage, executePlanStage, executeScheduleStage, executeApiStage, isStageComplete, resolveTemplate, // exported for testing
|
|
1169
1204
|
_resolvePipelineProjects, // exported for testing
|
|
1170
1205
|
_findMeetingsInRun, _findExistingPlanForMeeting, _findExistingPrdForPlan, // exported for testing
|
|
1171
1206
|
};
|
package/engine/shared.js
CHANGED
|
@@ -1923,6 +1923,7 @@ const ENGINE_DEFAULTS = {
|
|
|
1923
1923
|
minRetryGapMs: 120000, // 2min — minimum gap between retry dispatches for the same work item; prevents tight retry loops when an idempotent agent (e.g. review bailing out on a duplicate) cannot produce the expected output (#1770)
|
|
1924
1924
|
pipelineApiRetries: 2, // max attempts for pipeline API calls
|
|
1925
1925
|
pipelineApiRetryDelay: 2000, // ms delay between pipeline API retries
|
|
1926
|
+
pipelineApiTimeoutMs: 30000, // P-bfa1e-pipeline-state-machine-b — per-attempt request timeout for pipeline API calls; on timeout the request is destroyed and the attempt fails through the normal retry path. After all retries exhaust, executeApiStage returns FAILED instead of COMPLETED.
|
|
1926
1927
|
prAutoLinkRetries: 3, // max attempts for gh pr list lookup when auto-linking PR after merge (3s backoff between attempts)
|
|
1927
1928
|
rebaseQueueRetries: 3, // max rebase attempts per queued PR before giving up
|
|
1928
1929
|
versionCheckInterval: 3600000, // 1 hour — how often to check npm for updates (ms)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2076",
|
|
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"
|