@yemi33/minions 0.1.2314 → 0.1.2316

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.
@@ -12,6 +12,7 @@ The engine runs a tick every 10 seconds (configurable via `config.json` → `eng
12
12
  tick()
13
13
  1. checkTimeouts() Enforce runtime limits and stale-orphan cleanup
14
14
  1a. checkSteering() Drain steering messages queued by the dashboard
15
+ 1a2 checkSpawnPhaseStalls() Watchdog for agents stuck before their first heartbeat
15
16
  1b. checkIdleThreshold() Notify on excessive agent idleness
16
17
  1c. meetingTimeouts() Advance round-based meetings whose timer fired
17
18
  2. consolidateInbox() Merge learnings into notes.md (Haiku-powered)
@@ -31,6 +32,8 @@ tick()
31
32
  3a. pruneStalePrDispatches() Clear pending PR dispatches whose underlying PRs no longer warrant action
32
33
  3. discoverWork() Scan ALL linked projects for new tasks
33
34
  4. updateSnapshot() Write identity/now.md
35
+ 4a. memoryBaseline() Sample RSS/heap/GC every memoryBaselineEveryTicks ticks (default 6 ≈ 60s)
36
+ 4b. heapSnapshotRequest() Serve a pending guard-token heap-snapshot request, if any
34
37
  5. dispatch Spawn agents for pending items (up to maxConcurrent)
35
38
  ```
36
39
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Author: Rebecca (Architect) | Date: 2026-04-07 | Status: **Accepted — implementation in progress**
4
4
 
5
- > **Implementation status (as of 2026-06):** The `node:sqlite` recommendation in §3 has been adopted ahead of schedule. Phases 0–10 have shipped (events, dispatches, work_items, pull_requests, logs, metrics, watches, schedule_runs + pipeline_runs + managed_processes + worktree_pool, qa_runs + qa_sessions, pr_links, cooldowns + pending_rebases + cc_sessions + doc_sessions, and steering_deliveries (Phases 0–9); plans + prds + prd_items + prd_verify_prs (Phase 10, migration `015-plans-prds.js`) — see `CHANGELOG.md` and `engine/db/migrations/`). The SQLite schema lives under `engine/db/migrations/` and the singleton opens `engine/state.db` in WAL mode. Phase 8 added the first opt-out toggle for the JSON sidecars (`engine.qaDualWriteJson`, default true). Phase 9.4 went further and deleted the silent SQL-unavailable JSON fallbacks in the engine — SQL is now the only reader/writer for everything migrated; the JSON mirror layer is dual-written as a passive mirror and slated for deletion in Phase 9.5. Phase 10 dual-writes PRD state to SQL (`engine/prd-store.js`) and flips reads to SQL when the `prdReadsFromSql` feature flag is ON (default ON reversible). The "Phase 2: estimated Node 26 LTS" timeline in §3 is now historical context; treat sections 1–3 as design rationale rather than a forward plan.
5
+ > **Implementation status (as of 2026-06):** The `node:sqlite` recommendation in §3 has been adopted ahead of schedule. Phases 0–10 have shipped (events, dispatches, work_items, pull_requests, logs, metrics, watches, schedule_runs + pipeline_runs + managed_processes + worktree_pool, qa_runs + qa_sessions, pr_links, cooldowns + pending_rebases + cc_sessions + doc_sessions, and steering_deliveries (Phases 0–9); plans + prds + prd_items + prd_verify_prs (Phase 10, migration `015-plans-prds.js`) — see `CHANGELOG.md` and `engine/db/migrations/`). The SQLite schema lives under `engine/db/migrations/` and the singleton opens `engine/state.db` in WAL mode. Phase 8 added the first opt-out toggle for the JSON sidecars (`engine.qaDualWriteJson`, default true). Phase 9.4 went further and deleted the silent SQL-unavailable JSON fallbacks in the engine — SQL is now the only reader/writer for everything migrated; the JSON mirror layer is dual-written as a passive mirror and slated for deletion in Phase 9.5. Phase 10 dual-writes PRD state to SQL (`engine/prd-store.js`); the `prdReadsFromSql` read-flip flag was removed after soaking (default had been ON since 0.1.2090) — SQL is now the unconditional PRD read path in `engine/queries.js`, with SQL errors propagating instead of silently falling back to a disk scan. Phase 10 step 4.3 added a second, still-flagged toggle, `prdJoinFromFk`: it resolves the dashboard WI↔PRD-item join via the SQL FK (`work_items.prd_item_id` → `prd_items.id`) instead of the legacy feature-id string match, with a fallback to the string match when the flag is OFF or the FK is unstamped/NULL (default ON, reversible, expires 2026-12-01). The "Phase 2: estimated Node 26 LTS" timeline in §3 is now historical context; treat sections 1–3 as design rationale rather than a forward plan.
6
6
 
7
7
  ## Executive Summary
8
8
 
@@ -81,6 +81,10 @@ A paused cause **suppresses further auto-dispatch for that cause** until cleared
81
81
 
82
82
  The exported `recordPrNoOpFixAttempt` / `clearPrNoOpFixAttempt` / `sweepStalePrNoOpFixes` helpers in `engine/lifecycle.js` are the only sanctioned entry points. The dispatch evaluator gates re-dispatch on `shared.isPrNoOpFixCausePaused(pr, cause)` (which re-validates the fingerprint per call), the dashboard chip surface uses `shared.getPrPausedCauses(pr)` (same gate), and the `pollPrStatus` callback in `engine/ado.js` + `engine/github.js` calls `sweepStalePrNoOpFixes(pr)` each tick to GC records whose fingerprint no longer matches the live PR.
83
83
 
84
+ #### E2. Build-fix-ineffective tracker (`pr._buildFixIneffective`, issue #639)
85
+
86
+ A "branch unchanged" no-op report for a `BUILD_FAILURE`-cause fix used to be conflated with the generic `_noOpFixes[BUILD_FAILURE]` counter even when the build was still genuinely red on the unchanged head — silently masking a stuck failure as "handled". `checkBuildStillFailingLive()` re-polls host-specific live CI (`checkLiveBuildAndConflict` in `engine/github.js` / `engine/ado.js`) right after `detectPrFixBranchChange()` reports no branch change for a build-failure fix. If the build is still failing, `recordBuildFixIneffective()` tracks a **separate**, `headSha`-keyed counter (`pr._buildFixIneffective`) instead of bumping `_noOpFixes[BUILD_FAILURE]`, pausing after `prNoOpFixPauseAttempts` on the same head and writing a distinct `build-fix-ineffective-<pr>` inbox alert; `shared.isBuildFixIneffectivePaused()` re-validates the stored `headSha` against the live PR before honoring the pause (same fingerprint-revalidation pattern as `isPrNoOpFixCausePaused`), and `engine.js` wires `isBuildFixIneffectiveSuppressed()` into the `BUILD_FAILURE` dispatch gate alongside the existing no-op check. A real branch-changing fix clears the stale record. The dashboard PR list (`dashboard/js/render-prs.js`) renders a separate, non-interactive "infra issue suspected" chip for a paused `_buildFixIneffective` record (tooltip shows `reason`/`count`/`headSha`/`liveBuildStatus`) alongside the existing `_pausedCauses` chips.
87
+
84
88
  ## 5. Fix completes
85
89
 
86
90
  - `updatePrAfterFix()` (lifecycle.js) sets `reviewStatus = 'waiting'` + `fixedAt = ts()`
@@ -163,3 +167,4 @@ The exported `recordPrNoOpFixAttempt` / `clearPrNoOpFixAttempt` / `sweepStalePrN
163
167
  - Builds use the merge commit hash as `sourceVersion`, not the source branch commit — compare against `lastMergeCommit.commitId`
164
168
  - `partiallySucceeded` counts as passing (warnings, not failures)
165
169
  - A stale but passing build is still valid — don't re-trigger builds that already passed
170
+ - ADO never auto-clears a reviewer's numeric vote when new commits land, so a hard-reject `-10` vote stays live forever unless the reviewer explicitly re-votes. Both `pollPrStatus()` and the pre-dispatch `checkLiveReviewStatus()` treat a `-10` as **stale/superseded** (resolving to `waiting` instead of `changes-requested`) via `isStaleAdoRejectVote()` once a fix has completed after the vote's last review (`fixedAt > lastReviewedAt`) with no push since (`lastPushedAt <= fixedAt`) — otherwise the auto re-review-after-fix flow can never fire for that PR (issue #633).
@@ -48,6 +48,8 @@ Filenames follow the `YYYY-MM-DD-<agent>-<slug>.md` convention so chronological
48
48
 
49
49
  Agents read `knowledge/` directly with grep/glob/view. They **do not** write to it. See [Sweep-write-only constraint](#sweep-write-only-constraint) below.
50
50
 
51
+ **Full-body copies vs. condensed stubs.** When `engine/consolidation.js#classifyToKnowledgeBase` promotes an inbox note into `knowledge/`, it only copies the full body when the note is routed to `conventions/` or its content carries a reusable-knowledge section header (`## Patterns`, `## Conventions`, `## Gotchas`, `## Dependencies`, `## Best Practices`, etc.). Narrowly-scoped, one-off notes (a single PR review, a task status update) instead get a deterministic, LLM-free condensed stub — title + first-paragraph abstract + a link back to the archived inbox note — to avoid duplicating disk usage and prompt-token cost on every future dispatch. Every promoted entry's frontmatter gains a `reusable: true|false` field so this can be audited (issue #604).
52
+
51
53
  ### 3. `notes.md` — consolidated team notes
52
54
 
53
55
  `notes.md` is the rolling team notebook. The consolidation sweep periodically merges high-signal findings from the inbox into dated `### YYYY-MM-DD` sections at the top of `notes.md`, with links back to the underlying KB entries.
package/docs/watches.md CHANGED
@@ -169,7 +169,7 @@ I/O happens **outside the lock**: notifications via `writeToInbox`, follow-up ac
169
169
  | `dispatch-work-item` | Append a new WI to the project (or central) `work-items.json` with `createdBy: "watch:<id>"` |
170
170
  | `run-skill` | Wrapper around `dispatch-work-item` that asks the agent to run a specific `.claude` skill |
171
171
  | `webhook` | `http`/`https` request to an arbitrary URL (10s safety timeout, JSON or string body) |
172
- | `minions-api` | Loopback call to the in-process dashboard at `http://127.0.0.1:${MINIONS_PORT \|\| 7331}` — `endpoint` must start with `/api/`; sets `X-Minions-Internal: 1`; returns `{ok, status, summary, response}` for chain-step templating |
172
+ | `minions-api` | Loopback call to the in-process dashboard at `http://127.0.0.1:${shared.readDashboardPortFile(MINIONS_DIR)?.port \|\| 7331}` (reads the bound-port beacon file, not the `MINIONS_PORT` env, so it still resolves after an `EADDRINUSE` port scan) — `endpoint` must start with `/api/`; sets `X-Minions-Internal: 1`; returns `{ok, status, summary, response}` for chain-step templating |
173
173
  | `cancel-work-item` | Flip a WI to `WI_STATUS.CANCELLED` across all known work-items files |
174
174
  | `trigger-pipeline` | Start a new pipeline run (skipped if the pipeline already has an active run) |
175
175
  | `archive-plan` | Set PRD `status="archived"` + `archivedAt` |
package/engine/cleanup.js CHANGED
@@ -740,10 +740,22 @@ async function runCleanup(config, verbose = false) {
740
740
  shared._writeWorktreeSkipLiveInboxNote(full, 'cleanup.orphanWorktreeDirSweep', blockingInfo);
741
741
  continue;
742
742
  }
743
+ // W-mr2zu0hb000f20c7 — route through shared.removeWorktree instead of a
744
+ // raw fs.rmSync so this path gets the same retry-with-backoff (transient
745
+ // Windows EBUSY/locked-file handling) and real-repo-refusal safety net
746
+ // that every other worktree removal in this file uses. This dir already
747
+ // isn't registered in `git worktree list`, so the primary
748
+ // `git worktree remove --force` inside removeWorktree is expected to
749
+ // fail and fall through to its fs.rmSync + retry logic — that's fine,
750
+ // the safety checks (ownership marker already verified above; real-repo
751
+ // and live-dispatch refusals re-checked inside removeWorktree) still apply.
743
752
  try {
744
- fs.rmSync(full, { recursive: true, force: true });
745
- cleaned.orphanWorktreeDirs++;
746
- log('info', `Cleanup: removed orphan worktree dir ${full} (not registered in git)`);
753
+ if (shared.removeWorktree(full, root, wtRoot)) {
754
+ cleaned.orphanWorktreeDirs++;
755
+ log('info', `Cleanup: removed orphan worktree dir ${full} (not registered in git)`);
756
+ } else {
757
+ log('warn', `orphan worktree dir ${full}: removeWorktree declined (see prior log line for reason)`);
758
+ }
747
759
  } catch (err) {
748
760
  log('warn', `orphan worktree dir ${full}: ${err.message}`);
749
761
  }
package/engine/meeting.js CHANGED
@@ -231,18 +231,14 @@ function resolveMeetingContributionContent(output, structuredCompletion) {
231
231
  return resolveStructuredMeetingContent(structuredCompletion);
232
232
  }
233
233
 
234
- function truncateMeetingContext(text, maxBytes, label) {
235
- return shared.truncateTextBytes(text, maxBytes, `\n\n_...${label} truncated — review the meeting transcript if needed._`);
236
- }
237
-
238
234
  function formatMeetingContributions(entries, agents, emptyText, label, maxBytes) {
239
235
  const pairs = Object.entries(typeof entries === 'object' && entries ? entries : {});
240
236
  if (pairs.length === 0) return emptyText;
241
237
  const perEntryBytes = Math.max(1024, Math.floor(maxBytes / Math.max(1, pairs.length)));
242
238
  const combined = pairs.map(([agent, value]) =>
243
- `### ${agents[agent]?.name || agent}\n\n${truncateMeetingContext(value?.content || emptyText, perEntryBytes, `${label} entry`)}`
239
+ `### ${agents[agent]?.name || agent}\n\n${shared.truncateTextBytes(value?.content || emptyText, perEntryBytes, `\n\n_...${label} entry truncated — review the meeting transcript if needed._`)}`
244
240
  ).join('\n\n---\n\n');
245
- return truncateMeetingContext(combined, maxBytes, label);
241
+ return shared.truncateTextBytes(combined, maxBytes, `\n\n_...${label} truncated — review the meeting transcript if needed._`);
246
242
  }
247
243
 
248
244
  function stripMeetingSummaryMarkdown(text) {
@@ -542,10 +538,10 @@ function discoverMeetingWork(config) {
542
538
  const key = `${concludePrefix}${concluder}`;
543
539
  if (activeKeys.has(key)) continue;
544
540
 
545
- const humanNotes = truncateMeetingContext(
541
+ const humanNotes = shared.truncateTextBytes(
546
542
  (Array.isArray(meeting.humanNotes) ? meeting.humanNotes : []).map(n => '- ' + n).join('\n'),
547
543
  ENGINE_DEFAULTS.maxMeetingHumanNotesBytes,
548
- 'human meeting notes'
544
+ '\n\n_...human meeting notes truncated — review the meeting transcript if needed._'
549
545
  );
550
546
  const allFindings = formatMeetingContributions(
551
547
  meeting.findings,
@@ -605,10 +601,10 @@ function discoverMeetingWork(config) {
605
601
  const key = `meeting-${meeting.id}-r${round}-${agentId}`;
606
602
  if (activeKeys.has(key)) continue;
607
603
 
608
- const humanNotes = truncateMeetingContext(
604
+ const humanNotes = shared.truncateTextBytes(
609
605
  (meeting.humanNotes || []).map(n => '- ' + n).join('\n'),
610
606
  ENGINE_DEFAULTS.maxMeetingHumanNotesBytes,
611
- 'human meeting notes'
607
+ '\n\n_...human meeting notes truncated — review the meeting transcript if needed._'
612
608
  );
613
609
  const vars = {
614
610
  agent_name: agents[agentId]?.name || agentId,
@@ -26,10 +26,6 @@ const NOTES_INBOX_DIR = path.join(MINIONS_DIR, 'notes', 'inbox');
26
26
  const NOTES_ARCHIVE_DIR = path.join(MINIONS_DIR, 'notes', 'archive');
27
27
  const CONFIG_PATH = path.join(MINIONS_DIR, 'config.json');
28
28
 
29
- function truncatePipelineContext(text, maxBytes, label) {
30
- return shared.truncateTextBytes(text, maxBytes, `\n\n_...${label} truncated — inspect the upstream artifacts if needed._`);
31
- }
32
-
33
29
  // ── Pipeline CRUD ────────────────────────────────────────────────────────────
34
30
 
35
31
  function getPipelines() {
@@ -619,10 +615,10 @@ async function executePlanStage(stage, stageState, run, config, pipeline = {}) {
619
615
  try {
620
616
  const mtg = safeJson(path.join(MEETINGS_DIR, mid + '.json'));
621
617
  if (mtg) {
622
- const transcript = truncatePipelineContext(
618
+ const transcript = shared.truncateTextBytes(
623
619
  (mtg.transcript || []).map(formatTranscriptEntry).join('\n\n---\n\n'),
624
620
  ENGINE_DEFAULTS.maxPipelineMeetingContextBytes,
625
- `meeting transcript for ${mtg.title || mid}`
621
+ `\n\n_...meeting transcript for ${mtg.title || mid} truncated — inspect the upstream artifacts if needed._`
626
622
  );
627
623
  meetingContext += '# Meeting: ' + (mtg.title || mid) + '\n\n**Agenda:** ' + (mtg.agenda || '') + '\n\n' + transcript + '\n\n';
628
624
  }
@@ -633,15 +629,15 @@ async function executePlanStage(stage, stageState, run, config, pipeline = {}) {
633
629
  for (const depId of stage.dependsOn) {
634
630
  const depStage = run.stages[depId];
635
631
  if (depStage?.output && !depStage.artifacts?.meetings?.length) {
636
- meetingContext += '## From: ' + depId + '\n\n' + truncatePipelineContext(
632
+ meetingContext += '## From: ' + depId + '\n\n' + shared.truncateTextBytes(
637
633
  depStage.output,
638
634
  ENGINE_DEFAULTS.maxPipelineMeetingContextBytes,
639
- `pipeline stage output from ${depId}`
635
+ `\n\n_...pipeline stage output from ${depId} truncated — inspect the upstream artifacts if needed._`
640
636
  ) + '\n\n';
641
637
  }
642
638
  }
643
639
  }
644
- meetingContext = truncatePipelineContext(meetingContext, ENGINE_DEFAULTS.maxPipelineMeetingContextBytes, 'pipeline meeting context');
640
+ meetingContext = shared.truncateTextBytes(meetingContext, ENGINE_DEFAULTS.maxPipelineMeetingContextBytes, '\n\n_...pipeline meeting context truncated — inspect the upstream artifacts if needed._');
645
641
 
646
642
  // Use LLM to generate a structured plan (same approach as dashboard "Create Plan from Meeting" button)
647
643
  let content = '';
@@ -1259,7 +1255,7 @@ module.exports = {
1259
1255
  getPipelineRuns, getActiveRun, startRun, updateRunStage, upsertRunStage, completeRun,
1260
1256
  discoverPipelineWork,
1261
1257
  evaluateCondition, // exported for testing
1262
- truncatePipelineContext, executeTaskStage, executePlanStage, executeScheduleStage, executeApiStage, executeMeetingStage, executeMergePrsStage, isStageComplete, resolveTemplate, // exported for testing
1258
+ executeTaskStage, executePlanStage, executeScheduleStage, executeApiStage, executeMeetingStage, executeMergePrsStage, isStageComplete, resolveTemplate, // exported for testing
1263
1259
  _resolvePipelineProjects, // exported for testing
1264
1260
  _findMeetingsInRun, _findExistingPlanForMeeting, _findExistingPrdForPlan, _canonicalPlanName, // exported for testing
1265
1261
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2314",
3
+ "version": "0.1.2316",
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"