@yemi33/minions 0.1.2423 → 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 +12 -7
- package/docs/managed-spawn.md +21 -3
- package/docs/named-agents.md +3 -2
- package/docs/plan-lifecycle.md +32 -32
- package/docs/pr-auto-fix-dispatch.md +3 -3
- package/docs/pr-review-fix-loop.md +6 -6
- 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/lifecycle.js
CHANGED
|
@@ -10,12 +10,16 @@ const shared = require('./shared');
|
|
|
10
10
|
const { safeRead, safeJson, safeJsonArr, safeJsonNoRestore, safeWrite, mutateJsonFileLocked, mutateWorkItems, execAsync, getPrLinks,
|
|
11
11
|
log, ts, dateStamp, WI_STATUS, DONE_STATUSES, PLAN_TERMINAL_STATUSES, WORK_TYPE, PLAN_STATUS, PRD_ITEM_STATUS, PRD_MATERIALIZABLE, PR_STATUS, REVIEW_STATUS, RETRY_DELAY_MS, DISPATCH_RESULT,
|
|
12
12
|
ENGINE_DEFAULTS, DEFAULT_AGENT_METRICS, FAILURE_CLASS } = shared;
|
|
13
|
-
const { resolveRuntime } = require('./runtimes');
|
|
13
|
+
const { resolveRuntime, hasUnfinishedToolCall } = require('./runtimes');
|
|
14
14
|
const adoGitAuth = require('./ado-git-auth');
|
|
15
15
|
const queries = require('./queries');
|
|
16
16
|
const harness = require('./harness');
|
|
17
|
+
const reviewLearning = require('./review-learning');
|
|
17
18
|
const smallStateStore = require('./small-state-store');
|
|
18
19
|
const prdStore = require('./prd-store');
|
|
20
|
+
const features = require('./features');
|
|
21
|
+
const memoryStore = require('./memory-store');
|
|
22
|
+
const promotion = require('./promotion');
|
|
19
23
|
const { isBranchActive } = require('./cooldown');
|
|
20
24
|
const { resolveExecutionModel } = require('./execution-model');
|
|
21
25
|
const { worktreeMatchesBranch, getWorktreeBranch, cleanupMergedPrLocalBranch } = require('./cleanup');
|
|
@@ -2563,6 +2567,7 @@ async function updatePrAfterReview(agentId, pr, project, config, resultSummary,
|
|
|
2563
2567
|
// Drop it when reviewer requests changes again — that starts a new fix cycle.
|
|
2564
2568
|
...(target.minionsReview?.fixedAt && postReviewStatus !== REVIEW_STATUS.CHANGES_REQUESTED ? { fixedAt: target.minionsReview.fixedAt } : {}),
|
|
2565
2569
|
};
|
|
2570
|
+
target.minionsReview.findingIdentity = shared.prReviewFindingIdentity(target);
|
|
2566
2571
|
updatedTarget = { ...reviewPr, ...target };
|
|
2567
2572
|
return prs;
|
|
2568
2573
|
}, { defaultValue: [] });
|
|
@@ -2585,6 +2590,9 @@ async function updatePrAfterReview(agentId, pr, project, config, resultSummary,
|
|
|
2585
2590
|
project: reviewProject,
|
|
2586
2591
|
dispatchId: dispatchItem?.id || structuredCompletion?.dispatchId || null,
|
|
2587
2592
|
sourceItem: dispatchItem?.meta?.item?.id || null,
|
|
2593
|
+
postReviewStatus,
|
|
2594
|
+
reviewVerdict: verdictRaw,
|
|
2595
|
+
findingIdentity: shared.prReviewFindingIdentity(updatedTarget),
|
|
2588
2596
|
});
|
|
2589
2597
|
}
|
|
2590
2598
|
}
|
|
@@ -3820,18 +3828,15 @@ function extractSkillsFromOutput(output, agentId, dispatchItem, config, runtimeN
|
|
|
3820
3828
|
const projectSkillRoot = skillWriteTargets(effectiveRuntime, proj).project
|
|
3821
3829
|
|| path.resolve(proj.localPath, '.claude', 'skills');
|
|
3822
3830
|
const projectSkillPath = path.join(projectSkillRoot, skillDirName, 'SKILL.md');
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
description: `Create project-level skill \`${skillDirName}/SKILL.md\` in ${project}.\n\nWrite this file to \`${projectSkillPath}\` via a PR.\n\n## Skill Content\n\n\`\`\`\n${enrichedBlock}\n\`\`\``,
|
|
3830
|
-
priority: 'low', status: WI_STATUS.QUEUED, created: ts(), createdBy: `engine:skill-extraction:${agentName}` });
|
|
3831
|
-
return data;
|
|
3831
|
+
const queued = promotion.queueProjectSkillWorkItem({
|
|
3832
|
+
config,
|
|
3833
|
+
projectName: project,
|
|
3834
|
+
skillName: name,
|
|
3835
|
+
description: `Create project-level skill \`${skillDirName}/SKILL.md\` in ${project}.\n\nWrite this file to \`${projectSkillPath}\` via a PR.\n\n## Skill Content\n\n\`\`\`\n${enrichedBlock}\n\`\`\``,
|
|
3836
|
+
agentName,
|
|
3832
3837
|
});
|
|
3833
|
-
if (
|
|
3834
|
-
log('info', `Queued work item ${
|
|
3838
|
+
if (queued && queued.created && queued.workItemId) {
|
|
3839
|
+
log('info', `Queued work item ${queued.workItemId} to PR project skill "${name}" into ${project}`);
|
|
3835
3840
|
}
|
|
3836
3841
|
}
|
|
3837
3842
|
} else {
|
|
@@ -3904,93 +3909,84 @@ function updateAgentHistory(agentId, dispatchItem, result) {
|
|
|
3904
3909
|
log('info', `Updated history for ${agentId}`);
|
|
3905
3910
|
}
|
|
3906
3911
|
|
|
3907
|
-
function reviewFeedbackSourceMatches({ fileName, content, reviewerAgentId, pr, dispatchItem, structuredCompletion }) {
|
|
3908
|
-
if (String(fileName || '').startsWith('feedback-')) return false;
|
|
3909
|
-
const text = String(content || '');
|
|
3910
|
-
const scopedExpected = [
|
|
3911
|
-
dispatchItem?.id,
|
|
3912
|
-
dispatchItem?.meta?.item?.id,
|
|
3913
|
-
structuredCompletion?.dispatchId,
|
|
3914
|
-
pr?.minionsReview?.dispatchId,
|
|
3915
|
-
pr?.minionsReview?.sourceItem,
|
|
3916
|
-
].filter(Boolean).map(String);
|
|
3917
|
-
if (scopedExpected.length === 0) return true;
|
|
3918
|
-
|
|
3919
|
-
const fileAndContent = `${fileName || ''}\n${text}`;
|
|
3920
|
-
if (!scopedExpected.some(value => fileAndContent.includes(value))) {
|
|
3921
|
-
log('warn', `Skipping review feedback source ${fileName || '(unknown)'} for ${pr?.id || 'unknown PR'}: missing current dispatch/source marker for ${reviewerAgentId}`);
|
|
3922
|
-
return false;
|
|
3923
|
-
}
|
|
3924
|
-
|
|
3925
|
-
const prExpected = [
|
|
3926
|
-
pr?.id,
|
|
3927
|
-
pr?.url,
|
|
3928
|
-
].filter(Boolean).map(String);
|
|
3929
|
-
if (prExpected.some(value => fileAndContent.includes(value))) return true;
|
|
3930
|
-
|
|
3931
|
-
const prNumber = shared.getPrNumber(pr);
|
|
3932
|
-
if (prNumber != null) {
|
|
3933
|
-
const scope = shared.getPrScopeInfo(pr, pr.url || '')?.scope || shared.getProjectPrScope(dispatchItem?.meta?.project) || '';
|
|
3934
|
-
const numberMention = new RegExp(`(?:#|PR[-\\s])${prNumber}(?!\\d)`, 'i').test(fileAndContent);
|
|
3935
|
-
if (numberMention && (!scope || fileAndContent.toLowerCase().includes(scope.toLowerCase()))) return true;
|
|
3936
|
-
}
|
|
3937
|
-
|
|
3938
|
-
log('warn', `Skipping review feedback source ${fileName || '(unknown)'} for ${pr?.id || 'unknown PR'}: not tied to dispatch/PR scope for ${reviewerAgentId}`);
|
|
3939
|
-
return false;
|
|
3940
|
-
}
|
|
3941
|
-
|
|
3942
3912
|
function createReviewFeedbackForAuthor(reviewerAgentId, pr, config, opts = {}) {
|
|
3943
3913
|
|
|
3944
|
-
if (!pr?.id || !pr?.agent) return;
|
|
3914
|
+
if (!pr?.id || !pr?.agent) return null;
|
|
3945
3915
|
const authorAgentId = pr.agent.toLowerCase();
|
|
3946
|
-
if (!config.agents[authorAgentId]) return;
|
|
3947
|
-
|
|
3916
|
+
if (!config.agents?.[authorAgentId]) return null;
|
|
3917
|
+
if (opts.reviewVerdict !== REVIEW_STATUS.CHANGES_REQUESTED) return null;
|
|
3918
|
+
if (!features.isFeatureOn('review-learning-capture', config)) return null;
|
|
3919
|
+
|
|
3948
3920
|
const project = opts.project || opts.dispatchItem?.meta?.project || null;
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
reviewContent = matchedReviewContent.join('\n\n');
|
|
3975
|
-
}
|
|
3976
|
-
const prSlug = shared.safeSlugComponent(pr.id, 60);
|
|
3977
|
-
const content = `# Review Feedback for ${config.agents[authorAgentId]?.name || authorAgentId}\n\n` +
|
|
3978
|
-
`**PR:** ${pr.id} — ${pr.title || ''}\n` +
|
|
3979
|
-
`**Reviewer:** ${config.agents[reviewerAgentId]?.name || reviewerAgentId}\n` +
|
|
3980
|
-
`**Date:** ${today}\n\n` +
|
|
3981
|
-
`## What the reviewer found\n\n${reviewContent}\n\n` +
|
|
3982
|
-
`## Action Required\n\nRead this feedback carefully. When you work on similar tasks in the future, ` +
|
|
3983
|
-
`avoid the patterns flagged here. If you are assigned to fix this PR, ` +
|
|
3984
|
-
`address every point raised above.\n`;
|
|
3985
|
-
shared.writeToInbox('feedback', `${authorAgentId}-from-${reviewerAgentId}-${prSlug}`, content, null, {
|
|
3986
|
-
sourcePr: pr.id,
|
|
3987
|
-
reviewer: reviewerAgentId,
|
|
3988
|
-
author: authorAgentId,
|
|
3989
|
-
dispatchId: opts.dispatchId || opts.dispatchItem?.id || opts.structuredCompletion?.dispatchId || null,
|
|
3990
|
-
sourceItem: opts.sourceItem || opts.dispatchItem?.meta?.item?.id || null,
|
|
3991
|
-
project: project?.name || null,
|
|
3921
|
+
if (!project?.name) {
|
|
3922
|
+
log('warn', `Skipped review-learning candidate for ${pr.id}: project scope is unavailable`);
|
|
3923
|
+
return null;
|
|
3924
|
+
}
|
|
3925
|
+
|
|
3926
|
+
const reviewer = String(reviewerAgentId || '').trim().toLowerCase();
|
|
3927
|
+
const findingIdentity = String(opts.findingIdentity || shared.prReviewFindingIdentity(pr)).trim().toLowerCase();
|
|
3928
|
+
const prId = shared.getCanonicalPrId(project, pr, pr.url || '');
|
|
3929
|
+
const host = String(prId).slice(0, String(prId).indexOf(':')).toLowerCase();
|
|
3930
|
+
const dispatchId = opts.dispatchId || opts.dispatchItem?.id || opts.structuredCompletion?.dispatchId || null;
|
|
3931
|
+
const sourceItem = opts.sourceItem || opts.dispatchItem?.meta?.item?.id || null;
|
|
3932
|
+
const sourceHash = reviewLearning.hashReviewCommentContent(opts.reviewContent || '');
|
|
3933
|
+
const findingKey = reviewLearning.buildFindingKey({
|
|
3934
|
+
project: project.name,
|
|
3935
|
+
prId,
|
|
3936
|
+
sourceKey: `${host}:review-finding:${findingIdentity}`,
|
|
3937
|
+
});
|
|
3938
|
+
const priorCandidate = memoryStore.listMemoryRecords({
|
|
3939
|
+
sourceType: reviewLearning.REVIEW_LEARNING_SOURCE_TYPE,
|
|
3940
|
+
findingKey,
|
|
3941
|
+
limit: 1,
|
|
3942
|
+
})[0] || null;
|
|
3943
|
+
const priorEvents = memoryStore.listMemoryLifecycleEvents({
|
|
3944
|
+
findingKey,
|
|
3945
|
+
limit: 500,
|
|
3992
3946
|
});
|
|
3993
|
-
|
|
3947
|
+
const candidate = reviewLearning.upsertReviewLearningCandidate({
|
|
3948
|
+
findingKey,
|
|
3949
|
+
project: project.name,
|
|
3950
|
+
prId,
|
|
3951
|
+
authorAgent: authorAgentId,
|
|
3952
|
+
reviewerAgent: reviewer,
|
|
3953
|
+
sourceHash,
|
|
3954
|
+
workItemId: sourceItem,
|
|
3955
|
+
dispatchId,
|
|
3956
|
+
metadata: {
|
|
3957
|
+
findingIdentity,
|
|
3958
|
+
postReviewStatus: opts.postReviewStatus || null,
|
|
3959
|
+
reviewVerdict: REVIEW_STATUS.CHANGES_REQUESTED,
|
|
3960
|
+
},
|
|
3961
|
+
});
|
|
3962
|
+
let eventType = 'candidate-created';
|
|
3963
|
+
const hasCreationEvent = priorEvents.some(event => event.eventType === 'candidate-created');
|
|
3964
|
+
const hasCurrentSourceEvent = priorEvents.some(event => (
|
|
3965
|
+
(event.eventType === 'candidate-created' || event.eventType === 'candidate-updated')
|
|
3966
|
+
&& event.metadata.sourceHash === candidate.metadata.sourceHash
|
|
3967
|
+
));
|
|
3968
|
+
if (priorCandidate && hasCreationEvent && candidate.status === 'candidate'
|
|
3969
|
+
&& (candidate.metadata.sourceHash !== priorCandidate.metadata.sourceHash || !hasCurrentSourceEvent)) {
|
|
3970
|
+
eventType = 'candidate-updated';
|
|
3971
|
+
}
|
|
3972
|
+
memoryStore.recordMemoryLifecycleEvent({
|
|
3973
|
+
eventType,
|
|
3974
|
+
memoryId: candidate.id,
|
|
3975
|
+
findingKey,
|
|
3976
|
+
project: project.name,
|
|
3977
|
+
agent: authorAgentId,
|
|
3978
|
+
workItemId: candidate.metadata.workItemId || null,
|
|
3979
|
+
dispatchId: candidate.metadata.dispatchId || null,
|
|
3980
|
+
identity: eventType === 'candidate-updated'
|
|
3981
|
+
? candidate.metadata.sourceHash
|
|
3982
|
+
: (candidate.metadata.findingIdentity || findingIdentity),
|
|
3983
|
+
metadata: {
|
|
3984
|
+
findingIdentity: candidate.metadata.findingIdentity || findingIdentity,
|
|
3985
|
+
sourceHash: candidate.metadata.sourceHash,
|
|
3986
|
+
},
|
|
3987
|
+
});
|
|
3988
|
+
log('info', `Upserted review-learning candidate ${candidate.id} for ${authorAgentId} from ${reviewer} on ${pr.id}`);
|
|
3989
|
+
return candidate;
|
|
3994
3990
|
}
|
|
3995
3991
|
|
|
3996
3992
|
function updateMetrics(agentId, dispatchItem, result, taskUsage, prsCreatedCount, model) {
|
|
@@ -4185,6 +4181,720 @@ function handleInjectionFlag(dispatchItem, agentId, structuredCompletion, config
|
|
|
4185
4181
|
return { description, sources, at };
|
|
4186
4182
|
}
|
|
4187
4183
|
|
|
4184
|
+
function _canonicalReviewLearningSourceKey(value, host, fallbackKind = 'comment') {
|
|
4185
|
+
if (typeof value !== 'string' || !value.trim() || !host) return null;
|
|
4186
|
+
const raw = value.trim().toLowerCase();
|
|
4187
|
+
const candidates = [raw];
|
|
4188
|
+
if (!raw.startsWith(`${host}:`)) {
|
|
4189
|
+
if (host === 'github' && /^(?:issue|review|review-comment|issue-comment):/.test(raw)) {
|
|
4190
|
+
candidates.push(`${host}:${raw}`);
|
|
4191
|
+
} else if (host === 'ado' && /^\d+:\d+$/.test(raw)) {
|
|
4192
|
+
const [threadId, commentId] = raw.split(':');
|
|
4193
|
+
candidates.push(`${host}:thread:${threadId}:comment:${commentId}`);
|
|
4194
|
+
} else {
|
|
4195
|
+
candidates.push(`${host}:${fallbackKind}:${raw}`);
|
|
4196
|
+
}
|
|
4197
|
+
}
|
|
4198
|
+
for (const candidate of candidates) {
|
|
4199
|
+
try {
|
|
4200
|
+
return reviewLearning.normalizeFindingSourceKey(candidate);
|
|
4201
|
+
} catch {}
|
|
4202
|
+
}
|
|
4203
|
+
return null;
|
|
4204
|
+
}
|
|
4205
|
+
|
|
4206
|
+
function buildReviewLearningCompletionContext(dispatchItem, agentId) {
|
|
4207
|
+
const meta = dispatchItem?.meta || {};
|
|
4208
|
+
const pr = meta.pr;
|
|
4209
|
+
const project = meta.project;
|
|
4210
|
+
const canonicalPr = shared.getCanonicalPrId(project, pr, pr?.url || '');
|
|
4211
|
+
const prId = reviewLearning.normalizeCanonicalPrId(canonicalPr);
|
|
4212
|
+
const normalizedProject = reviewLearning.normalizeProjectKey(project?.name || meta.item?.project);
|
|
4213
|
+
const authorAgent = pr?.agent && String(pr.agent).toLowerCase() !== 'human'
|
|
4214
|
+
? pr.agent
|
|
4215
|
+
: (meta.item?.authorAgent || dispatchItem?.agent || agentId);
|
|
4216
|
+
const host = prId.slice(0, prId.indexOf(':'));
|
|
4217
|
+
const allowedSourceKeys = new Set();
|
|
4218
|
+
const addSourceKey = (value, fallbackKind) => {
|
|
4219
|
+
const sourceKey = _canonicalReviewLearningSourceKey(value, host, fallbackKind);
|
|
4220
|
+
if (sourceKey) allowedSourceKeys.add(sourceKey);
|
|
4221
|
+
};
|
|
4222
|
+
const addSourceEntries = (entries) => {
|
|
4223
|
+
if (!Array.isArray(entries)) return;
|
|
4224
|
+
for (const entry of entries) {
|
|
4225
|
+
if (typeof entry === 'string') {
|
|
4226
|
+
addSourceKey(entry);
|
|
4227
|
+
continue;
|
|
4228
|
+
}
|
|
4229
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue;
|
|
4230
|
+
addSourceKey(entry.sourceKey || entry.commentKey || entry.key || entry.id);
|
|
4231
|
+
}
|
|
4232
|
+
};
|
|
4233
|
+
|
|
4234
|
+
const findingIds = [
|
|
4235
|
+
meta.reviewFindingId,
|
|
4236
|
+
meta.review_finding_id,
|
|
4237
|
+
pr?.minionsReview ? shared.prReviewFindingIdentity(pr) : null,
|
|
4238
|
+
].filter(value => typeof value === 'string' && value.trim());
|
|
4239
|
+
for (const findingId of findingIds) addSourceKey(`${host}:review-finding:${findingId}`, 'review-finding');
|
|
4240
|
+
|
|
4241
|
+
addSourceEntries(meta.reviewLearningSourceKeys);
|
|
4242
|
+
addSourceEntries(meta.commentSources);
|
|
4243
|
+
addSourceEntries(meta.item?.meta?.commentSources);
|
|
4244
|
+
addSourceEntries(pr?.humanFeedback?.commentSources);
|
|
4245
|
+
addSourceKey(pr?.humanFeedback?.lastProcessedCommentKey);
|
|
4246
|
+
if (!pr?.humanFeedback?.lastProcessedCommentKey) {
|
|
4247
|
+
addSourceKey(pr?.humanFeedback?.lastProcessedCommentId);
|
|
4248
|
+
}
|
|
4249
|
+
|
|
4250
|
+
return {
|
|
4251
|
+
project: normalizedProject,
|
|
4252
|
+
prId,
|
|
4253
|
+
authorAgent,
|
|
4254
|
+
allowedSourceKeys,
|
|
4255
|
+
};
|
|
4256
|
+
}
|
|
4257
|
+
|
|
4258
|
+
function suppressReviewLearningCompletionPayload(structuredCompletion) {
|
|
4259
|
+
if (!structuredCompletion || typeof structuredCompletion !== 'object') return;
|
|
4260
|
+
delete structuredCompletion.reviewLearnings;
|
|
4261
|
+
delete structuredCompletion.reviewLearningApplications;
|
|
4262
|
+
delete structuredCompletion._reviewLearningDiagnostics;
|
|
4263
|
+
}
|
|
4264
|
+
|
|
4265
|
+
function normalizeReviewLearningCompletionPayload(dispatchItem, agentId, structuredCompletion) {
|
|
4266
|
+
const diagnostics = [];
|
|
4267
|
+
if (!structuredCompletion || typeof structuredCompletion !== 'object') {
|
|
4268
|
+
return { structuredCompletion, diagnostics };
|
|
4269
|
+
}
|
|
4270
|
+
const rejectField = (field, err) => {
|
|
4271
|
+
delete structuredCompletion[field];
|
|
4272
|
+
const detail = reviewLearning.redactBoundedText(
|
|
4273
|
+
err?.message || String(err),
|
|
4274
|
+
500,
|
|
4275
|
+
`${field} diagnostic`,
|
|
4276
|
+
);
|
|
4277
|
+
diagnostics.push(`${field} rejected: ${detail || 'invalid payload'}`);
|
|
4278
|
+
};
|
|
4279
|
+
|
|
4280
|
+
if (Object.prototype.hasOwnProperty.call(structuredCompletion, 'reviewLearnings')) {
|
|
4281
|
+
try {
|
|
4282
|
+
const context = buildReviewLearningCompletionContext(dispatchItem, agentId);
|
|
4283
|
+
structuredCompletion.reviewLearnings = reviewLearning.normalizeCompletionReviewLearnings(
|
|
4284
|
+
structuredCompletion.reviewLearnings,
|
|
4285
|
+
context,
|
|
4286
|
+
);
|
|
4287
|
+
} catch (err) {
|
|
4288
|
+
rejectField('reviewLearnings', err);
|
|
4289
|
+
}
|
|
4290
|
+
}
|
|
4291
|
+
if (Object.prototype.hasOwnProperty.call(structuredCompletion, 'reviewLearningApplications')) {
|
|
4292
|
+
try {
|
|
4293
|
+
structuredCompletion.reviewLearningApplications =
|
|
4294
|
+
reviewLearning.normalizeCompletionReviewLearningApplications(
|
|
4295
|
+
structuredCompletion.reviewLearningApplications,
|
|
4296
|
+
);
|
|
4297
|
+
} catch (err) {
|
|
4298
|
+
rejectField('reviewLearningApplications', err);
|
|
4299
|
+
}
|
|
4300
|
+
}
|
|
4301
|
+
if (diagnostics.length) structuredCompletion._reviewLearningDiagnostics = diagnostics;
|
|
4302
|
+
return { structuredCompletion, diagnostics };
|
|
4303
|
+
}
|
|
4304
|
+
|
|
4305
|
+
function recordReviewLearningCompletionDiagnostics(dispatchItem, agentId, diagnostics) {
|
|
4306
|
+
if (!Array.isArray(diagnostics) || diagnostics.length === 0) return;
|
|
4307
|
+
const messages = diagnostics.slice(0, reviewLearning.REVIEW_LEARNING_BOUNDS.diagnostics)
|
|
4308
|
+
.map(message => reviewLearning.redactBoundedText(
|
|
4309
|
+
message,
|
|
4310
|
+
reviewLearning.REVIEW_LEARNING_BOUNDS.diagnosticBytes,
|
|
4311
|
+
'review-learning completion diagnostic',
|
|
4312
|
+
));
|
|
4313
|
+
for (const message of messages) {
|
|
4314
|
+
log('warn', `Review-learning completion diagnostic for ${dispatchItem?.id || 'unknown'}: ${message}`);
|
|
4315
|
+
}
|
|
4316
|
+
try {
|
|
4317
|
+
const wiScope = resolveWorkItemScope(dispatchItem?.meta)
|
|
4318
|
+
|| dispatchItem?.meta?.project?.name
|
|
4319
|
+
|| null;
|
|
4320
|
+
const itemId = dispatchItem?.meta?.item?.id;
|
|
4321
|
+
if (!wiScope || !itemId) return;
|
|
4322
|
+
mutateWorkItems(wiScope, (items) => {
|
|
4323
|
+
if (!Array.isArray(items)) return items;
|
|
4324
|
+
const item = items.find(candidate => candidate.id === itemId);
|
|
4325
|
+
if (item) {
|
|
4326
|
+
const priorMessages = Array.isArray(item._reviewLearningDiagnostics?.messages)
|
|
4327
|
+
? item._reviewLearningDiagnostics.messages
|
|
4328
|
+
: [];
|
|
4329
|
+
item._reviewLearningDiagnostics = {
|
|
4330
|
+
at: ts(),
|
|
4331
|
+
dispatchId: dispatchItem.id || null,
|
|
4332
|
+
agent: agentId || dispatchItem.agent || null,
|
|
4333
|
+
messages: [...new Set([...priorMessages, ...messages])]
|
|
4334
|
+
.slice(-reviewLearning.REVIEW_LEARNING_BOUNDS.diagnostics),
|
|
4335
|
+
};
|
|
4336
|
+
}
|
|
4337
|
+
return items;
|
|
4338
|
+
});
|
|
4339
|
+
} catch (err) {
|
|
4340
|
+
log('warn', `Could not persist review-learning completion diagnostic: ${err.message}`);
|
|
4341
|
+
}
|
|
4342
|
+
}
|
|
4343
|
+
|
|
4344
|
+
function _groupReviewLearningEntries(entries) {
|
|
4345
|
+
const groups = new Map();
|
|
4346
|
+
for (const entry of entries) {
|
|
4347
|
+
const findingKey = reviewLearning.buildFindingKey({
|
|
4348
|
+
project: entry.project,
|
|
4349
|
+
prId: entry.source.pr,
|
|
4350
|
+
sourceKey: entry.source.sourceKey,
|
|
4351
|
+
});
|
|
4352
|
+
let group = groups.get(findingKey);
|
|
4353
|
+
if (!group) {
|
|
4354
|
+
group = {
|
|
4355
|
+
findingKey,
|
|
4356
|
+
project: entry.project,
|
|
4357
|
+
prId: entry.source.pr,
|
|
4358
|
+
sourceKey: entry.source.sourceKey,
|
|
4359
|
+
authorAgent: entry.authorAgent,
|
|
4360
|
+
disposition: entry.disposition,
|
|
4361
|
+
audience: entry.applicability,
|
|
4362
|
+
rules: [],
|
|
4363
|
+
files: [],
|
|
4364
|
+
symbols: [],
|
|
4365
|
+
tags: [],
|
|
4366
|
+
evidence: [],
|
|
4367
|
+
confidences: [],
|
|
4368
|
+
};
|
|
4369
|
+
groups.set(findingKey, group);
|
|
4370
|
+
}
|
|
4371
|
+
if (group.disposition !== entry.disposition) {
|
|
4372
|
+
group.error = `review learning source ${entry.source.sourceKey} has conflicting dispositions`;
|
|
4373
|
+
}
|
|
4374
|
+
if (group.audience !== entry.applicability) {
|
|
4375
|
+
group.error = `review learning source ${entry.source.sourceKey} has conflicting audiences`;
|
|
4376
|
+
}
|
|
4377
|
+
group.rules.push(entry.rule);
|
|
4378
|
+
group.files.push(...entry.files);
|
|
4379
|
+
group.symbols.push(...entry.symbols);
|
|
4380
|
+
group.tags.push(...entry.tags);
|
|
4381
|
+
group.evidence.push(...entry.evidence);
|
|
4382
|
+
group.confidences.push(entry.confidence);
|
|
4383
|
+
}
|
|
4384
|
+
return [...groups.values()].map((group) => {
|
|
4385
|
+
const facets = reviewLearning.normalizeReviewFacets(group);
|
|
4386
|
+
return {
|
|
4387
|
+
...group,
|
|
4388
|
+
rules: [...new Set(group.rules)].sort((a, b) => a.localeCompare(b)),
|
|
4389
|
+
files: facets.files,
|
|
4390
|
+
symbols: facets.symbols,
|
|
4391
|
+
tags: facets.tags,
|
|
4392
|
+
area: facets.area,
|
|
4393
|
+
evidence: [...new Set(group.evidence)].sort((a, b) => a.localeCompare(b)),
|
|
4394
|
+
confidence: Math.min(...group.confidences),
|
|
4395
|
+
};
|
|
4396
|
+
}).sort((a, b) => a.findingKey.localeCompare(b.findingKey));
|
|
4397
|
+
}
|
|
4398
|
+
|
|
4399
|
+
function _acceptedFixedEvidence(dispatchItem, branchChange, structuredCompletion) {
|
|
4400
|
+
if (shared.isFixLikeWorkType(dispatchItem?.type)
|
|
4401
|
+
&& branchChange?.changed === true
|
|
4402
|
+
&& branchChange.evidence === 'remote-head'
|
|
4403
|
+
&& String(branchChange.afterHead || '').trim()) {
|
|
4404
|
+
return {
|
|
4405
|
+
verifiedBy: 'remote-head',
|
|
4406
|
+
verifiedHeadSha: String(branchChange.afterHead).trim(),
|
|
4407
|
+
};
|
|
4408
|
+
}
|
|
4409
|
+
if (dispatchItem?.type !== WORK_TYPE.REVIEW
|
|
4410
|
+
|| reviewVerdictFromCompletion(structuredCompletion) !== REVIEW_STATUS.APPROVED) {
|
|
4411
|
+
return null;
|
|
4412
|
+
}
|
|
4413
|
+
const reviewerAgent = String(dispatchItem?.agent || '').trim().toLowerCase();
|
|
4414
|
+
const authorAgent = String(dispatchItem?.meta?.pr?.agent || '').trim().toLowerCase();
|
|
4415
|
+
if (reviewerAgent && authorAgent && reviewerAgent === authorAgent) return null;
|
|
4416
|
+
const reviewedHeadSha = String(dispatchItem?.meta?.reviewHeadSha || '').trim();
|
|
4417
|
+
const currentHeadSha = getPrFixBaselineHead(dispatchItem?.meta?.pr);
|
|
4418
|
+
if (!reviewedHeadSha || !currentHeadSha || reviewedHeadSha !== currentHeadSha) return null;
|
|
4419
|
+
return { verifiedBy: 'approved-current-head', verifiedHeadSha: currentHeadSha };
|
|
4420
|
+
}
|
|
4421
|
+
|
|
4422
|
+
function _followUpForReviewSource(sourceKey, followups) {
|
|
4423
|
+
const source = String(sourceKey || '').trim().toLowerCase();
|
|
4424
|
+
const aliases = new Set([source, source.slice(source.lastIndexOf(':') + 1)]);
|
|
4425
|
+
const ado = source.match(/^ado:thread:([^:]+):comment:([^:]+)$/);
|
|
4426
|
+
if (ado) aliases.add(`${ado[1]}:${ado[2]}`);
|
|
4427
|
+
const matches = [];
|
|
4428
|
+
for (const entry of Array.isArray(followups) ? followups : []) {
|
|
4429
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue;
|
|
4430
|
+
const parent = String(entry.parent_comment_id ?? entry.parentCommentId ?? '').trim().toLowerCase();
|
|
4431
|
+
const workItemId = String(entry.wi_id ?? entry.wiId ?? '').trim();
|
|
4432
|
+
if (!aliases.has(parent) || !/^[a-z0-9][a-z0-9._:-]{0,319}$/i.test(workItemId)) continue;
|
|
4433
|
+
matches.push({ workItemId, parent });
|
|
4434
|
+
}
|
|
4435
|
+
matches.sort((a, b) => a.workItemId.localeCompare(b.workItemId));
|
|
4436
|
+
return matches[0] || null;
|
|
4437
|
+
}
|
|
4438
|
+
|
|
4439
|
+
function _captureReviewLearningDiagnostic(result, sourceKey, error) {
|
|
4440
|
+
if (result.diagnostics.length >= reviewLearning.REVIEW_LEARNING_BOUNDS.diagnostics) return;
|
|
4441
|
+
const detail = reviewLearning.redactBoundedText(
|
|
4442
|
+
error?.message || String(error),
|
|
4443
|
+
500,
|
|
4444
|
+
'review-learning capture failure',
|
|
4445
|
+
);
|
|
4446
|
+
const prefix = sourceKey ? `Review-learning capture for ${sourceKey} failed: ` : 'Review-learning capture failed: ';
|
|
4447
|
+
result.diagnostics.push(reviewLearning.redactBoundedText(
|
|
4448
|
+
`${prefix}${detail || 'unknown persistence error'}`,
|
|
4449
|
+
reviewLearning.REVIEW_LEARNING_BOUNDS.diagnosticBytes,
|
|
4450
|
+
'review-learning capture diagnostic',
|
|
4451
|
+
));
|
|
4452
|
+
}
|
|
4453
|
+
|
|
4454
|
+
function _surfaceReviewLearningCaptureDiagnostics(
|
|
4455
|
+
dispatchItem,
|
|
4456
|
+
agentId,
|
|
4457
|
+
structuredCompletion,
|
|
4458
|
+
diagnostics,
|
|
4459
|
+
) {
|
|
4460
|
+
if (!diagnostics.length) return;
|
|
4461
|
+
structuredCompletion._reviewLearningDiagnostics = [...new Set([
|
|
4462
|
+
...(Array.isArray(structuredCompletion._reviewLearningDiagnostics)
|
|
4463
|
+
? structuredCompletion._reviewLearningDiagnostics
|
|
4464
|
+
: []),
|
|
4465
|
+
...diagnostics,
|
|
4466
|
+
])].slice(-reviewLearning.REVIEW_LEARNING_BOUNDS.diagnostics);
|
|
4467
|
+
recordReviewLearningCompletionDiagnostics(dispatchItem, agentId, diagnostics);
|
|
4468
|
+
}
|
|
4469
|
+
|
|
4470
|
+
function _recordReviewLearningCreatedEvent(lesson, group, provenance) {
|
|
4471
|
+
return memoryStore.recordMemoryLifecycleEvent({
|
|
4472
|
+
eventType: 'created',
|
|
4473
|
+
memoryId: lesson.id,
|
|
4474
|
+
findingKey: group.findingKey,
|
|
4475
|
+
project: group.project,
|
|
4476
|
+
agent: group.authorAgent,
|
|
4477
|
+
area: group.area,
|
|
4478
|
+
disposition: group.disposition,
|
|
4479
|
+
identity: lesson.id,
|
|
4480
|
+
metadata: {
|
|
4481
|
+
sourceKey: group.sourceKey,
|
|
4482
|
+
prId: group.prId,
|
|
4483
|
+
audience: group.audience,
|
|
4484
|
+
workItemId: provenance.workItemId,
|
|
4485
|
+
dispatchId: provenance.dispatchId,
|
|
4486
|
+
verifiedBy: provenance.verifiedBy,
|
|
4487
|
+
verifiedHeadSha: provenance.verifiedHeadSha,
|
|
4488
|
+
},
|
|
4489
|
+
});
|
|
4490
|
+
}
|
|
4491
|
+
|
|
4492
|
+
function _recordRepeatedFindingEvent(lesson, group, match, provenance) {
|
|
4493
|
+
return memoryStore.recordMemoryLifecycleEvent({
|
|
4494
|
+
eventType: 'repeated-finding',
|
|
4495
|
+
memoryId: lesson.id,
|
|
4496
|
+
findingKey: group.findingKey,
|
|
4497
|
+
project: group.project,
|
|
4498
|
+
agent: group.authorAgent,
|
|
4499
|
+
area: group.area,
|
|
4500
|
+
disposition: group.disposition,
|
|
4501
|
+
identity: match.lessonId,
|
|
4502
|
+
metadata: {
|
|
4503
|
+
priorLessonId: match.lessonId,
|
|
4504
|
+
matchReason: match.reason,
|
|
4505
|
+
sourceKey: group.sourceKey,
|
|
4506
|
+
workItemId: provenance.workItemId,
|
|
4507
|
+
dispatchId: provenance.dispatchId,
|
|
4508
|
+
},
|
|
4509
|
+
});
|
|
4510
|
+
}
|
|
4511
|
+
|
|
4512
|
+
function captureVerifiedReviewLearnings({
|
|
4513
|
+
dispatchItem,
|
|
4514
|
+
agentId,
|
|
4515
|
+
config,
|
|
4516
|
+
structuredCompletion,
|
|
4517
|
+
branchChange,
|
|
4518
|
+
reviewFixResolution,
|
|
4519
|
+
} = {}) {
|
|
4520
|
+
const result = { lessons: [], transitions: [], repeated: [], diagnostics: [] };
|
|
4521
|
+
const rawLearnings = structuredCompletion?.reviewLearnings;
|
|
4522
|
+
const supportedType = dispatchItem?.type === WORK_TYPE.REVIEW
|
|
4523
|
+
|| shared.isFixLikeWorkType(dispatchItem?.type);
|
|
4524
|
+
if (!supportedType || !Array.isArray(rawLearnings) || rawLearnings.length === 0) return result;
|
|
4525
|
+
if (!features.isFeatureOn('review-learning-capture', config || {})) return result;
|
|
4526
|
+
|
|
4527
|
+
let groups;
|
|
4528
|
+
try {
|
|
4529
|
+
const context = buildReviewLearningCompletionContext(dispatchItem, agentId);
|
|
4530
|
+
// Revalidate at the persistence boundary so future callers cannot bypass source binding.
|
|
4531
|
+
const normalized = reviewLearning.normalizeCompletionReviewLearnings(rawLearnings, context);
|
|
4532
|
+
groups = _groupReviewLearningEntries(normalized);
|
|
4533
|
+
} catch (err) {
|
|
4534
|
+
_captureReviewLearningDiagnostic(result, null, err);
|
|
4535
|
+
_surfaceReviewLearningCaptureDiagnostics(
|
|
4536
|
+
dispatchItem,
|
|
4537
|
+
agentId,
|
|
4538
|
+
structuredCompletion,
|
|
4539
|
+
result.diagnostics,
|
|
4540
|
+
);
|
|
4541
|
+
return result;
|
|
4542
|
+
}
|
|
4543
|
+
|
|
4544
|
+
let priorLessons;
|
|
4545
|
+
try {
|
|
4546
|
+
priorLessons = memoryStore.listActiveReviewLearnings({
|
|
4547
|
+
project: groups[0]?.project,
|
|
4548
|
+
limit: 500,
|
|
4549
|
+
});
|
|
4550
|
+
} catch (err) {
|
|
4551
|
+
_captureReviewLearningDiagnostic(result, null, err);
|
|
4552
|
+
_surfaceReviewLearningCaptureDiagnostics(
|
|
4553
|
+
dispatchItem,
|
|
4554
|
+
agentId,
|
|
4555
|
+
structuredCompletion,
|
|
4556
|
+
result.diagnostics,
|
|
4557
|
+
);
|
|
4558
|
+
return result;
|
|
4559
|
+
}
|
|
4560
|
+
const workItemId = dispatchItem?.meta?.item?.id || null;
|
|
4561
|
+
const dispatchId = dispatchItem?.id || null;
|
|
4562
|
+
const acceptedFixedEvidence = _acceptedFixedEvidence(
|
|
4563
|
+
dispatchItem,
|
|
4564
|
+
branchChange,
|
|
4565
|
+
structuredCompletion,
|
|
4566
|
+
);
|
|
4567
|
+
|
|
4568
|
+
for (const group of groups) {
|
|
4569
|
+
try {
|
|
4570
|
+
if (group.error) throw new TypeError(group.error);
|
|
4571
|
+
const evidence = [...group.evidence];
|
|
4572
|
+
const resolutionFindingId = String(reviewFixResolution?.findingId || '').trim().toLowerCase();
|
|
4573
|
+
const resolutionSourceKey = resolutionFindingId
|
|
4574
|
+
? `${group.prId.slice(0, group.prId.indexOf(':'))}:review-finding:${resolutionFindingId}`
|
|
4575
|
+
: '';
|
|
4576
|
+
if (resolutionSourceKey === group.sourceKey && reviewFixResolution?.currentCodeEvidence) {
|
|
4577
|
+
evidence.push(String(reviewFixResolution.currentCodeEvidence));
|
|
4578
|
+
}
|
|
4579
|
+
const evidenceKinds = reviewLearning.classifyExactEvidence(evidence);
|
|
4580
|
+
let proof = null;
|
|
4581
|
+
if (group.disposition === reviewLearning.REVIEW_LEARNING_DISPOSITIONS.ACCEPTED_FIXED) {
|
|
4582
|
+
proof = acceptedFixedEvidence;
|
|
4583
|
+
if (!proof) {
|
|
4584
|
+
throw new TypeError('accepted-fixed requires a verified remote branch advancement or approved current head');
|
|
4585
|
+
}
|
|
4586
|
+
} else if (group.disposition === reviewLearning.REVIEW_LEARNING_DISPOSITIONS.SATISFIED_ELSEWHERE) {
|
|
4587
|
+
if (evidenceKinds.length === 0) {
|
|
4588
|
+
throw new TypeError('satisfied-elsewhere requires exact current-code, commit, or test evidence');
|
|
4589
|
+
}
|
|
4590
|
+
proof = { verifiedBy: `exact-${evidenceKinds.join('+')}`, verifiedHeadSha: null };
|
|
4591
|
+
} else if ((group.disposition === reviewLearning.REVIEW_LEARNING_DISPOSITIONS.INVALID
|
|
4592
|
+
|| group.disposition === reviewLearning.REVIEW_LEARNING_DISPOSITIONS.STALE)
|
|
4593
|
+
&& evidenceKinds.length === 0) {
|
|
4594
|
+
throw new TypeError(`${group.disposition} requires exact trace evidence`);
|
|
4595
|
+
}
|
|
4596
|
+
|
|
4597
|
+
const provenance = {
|
|
4598
|
+
sourceKey: group.sourceKey,
|
|
4599
|
+
workItemId,
|
|
4600
|
+
dispatchId,
|
|
4601
|
+
captureAgent: agentId || dispatchItem?.agent || null,
|
|
4602
|
+
verifiedBy: proof?.verifiedBy || (evidenceKinds.length ? `exact-${evidenceKinds.join('+')}` : 'disposition'),
|
|
4603
|
+
verifiedHeadSha: proof?.verifiedHeadSha || null,
|
|
4604
|
+
evidenceKinds,
|
|
4605
|
+
};
|
|
4606
|
+
const positive = reviewLearning.POSITIVE_DISPOSITIONS.has(group.disposition);
|
|
4607
|
+
if (!positive) {
|
|
4608
|
+
const transitionOptions = {
|
|
4609
|
+
evidence: group.evidence.slice(0, reviewLearning.REVIEW_LEARNING_BOUNDS.transitionEvidence),
|
|
4610
|
+
evidenceKinds,
|
|
4611
|
+
workItemId,
|
|
4612
|
+
dispatchId,
|
|
4613
|
+
};
|
|
4614
|
+
if (group.disposition === reviewLearning.REVIEW_LEARNING_DISPOSITIONS.OUT_OF_SCOPE_FOLLOW_UP) {
|
|
4615
|
+
const followUp = _followUpForReviewSource(group.sourceKey, structuredCompletion.followups);
|
|
4616
|
+
if (!followUp) {
|
|
4617
|
+
throw new TypeError('out-of-scope-follow-up requires a linked follow-up work item');
|
|
4618
|
+
}
|
|
4619
|
+
transitionOptions.followUpWorkItemId = followUp.workItemId;
|
|
4620
|
+
transitionOptions.followUpSourceKey = group.sourceKey;
|
|
4621
|
+
transitionOptions.followUpParentCommentId = followUp.parent;
|
|
4622
|
+
}
|
|
4623
|
+
result.transitions.push(...reviewLearning.transitionReviewLearningCandidate(
|
|
4624
|
+
group.findingKey,
|
|
4625
|
+
group.disposition,
|
|
4626
|
+
transitionOptions,
|
|
4627
|
+
));
|
|
4628
|
+
continue;
|
|
4629
|
+
}
|
|
4630
|
+
|
|
4631
|
+
const repeatedMatch = reviewLearning.findDeterministicRepeatedLessonMatch(
|
|
4632
|
+
group,
|
|
4633
|
+
priorLessons.filter(record => record.sourceRef !== group.findingKey),
|
|
4634
|
+
);
|
|
4635
|
+
const lesson = reviewLearning.upsertReviewLearningLesson({
|
|
4636
|
+
findingKey: group.findingKey,
|
|
4637
|
+
project: group.project,
|
|
4638
|
+
prId: group.prId,
|
|
4639
|
+
authorAgent: group.authorAgent,
|
|
4640
|
+
audience: group.audience,
|
|
4641
|
+
disposition: group.disposition,
|
|
4642
|
+
rules: group.rules,
|
|
4643
|
+
files: group.files,
|
|
4644
|
+
symbols: group.symbols,
|
|
4645
|
+
tags: group.tags,
|
|
4646
|
+
evidence: group.evidence,
|
|
4647
|
+
confidence: group.confidence,
|
|
4648
|
+
metadata: provenance,
|
|
4649
|
+
});
|
|
4650
|
+
result.lessons.push(lesson);
|
|
4651
|
+
result.transitions.push(...reviewLearning.transitionReviewLearningCandidate(
|
|
4652
|
+
group.findingKey,
|
|
4653
|
+
group.disposition,
|
|
4654
|
+
{
|
|
4655
|
+
lessonId: lesson.id,
|
|
4656
|
+
evidence: group.evidence.slice(0, reviewLearning.REVIEW_LEARNING_BOUNDS.transitionEvidence),
|
|
4657
|
+
evidenceKinds,
|
|
4658
|
+
workItemId,
|
|
4659
|
+
dispatchId,
|
|
4660
|
+
verifiedBy: provenance.verifiedBy,
|
|
4661
|
+
verifiedHeadSha: provenance.verifiedHeadSha,
|
|
4662
|
+
},
|
|
4663
|
+
));
|
|
4664
|
+
try {
|
|
4665
|
+
_recordReviewLearningCreatedEvent(lesson, group, provenance);
|
|
4666
|
+
} catch (err) {
|
|
4667
|
+
_captureReviewLearningDiagnostic(result, group.sourceKey, err);
|
|
4668
|
+
}
|
|
4669
|
+
if (repeatedMatch) {
|
|
4670
|
+
try {
|
|
4671
|
+
const event = _recordRepeatedFindingEvent(
|
|
4672
|
+
lesson,
|
|
4673
|
+
group,
|
|
4674
|
+
repeatedMatch,
|
|
4675
|
+
provenance,
|
|
4676
|
+
);
|
|
4677
|
+
result.repeated.push(event);
|
|
4678
|
+
} catch (err) {
|
|
4679
|
+
_captureReviewLearningDiagnostic(result, group.sourceKey, err);
|
|
4680
|
+
}
|
|
4681
|
+
}
|
|
4682
|
+
} catch (err) {
|
|
4683
|
+
_captureReviewLearningDiagnostic(result, group.sourceKey, err);
|
|
4684
|
+
}
|
|
4685
|
+
}
|
|
4686
|
+
|
|
4687
|
+
_surfaceReviewLearningCaptureDiagnostics(
|
|
4688
|
+
dispatchItem,
|
|
4689
|
+
agentId,
|
|
4690
|
+
structuredCompletion,
|
|
4691
|
+
result.diagnostics,
|
|
4692
|
+
);
|
|
4693
|
+
return result;
|
|
4694
|
+
}
|
|
4695
|
+
|
|
4696
|
+
// P-bfc75585 — Application & contradiction lifecycle. Process
|
|
4697
|
+
// `reviewLearningApplications` from a completion, but only against lesson IDs
|
|
4698
|
+
// this exact dispatch was shown (the trusted retrieval trace) and only for
|
|
4699
|
+
// lessons in the dispatch's own project. Emits idempotent applied /
|
|
4700
|
+
// considered-not-applicable / contradicted events; an evidence-backed
|
|
4701
|
+
// contradiction additionally quarantines the lesson through the retracted
|
|
4702
|
+
// lifecycle and raises an operator alert. `considered-not-applicable` and
|
|
4703
|
+
// `applied` are task-local and never mutate the lesson.
|
|
4704
|
+
function _resolveDispatchProject(dispatchItem) {
|
|
4705
|
+
const meta = dispatchItem?.meta || {};
|
|
4706
|
+
const raw = meta.project?.name || meta.item?.project || '';
|
|
4707
|
+
if (!raw) return '';
|
|
4708
|
+
try {
|
|
4709
|
+
return reviewLearning.normalizeProjectKey(raw);
|
|
4710
|
+
} catch {
|
|
4711
|
+
return '';
|
|
4712
|
+
}
|
|
4713
|
+
}
|
|
4714
|
+
|
|
4715
|
+
function processReviewLearningApplications({
|
|
4716
|
+
dispatchItem,
|
|
4717
|
+
agentId,
|
|
4718
|
+
config,
|
|
4719
|
+
structuredCompletion,
|
|
4720
|
+
} = {}) {
|
|
4721
|
+
const result = {
|
|
4722
|
+
applied: [],
|
|
4723
|
+
notApplicable: [],
|
|
4724
|
+
contradicted: [],
|
|
4725
|
+
retracted: [],
|
|
4726
|
+
diagnostics: [],
|
|
4727
|
+
};
|
|
4728
|
+
const rawApplications = structuredCompletion?.reviewLearningApplications;
|
|
4729
|
+
if (!Array.isArray(rawApplications) || rawApplications.length === 0) return result;
|
|
4730
|
+
// Applications describe how recalled lessons fared, so they are only
|
|
4731
|
+
// meaningful when the recall path is active.
|
|
4732
|
+
if (!features.isFeatureOn('review-learning-recall', config || {})) return result;
|
|
4733
|
+
|
|
4734
|
+
let applications;
|
|
4735
|
+
try {
|
|
4736
|
+
// Revalidate at the persistence boundary so no caller can bypass bounds.
|
|
4737
|
+
applications = reviewLearning.normalizeCompletionReviewLearningApplications(rawApplications);
|
|
4738
|
+
} catch (err) {
|
|
4739
|
+
_captureReviewLearningDiagnostic(result, null, err);
|
|
4740
|
+
_surfaceReviewLearningCaptureDiagnostics(dispatchItem, agentId, structuredCompletion, result.diagnostics);
|
|
4741
|
+
return result;
|
|
4742
|
+
}
|
|
4743
|
+
|
|
4744
|
+
const project = _resolveDispatchProject(dispatchItem);
|
|
4745
|
+
if (!project) {
|
|
4746
|
+
_captureReviewLearningDiagnostic(result, null, new TypeError('review learning applications require a resolved project'));
|
|
4747
|
+
_surfaceReviewLearningCaptureDiagnostics(dispatchItem, agentId, structuredCompletion, result.diagnostics);
|
|
4748
|
+
return result;
|
|
4749
|
+
}
|
|
4750
|
+
const dispatchId = dispatchItem?.id || null;
|
|
4751
|
+
const workItemId = dispatchItem?.meta?.item?.id || null;
|
|
4752
|
+
const applyingAgent = agentId || dispatchItem?.agent || null;
|
|
4753
|
+
|
|
4754
|
+
// Trusted binding: only the lesson IDs actually recalled for THIS dispatch
|
|
4755
|
+
// may be reported on. An agent cannot report application (or trigger a
|
|
4756
|
+
// contradiction) for a lesson it was never shown.
|
|
4757
|
+
const shownIds = new Set();
|
|
4758
|
+
try {
|
|
4759
|
+
const trace = memoryStore.getLatestRetrievalTrace({
|
|
4760
|
+
dispatchId,
|
|
4761
|
+
retrievalKind: 'review-learning',
|
|
4762
|
+
project,
|
|
4763
|
+
});
|
|
4764
|
+
for (const id of trace?.selectedIds || []) shownIds.add(String(id).trim());
|
|
4765
|
+
} catch (err) {
|
|
4766
|
+
_captureReviewLearningDiagnostic(result, null, err);
|
|
4767
|
+
}
|
|
4768
|
+
|
|
4769
|
+
for (const application of applications) {
|
|
4770
|
+
try {
|
|
4771
|
+
if (!shownIds.has(application.id)) {
|
|
4772
|
+
throw new TypeError(`review learning ${application.id} was not shown to this dispatch`);
|
|
4773
|
+
}
|
|
4774
|
+
const lesson = memoryStore.getMemoryRecord(application.id);
|
|
4775
|
+
if (!lesson || lesson.sourceType !== reviewLearning.REVIEW_LEARNING_SOURCE_TYPE) {
|
|
4776
|
+
throw new TypeError(`review learning ${application.id} is not a review-learning record`);
|
|
4777
|
+
}
|
|
4778
|
+
if (lesson.scopeKey !== project) {
|
|
4779
|
+
throw new TypeError(
|
|
4780
|
+
`review learning ${application.id} belongs to project ${lesson.scopeKey}, not ${project}`,
|
|
4781
|
+
);
|
|
4782
|
+
}
|
|
4783
|
+
const findingKey = lesson.sourceRef;
|
|
4784
|
+
const area = lesson.metadata?.area || 'general';
|
|
4785
|
+
const disposition = lesson.metadata?.disposition || null;
|
|
4786
|
+
|
|
4787
|
+
let evidenceKinds = [];
|
|
4788
|
+
if (application.outcome === 'contradicted') {
|
|
4789
|
+
evidenceKinds = reviewLearning.classifyExactEvidence([application.evidence]);
|
|
4790
|
+
if (!evidenceKinds.includes('current-code')) {
|
|
4791
|
+
throw new TypeError('contradicted requires exact current-code evidence');
|
|
4792
|
+
}
|
|
4793
|
+
}
|
|
4794
|
+
|
|
4795
|
+
const event = memoryStore.recordMemoryLifecycleEvent({
|
|
4796
|
+
eventType: application.outcome,
|
|
4797
|
+
memoryId: lesson.id,
|
|
4798
|
+
findingKey,
|
|
4799
|
+
project,
|
|
4800
|
+
agent: applyingAgent,
|
|
4801
|
+
area,
|
|
4802
|
+
disposition,
|
|
4803
|
+
workItemId,
|
|
4804
|
+
dispatchId,
|
|
4805
|
+
identity: lesson.id,
|
|
4806
|
+
metadata: {
|
|
4807
|
+
outcome: application.outcome,
|
|
4808
|
+
evidence: application.evidence,
|
|
4809
|
+
...(evidenceKinds.length ? { evidenceKinds } : {}),
|
|
4810
|
+
},
|
|
4811
|
+
});
|
|
4812
|
+
if (application.outcome === 'applied') result.applied.push(event);
|
|
4813
|
+
else if (application.outcome === 'considered-not-applicable') result.notApplicable.push(event);
|
|
4814
|
+
else result.contradicted.push(event);
|
|
4815
|
+
|
|
4816
|
+
// Only a scoped, evidence-backed contradiction mutates the lesson:
|
|
4817
|
+
// quarantine it through the retracted lifecycle (provenance retained) and
|
|
4818
|
+
// raise an operator alert. considered-not-applicable / applied leave the
|
|
4819
|
+
// lesson untouched (task-local).
|
|
4820
|
+
if (application.outcome === 'contradicted') {
|
|
4821
|
+
const retraction = memoryStore.retractReviewLearningLesson(lesson.id, {
|
|
4822
|
+
reason: 'contradicted',
|
|
4823
|
+
contradictedBy: applyingAgent,
|
|
4824
|
+
dispatchId,
|
|
4825
|
+
workItemId,
|
|
4826
|
+
evidenceKinds,
|
|
4827
|
+
});
|
|
4828
|
+
const retractedEvent = memoryStore.recordMemoryLifecycleEvent({
|
|
4829
|
+
eventType: 'retracted',
|
|
4830
|
+
memoryId: lesson.id,
|
|
4831
|
+
findingKey,
|
|
4832
|
+
project,
|
|
4833
|
+
agent: applyingAgent,
|
|
4834
|
+
area,
|
|
4835
|
+
disposition,
|
|
4836
|
+
workItemId,
|
|
4837
|
+
dispatchId,
|
|
4838
|
+
identity: lesson.id,
|
|
4839
|
+
metadata: { reason: 'contradicted', evidenceKinds },
|
|
4840
|
+
});
|
|
4841
|
+
result.retracted.push(retractedEvent);
|
|
4842
|
+
if (retraction.changed) {
|
|
4843
|
+
_raiseReviewLearningContradictionAlert({
|
|
4844
|
+
lesson,
|
|
4845
|
+
project,
|
|
4846
|
+
applyingAgent,
|
|
4847
|
+
dispatchItem,
|
|
4848
|
+
evidence: application.evidence,
|
|
4849
|
+
evidenceKinds,
|
|
4850
|
+
});
|
|
4851
|
+
}
|
|
4852
|
+
}
|
|
4853
|
+
} catch (err) {
|
|
4854
|
+
_captureReviewLearningDiagnostic(result, application?.id || null, err);
|
|
4855
|
+
}
|
|
4856
|
+
}
|
|
4857
|
+
|
|
4858
|
+
_surfaceReviewLearningCaptureDiagnostics(dispatchItem, agentId, structuredCompletion, result.diagnostics);
|
|
4859
|
+
return result;
|
|
4860
|
+
}
|
|
4861
|
+
|
|
4862
|
+
function _raiseReviewLearningContradictionAlert({
|
|
4863
|
+
lesson,
|
|
4864
|
+
project,
|
|
4865
|
+
applyingAgent,
|
|
4866
|
+
dispatchItem,
|
|
4867
|
+
evidence,
|
|
4868
|
+
evidenceKinds,
|
|
4869
|
+
} = {}) {
|
|
4870
|
+
try {
|
|
4871
|
+
const detail = reviewLearning.redactBoundedText(
|
|
4872
|
+
evidence || '',
|
|
4873
|
+
reviewLearning.REVIEW_LEARNING_BOUNDS.evidenceBytes,
|
|
4874
|
+
'contradiction evidence',
|
|
4875
|
+
);
|
|
4876
|
+
const lines = [
|
|
4877
|
+
`Review-learning lesson \`${lesson.id}\` was contradicted with exact current-code evidence and quarantined (retracted).`,
|
|
4878
|
+
'',
|
|
4879
|
+
`- Project: ${project}`,
|
|
4880
|
+
`- Finding: ${lesson.sourceRef}`,
|
|
4881
|
+
`- Contradicted by: ${applyingAgent || 'unknown'}`,
|
|
4882
|
+
`- Dispatch: ${dispatchItem?.id || 'unknown'}`,
|
|
4883
|
+
`- Evidence kinds: ${(evidenceKinds || []).join(', ') || 'current-code'}`,
|
|
4884
|
+
`- Evidence: ${detail || '(redacted)'}`,
|
|
4885
|
+
'',
|
|
4886
|
+
'Restoring this lesson is an explicit human action (memory record restore).',
|
|
4887
|
+
];
|
|
4888
|
+
shared.writeToInbox('engine', `review-learning-contradicted-${lesson.id}`, lines.join('\n'), null, {
|
|
4889
|
+
kind: 'review-learning-contradiction',
|
|
4890
|
+
memoryId: lesson.id,
|
|
4891
|
+
project,
|
|
4892
|
+
});
|
|
4893
|
+
} catch (err) {
|
|
4894
|
+
log('warn', `Could not raise review-learning contradiction alert for ${lesson?.id}: ${err.message}`);
|
|
4895
|
+
}
|
|
4896
|
+
}
|
|
4897
|
+
|
|
4188
4898
|
function parseCompletionReportFile(dispatchItem, opts = {}) {
|
|
4189
4899
|
const reportPath = dispatchItem?.meta?.completionReportPath || shared.dispatchCompletionReportPath(dispatchItem?.id);
|
|
4190
4900
|
if (!reportPath || !fs.existsSync(reportPath)) {
|
|
@@ -4203,11 +4913,64 @@ function parseCompletionReportFile(dispatchItem, opts = {}) {
|
|
|
4203
4913
|
log('warn', `Ignoring completion report without status for ${dispatchItem?.id || 'unknown'}: ${reportPath}`);
|
|
4204
4914
|
return null;
|
|
4205
4915
|
}
|
|
4916
|
+
const normalizedStatus = normalizeCompletionStatus(report.status);
|
|
4917
|
+
if (!VALID_COMPLETION_STATUSES.has(normalizedStatus)) {
|
|
4918
|
+
log('warn', `Ignoring completion report with invalid status "${report.status}" for ${dispatchItem?.id || 'unknown'}: ${reportPath}`);
|
|
4919
|
+
return null;
|
|
4920
|
+
}
|
|
4206
4921
|
report._source = 'report-file';
|
|
4207
4922
|
report._path = reportPath;
|
|
4208
4923
|
return report;
|
|
4209
4924
|
}
|
|
4210
4925
|
|
|
4926
|
+
function requiredCompletionReportFailure({
|
|
4927
|
+
dispatchId,
|
|
4928
|
+
type,
|
|
4929
|
+
code,
|
|
4930
|
+
structuredCompletion,
|
|
4931
|
+
nonceMismatch,
|
|
4932
|
+
unfinishedToolCall = false,
|
|
4933
|
+
required = true,
|
|
4934
|
+
} = {}) {
|
|
4935
|
+
if (
|
|
4936
|
+
!required
|
|
4937
|
+
|| code !== 0
|
|
4938
|
+
|| nonceMismatch
|
|
4939
|
+
|| structuredCompletion
|
|
4940
|
+
|| shared.READ_ONLY_ROOT_TASK_TYPES.has(type)
|
|
4941
|
+
) {
|
|
4942
|
+
return null;
|
|
4943
|
+
}
|
|
4944
|
+
// W-ms0vk9nv000z985b — separate "the runtime turn was truncated" from
|
|
4945
|
+
// "the agent finished but skipped the report".
|
|
4946
|
+
//
|
|
4947
|
+
// `unfinishedToolCall` comes from the dispatch's OWN runtime adapter via the
|
|
4948
|
+
// optional `hasUnfinishedToolCall` hook (engine/runtimes/contract.js): the
|
|
4949
|
+
// adapter is the only component that knows its event vocabulary, so no
|
|
4950
|
+
// runtime-name branching happens here. It is true only when the stream holds
|
|
4951
|
+
// a tool call that was started and never completed — exactly the observed
|
|
4952
|
+
// Copilot case of exiting 0 moments after starting a long `npm test`.
|
|
4953
|
+
//
|
|
4954
|
+
// Deliberately NOT keyed on "the runtime produced no terminal text": a
|
|
4955
|
+
// phantom dispatch (bare `system/init`, garbled stdout) also has no terminal
|
|
4956
|
+
// text but was never truncated mid-work, and must keep the original class.
|
|
4957
|
+
//
|
|
4958
|
+
// Both remain failures: the completion-report contract is NOT weakened. The
|
|
4959
|
+
// truncated case just gets a class that names the real cause, so it is not
|
|
4960
|
+
// mistaken for agent negligence and retry is understood as "re-run the
|
|
4961
|
+
// truncated turn" rather than "the agent will forget the report again".
|
|
4962
|
+
if (unfinishedToolCall) {
|
|
4963
|
+
return {
|
|
4964
|
+
reason: `Runtime for dispatch ${dispatchId || 'unknown'} exited 0 mid-turn: a tool call was started and never completed, and no completion report was written`,
|
|
4965
|
+
failureClass: FAILURE_CLASS.RUNTIME_TRUNCATED_EXIT,
|
|
4966
|
+
};
|
|
4967
|
+
}
|
|
4968
|
+
return {
|
|
4969
|
+
reason: `Required completion report missing or invalid for mutating dispatch ${dispatchId || 'unknown'}`,
|
|
4970
|
+
failureClass: FAILURE_CLASS.COMPLETION_REPORT_MISSING,
|
|
4971
|
+
};
|
|
4972
|
+
}
|
|
4973
|
+
|
|
4211
4974
|
function normalizeCompletionArtifacts(rawArtifacts) {
|
|
4212
4975
|
let artifacts = rawArtifacts;
|
|
4213
4976
|
if (typeof artifacts === 'string') {
|
|
@@ -4398,6 +5161,18 @@ const NON_TERMINAL_COMPLETION_STATUSES = new Set([
|
|
|
4398
5161
|
'blocked', 'incomplete', 'to-be-continued',
|
|
4399
5162
|
'failed', 'failure', 'error',
|
|
4400
5163
|
]);
|
|
5164
|
+
const VALID_COMPLETION_STATUSES = new Set([
|
|
5165
|
+
'success', 'succeeded', 'successful', 'done', 'complete', 'completed',
|
|
5166
|
+
...NON_TERMINAL_COMPLETION_STATUSES,
|
|
5167
|
+
]);
|
|
5168
|
+
|
|
5169
|
+
// Terminal (clean) success statuses only — VALID_COMPLETION_STATUSES minus the
|
|
5170
|
+
// non-terminal (partial/failure) tokens. Used to gate trustedCompletionSuccess so
|
|
5171
|
+
// a nonce-verified success report can override a kill-induced nonzero exit
|
|
5172
|
+
// (W-mrza78j3001r7a5d).
|
|
5173
|
+
const TERMINAL_SUCCESS_COMPLETION_STATUSES = new Set([
|
|
5174
|
+
'success', 'succeeded', 'successful', 'done', 'complete', 'completed',
|
|
5175
|
+
]);
|
|
4401
5176
|
|
|
4402
5177
|
function detectNonTerminalResultSummary(_resultSummary, structuredCompletion, completionReport, opts) {
|
|
4403
5178
|
const candidates = [completionReport?.status, structuredCompletion?.status];
|
|
@@ -4685,17 +5460,26 @@ function validateReviewFixCompletion({ type, meta, branchChange, structuredCompl
|
|
|
4685
5460
|
const currentCodeEvidence = String(resolution?.currentCodeEvidence || '').trim();
|
|
4686
5461
|
const hasCurrentCodeReference = /(?:[a-z]:[\\/])?[\w./\\-]+\.[a-z0-9]+:\d+\b/i.test(currentCodeEvidence)
|
|
4687
5462
|
|| /\bcommit\s+[0-9a-f]{7,40}\b/i.test(currentCodeEvidence);
|
|
4688
|
-
|
|
5463
|
+
let canonicalDisposition = null;
|
|
5464
|
+
try {
|
|
5465
|
+
canonicalDisposition = reviewLearning.normalizeDisposition(disposition);
|
|
5466
|
+
} catch {}
|
|
5467
|
+
const validDisposition = canonicalDisposition === reviewLearning.REVIEW_LEARNING_DISPOSITIONS.SATISFIED_ELSEWHERE
|
|
5468
|
+
|| canonicalDisposition === reviewLearning.REVIEW_LEARNING_DISPOSITIONS.INVALID;
|
|
4689
5469
|
const explicitNoop = !!parseCompletionNoop(structuredCompletion);
|
|
4690
5470
|
|
|
4691
5471
|
if (explicitNoop && validDisposition && reportedFindingId === findingId && hasCurrentCodeReference) {
|
|
4692
|
-
return {
|
|
5472
|
+
return {
|
|
5473
|
+
valid: true,
|
|
5474
|
+
findingId,
|
|
5475
|
+
resolution: { ...resolution, disposition: canonicalDisposition },
|
|
5476
|
+
};
|
|
4693
5477
|
}
|
|
4694
5478
|
|
|
4695
5479
|
return {
|
|
4696
5480
|
valid: false,
|
|
4697
5481
|
findingId,
|
|
4698
|
-
reason: `Review-fix completion did not advance the PR branch and must explicitly prove the exact finding is
|
|
5482
|
+
reason: `Review-fix completion did not advance the PR branch and must explicitly prove the exact finding is satisfied elsewhere or invalid with current-code evidence. Expected reviewFindingResolution.findingId="${findingId}", disposition "satisfied-elsewhere" (legacy "already-resolved" is accepted) or "invalid", and a file:line or commit reference. Shared platform ownership, viewerDidAuthor, or reviewer/fixer agent equality is not evidence.`,
|
|
4699
5483
|
};
|
|
4700
5484
|
}
|
|
4701
5485
|
|
|
@@ -4742,33 +5526,6 @@ function extractReviewThreadIds(completion) {
|
|
|
4742
5526
|
return Array.from(ids);
|
|
4743
5527
|
}
|
|
4744
5528
|
|
|
4745
|
-
function reviewContentMatchesPr(content, pr, project) {
|
|
4746
|
-
const text = String(content || '').trim();
|
|
4747
|
-
if (!text) return false;
|
|
4748
|
-
const targetId = shared.getCanonicalPrId(project, pr, pr?.url || '');
|
|
4749
|
-
const targetNumber = shared.getPrNumber(pr);
|
|
4750
|
-
if (!targetId) return true;
|
|
4751
|
-
|
|
4752
|
-
const explicitRefs = new Set();
|
|
4753
|
-
for (const match of text.matchAll(/\b(?:github|ado):[A-Za-z0-9._~/-]+#\d+\b/g)) {
|
|
4754
|
-
explicitRefs.add(shared.getCanonicalPrId(project, match[0]));
|
|
4755
|
-
}
|
|
4756
|
-
for (const match of text.matchAll(/https?:\/\/[^\s)>"]+(?:\/pull\/|\/pullrequest\/)\d+[^\s)>"]*/gi)) {
|
|
4757
|
-
const url = match[0].replace(/[.,;:]+$/g, '');
|
|
4758
|
-
explicitRefs.add(shared.getCanonicalPrId(project, url, url));
|
|
4759
|
-
}
|
|
4760
|
-
if (explicitRefs.size > 0) return explicitRefs.size === 1 && explicitRefs.has(targetId);
|
|
4761
|
-
|
|
4762
|
-
const mentionedNumbers = new Set();
|
|
4763
|
-
for (const match of text.matchAll(/\bPR\s*(?:#|-)\s*(\d+)\b/gi)) {
|
|
4764
|
-
mentionedNumbers.add(parseInt(match[1], 10));
|
|
4765
|
-
}
|
|
4766
|
-
if (mentionedNumbers.size > 0 && targetNumber != null) {
|
|
4767
|
-
return mentionedNumbers.size === 1 && mentionedNumbers.has(targetNumber);
|
|
4768
|
-
}
|
|
4769
|
-
return true;
|
|
4770
|
-
}
|
|
4771
|
-
|
|
4772
5529
|
function writeNonCleanAgentReport(dispatchItem, agentId, outcome, structuredCompletion, resultSummary, exitCode) {
|
|
4773
5530
|
if (!dispatchItem?.id || !outcome) {
|
|
4774
5531
|
log('warn', 'Cannot write non-clean agent report without dispatch id and outcome');
|
|
@@ -5439,6 +6196,10 @@ function captureTaskMemoryEpisode(dispatchItem, agentId, outcome, structuredComp
|
|
|
5439
6196
|
async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config, opts) {
|
|
5440
6197
|
|
|
5441
6198
|
const detectPhantom = !!(opts && opts.detectPhantom);
|
|
6199
|
+
const completionReportRequired = !!(opts && opts.completionReportRequired);
|
|
6200
|
+
const preFinalizationFailure = shared.isPlainObject(opts?.preFinalizationFailure)
|
|
6201
|
+
? opts.preFinalizationFailure
|
|
6202
|
+
: null;
|
|
5442
6203
|
const type = dispatchItem.type;
|
|
5443
6204
|
const meta = dispatchItem.meta;
|
|
5444
6205
|
const isSuccess = code === 0;
|
|
@@ -5504,6 +6265,22 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5504
6265
|
}
|
|
5505
6266
|
|
|
5506
6267
|
const structuredCompletion = reportCompletion;
|
|
6268
|
+
const completionReportFailure = requiredCompletionReportFailure({
|
|
6269
|
+
dispatchId: dispatchItem.id,
|
|
6270
|
+
type,
|
|
6271
|
+
code,
|
|
6272
|
+
structuredCompletion,
|
|
6273
|
+
nonceMismatch,
|
|
6274
|
+
// W-ms0vk9nv000z985b — did the runtime die between a tool start and its
|
|
6275
|
+
// completion? Answered by THIS dispatch's adapter through the registry
|
|
6276
|
+
// facade, which fails safe to `false` for adapters that don't implement
|
|
6277
|
+
// the hook (so the classification is unchanged for them).
|
|
6278
|
+
unfinishedToolCall: hasUnfinishedToolCall(resolveRuntime(runtimeName), stdout),
|
|
6279
|
+
required: completionReportRequired,
|
|
6280
|
+
});
|
|
6281
|
+
if (completionReportFailure) {
|
|
6282
|
+
log('error', `${completionReportFailure.reason} (agent=${agentId}, exitCode=${code})`);
|
|
6283
|
+
}
|
|
5507
6284
|
if (structuredCompletion) {
|
|
5508
6285
|
if (structuredCompletion.summary) resultSummary = String(structuredCompletion.summary);
|
|
5509
6286
|
log('info', `Structured completion from ${agentId}: status=${structuredCompletion.status}, pr=${structuredCompletion.pr || 'N/A'}${structuredCompletion._source ? ` (${structuredCompletion._source})` : ''}`);
|
|
@@ -5530,6 +6307,18 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5530
6307
|
if (!structuredCompletion.status || /^(complete|success|done)/i.test(structuredCompletion.status)) {
|
|
5531
6308
|
structuredCompletion.status = 'failed-injection-flagged';
|
|
5532
6309
|
}
|
|
6310
|
+
suppressReviewLearningCompletionPayload(structuredCompletion);
|
|
6311
|
+
} else if (structuredCompletion) {
|
|
6312
|
+
const reviewLearningResult = normalizeReviewLearningCompletionPayload(
|
|
6313
|
+
dispatchItem,
|
|
6314
|
+
agentId,
|
|
6315
|
+
structuredCompletion,
|
|
6316
|
+
);
|
|
6317
|
+
recordReviewLearningCompletionDiagnostics(
|
|
6318
|
+
dispatchItem,
|
|
6319
|
+
agentId,
|
|
6320
|
+
reviewLearningResult.diagnostics,
|
|
6321
|
+
);
|
|
5533
6322
|
}
|
|
5534
6323
|
const completionGateSummary = resultSummary || (typeof stdout === 'string' && !stdout.includes('"type":') ? stdout : '');
|
|
5535
6324
|
|
|
@@ -5544,6 +6333,9 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5544
6333
|
branch: dispatchItem.meta?.branch || null,
|
|
5545
6334
|
sessionId,
|
|
5546
6335
|
agentsDir: AGENTS_DIR,
|
|
6336
|
+
// W-ms0vk9nv000z985b — anchor the session to the directory it ran in
|
|
6337
|
+
// so a later dispatch can refuse to resume it from a dead worktree.
|
|
6338
|
+
cwd: dispatchItem.worktreePath || null,
|
|
5547
6339
|
logger: { warn: (msg) => log('warn', msg) },
|
|
5548
6340
|
});
|
|
5549
6341
|
}
|
|
@@ -5584,7 +6376,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5584
6376
|
const benignNoop = nonceMismatch
|
|
5585
6377
|
? null
|
|
5586
6378
|
: classifyBenignZeroTargetOutcome(dispatchItem, structuredCompletion, completionGateSummary);
|
|
5587
|
-
const agentReportedFailure = !!nonceMismatch || (!benignNoop && (
|
|
6379
|
+
const agentReportedFailure = !!nonceMismatch || !!completionReportFailure || !!preFinalizationFailure || (!benignNoop && (
|
|
5588
6380
|
completionStatus.startsWith('fail')
|
|
5589
6381
|
|| completionStatus === 'error'
|
|
5590
6382
|
|| hasActionableFailureClass(structuredCompletion?.failure_class)
|
|
@@ -5602,7 +6394,22 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5602
6394
|
if (autoRecovered) {
|
|
5603
6395
|
log('info', `Auto-recovery: agent failed but created ${prsCreatedCount} PR(s) — upgrading ${meta.item.id} to done`);
|
|
5604
6396
|
}
|
|
5605
|
-
|
|
6397
|
+
// W-mrza78j3001r7a5d — trust a nonce-verified TERMINAL-success completion report
|
|
6398
|
+
// even when the process exited nonzero. The legacy stdout-only startup timer used
|
|
6399
|
+
// to `killGracefully` a productive-but-silent runtime, and other kill paths
|
|
6400
|
+
// (steering, spawn-phase watchdog) can also race a valid success report; without
|
|
6401
|
+
// this, `resolveTerminalDispatchResult` privileges the kill-induced nonzero exit
|
|
6402
|
+
// and persists result:error alongside structuredCompletion.status:success. Because
|
|
6403
|
+
// `agentReportedFailure` already folds in nonceMismatch / completionReportFailure /
|
|
6404
|
+
// preFinalizationFailure, this only fires for a clean, trusted success; hard
|
|
6405
|
+
// contract failures are still gated by the resolver's early-returns.
|
|
6406
|
+
const trustedCompletionSuccess = !!structuredCompletion
|
|
6407
|
+
&& !agentReportedFailure
|
|
6408
|
+
&& TERMINAL_SUCCESS_COMPLETION_STATUSES.has(completionStatus);
|
|
6409
|
+
if (trustedCompletionSuccess && !isSuccess) {
|
|
6410
|
+
log('info', `Trusted completion: nonce-verified success report from ${agentId} overrides process exit code ${code} for ${meta?.item?.id || dispatchItem.id}`);
|
|
6411
|
+
}
|
|
6412
|
+
const effectiveSuccess = !nonceMismatch && (benignNoop || (isSuccess && !agentReportedFailure) || autoRecovered || trustedCompletionSuccess);
|
|
5606
6413
|
if (!nonceMismatch) {
|
|
5607
6414
|
const episodeOutcome = effectiveSuccess
|
|
5608
6415
|
? (autoRecovered || completionStatus.startsWith('partial') ? 'partial' : 'success')
|
|
@@ -5921,7 +6728,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5921
6728
|
log('info', `No-op completion for ${meta?.item?.id || dispatchItem.id}: ${noopRationale.slice(0, 200)}`);
|
|
5922
6729
|
}
|
|
5923
6730
|
|
|
5924
|
-
if (effectiveSuccess && meta?.item?.id && !skipDoneStatus && !noopRationale) {
|
|
6731
|
+
if ((effectiveSuccess || completionReportFailure || preFinalizationFailure) && meta?.item?.id && !skipDoneStatus && !noopRationale) {
|
|
5925
6732
|
completionContractFailure = await enforcePrAttachmentContract(type, meta, agentId, config, resultSummary, stdout, { detectPhantom });
|
|
5926
6733
|
if (completionContractFailure?.severity === 'hard' || completionContractFailure?.nonTerminal) {
|
|
5927
6734
|
skipDoneStatus = true;
|
|
@@ -5931,7 +6738,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5931
6738
|
// W-mqsk1ip00006cbae — verify WI PR guard. After enforcePrAttachmentContract
|
|
5932
6739
|
// (which skips verify types), assert that a verify WI has a PR before
|
|
5933
6740
|
// allowing the done transition.
|
|
5934
|
-
if (effectiveSuccess && meta?.item?.id && !skipDoneStatus && !noopRationale) {
|
|
6741
|
+
if ((effectiveSuccess || completionReportFailure || preFinalizationFailure) && meta?.item?.id && !skipDoneStatus && !noopRationale) {
|
|
5935
6742
|
const verifyContractFailure = enforceVerifyPrContract(type, meta, agentId, config, resultSummary);
|
|
5936
6743
|
if (verifyContractFailure?.severity === 'hard') {
|
|
5937
6744
|
completionContractFailure = completionContractFailure || verifyContractFailure;
|
|
@@ -5939,6 +6746,47 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5939
6746
|
}
|
|
5940
6747
|
}
|
|
5941
6748
|
|
|
6749
|
+
if (completionReportFailure || preFinalizationFailure) skipDoneStatus = true;
|
|
6750
|
+
|
|
6751
|
+
if (effectiveSuccess && !skipDoneStatus && structuredCompletion) {
|
|
6752
|
+
try {
|
|
6753
|
+
captureVerifiedReviewLearnings({
|
|
6754
|
+
dispatchItem,
|
|
6755
|
+
agentId,
|
|
6756
|
+
config,
|
|
6757
|
+
structuredCompletion,
|
|
6758
|
+
branchChange: prFixBranchChange,
|
|
6759
|
+
reviewFixResolution,
|
|
6760
|
+
});
|
|
6761
|
+
} catch (err) {
|
|
6762
|
+
const captureFailure = { diagnostics: [] };
|
|
6763
|
+
_captureReviewLearningDiagnostic(captureFailure, null, err);
|
|
6764
|
+
_surfaceReviewLearningCaptureDiagnostics(
|
|
6765
|
+
dispatchItem,
|
|
6766
|
+
agentId,
|
|
6767
|
+
structuredCompletion,
|
|
6768
|
+
captureFailure.diagnostics,
|
|
6769
|
+
);
|
|
6770
|
+
}
|
|
6771
|
+
try {
|
|
6772
|
+
processReviewLearningApplications({
|
|
6773
|
+
dispatchItem,
|
|
6774
|
+
agentId,
|
|
6775
|
+
config,
|
|
6776
|
+
structuredCompletion,
|
|
6777
|
+
});
|
|
6778
|
+
} catch (err) {
|
|
6779
|
+
const applyFailure = { diagnostics: [] };
|
|
6780
|
+
_captureReviewLearningDiagnostic(applyFailure, null, err);
|
|
6781
|
+
_surfaceReviewLearningCaptureDiagnostics(
|
|
6782
|
+
dispatchItem,
|
|
6783
|
+
agentId,
|
|
6784
|
+
structuredCompletion,
|
|
6785
|
+
applyFailure.diagnostics,
|
|
6786
|
+
);
|
|
6787
|
+
}
|
|
6788
|
+
}
|
|
6789
|
+
|
|
5942
6790
|
if (effectiveSuccess && meta?.item?.id && !skipDoneStatus) {
|
|
5943
6791
|
meta._agentId = agentId;
|
|
5944
6792
|
if (noopRationale) {
|
|
@@ -6011,27 +6859,32 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
6011
6859
|
// (engine/lifecycle.js, "_wiMetaForSkip" block) — that way fast-path
|
|
6012
6860
|
// dispatchers that synthesize meta.qaRunId at the top level keep
|
|
6013
6861
|
// working too. The agent writes agents/<id>/qa-run-result.json before
|
|
6014
|
-
// exit.
|
|
6015
|
-
//
|
|
6016
|
-
//
|
|
6017
|
-
// the dashboard run list never shows a perma-pending row when the
|
|
6018
|
-
// agent crashed before exit.
|
|
6862
|
+
// exit. qa-runs consumes the file exactly once and verifies its runId
|
|
6863
|
+
// before this hook completes the linked record. Missing, malformed, stale,
|
|
6864
|
+
// or invalid-status sidecars terminalize the current run as errored.
|
|
6019
6865
|
const qaRunId = meta?.qaRunId || meta?.item?.meta?.qaRunId;
|
|
6020
|
-
|
|
6866
|
+
const lifecycleGateCanRetry = !!(completionReportFailure || preFinalizationFailure)
|
|
6867
|
+
&& completionContractFailure?.severity !== 'hard'
|
|
6868
|
+
&& completionContractFailure?.nonTerminal !== true
|
|
6869
|
+
&& Number(meta?.item?._retryCount || 0) < ENGINE_DEFAULTS.maxRetries;
|
|
6870
|
+
if (qaRunId && lifecycleGateCanRetry) {
|
|
6871
|
+
try { require('./qa-runs').prepareResultSidecar(agentId || '_unknown', qaRunId); }
|
|
6872
|
+
catch (e) { log('warn', `qa-validate deferred sidecar cleanup for ${qaRunId}: ${e.message}`); }
|
|
6873
|
+
log('warn', `qa-validate run ${qaRunId} remains pending while completion validation retries`);
|
|
6874
|
+
} else if (qaRunId) {
|
|
6021
6875
|
try {
|
|
6022
6876
|
const qaRuns = require('./qa-runs');
|
|
6023
|
-
const
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
const raw = fs.readFileSync(sidecarPath, 'utf8');
|
|
6027
|
-
parsed = JSON.parse(raw);
|
|
6028
|
-
} catch (e) {
|
|
6029
|
-
if (e.code !== 'ENOENT') {
|
|
6030
|
-
log('warn', `qa-validate sidecar parse for ${qaRunId}: ${e.message}`);
|
|
6031
|
-
}
|
|
6877
|
+
const consumed = qaRuns.consumeResultSidecar(agentId || '_unknown', qaRunId);
|
|
6878
|
+
if (consumed.cleanupError) {
|
|
6879
|
+
log('warn', `qa-validate sidecar cleanup for ${qaRunId}: ${consumed.cleanupError}`);
|
|
6032
6880
|
}
|
|
6033
|
-
|
|
6034
|
-
|
|
6881
|
+
const parsed = consumed.ok ? consumed.parsed : null;
|
|
6882
|
+
const sidecarCanComplete = parsed && (
|
|
6883
|
+
effectiveSuccess
|
|
6884
|
+
|| parsed.status === qaRuns.QA_RUN_STATUS.FAILED
|
|
6885
|
+
|| parsed.status === qaRuns.QA_RUN_STATUS.ERRORED
|
|
6886
|
+
);
|
|
6887
|
+
if (sidecarCanComplete) {
|
|
6035
6888
|
qaRuns.completeRun(qaRunId, {
|
|
6036
6889
|
status: parsed.status,
|
|
6037
6890
|
summary: typeof parsed.summary === 'string' ? parsed.summary : '',
|
|
@@ -6039,17 +6892,24 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
6039
6892
|
});
|
|
6040
6893
|
log('info', `qa-validate run ${qaRunId} → ${parsed.status} (${(parsed.artifacts || []).length} artifacts)`);
|
|
6041
6894
|
} else {
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6895
|
+
let summary;
|
|
6896
|
+
if (parsed) {
|
|
6897
|
+
summary = 'qa-validate dispatch failed completion validation';
|
|
6898
|
+
} else if (consumed.reason === 'missing') {
|
|
6899
|
+
summary = `qa-validate sidecar missing at ${consumed.filePath}`;
|
|
6900
|
+
} else if (consumed.reason === 'run-id-mismatch') {
|
|
6901
|
+
summary = `qa-validate sidecar runId mismatch (expected ${qaRunId}, got ${consumed.actualRunId || 'missing'})`;
|
|
6902
|
+
} else if (consumed.reason === 'invalid-status') {
|
|
6903
|
+
summary = `qa-validate sidecar malformed (status=${consumed.status})`;
|
|
6904
|
+
} else {
|
|
6905
|
+
summary = `qa-validate sidecar ${consumed.reason} at ${consumed.filePath}${consumed.detail ? `: ${consumed.detail}` : ''}`;
|
|
6906
|
+
}
|
|
6045
6907
|
qaRuns.completeRun(qaRunId, {
|
|
6046
6908
|
status: 'errored',
|
|
6047
|
-
summary
|
|
6048
|
-
? `qa-validate sidecar malformed (status=${parsed.status})`
|
|
6049
|
-
: `qa-validate sidecar missing at ${sidecarPath}`,
|
|
6909
|
+
summary,
|
|
6050
6910
|
artifacts: [],
|
|
6051
6911
|
});
|
|
6052
|
-
log('warn', `qa-validate run ${qaRunId} → errored (
|
|
6912
|
+
log('warn', `qa-validate run ${qaRunId} → errored (${parsed ? 'dispatch completion invalid' : `sidecar ${consumed.reason}`})`);
|
|
6053
6913
|
}
|
|
6054
6914
|
} catch (err) {
|
|
6055
6915
|
// qaRuns.completeRun throws on illegal transitions / missing run id.
|
|
@@ -6073,7 +6933,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
6073
6933
|
// it under `meta.item.meta`. Accept both.
|
|
6074
6934
|
const qaSessionId = meta?.sessionId || meta?.item?.meta?.sessionId;
|
|
6075
6935
|
const qaSessionPhase = meta?.sessionPhase || meta?.item?.meta?.sessionPhase;
|
|
6076
|
-
if (qaSessionId && qaSessionPhase) {
|
|
6936
|
+
if (qaSessionId && qaSessionPhase && !lifecycleGateCanRetry) {
|
|
6077
6937
|
try {
|
|
6078
6938
|
const qaSessions = require('./qa-sessions');
|
|
6079
6939
|
const scope = resolveWorkItemScope(meta);
|
|
@@ -6082,13 +6942,16 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
6082
6942
|
? resultSummary
|
|
6083
6943
|
: '';
|
|
6084
6944
|
if (qaSessionPhase === 'setup') {
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6945
|
+
if (!opts?.deferQaSessionSetup) {
|
|
6946
|
+
handleQaSessionSetupCompletion(meta, {
|
|
6947
|
+
effectiveSuccess,
|
|
6948
|
+
structuredCompletion,
|
|
6949
|
+
resultSummary,
|
|
6950
|
+
scope,
|
|
6951
|
+
project,
|
|
6952
|
+
reason: failureReason,
|
|
6953
|
+
});
|
|
6954
|
+
}
|
|
6092
6955
|
} else if (qaSessionPhase === 'draft') {
|
|
6093
6956
|
// The DRAFT agent reports the relative test file path in its
|
|
6094
6957
|
// structured completion as `testFile` (or the playbook can write a
|
|
@@ -6220,7 +7083,8 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
6220
7083
|
const _skipWorktreeRemovalForAcceptance =
|
|
6221
7084
|
!!meta?.keep_processes || !!_wiMetaForSkip.keep_processes ||
|
|
6222
7085
|
!!meta?.managed_spawn || !!_wiMetaForSkip.managed_spawn;
|
|
6223
|
-
|
|
7086
|
+
const _deferWorktreeRemoval = !!(opts && opts.deferWorktreeRemoval);
|
|
7087
|
+
if (meta?.branch && meta?.branchStrategy !== 'shared-branch' && !_skipWorktreeRemovalForAcceptance && !_deferWorktreeRemoval) {
|
|
6224
7088
|
try {
|
|
6225
7089
|
const project = meta.project || {};
|
|
6226
7090
|
const rootDir = project.localPath ? path.resolve(project.localPath) : null;
|
|
@@ -6376,7 +7240,27 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
6376
7240
|
const metricsResult = isAutoRetry ? 'retry' : finalResult;
|
|
6377
7241
|
updateMetrics(agentId, dispatchItem, metricsResult, taskUsage, prsCreatedCount, model);
|
|
6378
7242
|
|
|
6379
|
-
return { resultSummary, taskUsage, autoRecovered, structuredCompletion, completionContractFailure, agentReportedFailure, agentRetryable, nonceMismatch, benignNoop };
|
|
7243
|
+
return { resultSummary, taskUsage, autoRecovered, trustedCompletionSuccess, structuredCompletion, completionContractFailure, completionReportFailure, preFinalizationFailure, agentReportedFailure, agentRetryable, nonceMismatch, benignNoop, lifecycleGateCanRetry };
|
|
7244
|
+
}
|
|
7245
|
+
|
|
7246
|
+
function handleQaSessionSetupCompletion(meta, opts = {}) {
|
|
7247
|
+
const qaSessionId = meta?.sessionId || meta?.item?.meta?.sessionId;
|
|
7248
|
+
if (!qaSessionId) return null;
|
|
7249
|
+
const qaSessions = require('./qa-sessions');
|
|
7250
|
+
const scope = opts.scope || resolveWorkItemScope(meta);
|
|
7251
|
+
const project = opts.project !== undefined
|
|
7252
|
+
? opts.project
|
|
7253
|
+
: (meta?.project?.name || meta?.item?.project || null);
|
|
7254
|
+
const resultSummary = typeof opts.resultSummary === 'string' ? opts.resultSummary : '';
|
|
7255
|
+
return qaSessions.handleSetupCompleteAfterManagedSpawn(qaSessionId, {
|
|
7256
|
+
success: !!opts.effectiveSuccess,
|
|
7257
|
+
scope,
|
|
7258
|
+
project,
|
|
7259
|
+
failureClass: opts.failureClass
|
|
7260
|
+
|| (opts.structuredCompletion && opts.structuredCompletion.failure_class)
|
|
7261
|
+
|| null,
|
|
7262
|
+
reason: typeof opts.reason === 'string' && opts.reason ? opts.reason : resultSummary,
|
|
7263
|
+
});
|
|
6380
7264
|
}
|
|
6381
7265
|
|
|
6382
7266
|
// ─── PR → PRD Status Sync ─────────────────────────────────────────────────────
|
|
@@ -6640,6 +7524,19 @@ function classifyFailure(code, stdout = '', stderr = '') {
|
|
|
6640
7524
|
// Exit code 78 — configuration error (Claude CLI not found, bad setup)
|
|
6641
7525
|
if (code === 78) return FAILURE_CLASS.CONFIG_ERROR;
|
|
6642
7526
|
|
|
7527
|
+
// Stale resumed session (W-ms0vk9nv000z985b). A `--resume`d CLI whose
|
|
7528
|
+
// persisted session or working directory is gone dies before it writes any
|
|
7529
|
+
// stdout, so without this branch the run fell all the way through to the
|
|
7530
|
+
// EMPTY_OUTPUT catch-all — a class that says "the agent produced nothing"
|
|
7531
|
+
// and implies the agent crashed on startup, when in fact the engine handed
|
|
7532
|
+
// it an unusable session id. Checked early: the signal is unambiguous and
|
|
7533
|
+
// the message can otherwise be swallowed by the generic phrase matchers
|
|
7534
|
+
// below. Prevention lives in each adapter's getResumeSessionId; this is the
|
|
7535
|
+
// classification for the residual race.
|
|
7536
|
+
if (shared.isStaleResumeSessionSignal(combined)) {
|
|
7537
|
+
return FAILURE_CLASS.RUNTIME_SESSION_STALE;
|
|
7538
|
+
}
|
|
7539
|
+
|
|
6643
7540
|
// Max turns exhausted (error_max_turns) — definitive stop reason, retryable
|
|
6644
7541
|
// Must be checked FIRST — hook startup failures (e.g. curl exit code 28) can inject
|
|
6645
7542
|
// permission/auth text into stderr, but if the agent ran to turn exhaustion that's the
|
|
@@ -7162,9 +8059,11 @@ module.exports = {
|
|
|
7162
8059
|
enforceVerifyPrContract,
|
|
7163
8060
|
markMissingPrAttachment,
|
|
7164
8061
|
parseCompletionReportFile,
|
|
7165
|
-
//
|
|
7166
|
-
//
|
|
7167
|
-
//
|
|
8062
|
+
buildReviewLearningCompletionContext, // exported for testing
|
|
8063
|
+
normalizeReviewLearningCompletionPayload, // exported for testing
|
|
8064
|
+
suppressReviewLearningCompletionPayload, // exported for testing
|
|
8065
|
+
captureVerifiedReviewLearnings, // exported for testing
|
|
8066
|
+
processReviewLearningApplications, // exported for testing
|
|
7168
8067
|
writeNonCleanAgentReport,
|
|
7169
8068
|
// exported for testing
|
|
7170
8069
|
captureTaskMemoryEpisode,
|
|
@@ -7172,7 +8071,9 @@ module.exports = {
|
|
|
7172
8071
|
completionArtifactToNoteEntry,
|
|
7173
8072
|
mergeArtifactNotes,
|
|
7174
8073
|
promoteCompletionArtifacts,
|
|
8074
|
+
requiredCompletionReportFailure, // exported for testing
|
|
7175
8075
|
runPostCompletionHooks,
|
|
8076
|
+
handleQaSessionSetupCompletion, // exported for testing
|
|
7176
8077
|
applyGoalInvalidation,
|
|
7177
8078
|
syncPrdFromPrs,
|
|
7178
8079
|
advancePrdStatusOnAllItemsDone,
|