@yemi33/minions 0.1.2216 → 0.1.2218
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/dashboard.js +30 -0
- package/engine/cli.js +8 -2
- package/engine/consolidation.js +1 -1
- package/engine/lifecycle.js +1 -1
- package/engine/pipeline.js +48 -5
- package/engine/pr-action.js +45 -0
- package/package.json +1 -1
- package/prompts/cc-system.md +9 -0
package/dashboard.js
CHANGED
|
@@ -13033,6 +13033,36 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
13033
13033
|
}
|
|
13034
13034
|
}},
|
|
13035
13035
|
|
|
13036
|
+
{ method: 'POST', path: '/api/pr-action/offer-create-pr', desc: 'Offer a "Create PR" follow-up chip after Command Center makes a LOCAL code edit itself in a configured project (P-cc-createpr). Validates the project has uncommitted working-tree changes (git status --porcelain in project.localPath) and returns a create-pr follow-up chip — same CC-mediated shape as the pr-action chips, so the existing dashboard chip renderer/click handler light up unchanged. Returns {ok, hasChanges, branch, changedFileCount, followups}; followups is [] when there is nothing to PR. READ-ONLY: never stages/commits/pushes — clicking the chip routes a fresh CC turn that does the commit/push/PR-create/link.', params: 'project (configured project name), branch (optional — defaults to the working-tree current branch), contextOnly (optional bool — link the new PR as context-only instead of auto-managed)', handler: async (req, res) => {
|
|
13037
|
+
const body = await readBody(req);
|
|
13038
|
+
try {
|
|
13039
|
+
reloadConfig();
|
|
13040
|
+
const projectName = typeof body?.project === 'string' ? body.project.trim() : '';
|
|
13041
|
+
if (!projectName) return jsonReply(res, 400, { error: 'project is required' }, req);
|
|
13042
|
+
const project = shared.getProjects(CONFIG).find(p => p && p.name === projectName);
|
|
13043
|
+
if (!project) return jsonReply(res, 404, { error: `unknown project: ${projectName}` }, req);
|
|
13044
|
+
if (!project.localPath) return jsonReply(res, 400, { error: `project ${projectName} has no localPath (no working tree to PR)` }, req);
|
|
13045
|
+
let porcelain = '';
|
|
13046
|
+
let currentBranch = '';
|
|
13047
|
+
try {
|
|
13048
|
+
porcelain = await shared.shellSafeGit(['-C', project.localPath, 'status', '--porcelain']);
|
|
13049
|
+
currentBranch = (await shared.shellSafeGit(['-C', project.localPath, 'rev-parse', '--abbrev-ref', 'HEAD'])).trim();
|
|
13050
|
+
} catch (e) {
|
|
13051
|
+
return jsonReply(res, 400, { error: `git status failed for ${projectName}: ${e.message}` }, req);
|
|
13052
|
+
}
|
|
13053
|
+
const changedFileCount = porcelain.split('\n').map(s => s.trim()).filter(Boolean).length;
|
|
13054
|
+
const hasChanges = changedFileCount > 0;
|
|
13055
|
+
const branch = (typeof body?.branch === 'string' && body.branch.trim()) || currentBranch || '';
|
|
13056
|
+
const followups = hasChanges
|
|
13057
|
+
? prAction.buildCreatePrFollowups({ project: projectName, branch, contextOnly: !!body?.contextOnly })
|
|
13058
|
+
: [];
|
|
13059
|
+
recordCcTurnIfPresent(req, { kind: 'pr-action', id: '', title: `offer create-pr (${projectName})`, project: projectName, followups });
|
|
13060
|
+
return jsonReply(res, 200, { ok: true, hasChanges, branch, changedFileCount, followups }, req);
|
|
13061
|
+
} catch (e) {
|
|
13062
|
+
return jsonReply(res, e.statusCode || 400, { error: e.message }, req);
|
|
13063
|
+
}
|
|
13064
|
+
}},
|
|
13065
|
+
|
|
13036
13066
|
{ method: 'POST', path: '/api/pull-requests/observe', desc: 'Toggle canonical contextOnly flag on a tracked PR (public `observe` body param is preserved as the inverse for backward compat)', params: 'host (github|ado), slug, number, observe (boolean)', handler: async (req, res) => {
|
|
13037
13067
|
const body = await readBody(req);
|
|
13038
13068
|
reloadConfig();
|
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
|
-
|
|
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
|
-
|
|
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`);
|
package/engine/consolidation.js
CHANGED
|
@@ -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;
|
package/engine/lifecycle.js
CHANGED
|
@@ -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)}`);
|
package/engine/pipeline.js
CHANGED
|
@@ -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
|
-
|
|
837
|
-
run.stages[sub.id] =
|
|
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
|
|
971
|
-
return
|
|
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/engine/pr-action.js
CHANGED
|
@@ -128,6 +128,50 @@ function buildPrActionFollowups(record, opts = {}) {
|
|
|
128
128
|
}));
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
// ── "Create PR" follow-up (P-cc-createpr) ───────────────────────────────────
|
|
132
|
+
// Offered after Command Center makes a LOCAL code edit itself (not via a
|
|
133
|
+
// dispatched agent), so the user can open a PR for it with one click. Mirrors
|
|
134
|
+
// the PR_ACTION_FOLLOWUPS chip shape ({ kind, label, message }) so the existing
|
|
135
|
+
// dashboard renderer (_ccFollowupChips) + click handler (ccPrActionFollowup)
|
|
136
|
+
// light up unchanged. CC-mediated by the same deliberate design as the
|
|
137
|
+
// pr-action chips: clicking sends a fresh CC turn that commits/pushes/opens the
|
|
138
|
+
// PR and links it to the tracker — no new server-side git path, maximal reuse.
|
|
139
|
+
const CREATE_PR_FOLLOWUP = Object.freeze({ kind: 'create-pr', label: 'Create PR' });
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Build the "Create PR" follow-up chip(s) for a working-tree change CC just
|
|
143
|
+
* made in `project`. Returns `[]` when no project is given (nothing to act on).
|
|
144
|
+
*
|
|
145
|
+
* `branch` — the working-tree branch to commit on; falls back to "a new
|
|
146
|
+
* well-named branch" wording when absent.
|
|
147
|
+
* `contextOnly` — when true, the chip links the new PR as context-only instead
|
|
148
|
+
* of auto-managed (the default is auto-managed / contextOnly:false).
|
|
149
|
+
*
|
|
150
|
+
* Each chip: `{ kind:'create-pr', label, project, branch, message }` where
|
|
151
|
+
* `message` is the Command Center turn the chip click should send.
|
|
152
|
+
*/
|
|
153
|
+
function buildCreatePrFollowups({ project, branch, contextOnly = false } = {}) {
|
|
154
|
+
const proj = (project && String(project).trim()) || '';
|
|
155
|
+
if (!proj) return [];
|
|
156
|
+
const br = branch && String(branch).trim() ? String(branch).trim() : '';
|
|
157
|
+
const branchClause = br
|
|
158
|
+
? `on the current branch ${br}`
|
|
159
|
+
: 'on a new well-named branch off the project main branch';
|
|
160
|
+
const linkBody = contextOnly
|
|
161
|
+
? `{"url":"<new PR url>","project":"${proj}","contextOnly":true}`
|
|
162
|
+
: `{"url":"<new PR url>","project":"${proj}","contextOnly":false}`;
|
|
163
|
+
const trackNote = contextOnly
|
|
164
|
+
? 'so it is tracked but not auto-reviewed'
|
|
165
|
+
: 'so the engine auto-manages it (review -> fix -> re-review -> auto-merge)';
|
|
166
|
+
const message =
|
|
167
|
+
`Create a PR from the local changes you just made in the ${proj} project. ` +
|
|
168
|
+
`Steps: (1) in that project's working tree, stage and commit the modified files ${branchClause} with a clear conventional-commit message; ` +
|
|
169
|
+
`(2) push the branch; (3) open a PR against the project's main branch using the right CLI for the repo host (gh for GitHub, az repos for ADO); ` +
|
|
170
|
+
`(4) link it to the tracker by calling POST /api/pull-requests/link with ${linkBody} ${trackNote}. ` +
|
|
171
|
+
`Then show me the PR URL.`;
|
|
172
|
+
return [{ kind: CREATE_PR_FOLLOWUP.kind, label: CREATE_PR_FOLLOWUP.label, project: proj, branch: br || null, message }];
|
|
173
|
+
}
|
|
174
|
+
|
|
131
175
|
// Read-only system prompt for the projectless dispatch. Teaches the untrusted-
|
|
132
176
|
// input contract and the injection-signalling marker. Kept inline (no playbook
|
|
133
177
|
// render) because this path has no project/worktree context to inject.
|
|
@@ -538,6 +582,7 @@ module.exports = {
|
|
|
538
582
|
createPrAction,
|
|
539
583
|
buildPrActionPrompt,
|
|
540
584
|
buildPrActionFollowups,
|
|
585
|
+
buildCreatePrFollowups,
|
|
541
586
|
runPrAction,
|
|
542
587
|
postPrActionComment,
|
|
543
588
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2218",
|
|
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"
|
package/prompts/cc-system.md
CHANGED
|
@@ -89,6 +89,15 @@ If you start a small task and discover it's actually Medium (3+ files, more tool
|
|
|
89
89
|
|
|
90
90
|
When genuinely in doubt about the size, delegate — agents have isolated worktrees, full tool access, durable work-item tracking, and no turn limits.
|
|
91
91
|
|
|
92
|
+
### After you make a local code edit yourself — offer to open a PR
|
|
93
|
+
When you edit files **yourself** in a configured project's working tree (the Step-3 "do it yourself" path — NOT a change a dispatched agent made in its own worktree), don't leave the diff sitting uncommitted. After you finish editing, call:
|
|
94
|
+
```
|
|
95
|
+
curl -s -X POST http://localhost:{{dashboard_port}}/api/pr-action/offer-create-pr \
|
|
96
|
+
-H 'Content-Type: application/json' -H 'X-CC-Turn-Id: {{cc_turn_id}}' \
|
|
97
|
+
-d '{"project":"<project name>"}'
|
|
98
|
+
```
|
|
99
|
+
This checks the project's working tree (`git status --porcelain`) and, when there are uncommitted changes, returns a **`[Create PR]`** follow-up chip to the user (same chip mechanism as the `pr-action` `[Comment]`/`[Fix once]`/`[Track for auto-fix]` chips). When the user clicks it, you'll receive a turn instructing you to commit → push → open the PR → link it to the tracker. Pass `"contextOnly":true` if the PR should be tracked-but-not-auto-reviewed; omit it to have the engine auto-manage the PR (review → fix → re-review → auto-merge). If `hasChanges` is `false`, there's nothing to PR — skip the offer. Don't commit/push on your own initiative; surface the chip and let the user decide.
|
|
100
|
+
|
|
92
101
|
## When to dispatch vs answer inline
|
|
93
102
|
|
|
94
103
|
- **Action requests** (fix this, implement that, build X, run a build, investigate Y, review PR #N) → dispatch via `POST /api/work-items` (or one of the other state-changing endpoints below). Don't also try to do the work yourself in the chat unless it's truly Small per Step 1.
|