@yemi33/minions 0.1.2285 → 0.1.2286

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/dashboard.js +95 -60
  2. package/package.json +1 -1
package/dashboard.js CHANGED
@@ -8022,77 +8022,103 @@ const server = http.createServer(async (req, res) => {
8022
8022
  } catch (e) { return jsonReply(res, 400, { error: e.message }); }
8023
8023
  }
8024
8024
 
8025
+ // Shared by pause + reject (and any future PRD-stop handler): stop a PRD's
8026
+ // materialized work. Kills any active dispatch for the PRD's items, transitions
8027
+ // every non-completed WI sourced from the PRD to `targetStatus`, AND cancels the
8028
+ // still-running plan-to-prd regeneration WI for the PRD's source plan so it can't
8029
+ // silently rebuild the PRD we just paused/rejected. Returns the count of WIs
8030
+ // transitioned. Lockless reads → cleanDispatchEntries (atomic kill + remove) →
8031
+ // mutateWorkItems, mirroring the original pause flow. (RC3 — reject had no
8032
+ // cleanup; pause never stopped regeneration.)
8033
+ function stopPlanMaterializedWork(prdFile, prdSourcePlan, opts) {
8034
+ const targetStatus = opts.targetStatus;
8035
+ const wiPaths = [path.join(MINIONS_DIR, 'work-items.json')];
8036
+ for (const proj of PROJECTS) wiPaths.push(shared.projectWorkItemsPath(proj));
8037
+
8038
+ // Step 1: find dispatched item ids (read-only, no lock).
8039
+ const dispatchedItemIds = new Set();
8040
+ for (const wiPath of wiPaths) {
8041
+ try {
8042
+ for (const w of safeJsonArr(wiPath)) {
8043
+ if (w.sourcePlan !== prdFile) continue;
8044
+ if (w.completedAt || DONE_STATUSES.has(w.status)) continue;
8045
+ if (w.status === WI_STATUS.DISPATCHED && w.id) dispatchedItemIds.add(w.id);
8046
+ }
8047
+ } catch { /* file may not exist */ }
8048
+ }
8049
+
8050
+ // Step 2: kill active dispatches via the canonical primitive (resolves PIDs from
8051
+ // the pid sidecar, kills outside the dispatch lock, removes via mutateDispatch).
8052
+ if (dispatchedItemIds.size > 0) {
8053
+ cleanDispatchEntries((d) => {
8054
+ const itemId = d.meta?.item?.id;
8055
+ if (itemId && dispatchedItemIds.has(itemId)) return true;
8056
+ if (d.meta?.dispatchKey && [...dispatchedItemIds].some(id => d.meta.dispatchKey.includes(id))) return true;
8057
+ return false;
8058
+ });
8059
+ }
8060
+
8061
+ // Step 3: transition WIs per path (each lock held briefly, no nesting).
8062
+ let affected = 0;
8063
+ for (const wiPath of wiPaths) {
8064
+ try {
8065
+ mutateWorkItems(wiPath, items => {
8066
+ for (const w of items) {
8067
+ if (w.sourcePlan !== prdFile) continue;
8068
+ if (w.completedAt || DONE_STATUSES.has(w.status)) continue;
8069
+ if (w.status !== targetStatus) affected++;
8070
+ w.status = targetStatus;
8071
+ if (opts.stampField) w[opts.stampField] = opts.stampValue;
8072
+ delete w._resumedAt;
8073
+ delete w.dispatched_at;
8074
+ delete w.dispatched_to;
8075
+ delete w.failReason;
8076
+ delete w.failedAt;
8077
+ }
8078
+ });
8079
+ } catch (e) { console.error('stopPlanMaterializedWork work items:', e.message); }
8080
+ }
8081
+
8082
+ // Step 4: cancel the still-running plan-to-prd regeneration WI for this source
8083
+ // plan — otherwise a pending/dispatched plan-to-prd run rebuilds the PRD we just
8084
+ // stopped. (delete handles the DONE plan-to-prd WI separately to revert to draft.)
8085
+ if (prdSourcePlan) {
8086
+ try {
8087
+ const centralPath = path.join(MINIONS_DIR, 'work-items.json');
8088
+ mutateWorkItems(centralPath, items => {
8089
+ for (const w of items) {
8090
+ if (w.type === WORK_TYPE.PLAN_TO_PRD && w.planFile === prdSourcePlan &&
8091
+ !DONE_STATUSES.has(w.status) && w.status !== WI_STATUS.CANCELLED) {
8092
+ w.status = WI_STATUS.CANCELLED;
8093
+ w._cancelledBy = opts.stampValue || 'prd-stopped';
8094
+ }
8095
+ }
8096
+ });
8097
+ } catch (e) { console.error('stopPlanMaterializedWork plan-to-prd:', e.message); }
8098
+ }
8099
+ return affected;
8100
+ }
8101
+
8025
8102
  async function handlePlansPause(req, res) {
8026
8103
  try {
8027
8104
  const body = await readBody(req);
8028
8105
  if (!body.file) return jsonReply(res, 400, { error: 'file required' });
8029
8106
  if (!body.file.endsWith('.json')) return jsonReply(res, 400, { error: 'expected a PRD JSON filename (got `' + body.file + '`). Pass prd/<plan>.json, not the source plans/<plan>.md.' });
8030
8107
  const planPath = resolvePlanPath(body.file);
8031
- mutateJsonFileLocked(planPath, (plan) => {
8108
+ let prdSourcePlan = null;
8109
+ const updated = mutateJsonFileLocked(planPath, (plan) => {
8032
8110
  if (!plan || Array.isArray(plan) || typeof plan !== 'object') plan = {};
8033
8111
  plan.status = 'paused';
8034
8112
  plan.pausedAt = new Date().toISOString();
8035
8113
  return plan;
8036
8114
  }, { defaultValue: {} });
8115
+ prdSourcePlan = updated?.source_plan || null;
8037
8116
 
8038
- // Propagate pause to materialized work items across all projects:
8039
- // kill any active agent process and reset non-completed items to paused.
8040
- // Pattern: lockless reads → cleanDispatchEntries (atomic kill + remove) → mutateWorkItems.
8041
- const wiPaths = [path.join(MINIONS_DIR, 'work-items.json')];
8042
- for (const proj of PROJECTS) {
8043
- wiPaths.push(shared.projectWorkItemsPath(proj));
8044
- }
8045
- const dispatchPath = path.join(MINIONS_DIR, 'engine', 'dispatch.json');
8046
-
8047
- // Step 1: Read work items (read-only, no lock) to find plan items that are dispatched.
8048
- const dispatchedItemIds = new Set();
8049
- for (const wiPath of wiPaths) {
8050
- try {
8051
- const items = safeJsonArr(wiPath);
8052
- for (const w of items) {
8053
- if (w.sourcePlan !== body.file) continue;
8054
- if (w.completedAt || DONE_STATUSES.has(w.status)) continue;
8055
- if (w.status === WI_STATUS.DISPATCHED && w.id) dispatchedItemIds.add(w.id);
8056
- }
8057
- } catch { /* file may not exist */ }
8058
- }
8059
-
8060
- // Step 2: Route PID resolution + kill + dispatch removal through the canonical primitive.
8061
- // cleanDispatchEntries resolves PIDs from engine/tmp/dispatch-<id>-*/pid-<id>.pid
8062
- // (see shared.findDispatchPidFile), kills outside the dispatch lock, and removes the
8063
- // entries via mutateDispatch (SQL + JSON mirror in one atomic write).
8064
- // The defunct per-agent status sidecar reads/writes that used to live here are gone
8065
- // (engine/queries.js documents that file no longer exists).
8066
- if (dispatchedItemIds.size > 0) {
8067
- const matchFn = (d) => {
8068
- const itemId = d.meta?.item?.id;
8069
- if (itemId && dispatchedItemIds.has(itemId)) return true;
8070
- if (d.meta?.dispatchKey && [...dispatchedItemIds].some(id => d.meta.dispatchKey.includes(id))) return true;
8071
- return false;
8072
- };
8073
- cleanDispatchEntries(matchFn);
8074
- }
8075
-
8076
- // Step 3: Mutate work-items.json per path — pause items (each lock held briefly, no nesting).
8077
- let reset = 0;
8078
- for (const wiPath of wiPaths) {
8079
- try {
8080
- mutateWorkItems(wiPath, items => {
8081
- for (const w of items) {
8082
- if (w.sourcePlan !== body.file) continue;
8083
- if (w.completedAt || DONE_STATUSES.has(w.status)) continue;
8084
- if (w.status !== WI_STATUS.PAUSED) reset++;
8085
- w.status = WI_STATUS.PAUSED;
8086
- w._pausedBy = 'prd-pause';
8087
- delete w._resumedAt;
8088
- delete w.dispatched_at;
8089
- delete w.dispatched_to;
8090
- delete w.failReason;
8091
- delete w.failedAt;
8092
- }
8093
- });
8094
- } catch (e) { console.error('reset work items:', e.message); }
8095
- }
8117
+ // Propagate pause to materialized work items across all projects + cancel the
8118
+ // plan-to-prd regeneration WI so a paused PRD can't be silently rebuilt.
8119
+ const reset = stopPlanMaterializedWork(body.file, prdSourcePlan, {
8120
+ targetStatus: WI_STATUS.PAUSED, stampField: '_pausedBy', stampValue: 'prd-pause',
8121
+ });
8096
8122
 
8097
8123
  invalidateStatusCache();
8098
8124
  invalidatePlansCache();
@@ -8149,8 +8175,17 @@ const server = http.createServer(async (req, res) => {
8149
8175
  return data;
8150
8176
  }, { defaultValue: {} });
8151
8177
 
8178
+ // RC3: reject used to flip only the PRD status — its materialized work items
8179
+ // kept dispatching and any active agent kept running, and a pending plan-to-prd
8180
+ // run could rebuild the PRD. Reject is terminal, so cancel the materialized WIs,
8181
+ // kill active dispatches, and cancel the plan-to-prd regeneration WI.
8182
+ const cancelled = stopPlanMaterializedWork(body.file, plan?.source_plan || null, {
8183
+ targetStatus: WI_STATUS.CANCELLED, stampField: '_cancelledBy', stampValue: 'prd-rejected',
8184
+ });
8185
+
8186
+ invalidateStatusCache();
8152
8187
  invalidatePlansCache();
8153
- return jsonReply(res, 200, { ok: true, status: 'rejected' });
8188
+ return jsonReply(res, 200, { ok: true, status: 'rejected', cancelledWorkItems: cancelled });
8154
8189
  } catch (e) { return jsonReply(res, 400, { error: e.message }); }
8155
8190
  }
8156
8191
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2285",
3
+ "version": "0.1.2286",
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"