@yemi33/minions 0.1.2424 → 0.1.2425
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/bin/install-internal-minions.js +1209 -0
- package/bin/minions.js +75 -9
- package/dashboard/docs/typography.md +27 -12
- package/dashboard/js/command-center.js +13 -11
- package/dashboard/js/live-stream.js +1 -1
- package/dashboard/js/memory-search.js +388 -43
- package/dashboard/js/modal-qa.js +5 -5
- package/dashboard/js/qa.js +39 -26
- package/dashboard/js/refresh.js +37 -18
- package/dashboard/js/render-agents.js +14 -18
- package/dashboard/js/render-dispatch.js +14 -74
- package/dashboard/js/render-inbox.js +3 -3
- package/dashboard/js/render-meetings.js +8 -8
- package/dashboard/js/render-other.js +45 -20
- package/dashboard/js/render-pipelines.js +6 -6
- package/dashboard/js/render-plans.js +14 -14
- package/dashboard/js/render-prd.js +46 -46
- package/dashboard/js/render-prs.js +9 -65
- package/dashboard/js/render-schedules.js +5 -5
- package/dashboard/js/render-skills.js +6 -6
- package/dashboard/js/render-utils.js +4 -4
- package/dashboard/js/render-watches.js +4 -16
- package/dashboard/js/render-work-items.js +95 -107
- package/dashboard/js/settings.js +175 -74
- package/dashboard/layout.html +3 -2
- package/dashboard/pages/inbox.html +1 -1
- package/dashboard/pages/work.html +1 -1
- package/dashboard/shared/model-display.js +9 -0
- package/dashboard/shared/watches-source.js +43 -0
- package/dashboard/shared/welcome-popup.js +259 -0
- package/dashboard/slim/body.html +6 -6
- package/dashboard/slim/js/chat.js +4 -2
- package/dashboard/slim/js/history.js +10 -3
- package/dashboard/slim/js/modals-tiles.js +85 -16
- package/dashboard/slim/js/status.js +12 -16
- package/dashboard/slim/styles.css +24 -4
- package/dashboard/styles.css +62 -27
- package/dashboard-build.js +8 -2
- package/dashboard.js +3854 -2514
- package/docs/README.md +6 -4
- package/docs/api-errors.md +144 -0
- package/docs/auto-discovery.md +83 -61
- package/docs/capture-demos.js +173 -30
- package/docs/command-center.md +5 -1
- package/docs/completion-reports.md +127 -11
- package/docs/constants.md +15 -3
- package/docs/constellation-style-telemetry.md +2 -2
- package/docs/copilot-cli-schema.md +32 -22
- package/docs/cross-repo-plans.md +30 -30
- package/docs/demo/memory-system.html +1 -1
- package/docs/deprecated.json +132 -18
- package/docs/diagnostics-memory.md +8 -4
- package/docs/human-vs-automated.md +2 -2
- package/docs/internal-install.md +190 -0
- package/docs/kb-pr3223-cascade-archiving.md +16 -0
- package/docs/kb-pr696-merge-conflict-docs.md +23 -0
- package/docs/kb-sweep.md +2 -2
- package/docs/keep-processes.md +8 -1
- package/docs/live-checkout-mode.md +1 -1
- package/docs/managed-spawn.md +21 -3
- package/docs/named-agents.md +3 -2
- package/docs/plan-lifecycle.md +32 -32
- package/docs/pr-screenshots/pr-899/worker-pool-worktrees-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-899/worker-pool-worktrees-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-979/auto-fix-pane-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-979/auto-fix-pane-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-985/pr-column-em-dash-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-985/pr-column-em-dash-BEFORE.png +0 -0
- package/docs/qa-runbook-lifecycle.md +67 -51
- package/docs/qa-runbooks.md +2 -2
- package/docs/runtime-adapters.md +101 -50
- package/docs/security.md +28 -0
- package/docs/self-improvement.md +47 -13
- package/docs/skills.md +29 -0
- package/docs/slim-ux/concepts.md +2 -2
- package/docs/specs/agent-configurability.md +43 -41
- package/docs/specs/agent-rename.md +13 -13
- package/docs/team-memory.md +283 -14
- package/docs/tutorials/03-plan-driven-feature.md +3 -3
- package/docs/tutorials/04-runtimes-and-harness.md +18 -21
- package/docs/tutorials/08-operations-and-recovery.md +3 -2
- package/docs/tutorials/README.md +4 -3
- package/docs/visual-evidence-ci.md +103 -0
- package/docs/watches.md +35 -30
- package/docs/worktree-lifecycle.md +151 -13
- package/engine/acp-transport.js +1 -0
- package/engine/ado.js +80 -33
- package/engine/agent-api-validation.js +571 -0
- package/engine/agent-worker-pool.js +7 -6
- package/engine/api-contracts/agent-content.js +761 -0
- package/engine/api-contracts/cc-ops.js +219 -0
- package/engine/api-contracts/config-runtime.js +419 -0
- package/engine/api-contracts/core.js +138 -0
- package/engine/api-contracts/index.js +506 -0
- package/engine/api-contracts/orchestration.js +602 -0
- package/engine/api-contracts/pull-requests.js +323 -0
- package/engine/api-contracts/qa-process.js +764 -0
- package/engine/api-contracts/work-plan-prd.js +494 -0
- package/engine/api-validation.js +543 -0
- package/engine/cc-api-validation.js +586 -0
- package/engine/cc-worker-pool.js +9 -1
- package/engine/cleanup.js +393 -271
- package/engine/cli.js +65 -7
- package/engine/comment-classifier.js +70 -5
- package/engine/consolidation.js +123 -5
- package/engine/content-api-validation.js +606 -0
- package/engine/create-pr-worktree.js +22 -1
- package/engine/db/migrations/027-review-learning-lifecycle.js +162 -0
- package/engine/dispatch.js +33 -0
- package/engine/distribution.js +189 -0
- package/engine/execution-model.js +59 -0
- package/engine/features.js +12 -0
- package/engine/github.js +67 -25
- package/engine/harness-context.js +313 -0
- package/engine/inbox-store.js +1 -1
- package/engine/kb-sweep.js +161 -38
- package/engine/keep-process-sweep.js +43 -3
- package/engine/lifecycle.js +1066 -165
- package/engine/llm.js +4 -0
- package/engine/managed-spawn.js +463 -48
- package/engine/meeting.js +197 -34
- package/engine/memory-retrieval.js +280 -0
- package/engine/memory-store.js +821 -38
- package/engine/model-discovery.js +17 -3
- package/engine/pipeline.js +802 -14
- package/engine/plan-prd-validation.js +745 -0
- package/engine/playbook.js +206 -17
- package/engine/pooled-agent-process.js +6 -1
- package/engine/pr-action.js +1 -1
- package/engine/pr-issue-validation.js +653 -0
- package/engine/pr-resolve.js +75 -13
- package/engine/prd-store.js +39 -6
- package/engine/preflight.js +7 -2
- package/engine/process-utils.js +79 -31
- package/engine/projects.js +198 -12
- package/engine/promotion.js +371 -0
- package/engine/qa-from-prd.js +3 -0
- package/engine/qa-process-validation.js +572 -0
- package/engine/qa-runbooks.js +41 -37
- package/engine/qa-runners/maestro.js +11 -7
- package/engine/qa-runners/playwright.js +37 -14
- package/engine/qa-runners.js +9 -10
- package/engine/qa-runs.js +124 -11
- package/engine/qa-sessions.js +139 -40
- package/engine/quarantine-refs.js +81 -11
- package/engine/queries.js +163 -9
- package/engine/review-learning-backfill.js +482 -0
- package/engine/review-learning.js +1236 -0
- package/engine/runtimes/claude.js +60 -2
- package/engine/runtimes/codex.js +57 -16
- package/engine/runtimes/contract.js +21 -0
- package/engine/runtimes/copilot.js +51 -3
- package/engine/runtimes/index.js +1 -0
- package/engine/scheduler.js +201 -32
- package/engine/settings-validation.js +909 -0
- package/engine/shared.js +705 -208
- package/engine/spawn-agent.js +48 -21
- package/engine/steering-constraints.js +31 -0
- package/engine/steering-store.js +10 -2
- package/engine/steering.js +14 -2
- package/engine/timeout.js +404 -111
- package/engine/untrusted-fence.js +24 -10
- package/engine/watch-actions.js +7 -2
- package/engine/watches.js +353 -51
- package/engine/work-item-validation.js +561 -0
- package/engine/work-items-store.js +61 -1
- package/engine/worktree-gc.js +656 -37
- package/engine/worktree-preflight.js +154 -0
- package/engine.js +2068 -342
- package/package.json +1 -1
- package/playbooks/_pr-description-audit.md +88 -41
- package/playbooks/build-fix-complex.md +3 -3
- package/playbooks/docs.md +2 -2
- package/playbooks/fix.md +18 -18
- package/playbooks/implement.md +18 -18
- package/playbooks/qa-session-draft.md +10 -12
- package/playbooks/qa-session-execute.md +16 -12
- package/playbooks/qa-validate.md +8 -7
- package/playbooks/shared-rules.md +45 -0
- package/playbooks/test.md +4 -0
- package/prompts/cc-system.md +1 -1
- package/skills/check-self-authored-review-comment/SKILL.md +2 -2
package/engine/qa-sessions.js
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* session parks at `awaiting-approval` for human review; in
|
|
15
15
|
* `auto` mode it auto-chains EXECUTE.
|
|
16
16
|
* EXECUTE → runs the drafted test against the managed-spawn target, writes
|
|
17
|
-
*
|
|
18
|
-
*
|
|
17
|
+
* the engine-injected absolute qa-run-result.json path. The
|
|
18
|
+
* qa-runs lifecycle hook ingests the run-correlated sidecar; this
|
|
19
19
|
* module then transitions the session done/failed based on the
|
|
20
20
|
* resulting qa-run terminal status.
|
|
21
21
|
*
|
|
@@ -263,7 +263,7 @@ function validateSpec(spec) {
|
|
|
263
263
|
errors.push(`flowsRaw exceeds ${LIMITS.flowsMax} chars`);
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
-
const mode = spec.mode
|
|
266
|
+
const mode = spec.mode === undefined ? 'confirm' : spec.mode;
|
|
267
267
|
if (!VALID_MODES.has(mode)) {
|
|
268
268
|
errors.push('mode must be one of: ' + [...VALID_MODES].join(', '));
|
|
269
269
|
}
|
|
@@ -301,17 +301,18 @@ function validateSpec(spec) {
|
|
|
301
301
|
errors.push(`projects exceeds ${LIMITS.projectsMax} entries`);
|
|
302
302
|
} else {
|
|
303
303
|
const seen = new Set();
|
|
304
|
-
for (
|
|
304
|
+
for (let index = 0; index < spec.projects.length; index += 1) {
|
|
305
|
+
const p = spec.projects[index];
|
|
305
306
|
if (typeof p !== 'string' || p.length === 0) {
|
|
306
|
-
errors.push(
|
|
307
|
+
errors.push(`projects[${index}] must be a non-empty string`);
|
|
307
308
|
break;
|
|
308
309
|
}
|
|
309
310
|
if (p.length > LIMITS.projectMax) {
|
|
310
|
-
errors.push(`projects
|
|
311
|
+
errors.push(`projects[${index}] exceeds ${LIMITS.projectMax} chars`);
|
|
311
312
|
break;
|
|
312
313
|
}
|
|
313
314
|
if (seen.has(p)) {
|
|
314
|
-
errors.push(`projects
|
|
315
|
+
errors.push(`projects[${index}] duplicates an earlier entry: ${p}`);
|
|
315
316
|
break;
|
|
316
317
|
}
|
|
317
318
|
seen.add(p);
|
|
@@ -378,7 +379,7 @@ function createSession(spec) {
|
|
|
378
379
|
spec: {
|
|
379
380
|
target: { ...spec.target },
|
|
380
381
|
flowsRaw: spec.flowsRaw,
|
|
381
|
-
mode: spec.mode
|
|
382
|
+
mode: spec.mode === undefined ? 'confirm' : spec.mode,
|
|
382
383
|
capture: {
|
|
383
384
|
video: !!(spec.capture && spec.capture.video),
|
|
384
385
|
screenshots: !!(spec.capture && spec.capture.screenshots),
|
|
@@ -611,7 +612,7 @@ function setSessionQaRunId(id, qaRunId) {
|
|
|
611
612
|
* @returns {object} updated session
|
|
612
613
|
* @throws Error on unknown id, unsafe id, or illegal transition
|
|
613
614
|
*/
|
|
614
|
-
function transitionSession(id, toState, patch = {}) {
|
|
615
|
+
function transitionSession(id, toState, patch = {}, options = {}) {
|
|
615
616
|
if (!_isSafeSessionId(id)) throw new Error('qa-sessions: unsafe sessionId: ' + id);
|
|
616
617
|
if (!isValidState(toState)) throw new Error('qa-sessions: invalid target state: ' + toState);
|
|
617
618
|
|
|
@@ -621,6 +622,12 @@ function transitionSession(id, toState, patch = {}) {
|
|
|
621
622
|
if (!Array.isArray(sessions)) sessions = [];
|
|
622
623
|
const session = sessions.find(s => s && s.id === id);
|
|
623
624
|
if (!session) { transitionError = new Error(`qa-sessions: session not found: ${id}`); return sessions; }
|
|
625
|
+
if (options.expectedState && session.state !== options.expectedState) {
|
|
626
|
+
transitionError = new Error(
|
|
627
|
+
`qa-sessions: ${options.action || 'transition'} requires state ${options.expectedState}, got ${session.state}`,
|
|
628
|
+
);
|
|
629
|
+
return sessions;
|
|
630
|
+
}
|
|
624
631
|
try { validateTransition(session.state, toState); }
|
|
625
632
|
catch (e) { transitionError = e; return sessions; }
|
|
626
633
|
|
|
@@ -790,6 +797,8 @@ function buildSetupWorkItem(session, { project, primary, coServices, primaryProj
|
|
|
790
797
|
* @param {string} [opts.feedback] - natural-language feedback from /edit, threaded into the prompt as steering
|
|
791
798
|
*/
|
|
792
799
|
function buildDraftWorkItem(session, { project, feedback } = {}) {
|
|
800
|
+
const managedSpawnLookupPath = `/api/managed-processes/by-name/${encodeURIComponent(session.managedSpawnName)}`;
|
|
801
|
+
const testDir = qaTestsDirForSession(session.id);
|
|
793
802
|
const lines = [
|
|
794
803
|
`QA Session ${session.id} — DRAFT phase.`,
|
|
795
804
|
'',
|
|
@@ -797,8 +806,8 @@ function buildDraftWorkItem(session, { project, feedback } = {}) {
|
|
|
797
806
|
`Runner: ${session.spec.runner || '(auto-detected)'}`,
|
|
798
807
|
`Mode: ${session.spec.mode}`,
|
|
799
808
|
'',
|
|
800
|
-
`Managed-spawn target: \`${session.managedSpawnName}\` (live; query
|
|
801
|
-
`Write the test file
|
|
809
|
+
`Managed-spawn target: \`${session.managedSpawnName}\` (live; query ${managedSpawnLookupPath}).`,
|
|
810
|
+
`Write the test file under \`${testDir}\` using the runner's native format.`,
|
|
802
811
|
'See `playbooks/qa-session-draft.md` for the full contract.',
|
|
803
812
|
];
|
|
804
813
|
if (feedback) {
|
|
@@ -813,10 +822,10 @@ function buildDraftWorkItem(session, { project, feedback } = {}) {
|
|
|
813
822
|
|
|
814
823
|
/**
|
|
815
824
|
* Build the EXECUTE work item. The agent invokes the runner against the live
|
|
816
|
-
* spawn, captures artifacts per `capture`, and writes
|
|
817
|
-
*
|
|
818
|
-
*
|
|
819
|
-
*
|
|
825
|
+
* spawn, captures artifacts per `capture`, and writes the injected absolute
|
|
826
|
+
* result sidecar. The qaRunId hook in engine/lifecycle.js ingests it; our own
|
|
827
|
+
* session hook below transitions done/failed based on the resulting qa-run
|
|
828
|
+
* terminal status.
|
|
820
829
|
*
|
|
821
830
|
* @param {object} session
|
|
822
831
|
* @param {object} opts
|
|
@@ -832,12 +841,12 @@ function buildExecuteWorkItem(session, { qaRunId, project } = {}) {
|
|
|
832
841
|
description: [
|
|
833
842
|
`QA Session ${session.id} — EXECUTE phase.`,
|
|
834
843
|
'',
|
|
835
|
-
`Run
|
|
844
|
+
`Run \`${path.join(qaTestsDirForSession(session.id), session.testFile || 'test.<ext>')}\` against \`${session.managedSpawnName}\`.`,
|
|
836
845
|
`qaRunId: ${qaRunId}`,
|
|
837
846
|
'',
|
|
838
|
-
'Capture artifacts per session.spec.capture.
|
|
839
|
-
'
|
|
840
|
-
'
|
|
847
|
+
'Capture artifacts per session.spec.capture. Use the engine-injected',
|
|
848
|
+
'absolute artifact and qa-run-result.json paths so the linked qa-runs',
|
|
849
|
+
'record can ingest them. See `playbooks/qa-session-execute.md`.',
|
|
841
850
|
].join('\n'),
|
|
842
851
|
project,
|
|
843
852
|
});
|
|
@@ -872,18 +881,26 @@ function _summarizeTarget(target) {
|
|
|
872
881
|
// dispatch + work-items are lazy-required inside _queueWorkItem to keep
|
|
873
882
|
// `require('./qa-sessions')` cycle-safe at the top of lifecycle.js.
|
|
874
883
|
|
|
875
|
-
function
|
|
876
|
-
shared.mutateWorkItems(scope, (items) => {
|
|
877
|
-
if (!Array.isArray(items)) items = [];
|
|
878
|
-
if (!items.some(i => i && i.id === wi.id)) items.push(wi);
|
|
879
|
-
return items;
|
|
880
|
-
});
|
|
884
|
+
function _dispatchQueuedWorkItem(wi) {
|
|
881
885
|
const dispatch = require('./dispatch');
|
|
882
886
|
dispatch.addToDispatch({
|
|
883
887
|
type: wi.type,
|
|
884
888
|
agent: wi.agent || null,
|
|
885
889
|
meta: { item: wi, playbook: wi.meta.playbook },
|
|
886
890
|
});
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function _persistWorkItem(wi, scope) {
|
|
894
|
+
shared.mutateWorkItems(scope, (items) => {
|
|
895
|
+
if (!Array.isArray(items)) items = [];
|
|
896
|
+
if (!items.some(item => item && item.id === wi.id)) items.push(wi);
|
|
897
|
+
return items;
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function _queueWorkItem(wi, scope) {
|
|
902
|
+
_persistWorkItem(wi, scope);
|
|
903
|
+
_dispatchQueuedWorkItem(wi);
|
|
887
904
|
return wi.id;
|
|
888
905
|
}
|
|
889
906
|
|
|
@@ -1105,6 +1122,60 @@ function handleSetupComplete(sessionId, opts = {}) {
|
|
|
1105
1122
|
return wi.id;
|
|
1106
1123
|
}
|
|
1107
1124
|
|
|
1125
|
+
function _setupManagedSpawnRequirement(session, project) {
|
|
1126
|
+
const projects = Array.isArray(session?.spec?.projects) ? session.spec.projects : [];
|
|
1127
|
+
const isMulti = projects.length > 1;
|
|
1128
|
+
const isPrimary = !isMulti || project === session.primaryProject;
|
|
1129
|
+
const key = project || '__central__';
|
|
1130
|
+
return {
|
|
1131
|
+
name: session.managedSpawnName + (isPrimary ? '' : '-' + project),
|
|
1132
|
+
wiId: isMulti
|
|
1133
|
+
? (session.setupStatus?.[key]?.wiId || '')
|
|
1134
|
+
: (session.workItems?.setup || ''),
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
function managedSpawnNamesForSession(session) {
|
|
1139
|
+
if (!session || !_isNonEmptyString(session.managedSpawnName)) return [];
|
|
1140
|
+
const projects = Array.isArray(session.spec && session.spec.projects)
|
|
1141
|
+
? session.spec.projects.filter(_isNonEmptyString)
|
|
1142
|
+
: [];
|
|
1143
|
+
if (projects.length <= 1) return [session.managedSpawnName];
|
|
1144
|
+
const primary = session.primaryProject || projects[0];
|
|
1145
|
+
return projects.map(project => (
|
|
1146
|
+
project === primary ? session.managedSpawnName : `${session.managedSpawnName}-${project}`
|
|
1147
|
+
));
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
function handleSetupCompleteAfterManagedSpawn(sessionId, opts = {}) {
|
|
1151
|
+
const session = getSession(sessionId);
|
|
1152
|
+
if (!session) throw new Error('qa-sessions: session not found: ' + sessionId);
|
|
1153
|
+
if (!opts.success) return handleSetupComplete(sessionId, opts);
|
|
1154
|
+
|
|
1155
|
+
const requirement = _setupManagedSpawnRequirement(session, opts.project || null);
|
|
1156
|
+
const record = require('./managed-spawn').getManagedSpecByName(requirement.name);
|
|
1157
|
+
const hasExpectedOwner = _isNonEmptyString(requirement.wiId);
|
|
1158
|
+
const ownerMatches = hasExpectedOwner && record?.owner_wi === requirement.wiId;
|
|
1159
|
+
if (!record || record.alive !== true || record.healthy !== true || !ownerMatches) {
|
|
1160
|
+
let state = 'not registered';
|
|
1161
|
+
if (!hasExpectedOwner) {
|
|
1162
|
+
state = 'missing its expected SETUP work-item owner';
|
|
1163
|
+
} else if (record && !ownerMatches) {
|
|
1164
|
+
state = `owned by work item ${record.owner_wi || 'unknown'} instead of ${requirement.wiId}`;
|
|
1165
|
+
} else if (record) {
|
|
1166
|
+
state = `registered but ${record.alive !== true ? 'not alive' : 'not healthy'}`;
|
|
1167
|
+
}
|
|
1168
|
+
return handleSetupComplete(sessionId, {
|
|
1169
|
+
...opts,
|
|
1170
|
+
success: false,
|
|
1171
|
+
failureClass: shared.FAILURE_CLASS.INVALID_MANAGED_SPAWN,
|
|
1172
|
+
reason: `managed_spawn target ${requirement.name} is ${state}; QA SETUP cannot advance to DRAFT`,
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
return handleSetupComplete(sessionId, opts);
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1108
1179
|
/**
|
|
1109
1180
|
* Lifecycle hook: DRAFT WI completed. On success we either park at
|
|
1110
1181
|
* awaiting-approval (confirm mode — user must call /approve) or auto-chain
|
|
@@ -1202,28 +1273,52 @@ function handleExecuteComplete(sessionId, opts = {}) {
|
|
|
1202
1273
|
// ── User-initiated actions (called by dashboard endpoints) ─────────────────
|
|
1203
1274
|
|
|
1204
1275
|
/**
|
|
1205
|
-
*
|
|
1206
|
-
* the EXECUTE WI. Caller creates the qa-runs record and passes its id.
|
|
1276
|
+
* Atomically reserve an approval before its run or work item is created.
|
|
1207
1277
|
*/
|
|
1208
|
-
function
|
|
1278
|
+
function beginApproveDraft(sessionId, { qaRunId } = {}) {
|
|
1279
|
+
if (!_isNonEmptyString(qaRunId)) throw new Error('qa-sessions: beginApproveDraft requires qaRunId');
|
|
1280
|
+
return transitionSession(sessionId, QA_SESSION_STATE.EXECUTING, { qaRunId }, {
|
|
1281
|
+
expectedState: QA_SESSION_STATE.AWAITING_APPROVAL,
|
|
1282
|
+
action: 'approveDraft',
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
/** Queue EXECUTE after beginApproveDraft has committed its state transition. */
|
|
1287
|
+
function queueApprovedDraft(sessionId, { scope, qaRunId, project } = {}) {
|
|
1209
1288
|
if (!_isNonEmptyString(scope)) throw new Error('qa-sessions: approveDraft requires scope');
|
|
1210
1289
|
if (!_isNonEmptyString(qaRunId)) throw new Error('qa-sessions: approveDraft requires qaRunId');
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1290
|
+
if (!_isSafeSessionId(sessionId)) throw new Error('qa-sessions: unsafe sessionId: ' + sessionId);
|
|
1291
|
+
let wi = null;
|
|
1292
|
+
mutateQaSessions((sessions) => {
|
|
1293
|
+
const session = sessions.find(item => item && item.id === sessionId);
|
|
1294
|
+
if (!session) throw new Error('qa-sessions: session not found: ' + sessionId);
|
|
1295
|
+
if (session.state !== QA_SESSION_STATE.EXECUTING || session.qaRunId !== qaRunId) {
|
|
1296
|
+
throw new Error(
|
|
1297
|
+
`qa-sessions: queueApprovedDraft requires state executing with matching qaRunId, got ${session.state}`,
|
|
1298
|
+
);
|
|
1299
|
+
}
|
|
1300
|
+
wi = buildExecuteWorkItem(session, {
|
|
1301
|
+
qaRunId,
|
|
1302
|
+
project: project || session.spec.project || null,
|
|
1303
|
+
});
|
|
1304
|
+
_persistWorkItem(wi, scope);
|
|
1305
|
+
if (!session.workItems || typeof session.workItems !== 'object') {
|
|
1306
|
+
session.workItems = { setup: null, draft: null, execute: null };
|
|
1307
|
+
}
|
|
1308
|
+
session.workItems[SESSION_PHASE.EXECUTE] = wi.id;
|
|
1309
|
+
session.updatedAt = ts();
|
|
1310
|
+
_dispatchQueuedWorkItem(wi);
|
|
1311
|
+
return sessions;
|
|
1221
1312
|
});
|
|
1222
|
-
_queueWorkItem(wi, scope);
|
|
1223
|
-
setSessionWorkItem(sessionId, SESSION_PHASE.EXECUTE, wi.id);
|
|
1224
1313
|
return wi.id;
|
|
1225
1314
|
}
|
|
1226
1315
|
|
|
1316
|
+
/** Back-compatible one-shot wrapper for non-HTTP callers. */
|
|
1317
|
+
function approveDraft(sessionId, options = {}) {
|
|
1318
|
+
beginApproveDraft(sessionId, options);
|
|
1319
|
+
return queueApprovedDraft(sessionId, options);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1227
1322
|
/**
|
|
1228
1323
|
* POST /api/qa/sessions/<id>/edit — awaiting-approval → drafting, re-queue the
|
|
1229
1324
|
* DRAFT WI with the user's natural-language feedback threaded into the prompt.
|
|
@@ -1372,14 +1467,18 @@ module.exports = {
|
|
|
1372
1467
|
// Chain helpers (impure — call dispatch + work-items)
|
|
1373
1468
|
queueSetup,
|
|
1374
1469
|
handleSetupComplete,
|
|
1470
|
+
handleSetupCompleteAfterManagedSpawn,
|
|
1375
1471
|
handleDraftComplete,
|
|
1376
1472
|
handleExecuteComplete,
|
|
1377
1473
|
// User actions
|
|
1474
|
+
beginApproveDraft,
|
|
1475
|
+
queueApprovedDraft,
|
|
1378
1476
|
approveDraft,
|
|
1379
1477
|
editDraft,
|
|
1380
1478
|
cancelSession,
|
|
1381
1479
|
killSession,
|
|
1382
1480
|
dismissSession,
|
|
1481
|
+
managedSpawnNamesForSession,
|
|
1383
1482
|
// Status
|
|
1384
1483
|
summarizeSessionsForStatus,
|
|
1385
1484
|
summarizeActiveSessionsForStatus,
|
|
@@ -1,22 +1,43 @@
|
|
|
1
1
|
const shared = require('./shared');
|
|
2
2
|
|
|
3
|
+
const QUARANTINE_REF_RETENTION_MS = 10 * 24 * 60 * 60 * 1000;
|
|
4
|
+
const DELETE_CONCURRENCY = 8;
|
|
5
|
+
|
|
6
|
+
function quarantineRefCreatedAtMs(ref, fallbackCreatedAt) {
|
|
7
|
+
const timestamp = String(ref || '').split('/').pop();
|
|
8
|
+
const fromRef = Number(timestamp);
|
|
9
|
+
if (Number.isFinite(fromRef) && fromRef > 0) return fromRef;
|
|
10
|
+
const fallback = Date.parse(fallbackCreatedAt || '');
|
|
11
|
+
return Number.isFinite(fallback) ? fallback : null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function _readProjectQuarantineRefs(project) {
|
|
15
|
+
const output = await shared.shellSafeGit([
|
|
16
|
+
'for-each-ref',
|
|
17
|
+
'--format=%(refname)|%(objectname)|%(creatordate:iso-strict)',
|
|
18
|
+
'refs/minions/quarantine/',
|
|
19
|
+
'refs/minions/quarantine-wip/',
|
|
20
|
+
], { cwd: project.localPath, timeout: 15000 });
|
|
21
|
+
const seenRefs = new Set();
|
|
22
|
+
return String(output || '').split(/\r?\n/).filter(Boolean).map(line => {
|
|
23
|
+
const [ref, sha, fallbackCreatedAt] = line.split('|');
|
|
24
|
+
if (!ref || seenRefs.has(ref)) return null;
|
|
25
|
+
seenRefs.add(ref);
|
|
26
|
+
const createdAtMs = quarantineRefCreatedAtMs(ref, fallbackCreatedAt);
|
|
27
|
+
return { ref, sha, fallbackCreatedAt, createdAtMs };
|
|
28
|
+
}).filter(Boolean);
|
|
29
|
+
}
|
|
30
|
+
|
|
3
31
|
async function listQuarantineRefs(projects) {
|
|
4
32
|
const candidates = (Array.isArray(projects) ? projects : []).filter(project => project?.localPath);
|
|
5
33
|
const results = await Promise.allSettled(candidates.map(async project => {
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
'for-each-ref',
|
|
9
|
-
'--format=%(refname)|%(objectname)|%(creatordate:iso-strict)',
|
|
10
|
-
'refs/minions/quarantine',
|
|
11
|
-
'refs/minions/quarantine-wip',
|
|
12
|
-
], { cwd: localPath, timeout: 15000 });
|
|
13
|
-
return String(output || '').split(/\r?\n/).filter(Boolean).map(line => {
|
|
14
|
-
const [ref, sha, createdAt] = line.split('|');
|
|
34
|
+
const refs = await _readProjectQuarantineRefs(project);
|
|
35
|
+
return refs.map(({ ref, sha, fallbackCreatedAt, createdAtMs }) => {
|
|
15
36
|
return {
|
|
16
37
|
project: project.name || '',
|
|
17
38
|
ref,
|
|
18
39
|
sha,
|
|
19
|
-
createdAt:
|
|
40
|
+
createdAt: createdAtMs ? new Date(createdAtMs).toISOString() : (fallbackCreatedAt || ''),
|
|
20
41
|
kind: ref.startsWith('refs/minions/quarantine-wip/') ? 'wip' : 'commits',
|
|
21
42
|
};
|
|
22
43
|
});
|
|
@@ -30,4 +51,53 @@ async function listQuarantineRefs(projects) {
|
|
|
30
51
|
return items.sort((a, b) => String(b.createdAt || '').localeCompare(String(a.createdAt || '')));
|
|
31
52
|
}
|
|
32
53
|
|
|
33
|
-
|
|
54
|
+
async function _mapWithConcurrency(items, limit, worker) {
|
|
55
|
+
let next = 0;
|
|
56
|
+
const runners = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
57
|
+
while (next < items.length) {
|
|
58
|
+
const item = items[next++];
|
|
59
|
+
await worker(item);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
await Promise.all(runners);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function pruneExpiredQuarantineRefs(projects, {
|
|
66
|
+
now = Date.now(),
|
|
67
|
+
retentionMs = QUARANTINE_REF_RETENTION_MS,
|
|
68
|
+
} = {}) {
|
|
69
|
+
const candidates = (Array.isArray(projects) ? projects : []).filter(project => project?.localPath);
|
|
70
|
+
const results = await Promise.allSettled(candidates.map(async project => {
|
|
71
|
+
const refs = await _readProjectQuarantineRefs(project);
|
|
72
|
+
const expired = refs.filter(item => item.createdAtMs !== null && now - item.createdAtMs >= retentionMs);
|
|
73
|
+
|
|
74
|
+
let removed = 0;
|
|
75
|
+
await _mapWithConcurrency(expired, DELETE_CONCURRENCY, async item => {
|
|
76
|
+
await shared.shellSafeGit(['update-ref', '-d', item.ref, item.sha], {
|
|
77
|
+
cwd: project.localPath,
|
|
78
|
+
timeout: 15000,
|
|
79
|
+
});
|
|
80
|
+
removed++;
|
|
81
|
+
});
|
|
82
|
+
return removed;
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
let removed = 0;
|
|
86
|
+
const errors = [];
|
|
87
|
+
for (let i = 0; i < results.length; i++) {
|
|
88
|
+
const result = results[i];
|
|
89
|
+
if (result.status === 'fulfilled') removed += result.value;
|
|
90
|
+
else errors.push({
|
|
91
|
+
project: candidates[i]?.name || '',
|
|
92
|
+
error: result.reason?.message || String(result.reason),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return { removed, errors };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = {
|
|
99
|
+
QUARANTINE_REF_RETENTION_MS,
|
|
100
|
+
quarantineRefCreatedAtMs,
|
|
101
|
+
listQuarantineRefs,
|
|
102
|
+
pruneExpiredQuarantineRefs,
|
|
103
|
+
};
|
package/engine/queries.js
CHANGED
|
@@ -10,6 +10,7 @@ const path = require('path');
|
|
|
10
10
|
const os = require('os');
|
|
11
11
|
const shared = require('./shared');
|
|
12
12
|
const steering = require('./steering');
|
|
13
|
+
const { normalizeExecutionModel, resolveWorkItemModel } = require('./execution-model');
|
|
13
14
|
|
|
14
15
|
const { safeRead, safeReadDir, safeJson, safeJsonObj, safeWrite, getProjects, mutateJsonFileLocked,
|
|
15
16
|
parseSkillFrontmatter, KB_CATEGORIES,
|
|
@@ -427,6 +428,10 @@ function getMetrics() {
|
|
|
427
428
|
// SQL is the canonical (and only) metrics store after Phase 9.
|
|
428
429
|
const store = require('./metrics-store');
|
|
429
430
|
const metrics = store.readMetrics() || {};
|
|
431
|
+
const config = getConfig();
|
|
432
|
+
const engineConfig = config.engine || {};
|
|
433
|
+
const configuredAgents = config.agents || {};
|
|
434
|
+
const { resolveRuntime } = require('./runtimes');
|
|
430
435
|
|
|
431
436
|
for (const [agentId, m] of Object.entries(metrics)) {
|
|
432
437
|
if (agentId.startsWith('_')) continue;
|
|
@@ -493,6 +498,13 @@ function getMetrics() {
|
|
|
493
498
|
m.totalRuntimeMs = runtimeByAgent[agentId];
|
|
494
499
|
m.timedTasks = runtimeCountByAgent[agentId];
|
|
495
500
|
}
|
|
501
|
+
const runtimeName = shared.resolveAgentCli(configuredAgents[agentId], engineConfig);
|
|
502
|
+
m.runtime = runtimeName;
|
|
503
|
+
try {
|
|
504
|
+
m.costTracking = resolveRuntime(runtimeName).capabilities?.costTracking === true;
|
|
505
|
+
} catch {
|
|
506
|
+
m.costTracking = false;
|
|
507
|
+
}
|
|
496
508
|
}
|
|
497
509
|
|
|
498
510
|
return metrics;
|
|
@@ -679,6 +691,112 @@ function getInbox() {
|
|
|
679
691
|
// dispatch.completed (most recent) → done/error
|
|
680
692
|
// neither → idle
|
|
681
693
|
// Metadata (resultSummary, verdict, pr) is carried on dispatch entries.
|
|
694
|
+
function _runtimeModelEvidenceKey(runtime, requestedModel, agentId) {
|
|
695
|
+
return `${runtime}\0${requestedModel || ''}\0${agentId || ''}`;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function buildRuntimeModelEvidence(dispatch) {
|
|
699
|
+
const evidence = new Map();
|
|
700
|
+
for (const section of ['active', 'completed']) {
|
|
701
|
+
for (const item of (dispatch?.[section] || [])) {
|
|
702
|
+
const runtime = typeof item?.runtimeName === 'string' ? item.runtimeName.trim() : '';
|
|
703
|
+
const reported = normalizeExecutionModel(item?.executionModel);
|
|
704
|
+
const requested = normalizeExecutionModel(item?.requestedModel);
|
|
705
|
+
if (!runtime || !reported || (requested && requested.toLowerCase() !== 'auto')) continue;
|
|
706
|
+
const startedAt = Date.parse(item?.started_at || item?.created_at || item?.created || '');
|
|
707
|
+
const recency = Number.isFinite(startedAt) ? startedAt : 0;
|
|
708
|
+
for (const agentId of [item?.agent || '', '']) {
|
|
709
|
+
const key = _runtimeModelEvidenceKey(runtime, requested?.toLowerCase() || '', agentId);
|
|
710
|
+
const current = evidence.get(key);
|
|
711
|
+
if (!current || recency >= current.recency) evidence.set(key, { model: reported, recency });
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return evidence;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function resolveRuntimeModelDisplay({ runtime, configuredModel, agentId, dispatch, evidence } = {}) {
|
|
719
|
+
const runtimeName = typeof runtime === 'string' ? runtime.trim() : '';
|
|
720
|
+
const configured = normalizeExecutionModel(configuredModel);
|
|
721
|
+
const autoSelected = configured && configured.toLowerCase() === 'auto';
|
|
722
|
+
if (configured && !autoSelected) return { model: configured, source: 'configured' };
|
|
723
|
+
if (!runtimeName) return { model: null, source: 'unobserved' };
|
|
724
|
+
|
|
725
|
+
const index = evidence || buildRuntimeModelEvidence(dispatch || getDispatch());
|
|
726
|
+
const requested = autoSelected ? 'auto' : '';
|
|
727
|
+
const latest = index.get(_runtimeModelEvidenceKey(runtimeName, requested, agentId));
|
|
728
|
+
return latest
|
|
729
|
+
? { model: latest.model, source: 'runtime-reported' }
|
|
730
|
+
: { model: null, source: 'unobserved' };
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* W-ms0ytfdd000d5a26 — Index each work item's owning dispatch entry so callers
|
|
735
|
+
* can report the model that ACTUALLY ran rather than re-deriving it from
|
|
736
|
+
* current config (which would rewrite history whenever an operator repins a
|
|
737
|
+
* model). An active dispatch is the live run and always beats a completed one;
|
|
738
|
+
* among entries of the same kind the most recently started wins.
|
|
739
|
+
*
|
|
740
|
+
* Note `dispatch.completed` is capped (engine/dispatch.js), so an old work item
|
|
741
|
+
* can legitimately have no entry here — callers must handle that.
|
|
742
|
+
*/
|
|
743
|
+
function buildWorkItemDispatchIndex(dispatch) {
|
|
744
|
+
const index = new Map();
|
|
745
|
+
const startedAtMs = (entry) => Date.parse(entry?.started_at || entry?.created_at || entry?.created || '') || 0;
|
|
746
|
+
const consider = (entry, active) => {
|
|
747
|
+
const key = _dispatchWorkItemIdentityKey(entry);
|
|
748
|
+
if (!key) return;
|
|
749
|
+
const current = index.get(key);
|
|
750
|
+
if (!current
|
|
751
|
+
|| (active && !current.active)
|
|
752
|
+
|| (active === current.active && startedAtMs(entry) >= startedAtMs(current.entry))) {
|
|
753
|
+
index.set(key, { entry, active });
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
for (const entry of (dispatch?.completed || [])) consider(entry, false);
|
|
757
|
+
for (const entry of (dispatch?.active || [])) consider(entry, true);
|
|
758
|
+
return index;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Current-config runtime + model for an agent, used only as the projection for
|
|
763
|
+
* work items that have never been dispatched. Shorthands are normalized through
|
|
764
|
+
* the adapter's own `resolveModel` so the projection matches what dispatch
|
|
765
|
+
* would actually send — capability-based, never a runtime-name branch.
|
|
766
|
+
*/
|
|
767
|
+
function resolveConfiguredAgentModel(config, agentId, cache) {
|
|
768
|
+
const key = agentId || '';
|
|
769
|
+
if (cache && cache.has(key)) return cache.get(key);
|
|
770
|
+
const engine = config?.engine || {};
|
|
771
|
+
const agentConfig = (agentId && config?.agents?.[agentId]) || null;
|
|
772
|
+
const runtimeName = shared.resolveAgentCli(agentConfig, engine);
|
|
773
|
+
const configured = shared.resolveAgentModel(agentConfig, engine) || null;
|
|
774
|
+
let model = configured;
|
|
775
|
+
try {
|
|
776
|
+
const runtime = require('./runtimes').resolveRuntime(runtimeName);
|
|
777
|
+
if (typeof runtime?.resolveModel === 'function') model = runtime.resolveModel(configured) || configured;
|
|
778
|
+
} catch { /* registry unavailable — fall back to the raw configured value */ }
|
|
779
|
+
const resolved = { runtimeName, model };
|
|
780
|
+
if (cache) cache.set(key, resolved);
|
|
781
|
+
return resolved;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Effective-model descriptor for a single work item, shaped like the
|
|
786
|
+
* `runtime`/`model`/`modelSource` triple getAgents() already publishes:
|
|
787
|
+
* `{ model, label, source, runtime, persisted }`.
|
|
788
|
+
*/
|
|
789
|
+
function resolveWorkItemModelInfo(item, { config, dispatchIndex, configCache } = {}) {
|
|
790
|
+
const found = dispatchIndex ? dispatchIndex.get(_workItemIdentityKey(item)) : null;
|
|
791
|
+
const agentId = item?.dispatched_to || item?.agent || '';
|
|
792
|
+
const configured = resolveConfiguredAgentModel(config, agentId, configCache);
|
|
793
|
+
return resolveWorkItemModel({
|
|
794
|
+
dispatchEntry: found ? found.entry : null,
|
|
795
|
+
configuredRuntime: configured.runtimeName,
|
|
796
|
+
configuredModel: configured.model,
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
|
|
682
800
|
function getAgentStatus(agentId, context = {}) {
|
|
683
801
|
const dispatch = context.dispatch || getDispatch();
|
|
684
802
|
|
|
@@ -771,6 +889,7 @@ function getAgents(config) {
|
|
|
771
889
|
|
|
772
890
|
// Include temp agents that are currently active so they show up in agent tiles
|
|
773
891
|
const dispatch = getDispatch();
|
|
892
|
+
const runtimeModelEvidence = buildRuntimeModelEvidence(dispatch);
|
|
774
893
|
let leanWorkItems = [];
|
|
775
894
|
try { leanWorkItems = getWorkItems(config, { enrich: false }); } catch { /* optional fallback data */ }
|
|
776
895
|
const seen = new Set(roster.map(a => a.id));
|
|
@@ -787,11 +906,16 @@ function getAgents(config) {
|
|
|
787
906
|
// → engine.defaultCli → 'copilot'. Surfaced so the dashboard can show a
|
|
788
907
|
// runtime tag next to the agent name.
|
|
789
908
|
const runtime = shared.resolveAgentCli(a, config.engine || {});
|
|
790
|
-
//
|
|
791
|
-
//
|
|
792
|
-
// `undefined` when neither the per-agent override nor `engine.defaultModel`
|
|
793
|
-
// is set, which the renderer treats as "runtime default" and omits.
|
|
909
|
+
// Keep the configured model separate from the display model. When no model
|
|
910
|
+
// is pinned, the runtime-reported execution model is authoritative.
|
|
794
911
|
const model = shared.resolveAgentModel(a, config.engine || {}) || null;
|
|
912
|
+
const modelDisplay = resolveRuntimeModelDisplay({
|
|
913
|
+
runtime,
|
|
914
|
+
configuredModel: model,
|
|
915
|
+
agentId: a.id,
|
|
916
|
+
dispatch,
|
|
917
|
+
evidence: runtimeModelEvidence,
|
|
918
|
+
});
|
|
795
919
|
const inboxFiles = allInboxFiles.filter(f => f.includes(a.id));
|
|
796
920
|
let steeringInboxFiles = [];
|
|
797
921
|
try { steeringInboxFiles = steering.listUnreadSteeringMessages(a.id); } catch { steeringInboxFiles = []; }
|
|
@@ -826,7 +950,15 @@ function getAgents(config) {
|
|
|
826
950
|
const chartered = fs.existsSync(path.join(AGENTS_DIR, a.id, 'charter.md'));
|
|
827
951
|
if (lastAction.length > 120) lastAction = lastAction.slice(0, 120) + '...';
|
|
828
952
|
return {
|
|
829
|
-
...a,
|
|
953
|
+
...a,
|
|
954
|
+
runtime,
|
|
955
|
+
model,
|
|
956
|
+
displayModel: modelDisplay.model,
|
|
957
|
+
modelSource: modelDisplay.source,
|
|
958
|
+
status: s.status,
|
|
959
|
+
lastAction,
|
|
960
|
+
lastActionPrefix,
|
|
961
|
+
lastActionAt,
|
|
830
962
|
// Descriptive capability tags; tolerate legacy `skills` configs by
|
|
831
963
|
// normalizing to `expertise` for the dashboard/settings UI.
|
|
832
964
|
expertise: a.expertise ?? a.skills ?? [],
|
|
@@ -1896,10 +2028,14 @@ function getWorkItems(config, opts) {
|
|
|
1896
2028
|
}
|
|
1897
2029
|
}
|
|
1898
2030
|
|
|
1899
|
-
// Cross-reference with dispatch pending entries to surface
|
|
2031
|
+
// Cross-reference with dispatch pending entries to surface allocation gates
|
|
2032
|
+
// and skipReason + blockedBy (#617).
|
|
1900
2033
|
const pendingByWiId = new Map();
|
|
1901
2034
|
for (const d of (dispatch.pending || [])) {
|
|
1902
|
-
if (
|
|
2035
|
+
if (
|
|
2036
|
+
d.meta?.item?.id
|
|
2037
|
+
&& (d.skipReason || d._pendingReason || d._worktreeBranchHeld)
|
|
2038
|
+
) {
|
|
1903
2039
|
pendingByWiId.set(_dispatchWorkItemIdentityKey(d), d);
|
|
1904
2040
|
}
|
|
1905
2041
|
}
|
|
@@ -1912,7 +2048,11 @@ function getWorkItems(config, opts) {
|
|
|
1912
2048
|
for (const item of allItems) {
|
|
1913
2049
|
const pendingEntry = pendingByWiId.get(_workItemIdentityKey(item));
|
|
1914
2050
|
if (!pendingEntry) continue;
|
|
1915
|
-
item._skipReason = pendingEntry.skipReason;
|
|
2051
|
+
if (pendingEntry.skipReason) item._skipReason = pendingEntry.skipReason;
|
|
2052
|
+
if (pendingEntry._pendingReason) item._pendingReason = pendingEntry._pendingReason;
|
|
2053
|
+
if (pendingEntry._worktreeBranchHeld) {
|
|
2054
|
+
item._worktreeBranchHeld = pendingEntry._worktreeBranchHeld;
|
|
2055
|
+
}
|
|
1916
2056
|
if (pendingEntry.skipReason === 'branch_locked' && pendingEntry.meta?.branch) {
|
|
1917
2057
|
const blocker = branchToAgent.get(pendingEntry.meta.branch);
|
|
1918
2058
|
if (blocker) item._blockedBy = blocker;
|
|
@@ -1972,6 +2112,9 @@ function getWorkItems(config, opts) {
|
|
|
1972
2112
|
if (key) dispatchByWiId[key] = d.id;
|
|
1973
2113
|
}
|
|
1974
2114
|
const _agentDirCache = {};
|
|
2115
|
+
// W-ms0ytfdd000d5a26 — effective-model resolution inputs, built once per call.
|
|
2116
|
+
const _wiDispatchIndex = buildWorkItemDispatchIndex(dispatch);
|
|
2117
|
+
const _wiModelConfigCache = new Map();
|
|
1975
2118
|
const _inboxFiles = safeReadDir(INBOX_DIR);
|
|
1976
2119
|
const _archiveFiles = safeReadDir(ARCHIVE_DIR);
|
|
1977
2120
|
// Use snapshot — sync access; cold start before any async warm returns [].
|
|
@@ -1999,6 +2142,14 @@ function getWorkItems(config, opts) {
|
|
|
1999
2142
|
for (const item of allItems) {
|
|
2000
2143
|
const arts = shared.isPlainObject(item._artifacts) ? { ...item._artifacts } : {};
|
|
2001
2144
|
const agentId = item.dispatched_to || item.agent;
|
|
2145
|
+
// W-ms0ytfdd000d5a26 — the model this item actually ran on (or, for items
|
|
2146
|
+
// that never ran, the projection from current config). Persisted dispatch
|
|
2147
|
+
// evidence wins so repinning a model later cannot rewrite history.
|
|
2148
|
+
item._model = resolveWorkItemModelInfo(item, {
|
|
2149
|
+
config,
|
|
2150
|
+
dispatchIndex: _wiDispatchIndex,
|
|
2151
|
+
configCache: _wiModelConfigCache,
|
|
2152
|
+
});
|
|
2002
2153
|
if (agentId) {
|
|
2003
2154
|
// Output log — match by dispatch ID (output-{dispatchId}.log)
|
|
2004
2155
|
const dispatchId = dispatchByWiId[_workItemIdentityKey(item)];
|
|
@@ -3268,7 +3419,10 @@ module.exports = {
|
|
|
3268
3419
|
_buildNotesByWiMap, // exported for testing
|
|
3269
3420
|
|
|
3270
3421
|
// Agents
|
|
3271
|
-
getAgentStatus, getAgents, getAgentDetail,
|
|
3422
|
+
getAgentStatus, getAgents, getAgentDetail, buildRuntimeModelEvidence, resolveRuntimeModelDisplay,
|
|
3423
|
+
|
|
3424
|
+
// Effective work-item model (W-ms0ytfdd000d5a26)
|
|
3425
|
+
buildWorkItemDispatchIndex, resolveConfiguredAgentModel, resolveWorkItemModelInfo,
|
|
3272
3426
|
|
|
3273
3427
|
// Pull requests
|
|
3274
3428
|
getPrs, getPullRequests,
|