@yemi33/minions 0.1.2196 → 0.1.2198
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/command-center.js +32 -2
- package/dashboard/js/modal-qa.js +21 -3
- package/dashboard.js +95 -0
- package/engine/db/migrations/014-pr-fix-target-prefs.js +29 -0
- package/engine/dispatch.js +39 -4
- package/engine/gh-token.js +31 -0
- package/engine/lifecycle.js +21 -7
- package/engine/pr-action.js +391 -0
- package/engine/pr-clone-keep.js +370 -0
- package/engine/pr-devbox.js +322 -0
- package/engine/pr-fix-target-store.js +123 -0
- package/engine/pr-fix-target.js +418 -0
- package/engine/pr-remote-patch.js +384 -0
- package/engine/pr-resolve.js +443 -0
- package/engine/pr-temp-clone.js +414 -0
- package/engine/pr-track.js +209 -0
- package/engine/shared.js +35 -13
- package/engine.js +4 -6
- package/package.json +1 -1
- package/prompts/cc-system.md +33 -0
|
@@ -1447,11 +1447,41 @@ function _ccActionResultLine(action, result) {
|
|
|
1447
1447
|
}
|
|
1448
1448
|
if (result && result.ok) {
|
|
1449
1449
|
var duplicate = result.duplicate ? ' <span style="color:var(--orange)">already exists</span>' : '';
|
|
1450
|
-
|
|
1450
|
+
var chip = '<div class="cc-action-feedback-chip" style="' + CC_ACTION_CHIP_STYLE + ';color:var(--green)">✓ ' + label + duplicate + '</div>';
|
|
1451
|
+
// P-ppa10004: a successful pr-action carries follow-up affordances
|
|
1452
|
+
// ([Comment] [Fix once] [Track for auto-fix]); render them as clickable
|
|
1453
|
+
// chips that send a templated CC turn.
|
|
1454
|
+
var followups = (result && result.followups) || (action && action.followups);
|
|
1455
|
+
return chip + _ccFollowupChips(followups);
|
|
1451
1456
|
}
|
|
1452
1457
|
return '';
|
|
1453
1458
|
}
|
|
1454
1459
|
|
|
1460
|
+
// Render follow-up action chips (P-ppa10004). Each chip click sends its
|
|
1461
|
+
// templated CC message as a fresh turn via ccPrActionFollowup.
|
|
1462
|
+
function _ccFollowupChips(followups) {
|
|
1463
|
+
if (!Array.isArray(followups) || followups.length === 0) return '';
|
|
1464
|
+
var btns = followups.map(function(f) {
|
|
1465
|
+
if (!f || !f.label || !f.message) return '';
|
|
1466
|
+
return '<button type="button" class="cc-followup-chip" onclick="ccPrActionFollowup(' + _ccJsArg(f.message) + ')" ' +
|
|
1467
|
+
'style="padding:3px 10px;border-radius:4px;font-size:var(--text-sm);border:1px solid var(--border);background:var(--surface2);color:var(--blue);cursor:pointer">' +
|
|
1468
|
+
escHtml(f.label) + '</button>';
|
|
1469
|
+
}).filter(Boolean).join('');
|
|
1470
|
+
if (!btns) return '';
|
|
1471
|
+
return '<div class="cc-followup-chips" style="margin:4px 0 0 0;display:flex;flex-wrap:wrap;gap:6px">' + btns + '</div>';
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// Click handler for a follow-up action chip: drop the templated message into
|
|
1475
|
+
// the input and send it as a normal CC turn (so the user sees their intent and
|
|
1476
|
+
// CC routes it — forward-compatible with the Phase 2/3 flows).
|
|
1477
|
+
function ccPrActionFollowup(message) {
|
|
1478
|
+
if (!message) return;
|
|
1479
|
+
var input = document.getElementById('cc-input');
|
|
1480
|
+
if (!input) return;
|
|
1481
|
+
input.value = String(message);
|
|
1482
|
+
ccSend();
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1455
1485
|
|
|
1456
1486
|
function _ccAppendHtmlToMessage(tabOrId, messageId, html) {
|
|
1457
1487
|
if (!messageId || !html) return false;
|
|
@@ -2163,4 +2193,4 @@ if (document.readyState === 'loading') {
|
|
|
2163
2193
|
ccInitResize();
|
|
2164
2194
|
}
|
|
2165
2195
|
|
|
2166
|
-
window.MinionsCC = { toggleCommandCenter, ccNewSession, ccNewTab, ccSwitchTab, ccCloseTab, ccRenderTabBar, ccRestoreMessages, ccSaveState, ccUpdateSessionIndicator, ccAddMessage, ccSend, ccAbort, ccExecuteAction };
|
|
2196
|
+
window.MinionsCC = { toggleCommandCenter, ccNewSession, ccNewTab, ccSwitchTab, ccCloseTab, ccRenderTabBar, ccRestoreMessages, ccSaveState, ccUpdateSessionIndicator, ccAddMessage, ccSend, ccAbort, ccExecuteAction, ccPrActionFollowup };
|
package/dashboard/js/modal-qa.js
CHANGED
|
@@ -335,6 +335,19 @@ function _qaBuildRawErrorHtml(err) {
|
|
|
335
335
|
'</details>';
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
+
function _qaDocStreamErrorMessage(evt) {
|
|
339
|
+
if (!evt) return 'Failed';
|
|
340
|
+
if (typeof evt.error === 'string' && evt.error) return evt.error;
|
|
341
|
+
if (evt.message) return String(evt.message);
|
|
342
|
+
if (evt.errorMessage) return String(evt.errorMessage);
|
|
343
|
+
if (evt.error && typeof evt.error === 'object') {
|
|
344
|
+
if (evt.error.errorMessage) return String(evt.error.errorMessage);
|
|
345
|
+
if (evt.error.message) return String(evt.error.message);
|
|
346
|
+
if (evt.error.stderr) return String(evt.error.stderr);
|
|
347
|
+
}
|
|
348
|
+
return 'Failed';
|
|
349
|
+
}
|
|
350
|
+
|
|
338
351
|
function _qaBuildAssistantHtml(text, opts) {
|
|
339
352
|
const body = opts?.isError ? escHtml(text) : renderMd(text);
|
|
340
353
|
const style = opts?.isError
|
|
@@ -713,6 +726,7 @@ async function _processQaMessage(message, selection, opts) {
|
|
|
713
726
|
const decoder = new TextDecoder();
|
|
714
727
|
let buf = '';
|
|
715
728
|
let terminalEventSeen = false;
|
|
729
|
+
let pendingStreamErrorEvent = null;
|
|
716
730
|
|
|
717
731
|
async function _qaHandleStreamEvent(evt) {
|
|
718
732
|
if (!evt || !evt.type) return;
|
|
@@ -842,8 +856,8 @@ async function _processQaMessage(message, selection, opts) {
|
|
|
842
856
|
return;
|
|
843
857
|
}
|
|
844
858
|
if (evt.type === 'error') {
|
|
845
|
-
|
|
846
|
-
|
|
859
|
+
pendingStreamErrorEvent = evt;
|
|
860
|
+
return;
|
|
847
861
|
}
|
|
848
862
|
}
|
|
849
863
|
|
|
@@ -867,7 +881,11 @@ async function _processQaMessage(message, selection, opts) {
|
|
|
867
881
|
await _qaHandleStreamEvent(JSON.parse(line.slice(6)));
|
|
868
882
|
}
|
|
869
883
|
}
|
|
870
|
-
if (!terminalEventSeen)
|
|
884
|
+
if (!terminalEventSeen) {
|
|
885
|
+
throw new Error(pendingStreamErrorEvent
|
|
886
|
+
? _qaDocStreamErrorMessage(pendingStreamErrorEvent)
|
|
887
|
+
: 'The response stream ended before completion.');
|
|
888
|
+
}
|
|
871
889
|
} catch (e) {
|
|
872
890
|
clearInterval(qaTimer);
|
|
873
891
|
_clearQaStreamWatchdog();
|
package/dashboard.js
CHANGED
|
@@ -42,6 +42,10 @@ const { wrapUntrusted, buildSource } = require('./engine/untrusted-fence');
|
|
|
42
42
|
const steering = require('./engine/steering');
|
|
43
43
|
const steeringStore = require('./engine/steering-store');
|
|
44
44
|
const projectDiscovery = require('./engine/project-discovery');
|
|
45
|
+
const prAction = require('./engine/pr-action');
|
|
46
|
+
const prResolve = require('./engine/pr-resolve');
|
|
47
|
+
const prFixTarget = require('./engine/pr-fix-target');
|
|
48
|
+
const prTrack = require('./engine/pr-track');
|
|
45
49
|
const features = require('./engine/features');
|
|
46
50
|
const ccWorkerPool = require('./engine/cc-worker-pool');
|
|
47
51
|
const diagnosticsMemory = require('./engine/diagnostics-memory');
|
|
@@ -3404,6 +3408,9 @@ function _buildSyntheticActionResultsForTurn(turnId, message, requestedAt) {
|
|
|
3404
3408
|
};
|
|
3405
3409
|
if (entry.project) action.project = entry.project;
|
|
3406
3410
|
if (entry.path) action.path = entry.path;
|
|
3411
|
+
// P-ppa10004: pr-action chips carry follow-up affordances ([Comment]
|
|
3412
|
+
// [Fix once] [Track for auto-fix]) so the renderer can offer the next step.
|
|
3413
|
+
if (Array.isArray(entry.followups) && entry.followups.length) action.followups = entry.followups;
|
|
3407
3414
|
actions.push(action);
|
|
3408
3415
|
const result = {
|
|
3409
3416
|
ok: true,
|
|
@@ -3419,6 +3426,7 @@ function _buildSyntheticActionResultsForTurn(turnId, message, requestedAt) {
|
|
|
3419
3426
|
if (entry.id) result.id = entry.id;
|
|
3420
3427
|
if (entry.project) result.project = entry.project;
|
|
3421
3428
|
if (entry.path) result.path = entry.path;
|
|
3429
|
+
if (Array.isArray(entry.followups) && entry.followups.length) result.followups = entry.followups;
|
|
3422
3430
|
results.push(result);
|
|
3423
3431
|
}
|
|
3424
3432
|
return { actions, results };
|
|
@@ -3435,6 +3443,7 @@ function _ccTurnEntryToActionType(kind) {
|
|
|
3435
3443
|
case 'pipeline-run': return 'trigger-pipeline';
|
|
3436
3444
|
case 'watch': return 'create-watch';
|
|
3437
3445
|
case 'meeting': return 'create-meeting';
|
|
3446
|
+
case 'pr-action': return 'pr-action';
|
|
3438
3447
|
default: return kind;
|
|
3439
3448
|
}
|
|
3440
3449
|
}
|
|
@@ -12606,6 +12615,92 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
12606
12615
|
})();
|
|
12607
12616
|
}},
|
|
12608
12617
|
|
|
12618
|
+
{ method: 'POST', path: '/api/pr-action', desc: 'User-initiated read-only action on any GitHub/ADO PR URL (no project/clone required). Resolves the PR via the projectless resolver. Default: returns an observable dispatch handle. With `execute:true`: runs the read-only action now (fetch → fence → DIRECT LLM) and returns its output plus follow-up action chips ([Comment] [Fix once] [Track for auto-fix]). Read-only only — no auto-polling/discovery.', params: 'url (PR URL or canonical id), action (review|summarize|comment|triage), execute (optional bool — run now and return output + follow-up chips)', handler: async (req, res) => {
|
|
12619
|
+
const body = await readBody(req);
|
|
12620
|
+
try {
|
|
12621
|
+
if (body && body.execute) {
|
|
12622
|
+
reloadConfig();
|
|
12623
|
+
const record = await prAction.runPrAction({ url: body?.url, action: body?.action }, { engineConfig: CONFIG.engine });
|
|
12624
|
+
const followups = record.status === 'done' ? prAction.buildPrActionFollowups(record, { prUrl: body?.url }) : [];
|
|
12625
|
+
recordCcTurnIfPresent(req, { kind: 'pr-action', id: record.id, title: `${record.action} ${record.prId || ''}`.trim(), followups });
|
|
12626
|
+
return jsonReply(res, 200, { ok: record.status !== 'failed', ...record, followups }, req);
|
|
12627
|
+
}
|
|
12628
|
+
const handle = await prAction.createPrAction({ url: body?.url, action: body?.action });
|
|
12629
|
+
recordCcTurnIfPresent(req, { kind: 'pr-action', id: handle.id, title: `${handle.action} ${handle.prId || ''}`.trim() });
|
|
12630
|
+
return jsonReply(res, 200, { ok: true, ...handle }, req);
|
|
12631
|
+
} catch (e) {
|
|
12632
|
+
return jsonReply(res, e.statusCode || 400, { error: e.message }, req);
|
|
12633
|
+
}
|
|
12634
|
+
}},
|
|
12635
|
+
|
|
12636
|
+
{ method: 'POST', path: '/api/pr-action/fix', desc: 'Plan a one-off fix on any GitHub/ADO PR URL (Phase 2, P-ppa10005 — decision/routing surface only). Without `target`: returns the execution-target choice. If the PR’s repo is already a configured project, returns {status:"project-ready"} (skip the prompt, use the existing project fix path); otherwise {status:"awaiting-target-choice"} with options [clone-keep, temp-clone (default), remote-patch, devbox] plus a CC prompt + dashboard modal — NOTHING is cloned. With `target`: validates the choice and returns the routing intent {status:"routed", executor} to the matching executor. temp-clone (default) is implemented in engine/pr-temp-clone.js (executor.module:"pr-temp-clone"), clone-keep in engine/pr-clone-keep.js (executor.module:"pr-clone-keep", auto-clone + persistent project registration), remote-patch in engine/pr-remote-patch.js (executor.module:"pr-remote-patch", GitHub Contents API single-file edit, NO clone and NO validation — GitHub-only), and devbox in engine/pr-devbox.js (executor.module:"pr-devbox", P-ppa10011 stretch — ephemeral clone+fix+push ON A DEVBOX, zero local footprint, never auto-selected). Never clones silently. P-ppa10009: pass `remember:true` alongside `target` to remember the choice per repo so subsequent fixes on that repo skip the prompt; a remembered choice makes the no-`target` call return {status:"routed", fromRemembered:true} instead of prompting. Pass `reprompt:true` to force the prompt despite a remembered choice (override). Clear a remembered choice via POST /api/pr-action/fix-target/forget.', params: 'url (PR URL or canonical id), target (optional — clone-keep|temp-clone|remote-patch|devbox), remember (optional bool — persist target per repo), reprompt (optional bool — force the choice despite a remembered one)', handler: async (req, res) => {
|
|
12637
|
+
const body = await readBody(req);
|
|
12638
|
+
try {
|
|
12639
|
+
reloadConfig();
|
|
12640
|
+
if (body && body.target != null && body.target !== '') {
|
|
12641
|
+
// `remember:true` persists this choice per repo so future fixes skip
|
|
12642
|
+
// the prompt (P-ppa10009).
|
|
12643
|
+
const routed = prFixTarget.resolvePrFixTarget({ url: body?.url }, body.target, { config: CONFIG, remember: !!body.remember });
|
|
12644
|
+
recordCcTurnIfPresent(req, { kind: 'pr-action', id: routed.prId || '', title: `fix ${routed.prId || ''} → ${routed.target}`.trim() });
|
|
12645
|
+
return jsonReply(res, 200, { ok: true, ...routed }, req);
|
|
12646
|
+
}
|
|
12647
|
+
// `reprompt:true` forces the execution-target prompt even when a per-repo
|
|
12648
|
+
// choice is remembered (the override path for P-ppa10009).
|
|
12649
|
+
const plan = prFixTarget.planPrFix({ url: body?.url }, { config: CONFIG, prUrl: body?.url, ignoreRemembered: !!body?.reprompt });
|
|
12650
|
+
recordCcTurnIfPresent(req, { kind: 'pr-action', id: plan.prId || '', title: `fix ${plan.prId || ''} (${plan.status})`.trim() });
|
|
12651
|
+
return jsonReply(res, 200, { ok: true, ...plan }, req);
|
|
12652
|
+
} catch (e) {
|
|
12653
|
+
return jsonReply(res, e.statusCode || 400, { error: e.message }, req);
|
|
12654
|
+
}
|
|
12655
|
+
}},
|
|
12656
|
+
|
|
12657
|
+
{ method: 'POST', path: '/api/pr-action/fix-target/forget', desc: 'Clear/override the remembered per-repo execution-target choice (Phase 3, P-ppa10009). Given a PR `url` (or canonical id), forgets the remembered clone-keep|temp-clone|remote-patch choice for that repo scope so the next fix re-prompts. Returns {cleared:true} if a remembered choice existed, {cleared:false} otherwise.', params: 'url (PR URL or canonical id)', handler: async (req, res) => {
|
|
12658
|
+
const body = await readBody(req);
|
|
12659
|
+
try {
|
|
12660
|
+
const ref = prResolve.normalizePrRef(typeof body?.url === 'string' ? body.url.trim() : '');
|
|
12661
|
+
if (!ref) return jsonReply(res, 400, { error: `unrecognized PR reference: ${JSON.stringify(String(body?.url || '').slice(0, 120))}` }, req);
|
|
12662
|
+
const cleared = prFixTarget.clearRememberedTargetForRef(ref);
|
|
12663
|
+
recordCcTurnIfPresent(req, { kind: 'pr-action', id: ref.id || '', title: `forget fix target ${ref.id || ''}`.trim() });
|
|
12664
|
+
return jsonReply(res, 200, { ok: true, cleared, prId: ref.id || null }, req);
|
|
12665
|
+
} catch (e) {
|
|
12666
|
+
return jsonReply(res, e.statusCode || 400, { error: e.message }, req);
|
|
12667
|
+
}
|
|
12668
|
+
}},
|
|
12669
|
+
|
|
12670
|
+
{ method: 'POST', path: '/api/pr-action/track', desc: 'Promote a one-off PR to TRACKED auto-fix (Phase 3, P-ppa10010 — enrollment wiring). If the PR’s repo is NOT a configured project, returns {status:"awaiting-target-choice"} with options [clone-keep (recommended for ongoing tracking), temp-clone, remote-patch] + a CC prompt and dashboard modal — NOTHING is cloned (pick "clone-keep" via POST /api/pr-action/fix to register a persistent project, then call this again to finish enrollment). If the repo IS a configured project, enrolls the PR via the manual-link path with contextOnly:false (the canonical shared.isAutoManagedPrRecord signal) and returns {status:"enrolled", autoManaged:true}. From enrollment on the EXISTING pollers/discoverFromPrs → review/fix/re-review → auto-merge pipeline runs unchanged — no new fix engine.', params: 'url (PR URL or canonical id)', handler: async (req, res) => {
|
|
12671
|
+
const body = await readBody(req);
|
|
12672
|
+
try {
|
|
12673
|
+
reloadConfig();
|
|
12674
|
+
const plan = prTrack.planPrTrack({ url: body?.url }, { config: CONFIG, prUrl: body?.url });
|
|
12675
|
+
if (plan.status === 'awaiting-target-choice') {
|
|
12676
|
+
// Unconfigured repo — ongoing tracking needs a persistent checkout, so
|
|
12677
|
+
// pause for the Clone & keep choice. NOTHING is cloned/enrolled yet.
|
|
12678
|
+
recordCcTurnIfPresent(req, { kind: 'pr-action', id: plan.prId || '', title: `track ${plan.prId || ''} (${plan.status})`.trim() });
|
|
12679
|
+
return jsonReply(res, 200, { ok: true, ...plan }, req);
|
|
12680
|
+
}
|
|
12681
|
+
// project-ready: enroll via the existing manual-link path. Linking with
|
|
12682
|
+
// contextOnly:false creates the record if missing and PROMOTES an existing
|
|
12683
|
+
// context-only record to auto-managed (upsert refuses to demote a managed
|
|
12684
|
+
// PR). No new engine — the existing pipeline discovers it next tick.
|
|
12685
|
+
const enrollment = prTrack.buildTrackEnrollment(plan, { prUrl: body?.url });
|
|
12686
|
+
const linkResult = linkPullRequestForTracking({ url: enrollment.url, contextOnly: enrollment.contextOnly }, CONFIG);
|
|
12687
|
+
invalidateStatusCache();
|
|
12688
|
+
const autoManaged = shared.isAutoManagedPrRecord(linkResult.record);
|
|
12689
|
+
recordCcTurnIfPresent(req, { kind: 'pr-action', id: linkResult.id, title: `track ${linkResult.id} → auto-fix`.trim() });
|
|
12690
|
+
return jsonReply(res, 200, {
|
|
12691
|
+
ok: true,
|
|
12692
|
+
status: 'enrolled',
|
|
12693
|
+
prId: linkResult.id,
|
|
12694
|
+
autoManaged,
|
|
12695
|
+
contextOnly: false,
|
|
12696
|
+
project: linkResult.targetProject?.name || 'central',
|
|
12697
|
+
projectName: plan.projectName || linkResult.targetProject?.name || null,
|
|
12698
|
+
}, req);
|
|
12699
|
+
} catch (e) {
|
|
12700
|
+
return jsonReply(res, e.statusCode || 400, { error: e.message }, req);
|
|
12701
|
+
}
|
|
12702
|
+
}},
|
|
12703
|
+
|
|
12609
12704
|
{ 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) => {
|
|
12610
12705
|
const body = await readBody(req);
|
|
12611
12706
|
reloadConfig();
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// engine/db/migrations/014-pr-fix-target-prefs.js
|
|
2
|
+
//
|
|
3
|
+
// Plan: projectless-pr-actions, Phase 3, item P-ppa10009 — "Remember
|
|
4
|
+
// execution-target choice per repo (optional)".
|
|
5
|
+
//
|
|
6
|
+
// Adds the `pr_fix_target_prefs` table — one row per repo scope — so the
|
|
7
|
+
// execution-target choice surface (engine/pr-fix-target.js) can OPTIONALLY
|
|
8
|
+
// remember the user's pick (clone-keep | temp-clone | remote-patch) keyed by
|
|
9
|
+
// canonical PR scope (github:owner/repo / ado:org/proj/repo). When a remembered
|
|
10
|
+
// choice exists, a repeat fix on that repo routes straight to the executor
|
|
11
|
+
// instead of re-prompting. Absence of a row still prompts (the safe default —
|
|
12
|
+
// never clone silently). The choice is clearable/overridable.
|
|
13
|
+
//
|
|
14
|
+
// SQL-first per CLAUDE.md "New state goes into SQL first" — no JSON sidecar;
|
|
15
|
+
// reads/writes go through engine/pr-fix-target-store.js.
|
|
16
|
+
|
|
17
|
+
module.exports = {
|
|
18
|
+
version: 14,
|
|
19
|
+
description: 'pr_fix_target_prefs: remembered per-repo execution-target choices',
|
|
20
|
+
up(db) {
|
|
21
|
+
db.exec(`
|
|
22
|
+
CREATE TABLE pr_fix_target_prefs (
|
|
23
|
+
repo_scope TEXT PRIMARY KEY,
|
|
24
|
+
target TEXT NOT NULL,
|
|
25
|
+
updated_at INTEGER NOT NULL
|
|
26
|
+
);
|
|
27
|
+
`);
|
|
28
|
+
},
|
|
29
|
+
};
|
package/engine/dispatch.js
CHANGED
|
@@ -12,7 +12,7 @@ const dispatchEvents = require('./dispatch-events');
|
|
|
12
12
|
|
|
13
13
|
const { safeJsonArr, mutateWorkItems,
|
|
14
14
|
mutatePullRequests, getProjects, projectPrPath, log, ts, dateStamp,
|
|
15
|
-
sidecarDispatchPrompt, deleteDispatchPromptSidecar,
|
|
15
|
+
sidecarDispatchPrompt, deleteDispatchPromptSidecar, DONE_STATUSES,
|
|
16
16
|
WI_STATUS, WORK_TYPE, DISPATCH_RESULT, ENGINE_DEFAULTS, AGENT_STATUS, FAILURE_CLASS, PR_STATUS } = shared;
|
|
17
17
|
const { getConfig, INBOX_DIR } = queries;
|
|
18
18
|
|
|
@@ -426,12 +426,12 @@ function getStalePrDispatchReason(entry, config) {
|
|
|
426
426
|
// 1. `entry.meta?.source !== 'work-item'` — blocks any non-work-item dispatch
|
|
427
427
|
// (e.g. legacy `pr`/`pr-human-feedback` discovery paths) from running
|
|
428
428
|
// against a context-only PR. Auto-discovery in engine.js#discoverFromPrs
|
|
429
|
-
// already skips
|
|
429
|
+
// already skips context-only PRs at the source, so this is defense-in-depth.
|
|
430
430
|
// 2. `!NON_MUTATING_DISPATCH_TYPES.has(entry.type)` — even an explicit
|
|
431
431
|
// work-item dispatch is dropped if it's a mutating type (fix, implement,
|
|
432
432
|
// test, verify, decompose, docs). Only review/ask/explore — which never
|
|
433
433
|
// push to the PR's source branch — are allowed through.
|
|
434
|
-
if (tracked
|
|
434
|
+
if (shared.isContextOnlyPrRecord(tracked)
|
|
435
435
|
&& (entry.meta?.source !== 'work-item' || !NON_MUTATING_DISPATCH_TYPES.has(entry.type))) {
|
|
436
436
|
return `PR ${tracked.id || prLabel} is context-only`;
|
|
437
437
|
}
|
|
@@ -445,6 +445,40 @@ function getStalePrDispatchReason(entry, config) {
|
|
|
445
445
|
return '';
|
|
446
446
|
}
|
|
447
447
|
|
|
448
|
+
function cancelSourceWorkItemForPrunedDispatch(entry, reason) {
|
|
449
|
+
const itemId = entry?.meta?.item?.id;
|
|
450
|
+
if (!itemId) return false;
|
|
451
|
+
let wiPath;
|
|
452
|
+
try { wiPath = lifecycle().resolveWorkItemPath(entry.meta); }
|
|
453
|
+
catch (e) {
|
|
454
|
+
log('warn', `Failed to resolve source work item for pruned dispatch ${entry.id}: ${e.message}`);
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
if (!wiPath) return false;
|
|
458
|
+
|
|
459
|
+
let cancelled = false;
|
|
460
|
+
try {
|
|
461
|
+
mutateWorkItems(wiPath, (items) => {
|
|
462
|
+
if (!Array.isArray(items)) return items;
|
|
463
|
+
const wi = items.find(w => w && w.id === itemId);
|
|
464
|
+
if (!wi) return items;
|
|
465
|
+
if (DONE_STATUSES.has(wi.status) || wi.status === WI_STATUS.FAILED || wi.status === WI_STATUS.CANCELLED) return items;
|
|
466
|
+
wi.status = WI_STATUS.CANCELLED;
|
|
467
|
+
wi._cancelledBy = reason || 'stale PR dispatch pruned';
|
|
468
|
+
wi.cancelledAt = ts();
|
|
469
|
+
wi._lastDispatchResult = 'pruned-stale-pr-dispatch';
|
|
470
|
+
delete wi.dispatched_at;
|
|
471
|
+
delete wi.dispatched_to;
|
|
472
|
+
delete wi._pendingReason;
|
|
473
|
+
cancelled = true;
|
|
474
|
+
return items;
|
|
475
|
+
}, { skipWriteIfUnchanged: true });
|
|
476
|
+
} catch (e) {
|
|
477
|
+
log('warn', `Failed to cancel work item ${itemId} for pruned dispatch ${entry.id}: ${e.message}`);
|
|
478
|
+
}
|
|
479
|
+
return cancelled;
|
|
480
|
+
}
|
|
481
|
+
|
|
448
482
|
function pruneStalePrDispatches(config = queries.getConfig()) {
|
|
449
483
|
const removed = [];
|
|
450
484
|
mutateDispatch((dispatch) => {
|
|
@@ -459,7 +493,8 @@ function pruneStalePrDispatches(config = queries.getConfig()) {
|
|
|
459
493
|
|
|
460
494
|
for (const { entry, reason } of removed) {
|
|
461
495
|
try { deleteDispatchPromptSidecar(entry); } catch { /* cleanup best-effort */ }
|
|
462
|
-
|
|
496
|
+
const cancelled = cancelSourceWorkItemForPrunedDispatch(entry, reason);
|
|
497
|
+
log('info', `Dropped stale PR dispatch ${entry.id}: ${reason}${cancelled ? ' (source work item cancelled)' : ''}`);
|
|
463
498
|
}
|
|
464
499
|
return removed.length;
|
|
465
500
|
}
|
package/engine/gh-token.js
CHANGED
|
@@ -109,6 +109,35 @@ function resolveTokenForSlug(slug, opts = {}) {
|
|
|
109
109
|
return _fetchTokenForAccount(account, opts);
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Resolve a GitHub PAT for a named account directly (bypassing slug→account
|
|
114
|
+
* mapping). Used by the projectless PR resolver's unknown-slug fallback
|
|
115
|
+
* (engine/pr-resolve.js) to try each authed account in turn WITHOUT touching
|
|
116
|
+
* the global active `gh auth` profile. Returns the token string or null.
|
|
117
|
+
*/
|
|
118
|
+
function tokenForAccount(account, opts = {}) {
|
|
119
|
+
return _fetchTokenForAccount(account, opts);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* List the distinct gh account names configured in `engine.ghAccounts`. This is
|
|
124
|
+
* the candidate set the unknown-slug fallback iterates over (per the plan:
|
|
125
|
+
* "fall back across the available authed accounts" rather than relying on the
|
|
126
|
+
* active `gh auth` profile). Order follows the config object's own key order.
|
|
127
|
+
*/
|
|
128
|
+
function listConfiguredAccounts(opts = {}) {
|
|
129
|
+
const config = _readConfig(opts);
|
|
130
|
+
const accounts = (config && config.engine && config.engine.ghAccounts) || {};
|
|
131
|
+
if (!accounts || typeof accounts !== 'object') return [];
|
|
132
|
+
const seen = new Set();
|
|
133
|
+
const out = [];
|
|
134
|
+
for (const value of Object.values(accounts)) {
|
|
135
|
+
const name = value == null ? '' : String(value);
|
|
136
|
+
if (name && !seen.has(name)) { seen.add(name); out.push(name); }
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
|
|
112
141
|
/** Test-only: prime a slug→token override that bypasses config + shell-out. */
|
|
113
142
|
function _setTokenForTest(slug, token) {
|
|
114
143
|
if (!slug) return;
|
|
@@ -127,6 +156,8 @@ function _clearTokenCache() {
|
|
|
127
156
|
module.exports = {
|
|
128
157
|
resolveTokenForSlug,
|
|
129
158
|
resolveAccountForSlug,
|
|
159
|
+
tokenForAccount,
|
|
160
|
+
listConfiguredAccounts,
|
|
130
161
|
_setTokenForTest,
|
|
131
162
|
_clearTokenCache,
|
|
132
163
|
// Constants exposed for tests + diagnostics.
|
package/engine/lifecycle.js
CHANGED
|
@@ -790,6 +790,19 @@ function reconcilePrdStatuses(config) {
|
|
|
790
790
|
|
|
791
791
|
// ─── PR Sync from Output ─────────────────────────────────────────────────────
|
|
792
792
|
|
|
793
|
+
const ONE_SHOT_CONTEXT_PR_TYPES = new Set([
|
|
794
|
+
WORK_TYPE.REVIEW,
|
|
795
|
+
WORK_TYPE.ASK,
|
|
796
|
+
WORK_TYPE.EXPLORE,
|
|
797
|
+
]);
|
|
798
|
+
|
|
799
|
+
function shouldSyncPrAsContextOnly(meta) {
|
|
800
|
+
const item = meta?.item;
|
|
801
|
+
if (!item?.oneShot || item.skipPr) return false;
|
|
802
|
+
const type = String(item.type || item.itemType || '').trim();
|
|
803
|
+
return ONE_SHOT_CONTEXT_PR_TYPES.has(type);
|
|
804
|
+
}
|
|
805
|
+
|
|
793
806
|
function syncPrsFromOutput(output, agentId, meta, config, opts = {}) {
|
|
794
807
|
const { structuredCompletion = null } = opts;
|
|
795
808
|
const outputText = String(output || '');
|
|
@@ -996,13 +1009,14 @@ function syncPrsFromOutput(output, agentId, meta, config, opts = {}) {
|
|
|
996
1009
|
url: prUrl,
|
|
997
1010
|
prdItems: meta?.item?.id ? [meta.item.id] : [],
|
|
998
1011
|
sourcePlan: meta?.item?.sourcePlan || '',
|
|
999
|
-
itemType: meta?.item?.itemType || ''
|
|
1012
|
+
itemType: meta?.item?.itemType || '',
|
|
1013
|
+
contextOnly: shouldSyncPrAsContextOnly(meta),
|
|
1000
1014
|
};
|
|
1001
1015
|
applyScheduleContextToPrEntry(entry, meta?.item, config);
|
|
1002
|
-
// Issue #1772: one-off dispatches (
|
|
1003
|
-
//
|
|
1004
|
-
//
|
|
1005
|
-
|
|
1016
|
+
// Issue #1772: explicit one-off review/ask/explore dispatches (for example,
|
|
1017
|
+
// "review this PR" via CC) must stay reference-only. QA-session setup and
|
|
1018
|
+
// other oneShot+skipPr PR-targeted work are operational context, not an
|
|
1019
|
+
// observe toggle, so they must not demote a real tracked PR.
|
|
1006
1020
|
newPrsByPath.get(prPath).entries.push({ prId, fullId, entry });
|
|
1007
1021
|
}
|
|
1008
1022
|
|
|
@@ -1145,7 +1159,7 @@ function _existingPrRecordForCanonicalId(canonicalPrId) {
|
|
|
1145
1159
|
* can render the real merged/abandoned status instead of a perpetual blue ○.
|
|
1146
1160
|
*
|
|
1147
1161
|
* Idempotent: returns `{ enrolled: false, reason: 'already_tracked' }` if a row
|
|
1148
|
-
* already exists. Marks new records with `
|
|
1162
|
+
* already exists. Marks new records with `contextOnly: true` so the engine
|
|
1149
1163
|
* doesn't try to manage them (no re-dispatch of fix/review loops). Live state
|
|
1150
1164
|
* is fetched via `gh pr view` for GitHub; ADO enrollment is best-effort with
|
|
1151
1165
|
* a conservative ACTIVE default.
|
|
@@ -1259,7 +1273,7 @@ async function enrollPrFromCanonicalId(canonicalPrId, project, opts = {}) {
|
|
|
1259
1273
|
url,
|
|
1260
1274
|
prdItems: opts.itemId ? [opts.itemId] : [],
|
|
1261
1275
|
_attachedAt: ts(),
|
|
1262
|
-
|
|
1276
|
+
contextOnly: true,
|
|
1263
1277
|
_enrolledBy: 'enrollPrFromCanonicalId',
|
|
1264
1278
|
};
|
|
1265
1279
|
if (liveState && liveState.mergedAt) entry.mergedAt = liveState.mergedAt;
|