@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.
@@ -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.",
@@ -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
- function executeApiStage(stage, stageState, run) {
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
- const maxAttempts = ENGINE_DEFAULTS.pipelineApiRetries;
717
- const retryDelay = ENGINE_DEFAULTS.pipelineApiRetryDelay;
718
- const makeRequest = (attempt) => {
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
- if (res.statusCode >= 400) {
728
- log('warn', `Pipeline API call to ${call.endpoint} returned ${res.statusCode} (attempt ${attempt})`);
729
- if (attempt < maxAttempts) setTimeout(() => makeRequest(attempt + 1), retryDelay);
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
- log('warn', `Pipeline API call to ${call.endpoint} failed: ${err.message} (attempt ${attempt})`);
734
- if (attempt < maxAttempts) setTimeout(() => makeRequest(attempt + 1), retryDelay);
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
- if (attempt < maxAttempts) setTimeout(() => makeRequest(attempt + 1), retryDelay);
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
- makeRequest(1);
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.2075",
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"