@yemi33/minions 0.1.2174 → 0.1.2175

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.
Files changed (2) hide show
  1. package/engine/watches.js +72 -1
  2. package/package.json +1 -1
package/engine/watches.js CHANGED
@@ -150,7 +150,14 @@ function registerTargetType(type, spec) {
150
150
  throw new Error(`registerTargetType(${type}): absoluteConditions entry '${c}' is not in conditions[]`);
151
151
  }
152
152
  }
153
- TARGET_TYPES[type] = { ...spec, absoluteConditions: absoluteSet };
153
+ // W-mqa63opd000ha836 optional hook: returns true when the target has
154
+ // reached a state from which `condition` can never fire again (e.g. a
155
+ // PR's status is `merged` and we're watching `build-fail`). Default
156
+ // returns false (back-compat — current target types unchanged).
157
+ const terminalFn = typeof spec.isTerminalForCondition === 'function'
158
+ ? spec.isTerminalForCondition
159
+ : () => false;
160
+ TARGET_TYPES[type] = { ...spec, absoluteConditions: absoluteSet, isTerminalForCondition: terminalFn };
154
161
  }
155
162
 
156
163
  /** Returns the registered spec for a target type, or null. */
@@ -517,6 +524,40 @@ function checkWatches(config, state) {
517
524
  });
518
525
  }
519
526
 
527
+ // W-mqa63opd000ha836 — auto-expire watches whose target has reached a
528
+ // terminal state from which the watched condition can never fire again.
529
+ // Independent of the absolute-condition fire-once path above: covers the
530
+ // long-tail case where a `build-fail` (or similar) watch was armed on an
531
+ // active PR that subsequently merged without ever tripping its condition.
532
+ // Without this, the watch sits `active` forever, polling every interval.
533
+ //
534
+ // Guardrail: do NOT auto-expire until the watch has had a chance to fire
535
+ // on its first real check — `triggerCount > 0` covers post-fire watches;
536
+ // `prevState.status === entity.status` covers second-and-later checks
537
+ // (initial _captureState on tick-1 makes these equal, so a watch armed
538
+ // on an already-terminal target with `condition: merged` still fires
539
+ // once via the absolute-condition path before this branch expires it).
540
+ if (watch.status === WATCH_STATUS.ACTIVE) {
541
+ const _ttForTerm = TARGET_TYPES[watch.targetType];
542
+ if (_ttForTerm && typeof _ttForTerm.isTerminalForCondition === 'function') {
543
+ try {
544
+ const _entityForTerm = _ttForTerm.fetchEntity(watch.target, state || {});
545
+ if (_entityForTerm) {
546
+ const _hadShot = (watch.triggerCount || 0) > 0
547
+ || (previousState && previousState.status !== undefined
548
+ && previousState.status === _entityForTerm.status);
549
+ if (_hadShot && _ttForTerm.isTerminalForCondition(watch.condition, _entityForTerm, previousState)) {
550
+ watch.status = WATCH_STATUS.EXPIRED;
551
+ const _termStatus = _entityForTerm.status;
552
+ log('info', `Watch auto-expired (terminal target state): ${watch.id} — ${watch.targetType} ${watch.target} is ${_termStatus}, condition ${watch.condition} cannot fire again`);
553
+ }
554
+ }
555
+ } catch (termErr) {
556
+ log('warn', `Watch terminal-state check error (${watch.id}): ${termErr.message}`);
557
+ }
558
+ }
559
+ }
560
+
520
561
  // Capture state for change detection on next check
521
562
  watch._lastState = _captureState(watch, state);
522
563
 
@@ -704,6 +745,36 @@ registerTargetType(WATCH_TARGET_TYPE.PR, {
704
745
  WATCH_CONDITION.MERGED, WATCH_CONDITION.BUILD_FAIL, WATCH_CONDITION.BUILD_PASS,
705
746
  WATCH_CONDITION.READY_FOR_MERGE,
706
747
  ],
748
+ // W-mqa63opd000ha836 — zombie-watch janitor: once a PR reaches a terminal
749
+ // status (merged / closed / abandoned), most PR conditions can never fire
750
+ // again so the watch should auto-expire instead of polling forever.
751
+ //
752
+ // Excluded from terminal-expire:
753
+ // - merged — already handled by the absoluteConditions fire-once
754
+ // path; including it here is redundant and risks
755
+ // expiring before the watch's one shot to fire.
756
+ // - status-change — could theoretically still fire on a status flip
757
+ // (e.g. closed→reopened on GitHub), even though rare.
758
+ // - any — same rationale as status-change.
759
+ //
760
+ // Build-fail / build-pass / vote-change / new-comments / head-commit-change /
761
+ // mergeable-flipped / behind-master / ready-for-merge / draft-flipped all
762
+ // mutate fields that are frozen the moment the PR is merged/closed/abandoned,
763
+ // so a fresh fire is impossible.
764
+ isTerminalForCondition: (condition, pr) => {
765
+ if (!pr || !pr.status) return false;
766
+ const status = pr.status;
767
+ const isTerminal = status === shared.PR_STATUS.MERGED
768
+ || status === shared.PR_STATUS.CLOSED
769
+ || status === shared.PR_STATUS.ABANDONED;
770
+ if (!isTerminal) return false;
771
+ if (condition === WATCH_CONDITION.MERGED
772
+ || condition === WATCH_CONDITION.STATUS_CHANGE
773
+ || condition === WATCH_CONDITION.ANY) {
774
+ return false;
775
+ }
776
+ return true;
777
+ },
707
778
  fetchEntity: (target, state) => findPrByTarget(state.pullRequests, target),
708
779
  captureState: (pr) => ({
709
780
  status: pr.status, buildStatus: pr.buildStatus, reviewStatus: pr.reviewStatus,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2174",
3
+ "version": "0.1.2175",
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"