@yemi33/minions 0.1.2216 → 0.1.2217

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/engine/cli.js CHANGED
@@ -1255,7 +1255,10 @@ const commands = {
1255
1255
  }
1256
1256
  const control = getControl();
1257
1257
  if (control.pid && control.pid !== process.pid) {
1258
- try { process.kill(control.pid); } catch { /* process may be dead */ }
1258
+ // killGracefully so the engine's SIGTERM graceful-shutdown handler runs
1259
+ // and recurses into the child tree (bare process.kill defaults to a
1260
+ // non-recursing SIGTERM that is Windows-broken — CLAUDE.md Footgun #4).
1261
+ shared.killGracefully({ pid: control.pid });
1259
1262
  }
1260
1263
  mutateControl(() => ({ state: 'stopped', stopped_at: e.ts() }));
1261
1264
  e.log('info', 'Engine stopped');
@@ -1793,7 +1796,10 @@ const commands = {
1793
1796
  let pidNum = NaN;
1794
1797
  try { pidNum = shared.validatePid(raw); } catch { /* invalid — skip */ }
1795
1798
  if (pidNum > 0) {
1796
- try { process.kill(pidNum); console.log(`Killed process ${pidNum} (${fileName})`); }
1799
+ // killImmediate force-kills the whole process tree (bare process.kill
1800
+ // defaults to a non-recursing SIGTERM that is Windows-broken and
1801
+ // leaks child processes — CLAUDE.md Footgun #4).
1802
+ try { shared.killImmediate({ pid: pidNum }); console.log(`Killed process ${pidNum} (${fileName})`); }
1797
1803
  catch { console.log(`Process ${pidNum} already dead`); }
1798
1804
  } else {
1799
1805
  console.log(`Skipping ${fileName}: invalid or empty PID`);
@@ -541,7 +541,7 @@ function reconcileAndAppendToAgentMemory(item, knownAgents, config) {
541
541
  try { safeWrite(memPath + '.bak', beforeLock); }
542
542
  catch (err) { log('warn', `agent-memory reconcile: backup write failed for ${agent}: ${err.message}`); }
543
543
 
544
- const next = pruneAgentMemoryToBudget(updated + entry, agent);
544
+ const next = pruneAgentMemoryToBudget(updated + entry, agent, _pruneOptsFromConfig(config));
545
545
  safeWrite(memPath, next);
546
546
  reconciled = true;
547
547
  const skippedCount = skipped.length;
@@ -469,7 +469,7 @@ function archivePlan(planFile, plan, projects, config) {
469
469
  const mdFiles = fs.readdirSync(PLANS_DIR).filter(f => f.endsWith('.md'));
470
470
  for (const md of mdFiles) {
471
471
  const mdContent = shared.safeRead(path.join(PLANS_DIR, md)) || '';
472
- if (mdContent.includes(projectName) || mdContent.includes(plan.plan_summary?.slice(0, 40) || '___nomatch___')) {
472
+ if ((projectName && mdContent.includes(projectName)) || mdContent.includes(plan.plan_summary?.slice(0, 40) || '___nomatch___')) {
473
473
  try {
474
474
  const mdDest = shared.moveFileNoClobber(path.join(PLANS_DIR, md), planArchiveDir, md);
475
475
  log('info', `Archived source plan: plans/archive/${path.basename(mdDest)}`);
@@ -126,6 +126,24 @@ function updateRunStage(pipelineId, runId, stageId, updates) {
126
126
  });
127
127
  }
128
128
 
129
+ // Like updateRunStage, but creates the stage entry when the run never seeded it.
130
+ // startRun only seeds top-level `pipeline.stages` ids; nested parallel sub-stages
131
+ // live under `stage.stages`, so the run's `stages` map has no slot for them until
132
+ // the parallel executor persists one. updateRunStage no-ops on a missing entry, so
133
+ // the parallel path needs this upsert to make sub-stage state survive a disk
134
+ // round-trip (without it the cross-tick PARALLEL completion check reads each sub
135
+ // id as undefined forever and the parent wedges in RUNNING — P-9c1a4e7b).
136
+ function upsertRunStage(pipelineId, runId, stageId, state) {
137
+ mutatePipelineRuns((data) => {
138
+ const runs = data[pipelineId] || [];
139
+ const run = runs.find(r => r.runId === runId);
140
+ if (!run) return data;
141
+ if (!run.stages || typeof run.stages !== 'object') run.stages = {};
142
+ run.stages[stageId] = { ...(run.stages[stageId] || {}), ...state };
143
+ return data;
144
+ });
145
+ }
146
+
129
147
  function completeRun(pipelineId, runId, status) {
130
148
  // Stages whose status is in this set are considered "terminal" and must not
131
149
  // be touched by the run-close sweep. Anything else (RUNNING, PENDING,
@@ -829,12 +847,17 @@ function executeConditionStage(stage, stageState, run, pipeline, config) {
829
847
 
830
848
  async function executeParallelStage(stage, stageState, run, pipeline, config) {
831
849
  const subStages = stage.stages || [];
832
- const subResults = {};
833
850
  for (const sub of subStages) {
834
851
  if (!run.stages[sub.id] || run.stages[sub.id].status === PIPELINE_STATUS.PENDING) {
835
852
  const result = await executeStage(sub, run, pipeline, config);
836
- subResults[sub.id] = result;
837
- run.stages[sub.id] = { ...run.stages[sub.id] || {}, ...result, startedAt: ts() };
853
+ const subState = { ...(run.stages[sub.id] || {}), ...result, startedAt: ts() };
854
+ run.stages[sub.id] = subState;
855
+ // Persist sub-stage state to disk. getActiveRun re-reads the run fresh from
856
+ // disk every tick, so a sub-stage mutated only on the in-memory run object is
857
+ // invisible to the next tick's PARALLEL completion check — the parent then
858
+ // wedges in RUNNING forever. updateRunStage only mutates an existing entry and
859
+ // sub-stage ids are never seeded by startRun, so upsert to seed + persist.
860
+ upsertRunStage(pipeline.id, run.runId, sub.id, subState);
838
861
  }
839
862
  }
840
863
  // Parent is running until all subs complete
@@ -966,9 +989,29 @@ function isStageComplete(stage, stageState, run, config) {
966
989
  return stageState.status === PIPELINE_STATUS.COMPLETED;
967
990
  case STAGE_TYPE.PARALLEL: {
968
991
  const subIds = artifacts.subStages || [];
992
+ if (subIds.length === 0) return true;
993
+ const subDefs = stage.stages || [];
969
994
  return subIds.every(id => {
970
- const sub = run.stages[id];
971
- return sub && (sub.status === PIPELINE_STATUS.COMPLETED || sub.status === PIPELINE_STATUS.FAILED);
995
+ const subState = run.stages[id];
996
+ if (!subState) return false; // sub-stage state not yet persisted not complete
997
+ if (subState.status === PIPELINE_STATUS.COMPLETED || subState.status === PIPELINE_STATUS.FAILED) return true;
998
+ // Async sub-stages (task/meeting/plan/…) persist as RUNNING; their underlying
999
+ // work (work items, meetings, …) finishes on a later tick. The driver never
1000
+ // re-executes a RUNNING parallel parent, so this completion check is the only
1001
+ // place that advances them: evaluate each sub through its own completion logic
1002
+ // and promote its persisted status so the run closes cleanly (no false
1003
+ // non-terminal anomaly) and the dashboard reflects reality (P-9c1a4e7b).
1004
+ const subDef = subDefs.find(s => s.id === id);
1005
+ if (!subDef) return false;
1006
+ if (isStageComplete(subDef, subState, run, config)) {
1007
+ subState.status = PIPELINE_STATUS.COMPLETED;
1008
+ subState.completedAt = subState.completedAt || ts();
1009
+ updateRunStage(run.pipelineId, run.runId, id, {
1010
+ status: PIPELINE_STATUS.COMPLETED, completedAt: subState.completedAt,
1011
+ });
1012
+ return true;
1013
+ }
1014
+ return false;
972
1015
  });
973
1016
  }
974
1017
  default:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2216",
3
+ "version": "0.1.2217",
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"