@ktpartners/dgs-platform 2.6.2
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/LICENSE +38 -0
- package/README.md +851 -0
- package/agents/dgs-codebase-cross-analyzer.md +183 -0
- package/agents/dgs-codebase-mapper.md +782 -0
- package/agents/dgs-codebase-synthesizer.md +156 -0
- package/agents/dgs-debugger.md +1256 -0
- package/agents/dgs-executor.md +550 -0
- package/agents/dgs-integration-checker.md +481 -0
- package/agents/dgs-nyquist-auditor.md +178 -0
- package/agents/dgs-phase-researcher.md +563 -0
- package/agents/dgs-phase-verifier.md +450 -0
- package/agents/dgs-plan-checker.md +708 -0
- package/agents/dgs-planner.md +1324 -0
- package/agents/dgs-project-researcher.md +631 -0
- package/agents/dgs-research-synthesizer.md +249 -0
- package/agents/dgs-roadmapper.md +652 -0
- package/agents/dgs-verifier.md +607 -0
- package/bin/install.js +2073 -0
- package/commands/dgs/add-doc.md +45 -0
- package/commands/dgs/add-idea.md +38 -0
- package/commands/dgs/add-phase.md +43 -0
- package/commands/dgs/add-repo.md +54 -0
- package/commands/dgs/add-tests.md +41 -0
- package/commands/dgs/add-todo.md +47 -0
- package/commands/dgs/approve-spec.md +38 -0
- package/commands/dgs/audit-milestone.md +36 -0
- package/commands/dgs/audit-phase.md +37 -0
- package/commands/dgs/cancel-job.md +23 -0
- package/commands/dgs/capture-principle.md +143 -0
- package/commands/dgs/check-todos.md +45 -0
- package/commands/dgs/cleanup.md +18 -0
- package/commands/dgs/complete-milestone.md +136 -0
- package/commands/dgs/complete-project.md +70 -0
- package/commands/dgs/consolidate-ideas.md +50 -0
- package/commands/dgs/create-milestone-job.md +37 -0
- package/commands/dgs/debug.md +164 -0
- package/commands/dgs/develop-idea.md +53 -0
- package/commands/dgs/discuss-idea.md +41 -0
- package/commands/dgs/discuss-phase.md +83 -0
- package/commands/dgs/execute-phase.md +41 -0
- package/commands/dgs/fast.md +38 -0
- package/commands/dgs/find-related-ideas.md +43 -0
- package/commands/dgs/health.md +28 -0
- package/commands/dgs/help.md +22 -0
- package/commands/dgs/import-spec.md +36 -0
- package/commands/dgs/init-product.md +28 -0
- package/commands/dgs/insert-phase.md +32 -0
- package/commands/dgs/join-discord.md +18 -0
- package/commands/dgs/list-docs.md +40 -0
- package/commands/dgs/list-ideas.md +42 -0
- package/commands/dgs/list-jobs.md +22 -0
- package/commands/dgs/list-phase-assumptions.md +46 -0
- package/commands/dgs/list-projects.md +57 -0
- package/commands/dgs/list-specs.md +40 -0
- package/commands/dgs/map-codebase.md +92 -0
- package/commands/dgs/new-milestone.md +44 -0
- package/commands/dgs/new-project.md +42 -0
- package/commands/dgs/node-repair.md +26 -0
- package/commands/dgs/overlap-check.md +20 -0
- package/commands/dgs/pause-work.md +38 -0
- package/commands/dgs/plan-milestone-gaps.md +34 -0
- package/commands/dgs/plan-phase.md +44 -0
- package/commands/dgs/progress.md +24 -0
- package/commands/dgs/quick.md +41 -0
- package/commands/dgs/reactivate-project.md +70 -0
- package/commands/dgs/reapply-patches.md +110 -0
- package/commands/dgs/refine-spec.md +38 -0
- package/commands/dgs/reject-idea.md +43 -0
- package/commands/dgs/remove-doc.md +44 -0
- package/commands/dgs/remove-phase.md +31 -0
- package/commands/dgs/remove-repo.md +69 -0
- package/commands/dgs/research-idea.md +43 -0
- package/commands/dgs/research-phase.md +189 -0
- package/commands/dgs/restore-idea.md +45 -0
- package/commands/dgs/resume-work.md +40 -0
- package/commands/dgs/rollback-job.md +24 -0
- package/commands/dgs/run-job.md +35 -0
- package/commands/dgs/search.md +40 -0
- package/commands/dgs/set-profile.md +34 -0
- package/commands/dgs/settings.md +38 -0
- package/commands/dgs/switch-project.md +58 -0
- package/commands/dgs/undo-consolidation.md +42 -0
- package/commands/dgs/update-idea.md +44 -0
- package/commands/dgs/update.md +37 -0
- package/commands/dgs/validate-phase.md +35 -0
- package/commands/dgs/verify-work.md +39 -0
- package/commands/dgs/write-spec.md +49 -0
- package/deliver-great-systems/.planning/phases/09-backend-wiring-and-error-handling/09-01-SUMMARY.md +84 -0
- package/deliver-great-systems/.planning/phases/09-backend-wiring-and-error-handling/09-02-SUMMARY.md +86 -0
- package/deliver-great-systems/.planning/phases/10-v1-to-v2-migration-flow/10-01-SUMMARY.md +85 -0
- package/deliver-great-systems/bin/dgs-tools.cjs +1444 -0
- package/deliver-great-systems/bin/lib/auto-test.cjs +1365 -0
- package/deliver-great-systems/bin/lib/commands.cjs +570 -0
- package/deliver-great-systems/bin/lib/config.cjs +417 -0
- package/deliver-great-systems/bin/lib/conflict-agent.cjs +1063 -0
- package/deliver-great-systems/bin/lib/conflict-agent.test.cjs +554 -0
- package/deliver-great-systems/bin/lib/context.cjs +929 -0
- package/deliver-great-systems/bin/lib/context.test.cjs +693 -0
- package/deliver-great-systems/bin/lib/core.cjs +744 -0
- package/deliver-great-systems/bin/lib/core.test.cjs +822 -0
- package/deliver-great-systems/bin/lib/docs.cjs +919 -0
- package/deliver-great-systems/bin/lib/docs.test.cjs +211 -0
- package/deliver-great-systems/bin/lib/execution.cjs +705 -0
- package/deliver-great-systems/bin/lib/execution.test.cjs +1472 -0
- package/deliver-great-systems/bin/lib/frontmatter.cjs +324 -0
- package/deliver-great-systems/bin/lib/ideas.cjs +1406 -0
- package/deliver-great-systems/bin/lib/ideas.test.cjs +1417 -0
- package/deliver-great-systems/bin/lib/identity.cjs +125 -0
- package/deliver-great-systems/bin/lib/init.cjs +1114 -0
- package/deliver-great-systems/bin/lib/init.test.cjs +1271 -0
- package/deliver-great-systems/bin/lib/jobs.cjs +2015 -0
- package/deliver-great-systems/bin/lib/jobs.test.cjs +2619 -0
- package/deliver-great-systems/bin/lib/merge-conflicts.cjs +654 -0
- package/deliver-great-systems/bin/lib/merge-conflicts.test.cjs +370 -0
- package/deliver-great-systems/bin/lib/migration.cjs +352 -0
- package/deliver-great-systems/bin/lib/migration.test.cjs +582 -0
- package/deliver-great-systems/bin/lib/milestone.cjs +243 -0
- package/deliver-great-systems/bin/lib/overlap.cjs +437 -0
- package/deliver-great-systems/bin/lib/overlap.test.cjs +747 -0
- package/deliver-great-systems/bin/lib/path-audit.test.cjs +384 -0
- package/deliver-great-systems/bin/lib/paths.cjs +144 -0
- package/deliver-great-systems/bin/lib/paths.test.cjs +486 -0
- package/deliver-great-systems/bin/lib/phase.cjs +910 -0
- package/deliver-great-systems/bin/lib/projects.cjs +691 -0
- package/deliver-great-systems/bin/lib/projects.test.cjs +871 -0
- package/deliver-great-systems/bin/lib/repos.cjs +1432 -0
- package/deliver-great-systems/bin/lib/repos.test.cjs +1882 -0
- package/deliver-great-systems/bin/lib/roadmap.cjs +305 -0
- package/deliver-great-systems/bin/lib/search.cjs +570 -0
- package/deliver-great-systems/bin/lib/specs.cjs +1303 -0
- package/deliver-great-systems/bin/lib/state.cjs +893 -0
- package/deliver-great-systems/bin/lib/template.cjs +228 -0
- package/deliver-great-systems/bin/lib/test-helpers.cjs +291 -0
- package/deliver-great-systems/bin/lib/verify.cjs +796 -0
- package/deliver-great-systems/references/checkpoints.md +776 -0
- package/deliver-great-systems/references/conflict-resolution.md +66 -0
- package/deliver-great-systems/references/context-tiers.md +166 -0
- package/deliver-great-systems/references/continuation-format.md +249 -0
- package/deliver-great-systems/references/decimal-phase-calculation.md +67 -0
- package/deliver-great-systems/references/git-integration.md +250 -0
- package/deliver-great-systems/references/git-planning-commit.md +40 -0
- package/deliver-great-systems/references/model-profile-resolution.md +36 -0
- package/deliver-great-systems/references/model-profiles.md +95 -0
- package/deliver-great-systems/references/phase-argument-parsing.md +61 -0
- package/deliver-great-systems/references/planning-config.md +224 -0
- package/deliver-great-systems/references/questioning.md +162 -0
- package/deliver-great-systems/references/spec-review-loop.md +177 -0
- package/deliver-great-systems/references/tdd.md +265 -0
- package/deliver-great-systems/references/ui-brand.md +160 -0
- package/deliver-great-systems/references/verification-patterns.md +612 -0
- package/deliver-great-systems/templates/DEBUG.md +166 -0
- package/deliver-great-systems/templates/UAT.md +251 -0
- package/deliver-great-systems/templates/VALIDATION.md +95 -0
- package/deliver-great-systems/templates/claude-md.md +74 -0
- package/deliver-great-systems/templates/codebase/architecture.md +257 -0
- package/deliver-great-systems/templates/codebase/concerns.md +312 -0
- package/deliver-great-systems/templates/codebase/conventions.md +309 -0
- package/deliver-great-systems/templates/codebase/integrations.md +282 -0
- package/deliver-great-systems/templates/codebase/stack.md +188 -0
- package/deliver-great-systems/templates/codebase/structure.md +287 -0
- package/deliver-great-systems/templates/codebase/testing.md +482 -0
- package/deliver-great-systems/templates/config.json +38 -0
- package/deliver-great-systems/templates/context.md +354 -0
- package/deliver-great-systems/templates/continue-here.md +80 -0
- package/deliver-great-systems/templates/debug-subagent-prompt.md +93 -0
- package/deliver-great-systems/templates/discovery.md +148 -0
- package/deliver-great-systems/templates/milestone-archive.md +125 -0
- package/deliver-great-systems/templates/milestone.md +117 -0
- package/deliver-great-systems/templates/phase-prompt.md +615 -0
- package/deliver-great-systems/templates/planner-subagent-prompt.md +119 -0
- package/deliver-great-systems/templates/project.md +186 -0
- package/deliver-great-systems/templates/requirements.md +233 -0
- package/deliver-great-systems/templates/research-project/ARCHITECTURE.md +206 -0
- package/deliver-great-systems/templates/research-project/FEATURES.md +149 -0
- package/deliver-great-systems/templates/research-project/PITFALLS.md +202 -0
- package/deliver-great-systems/templates/research-project/STACK.md +122 -0
- package/deliver-great-systems/templates/research-project/SUMMARY.md +172 -0
- package/deliver-great-systems/templates/research.md +554 -0
- package/deliver-great-systems/templates/retrospective.md +54 -0
- package/deliver-great-systems/templates/roadmap.md +204 -0
- package/deliver-great-systems/templates/state.md +178 -0
- package/deliver-great-systems/templates/summary-complex.md +59 -0
- package/deliver-great-systems/templates/summary-minimal.md +41 -0
- package/deliver-great-systems/templates/summary-standard.md +48 -0
- package/deliver-great-systems/templates/summary.md +253 -0
- package/deliver-great-systems/templates/user-setup.md +313 -0
- package/deliver-great-systems/templates/verification-report.md +324 -0
- package/deliver-great-systems/workflows/add-doc.md +151 -0
- package/deliver-great-systems/workflows/add-idea.md +96 -0
- package/deliver-great-systems/workflows/add-phase.md +120 -0
- package/deliver-great-systems/workflows/add-tests.md +359 -0
- package/deliver-great-systems/workflows/add-todo.md +162 -0
- package/deliver-great-systems/workflows/approve-spec.md +194 -0
- package/deliver-great-systems/workflows/audit-milestone.md +364 -0
- package/deliver-great-systems/workflows/audit-phase.md +462 -0
- package/deliver-great-systems/workflows/cancel-job.md +108 -0
- package/deliver-great-systems/workflows/check-todos.md +181 -0
- package/deliver-great-systems/workflows/cleanup.md +247 -0
- package/deliver-great-systems/workflows/codereview.md +526 -0
- package/deliver-great-systems/workflows/complete-milestone.md +1298 -0
- package/deliver-great-systems/workflows/consolidate-ideas.md +365 -0
- package/deliver-great-systems/workflows/create-milestone-job.md +177 -0
- package/deliver-great-systems/workflows/develop-idea.md +544 -0
- package/deliver-great-systems/workflows/diagnose-issues.md +231 -0
- package/deliver-great-systems/workflows/discovery-phase.md +301 -0
- package/deliver-great-systems/workflows/discuss-idea.md +263 -0
- package/deliver-great-systems/workflows/discuss-phase.md +733 -0
- package/deliver-great-systems/workflows/execute-phase.md +571 -0
- package/deliver-great-systems/workflows/execute-plan.md +592 -0
- package/deliver-great-systems/workflows/find-related-ideas.md +271 -0
- package/deliver-great-systems/workflows/health.md +173 -0
- package/deliver-great-systems/workflows/help.md +997 -0
- package/deliver-great-systems/workflows/import-spec.md +381 -0
- package/deliver-great-systems/workflows/init-product.md +767 -0
- package/deliver-great-systems/workflows/insert-phase.md +138 -0
- package/deliver-great-systems/workflows/list-docs.md +119 -0
- package/deliver-great-systems/workflows/list-ideas.md +154 -0
- package/deliver-great-systems/workflows/list-jobs.md +89 -0
- package/deliver-great-systems/workflows/list-phase-assumptions.md +192 -0
- package/deliver-great-systems/workflows/list-specs.md +101 -0
- package/deliver-great-systems/workflows/map-codebase.md +621 -0
- package/deliver-great-systems/workflows/new-milestone.md +591 -0
- package/deliver-great-systems/workflows/new-project.md +1113 -0
- package/deliver-great-systems/workflows/node-repair.md +94 -0
- package/deliver-great-systems/workflows/overlap-check.md +86 -0
- package/deliver-great-systems/workflows/pause-work.md +134 -0
- package/deliver-great-systems/workflows/plan-milestone-gaps.md +306 -0
- package/deliver-great-systems/workflows/plan-phase.md +698 -0
- package/deliver-great-systems/workflows/progress.md +386 -0
- package/deliver-great-systems/workflows/quick.md +845 -0
- package/deliver-great-systems/workflows/refine-spec.md +275 -0
- package/deliver-great-systems/workflows/reject-idea.md +109 -0
- package/deliver-great-systems/workflows/remove-doc.md +117 -0
- package/deliver-great-systems/workflows/remove-phase.md +163 -0
- package/deliver-great-systems/workflows/research-idea.md +325 -0
- package/deliver-great-systems/workflows/research-phase.md +81 -0
- package/deliver-great-systems/workflows/restore-idea.md +101 -0
- package/deliver-great-systems/workflows/resume-project.md +311 -0
- package/deliver-great-systems/workflows/rollback-job.md +130 -0
- package/deliver-great-systems/workflows/run-job.md +498 -0
- package/deliver-great-systems/workflows/search.md +130 -0
- package/deliver-great-systems/workflows/set-profile.md +83 -0
- package/deliver-great-systems/workflows/settings.md +470 -0
- package/deliver-great-systems/workflows/transition.md +563 -0
- package/deliver-great-systems/workflows/undo-consolidation.md +155 -0
- package/deliver-great-systems/workflows/update-idea.md +157 -0
- package/deliver-great-systems/workflows/update.md +242 -0
- package/deliver-great-systems/workflows/validate-phase.md +177 -0
- package/deliver-great-systems/workflows/verify-phase.md +253 -0
- package/deliver-great-systems/workflows/verify-work.md +671 -0
- package/deliver-great-systems/workflows/write-spec.md +450 -0
- package/hooks/dist/dgs-check-update.js +62 -0
- package/hooks/dist/dgs-context-monitor.js +141 -0
- package/hooks/dist/dgs-statusline.js +115 -0
- package/package.json +60 -0
- package/scripts/build-hooks.js +43 -0
|
@@ -0,0 +1,1063 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conflict Agent -- Resolution engine, report generation, verification, semantic detection, and cascading context
|
|
3
|
+
*
|
|
4
|
+
* Provides the conflict resolution brain that takes a conflicted merge state,
|
|
5
|
+
* reads plan context for both sides, produces resolved file contents, and
|
|
6
|
+
* records the full audit trail. Uses Phase 38's detection and classification primitives.
|
|
7
|
+
*
|
|
8
|
+
* Exported functions:
|
|
9
|
+
* resolveConflicts Full resolution flow (alias for cmdConflictAgentRun)
|
|
10
|
+
* verifyResolution Post-resolution test/lint verification
|
|
11
|
+
* detectSemanticConflicts Flag same-domain changes in cleanly merged files
|
|
12
|
+
* selectResolutionStrategy Per-hunk strategy selection from classified hunks
|
|
13
|
+
* resolveFileContent Apply per-hunk strategies to produce resolved file
|
|
14
|
+
* buildResolutionReport Generate RESOLUTION-REPORT.md content
|
|
15
|
+
* createLearningContext Extract learnings from completed resolutions
|
|
16
|
+
* mergeLearningContexts Accumulate learnings across sequential phase merges
|
|
17
|
+
* cmdConflictAgentRun CLI handler: orchestrate full resolution
|
|
18
|
+
* cmdConflictAgentResolveFile CLI handler: resolve a single file
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const { execSync } = require('child_process');
|
|
24
|
+
const { execGit, safeReadFile, loadConfig, output, error } = require('./core.cjs');
|
|
25
|
+
const { classifyConflictHunks, cmdMergeConflictsDetect, cmdMergeConflictsContext } = require('./merge-conflicts.cjs');
|
|
26
|
+
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
27
|
+
const { parseReposMd } = require('./repos.cjs');
|
|
28
|
+
|
|
29
|
+
// ─── Section 1: Resolution Strategy Selection ────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Select a resolution strategy for each classified hunk.
|
|
33
|
+
*
|
|
34
|
+
* Pure function taking classified hunks (from classifyConflictHunks),
|
|
35
|
+
* plan context for both sides, and optional accumulated learnings.
|
|
36
|
+
*
|
|
37
|
+
* @param {Array<{type: string, confidence: string, ours: string, theirs: string, start_line: number, end_line: number}>} classifiedHunks
|
|
38
|
+
* @param {Array<{task_name: string, action_excerpt: string}>|null} planContext
|
|
39
|
+
* @param {Object|null} learnings - Accumulated learnings from previous resolutions
|
|
40
|
+
* @returns {Array<{hunk_index: number, strategy: string, confidence: string, reasoning: string}>}
|
|
41
|
+
*/
|
|
42
|
+
function selectResolutionStrategy(classifiedHunks, planContext, learnings) {
|
|
43
|
+
const contextText = planContext && Array.isArray(planContext)
|
|
44
|
+
? planContext.map(c => (c.task_name || '') + ' ' + (c.action_excerpt || '')).join(' ').toLowerCase()
|
|
45
|
+
: '';
|
|
46
|
+
|
|
47
|
+
return classifiedHunks.map((hunk, index) => {
|
|
48
|
+
let strategy = 'escalate';
|
|
49
|
+
let confidence = hunk.confidence || 'LOW';
|
|
50
|
+
let reasoning = '';
|
|
51
|
+
|
|
52
|
+
switch (hunk.type) {
|
|
53
|
+
case 'ADDITIVE': {
|
|
54
|
+
// Take the side that adds content
|
|
55
|
+
const oursEmpty = !hunk.ours || hunk.ours.trim().length === 0;
|
|
56
|
+
const theirsEmpty = !hunk.theirs || hunk.theirs.trim().length === 0;
|
|
57
|
+
|
|
58
|
+
if (oursEmpty && !theirsEmpty) {
|
|
59
|
+
strategy = 'theirs';
|
|
60
|
+
confidence = 'HIGH';
|
|
61
|
+
reasoning = 'Additive hunk: theirs adds new content, ours is empty';
|
|
62
|
+
} else if (theirsEmpty && !oursEmpty) {
|
|
63
|
+
strategy = 'ours';
|
|
64
|
+
confidence = 'HIGH';
|
|
65
|
+
reasoning = 'Additive hunk: ours adds new content, theirs is empty';
|
|
66
|
+
} else {
|
|
67
|
+
// Both sides have content but one is a superset -- take the superset
|
|
68
|
+
const oursLines = hunk.ours.split('\n').filter(l => l.trim());
|
|
69
|
+
const theirsLines = hunk.theirs.split('\n').filter(l => l.trim());
|
|
70
|
+
if (theirsLines.length >= oursLines.length) {
|
|
71
|
+
strategy = 'theirs';
|
|
72
|
+
confidence = 'HIGH';
|
|
73
|
+
reasoning = 'Additive hunk: theirs is a superset of ours';
|
|
74
|
+
} else {
|
|
75
|
+
strategy = 'ours';
|
|
76
|
+
confidence = 'HIGH';
|
|
77
|
+
reasoning = 'Additive hunk: ours is a superset of theirs';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
case 'DELETION': {
|
|
84
|
+
// Take the side that keeps content unless plan context says deletion was intentional
|
|
85
|
+
const deletionIntentional = contextText.includes('remove') || contextText.includes('delete') ||
|
|
86
|
+
contextText.includes('deprecat') || contextText.includes('clean up') || contextText.includes('strip');
|
|
87
|
+
|
|
88
|
+
if (deletionIntentional) {
|
|
89
|
+
// Plan context suggests deletion was intentional
|
|
90
|
+
const oursEmpty = !hunk.ours || hunk.ours.trim().length === 0;
|
|
91
|
+
strategy = oursEmpty ? 'ours' : 'theirs';
|
|
92
|
+
confidence = 'MEDIUM';
|
|
93
|
+
reasoning = 'Deletion hunk: plan context suggests removal was intentional';
|
|
94
|
+
} else {
|
|
95
|
+
// Keep content (take the non-empty side)
|
|
96
|
+
const oursEmpty = !hunk.ours || hunk.ours.trim().length === 0;
|
|
97
|
+
strategy = oursEmpty ? 'theirs' : 'ours';
|
|
98
|
+
confidence = 'MEDIUM';
|
|
99
|
+
reasoning = 'Deletion hunk: keeping content (no plan context for intentional deletion)';
|
|
100
|
+
}
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
case 'STRUCTURAL': {
|
|
105
|
+
// Attempt combined resolution if changes are to different structural elements
|
|
106
|
+
const oursLines = (hunk.ours || '').split('\n').filter(l => l.trim());
|
|
107
|
+
const theirsLines = (hunk.theirs || '').split('\n').filter(l => l.trim());
|
|
108
|
+
|
|
109
|
+
// Check if both sides add imports/requires (combinable)
|
|
110
|
+
const oursImports = oursLines.filter(l => /^\s*(const\s+\w+\s*=\s*require|import\s+|require\s*\()/.test(l));
|
|
111
|
+
const theirsImports = theirsLines.filter(l => /^\s*(const\s+\w+\s*=\s*require|import\s+|require\s*\()/.test(l));
|
|
112
|
+
|
|
113
|
+
if (oursImports.length > 0 && theirsImports.length > 0) {
|
|
114
|
+
// Both sides add imports -- can combine
|
|
115
|
+
strategy = 'combined';
|
|
116
|
+
confidence = 'HIGH';
|
|
117
|
+
reasoning = 'Structural hunk: both sides add imports/requires, combining and deduplicating';
|
|
118
|
+
} else if (oursImports.length > 0 || theirsImports.length > 0) {
|
|
119
|
+
// Only one side has imports
|
|
120
|
+
strategy = 'combined';
|
|
121
|
+
confidence = 'MEDIUM';
|
|
122
|
+
reasoning = 'Structural hunk: one side adds imports, combining with other changes';
|
|
123
|
+
} else {
|
|
124
|
+
// Same structural element modified differently
|
|
125
|
+
const oursSet = new Set(oursLines.map(l => l.trim()));
|
|
126
|
+
const theirsSet = new Set(theirsLines.map(l => l.trim()));
|
|
127
|
+
const overlap = oursLines.filter(l => theirsSet.has(l.trim()));
|
|
128
|
+
|
|
129
|
+
if (overlap.length === 0) {
|
|
130
|
+
// Different structural elements -- can combine
|
|
131
|
+
strategy = 'combined';
|
|
132
|
+
confidence = 'MEDIUM';
|
|
133
|
+
reasoning = 'Structural hunk: changes target different structural elements';
|
|
134
|
+
} else {
|
|
135
|
+
// Same element modified differently
|
|
136
|
+
strategy = 'escalate';
|
|
137
|
+
confidence = 'LOW';
|
|
138
|
+
reasoning = 'Structural hunk: same structural element modified differently, needs manual review';
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
case 'DIVERGENT': {
|
|
145
|
+
// Use plan context to determine intent
|
|
146
|
+
if (contextText.length > 0) {
|
|
147
|
+
// Check if both plan tasks explain their changes clearly
|
|
148
|
+
const oursLines = (hunk.ours || '').split('\n').filter(l => l.trim());
|
|
149
|
+
const theirsLines = (hunk.theirs || '').split('\n').filter(l => l.trim());
|
|
150
|
+
const oursSet = new Set(oursLines.map(l => l.trim()));
|
|
151
|
+
const theirsSet = new Set(theirsLines.map(l => l.trim()));
|
|
152
|
+
const overlap = oursLines.filter(l => theirsSet.has(l.trim()));
|
|
153
|
+
|
|
154
|
+
if (overlap.length === 0) {
|
|
155
|
+
// No overlapping lines, changes don't conflict semantically
|
|
156
|
+
strategy = 'combined';
|
|
157
|
+
confidence = 'MEDIUM';
|
|
158
|
+
reasoning = 'Divergent hunk: plan context available, no overlapping lines, attempting combined resolution';
|
|
159
|
+
} else {
|
|
160
|
+
strategy = 'escalate';
|
|
161
|
+
confidence = 'LOW';
|
|
162
|
+
reasoning = 'Divergent hunk: overlapping lines with plan context, needs manual review';
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
strategy = 'escalate';
|
|
166
|
+
confidence = 'LOW';
|
|
167
|
+
reasoning = 'Divergent hunk: no plan context available, needs manual review';
|
|
168
|
+
}
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
default:
|
|
173
|
+
strategy = 'escalate';
|
|
174
|
+
confidence = 'LOW';
|
|
175
|
+
reasoning = 'Unknown hunk type: ' + hunk.type;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Boost confidence if accumulated learnings contain info about same domain/file
|
|
179
|
+
if (learnings && learnings.domain_changes) {
|
|
180
|
+
const domains = Object.keys(learnings.domain_changes);
|
|
181
|
+
if (domains.length > 0) {
|
|
182
|
+
const hunkText = ((hunk.ours || '') + ' ' + (hunk.theirs || '')).toLowerCase();
|
|
183
|
+
const domainMatch = domains.some(d => hunkText.includes(d.toLowerCase()));
|
|
184
|
+
|
|
185
|
+
if (domainMatch) {
|
|
186
|
+
if (confidence === 'LOW') {
|
|
187
|
+
confidence = 'MEDIUM';
|
|
188
|
+
reasoning += '. Confidence boosted by accumulated learnings from same domain';
|
|
189
|
+
} else if (confidence === 'MEDIUM') {
|
|
190
|
+
confidence = 'HIGH';
|
|
191
|
+
reasoning += '. Confidence boosted by accumulated learnings from same domain';
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return { hunk_index: index, strategy, confidence, reasoning };
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ─── Section 2: Whole-File Resolution ────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Resolve a file's content by applying per-hunk strategy decisions.
|
|
205
|
+
*
|
|
206
|
+
* @param {string} fileContent - File content with conflict markers
|
|
207
|
+
* @param {Array<{hunk_index: number, strategy: string}>} strategies - Per-hunk strategy decisions
|
|
208
|
+
* @param {string|null} mergeBaseContent - Optional file content from merge-base
|
|
209
|
+
* @returns {{resolved_content: string, has_unresolved: boolean, applied: number, escalated: number}}
|
|
210
|
+
*/
|
|
211
|
+
function resolveFileContent(fileContent, strategies, mergeBaseContent) {
|
|
212
|
+
const lines = fileContent.split('\n');
|
|
213
|
+
const result = [];
|
|
214
|
+
let inConflict = false;
|
|
215
|
+
let inOurs = false;
|
|
216
|
+
let hunkIndex = 0;
|
|
217
|
+
let oursLines = [];
|
|
218
|
+
let theirsLines = [];
|
|
219
|
+
let applied = 0;
|
|
220
|
+
let escalated = 0;
|
|
221
|
+
|
|
222
|
+
for (let i = 0; i < lines.length; i++) {
|
|
223
|
+
const line = lines[i];
|
|
224
|
+
|
|
225
|
+
if (/^<{7}/.test(line)) {
|
|
226
|
+
inConflict = true;
|
|
227
|
+
inOurs = true;
|
|
228
|
+
oursLines = [];
|
|
229
|
+
theirsLines = [];
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (inConflict && /^={7}/.test(line)) {
|
|
234
|
+
inOurs = false;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (inConflict && /^>{7}/.test(line)) {
|
|
239
|
+
// End of conflict hunk -- apply strategy
|
|
240
|
+
const strategyEntry = strategies.find(s => s.hunk_index === hunkIndex);
|
|
241
|
+
const strategy = strategyEntry ? strategyEntry.strategy : 'escalate';
|
|
242
|
+
|
|
243
|
+
switch (strategy) {
|
|
244
|
+
case 'ours':
|
|
245
|
+
result.push(...oursLines);
|
|
246
|
+
applied++;
|
|
247
|
+
break;
|
|
248
|
+
|
|
249
|
+
case 'theirs':
|
|
250
|
+
result.push(...theirsLines);
|
|
251
|
+
applied++;
|
|
252
|
+
break;
|
|
253
|
+
|
|
254
|
+
case 'combined': {
|
|
255
|
+
// For structural hunks with imports/requires, deduplicate and sort
|
|
256
|
+
const oursImportLines = oursLines.filter(l => /^\s*(const\s+\w+\s*=\s*require|import\s+|require\s*\()/.test(l));
|
|
257
|
+
const theirsImportLines = theirsLines.filter(l => /^\s*(const\s+\w+\s*=\s*require|import\s+|require\s*\()/.test(l));
|
|
258
|
+
|
|
259
|
+
if (oursImportLines.length > 0 && theirsImportLines.length > 0 &&
|
|
260
|
+
oursImportLines.length === oursLines.length && theirsImportLines.length === theirsLines.length) {
|
|
261
|
+
// All lines are imports -- deduplicate and sort alphabetically
|
|
262
|
+
const allImports = [...oursLines, ...theirsLines];
|
|
263
|
+
const unique = [...new Set(allImports.map(l => l.trim()))];
|
|
264
|
+
unique.sort((a, b) => a.localeCompare(b));
|
|
265
|
+
result.push(...unique);
|
|
266
|
+
} else {
|
|
267
|
+
// General combined: ours first, then theirs
|
|
268
|
+
result.push(...oursLines);
|
|
269
|
+
result.push(...theirsLines);
|
|
270
|
+
}
|
|
271
|
+
applied++;
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
case 'escalate':
|
|
276
|
+
default:
|
|
277
|
+
// Leave conflict markers in place
|
|
278
|
+
result.push('<<<<<<< ours');
|
|
279
|
+
result.push(...oursLines);
|
|
280
|
+
result.push('=======');
|
|
281
|
+
result.push(...theirsLines);
|
|
282
|
+
result.push('>>>>>>> theirs');
|
|
283
|
+
escalated++;
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
hunkIndex++;
|
|
288
|
+
inConflict = false;
|
|
289
|
+
inOurs = false;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (inConflict) {
|
|
294
|
+
if (inOurs) {
|
|
295
|
+
oursLines.push(line);
|
|
296
|
+
} else {
|
|
297
|
+
theirsLines.push(line);
|
|
298
|
+
}
|
|
299
|
+
} else {
|
|
300
|
+
result.push(line);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const resolvedContent = result.join('\n');
|
|
305
|
+
return {
|
|
306
|
+
resolved_content: resolvedContent,
|
|
307
|
+
has_unresolved: escalated > 0,
|
|
308
|
+
applied,
|
|
309
|
+
escalated,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ─── Section 3: Resolution Report ────────────────────────────────────────────
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Build a RESOLUTION-REPORT.md content string from per-file resolution results.
|
|
317
|
+
*
|
|
318
|
+
* @param {Array<{file: string, hunks: Array, strategies: Array, result: Object}>} fileResults
|
|
319
|
+
* @param {{branches: {ours: string, theirs: string}, phase: string, timestamp: string, semantic_warnings: Array}} metadata
|
|
320
|
+
* @returns {string}
|
|
321
|
+
*/
|
|
322
|
+
function buildResolutionReport(fileResults, metadata) {
|
|
323
|
+
const lines = [];
|
|
324
|
+
|
|
325
|
+
lines.push('# Conflict Resolution Report');
|
|
326
|
+
lines.push('');
|
|
327
|
+
lines.push('## Metadata');
|
|
328
|
+
lines.push('');
|
|
329
|
+
lines.push('| Field | Value |');
|
|
330
|
+
lines.push('| ----- | ----- |');
|
|
331
|
+
lines.push(`| Phase | ${metadata.phase || 'unknown'} |`);
|
|
332
|
+
lines.push(`| Ours Branch | ${metadata.branches?.ours || 'unknown'} |`);
|
|
333
|
+
lines.push(`| Theirs Branch | ${metadata.branches?.theirs || 'unknown'} |`);
|
|
334
|
+
lines.push(`| Timestamp | ${metadata.timestamp || new Date().toISOString()} |`);
|
|
335
|
+
lines.push(`| Total Files | ${fileResults.length} |`);
|
|
336
|
+
|
|
337
|
+
// Confidence breakdown
|
|
338
|
+
const confidenceCounts = { HIGH: 0, MEDIUM: 0, LOW: 0 };
|
|
339
|
+
const strategyCounts = { ours: 0, theirs: 0, combined: 0, escalate: 0 };
|
|
340
|
+
|
|
341
|
+
for (const fr of fileResults) {
|
|
342
|
+
for (const s of (fr.strategies || [])) {
|
|
343
|
+
if (confidenceCounts[s.confidence] !== undefined) confidenceCounts[s.confidence]++;
|
|
344
|
+
if (strategyCounts[s.strategy] !== undefined) strategyCounts[s.strategy]++;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
lines.push(`| HIGH Confidence | ${confidenceCounts.HIGH} |`);
|
|
349
|
+
lines.push(`| MEDIUM Confidence | ${confidenceCounts.MEDIUM} |`);
|
|
350
|
+
lines.push(`| LOW Confidence | ${confidenceCounts.LOW} |`);
|
|
351
|
+
lines.push('');
|
|
352
|
+
|
|
353
|
+
// Resolved Conflicts section
|
|
354
|
+
lines.push('## Resolved Conflicts');
|
|
355
|
+
lines.push('');
|
|
356
|
+
|
|
357
|
+
for (const fr of fileResults) {
|
|
358
|
+
lines.push(`### ${fr.file}`);
|
|
359
|
+
lines.push('');
|
|
360
|
+
|
|
361
|
+
if (fr.strategies && fr.hunks) {
|
|
362
|
+
for (let i = 0; i < fr.strategies.length; i++) {
|
|
363
|
+
const s = fr.strategies[i];
|
|
364
|
+
const h = fr.hunks[i];
|
|
365
|
+
|
|
366
|
+
lines.push(`**Hunk ${i + 1}** (lines ${h ? h.start_line : '?'}-${h ? h.end_line : '?'})`);
|
|
367
|
+
lines.push('');
|
|
368
|
+
lines.push(`- Classification: ${h ? h.type : 'unknown'}`);
|
|
369
|
+
lines.push(`- Strategy: ${s.strategy}`);
|
|
370
|
+
lines.push(`- Confidence: ${s.confidence}`);
|
|
371
|
+
lines.push(`- Reasoning: ${s.reasoning}`);
|
|
372
|
+
lines.push('');
|
|
373
|
+
|
|
374
|
+
// Before/after snippets (truncated to 10 lines each side)
|
|
375
|
+
if (h) {
|
|
376
|
+
const oursSnippet = (h.ours || '').split('\n').slice(0, 10).join('\n');
|
|
377
|
+
const theirsSnippet = (h.theirs || '').split('\n').slice(0, 10).join('\n');
|
|
378
|
+
|
|
379
|
+
if (oursSnippet.trim()) {
|
|
380
|
+
lines.push('<details><summary>Ours side (truncated)</summary>');
|
|
381
|
+
lines.push('');
|
|
382
|
+
lines.push('```');
|
|
383
|
+
lines.push(oursSnippet);
|
|
384
|
+
lines.push('```');
|
|
385
|
+
lines.push('</details>');
|
|
386
|
+
lines.push('');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (theirsSnippet.trim()) {
|
|
390
|
+
lines.push('<details><summary>Theirs side (truncated)</summary>');
|
|
391
|
+
lines.push('');
|
|
392
|
+
lines.push('```');
|
|
393
|
+
lines.push(theirsSnippet);
|
|
394
|
+
lines.push('```');
|
|
395
|
+
lines.push('</details>');
|
|
396
|
+
lines.push('');
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (fr.result) {
|
|
403
|
+
lines.push(`**Result:** ${fr.result.applied} applied, ${fr.result.escalated} escalated`);
|
|
404
|
+
lines.push('');
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Semantic Warnings section
|
|
409
|
+
lines.push('## Semantic Warnings');
|
|
410
|
+
lines.push('');
|
|
411
|
+
|
|
412
|
+
const warnings = metadata.semantic_warnings || [];
|
|
413
|
+
if (warnings.length === 0) {
|
|
414
|
+
lines.push('No semantic conflicts detected.');
|
|
415
|
+
} else {
|
|
416
|
+
for (const w of warnings) {
|
|
417
|
+
lines.push(`- **${w.file}** (${w.risk}): ${w.domain} -- ours: ${w.ours_changes}, theirs: ${w.theirs_changes}`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
lines.push('');
|
|
421
|
+
|
|
422
|
+
// Summary section
|
|
423
|
+
lines.push('## Summary');
|
|
424
|
+
lines.push('');
|
|
425
|
+
lines.push('| Strategy | Count |');
|
|
426
|
+
lines.push('| -------- | ----- |');
|
|
427
|
+
lines.push(`| ours | ${strategyCounts.ours} |`);
|
|
428
|
+
lines.push(`| theirs | ${strategyCounts.theirs} |`);
|
|
429
|
+
lines.push(`| combined | ${strategyCounts.combined} |`);
|
|
430
|
+
lines.push(`| escalate | ${strategyCounts.escalate} |`);
|
|
431
|
+
lines.push('');
|
|
432
|
+
lines.push('| Confidence | Count |');
|
|
433
|
+
lines.push('| ---------- | ----- |');
|
|
434
|
+
lines.push(`| HIGH | ${confidenceCounts.HIGH} |`);
|
|
435
|
+
lines.push(`| MEDIUM | ${confidenceCounts.MEDIUM} |`);
|
|
436
|
+
lines.push(`| LOW | ${confidenceCounts.LOW} |`);
|
|
437
|
+
lines.push('');
|
|
438
|
+
|
|
439
|
+
const allHigh = confidenceCounts.LOW === 0 && confidenceCounts.MEDIUM === 0;
|
|
440
|
+
const hasLow = confidenceCounts.LOW > 0;
|
|
441
|
+
const assessment = allHigh ? 'Clean merge -- all resolutions are HIGH confidence'
|
|
442
|
+
: hasLow ? 'Manual review recommended -- LOW confidence resolutions present'
|
|
443
|
+
: 'Mostly clean merge -- MEDIUM confidence resolutions may need review';
|
|
444
|
+
|
|
445
|
+
lines.push(`**Overall Assessment:** ${assessment}`);
|
|
446
|
+
lines.push('');
|
|
447
|
+
|
|
448
|
+
return lines.join('\n');
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ─── Section 4: Post-Resolution Verification ─────────────────────────────────
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Run post-resolution verification (tests, linting, syntax check) on resolved files.
|
|
455
|
+
*
|
|
456
|
+
* @param {string} cwd - Working directory
|
|
457
|
+
* @param {string[]} resolvedFiles - List of resolved file paths
|
|
458
|
+
* @returns {{passed: boolean, test_output: string|null, lint_output: string|null, errors: string[]}}
|
|
459
|
+
*/
|
|
460
|
+
function verifyResolution(cwd, resolvedFiles) {
|
|
461
|
+
const errors = [];
|
|
462
|
+
let testOutput = null;
|
|
463
|
+
let lintOutput = null;
|
|
464
|
+
|
|
465
|
+
// Check for package.json with test/lint scripts
|
|
466
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
467
|
+
const pkgContent = safeReadFile(pkgPath);
|
|
468
|
+
let pkg = null;
|
|
469
|
+
|
|
470
|
+
if (pkgContent) {
|
|
471
|
+
try {
|
|
472
|
+
pkg = JSON.parse(pkgContent);
|
|
473
|
+
} catch {
|
|
474
|
+
// Invalid package.json
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const hasTestScript = pkg && pkg.scripts && pkg.scripts.test && pkg.scripts.test !== 'echo "Error: no test specified" && exit 1';
|
|
479
|
+
const hasLintScript = pkg && pkg.scripts && (pkg.scripts.lint || pkg.scripts.eslint);
|
|
480
|
+
|
|
481
|
+
// Run tests if available
|
|
482
|
+
if (hasTestScript) {
|
|
483
|
+
try {
|
|
484
|
+
testOutput = execSync('npm test', {
|
|
485
|
+
cwd,
|
|
486
|
+
stdio: 'pipe',
|
|
487
|
+
encoding: 'utf-8',
|
|
488
|
+
timeout: 60000,
|
|
489
|
+
});
|
|
490
|
+
} catch (err) {
|
|
491
|
+
testOutput = (err.stdout || '') + '\n' + (err.stderr || '');
|
|
492
|
+
errors.push('Test suite failed: ' + (err.stderr || err.message || '').slice(0, 500));
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Run linter if available
|
|
497
|
+
if (hasLintScript) {
|
|
498
|
+
const lintCmd = pkg.scripts.lint ? 'npm run lint' : 'npm run eslint';
|
|
499
|
+
try {
|
|
500
|
+
lintOutput = execSync(lintCmd, {
|
|
501
|
+
cwd,
|
|
502
|
+
stdio: 'pipe',
|
|
503
|
+
encoding: 'utf-8',
|
|
504
|
+
timeout: 30000,
|
|
505
|
+
});
|
|
506
|
+
} catch (err) {
|
|
507
|
+
lintOutput = (err.stdout || '') + '\n' + (err.stderr || '');
|
|
508
|
+
errors.push('Linter failed: ' + (err.stderr || err.message || '').slice(0, 500));
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// If no test/lint scripts, check syntax for JS/CJS/TS files
|
|
513
|
+
if (!hasTestScript && !hasLintScript) {
|
|
514
|
+
for (const filePath of resolvedFiles) {
|
|
515
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
516
|
+
if (['.js', '.cjs', '.mjs', '.ts'].includes(ext)) {
|
|
517
|
+
const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
|
|
518
|
+
try {
|
|
519
|
+
execSync('node --check ' + JSON.stringify(fullPath), {
|
|
520
|
+
cwd,
|
|
521
|
+
stdio: 'pipe',
|
|
522
|
+
encoding: 'utf-8',
|
|
523
|
+
timeout: 10000,
|
|
524
|
+
});
|
|
525
|
+
} catch (err) {
|
|
526
|
+
errors.push('Syntax error in ' + filePath + ': ' + (err.stderr || err.message || '').slice(0, 300));
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return {
|
|
533
|
+
passed: errors.length === 0,
|
|
534
|
+
test_output: testOutput,
|
|
535
|
+
lint_output: lintOutput,
|
|
536
|
+
errors,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// ─── Section 5: Semantic Conflict Detection ──────────────────────────────────
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Detect semantic conflicts in cleanly merged files.
|
|
544
|
+
*
|
|
545
|
+
* For files that merged cleanly (no conflict markers), checks if both branches
|
|
546
|
+
* modified the same file and if plan context shows they touched the same domain.
|
|
547
|
+
*
|
|
548
|
+
* @param {string} cwd - Working directory
|
|
549
|
+
* @param {string[]} cleanFiles - Files that merged cleanly
|
|
550
|
+
* @param {Array<{task_name: string, action_excerpt: string}>|null} planContext
|
|
551
|
+
* @returns {Array<{file: string, domain: string, ours_changes: string, theirs_changes: string, risk: string}>}
|
|
552
|
+
*/
|
|
553
|
+
function detectSemanticConflicts(cwd, cleanFiles, planContext) {
|
|
554
|
+
const warnings = [];
|
|
555
|
+
|
|
556
|
+
for (const filePath of cleanFiles) {
|
|
557
|
+
// Check if BOTH branches modified this file
|
|
558
|
+
const oursLog = execGit(cwd, ['log', '--oneline', 'MERGE_HEAD..HEAD', '--', filePath]);
|
|
559
|
+
const theirsLog = execGit(cwd, ['log', '--oneline', 'HEAD..MERGE_HEAD', '--', filePath]);
|
|
560
|
+
|
|
561
|
+
const oursModified = oursLog.exitCode === 0 && oursLog.stdout && oursLog.stdout.trim().length > 0;
|
|
562
|
+
const theirsModified = theirsLog.exitCode === 0 && theirsLog.stdout && theirsLog.stdout.trim().length > 0;
|
|
563
|
+
|
|
564
|
+
if (!oursModified || !theirsModified) continue;
|
|
565
|
+
|
|
566
|
+
// Both branches modified this file -- check if same domain
|
|
567
|
+
const oursChanges = oursLog.stdout.split('\n').filter(l => l.trim()).map(l => l.trim()).join('; ');
|
|
568
|
+
const theirsChanges = theirsLog.stdout.split('\n').filter(l => l.trim()).map(l => l.trim()).join('; ');
|
|
569
|
+
|
|
570
|
+
// Extract domain from file path and plan context
|
|
571
|
+
const fileName = path.basename(filePath, path.extname(filePath));
|
|
572
|
+
let domain = fileName;
|
|
573
|
+
|
|
574
|
+
// Check plan context for domain overlap
|
|
575
|
+
let risk = 'info';
|
|
576
|
+
if (planContext && Array.isArray(planContext)) {
|
|
577
|
+
const contextText = planContext.map(c => (c.task_name || '') + ' ' + (c.action_excerpt || '')).join(' ').toLowerCase();
|
|
578
|
+
|
|
579
|
+
// Check if plan context mentions this file's domain
|
|
580
|
+
if (contextText.includes(fileName.toLowerCase())) {
|
|
581
|
+
// Both branches have plan context for this file's domain -- higher risk
|
|
582
|
+
risk = 'warning';
|
|
583
|
+
domain = fileName + ' (plan context overlap)';
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Check for same-function/class/exports modifications using diff
|
|
588
|
+
const oursDiff = execGit(cwd, ['diff', 'MERGE_HEAD...HEAD', '--', filePath]);
|
|
589
|
+
const theirsDiff = execGit(cwd, ['diff', 'HEAD...MERGE_HEAD', '--', filePath]);
|
|
590
|
+
|
|
591
|
+
if (oursDiff.exitCode === 0 && theirsDiff.exitCode === 0) {
|
|
592
|
+
const oursFunctions = extractModifiedFunctions(oursDiff.stdout);
|
|
593
|
+
const theirsFunctions = extractModifiedFunctions(theirsDiff.stdout);
|
|
594
|
+
|
|
595
|
+
const sharedFunctions = oursFunctions.filter(f => theirsFunctions.includes(f));
|
|
596
|
+
if (sharedFunctions.length > 0) {
|
|
597
|
+
risk = 'warning';
|
|
598
|
+
domain = sharedFunctions.join(', ');
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
warnings.push({
|
|
603
|
+
file: filePath,
|
|
604
|
+
domain,
|
|
605
|
+
ours_changes: oursChanges.slice(0, 200),
|
|
606
|
+
theirs_changes: theirsChanges.slice(0, 200),
|
|
607
|
+
risk,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return warnings;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Extract function/class names from a git diff output.
|
|
616
|
+
*
|
|
617
|
+
* @param {string} diffOutput - Output from git diff
|
|
618
|
+
* @returns {string[]}
|
|
619
|
+
*/
|
|
620
|
+
function extractModifiedFunctions(diffOutput) {
|
|
621
|
+
const functions = [];
|
|
622
|
+
const lines = diffOutput.split('\n');
|
|
623
|
+
|
|
624
|
+
for (const line of lines) {
|
|
625
|
+
if (!line.startsWith('+') && !line.startsWith('-')) continue;
|
|
626
|
+
if (line.startsWith('+++') || line.startsWith('---')) continue;
|
|
627
|
+
|
|
628
|
+
const content = line.slice(1);
|
|
629
|
+
const funcMatch = content.match(/^\s*(?:function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:function|async\s+function|\(|async\s*\())/);
|
|
630
|
+
if (funcMatch) {
|
|
631
|
+
functions.push(funcMatch[1] || funcMatch[2]);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const classMatch = content.match(/^\s*class\s+(\w+)/);
|
|
635
|
+
if (classMatch) {
|
|
636
|
+
functions.push(classMatch[1]);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const exportsMatch = content.match(/^\s*module\.exports\s*[.=]/);
|
|
640
|
+
if (exportsMatch) {
|
|
641
|
+
functions.push('module.exports');
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
return [...new Set(functions)];
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// ─── Section 6: Cascading Context ────────────────────────────────────────────
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Extract learnings from completed resolution results.
|
|
652
|
+
*
|
|
653
|
+
* @param {Array<{file: string, strategies: Array, result: Object}>} resolutionResults
|
|
654
|
+
* @returns {{files_resolved: string[], strategies_used: Array, domain_changes: Object, phase_merges_completed: number}}
|
|
655
|
+
*/
|
|
656
|
+
function createLearningContext(resolutionResults) {
|
|
657
|
+
const filesResolved = [];
|
|
658
|
+
const strategiesUsed = [];
|
|
659
|
+
const domainChanges = {};
|
|
660
|
+
|
|
661
|
+
for (const res of resolutionResults) {
|
|
662
|
+
filesResolved.push(res.file);
|
|
663
|
+
|
|
664
|
+
if (res.strategies) {
|
|
665
|
+
for (const s of res.strategies) {
|
|
666
|
+
strategiesUsed.push({ file: res.file, strategy: s.strategy });
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Extract domain from file path
|
|
671
|
+
const domain = path.basename(res.file, path.extname(res.file));
|
|
672
|
+
domainChanges[domain] = `Resolved via ${(res.strategies || []).map(s => s.strategy).join(', ')}`;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
return {
|
|
676
|
+
files_resolved: filesResolved,
|
|
677
|
+
strategies_used: strategiesUsed,
|
|
678
|
+
domain_changes: domainChanges,
|
|
679
|
+
phase_merges_completed: 1,
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Accumulate learnings across sequential phase merges.
|
|
685
|
+
*
|
|
686
|
+
* @param {Object|null} existing - Existing learnings context
|
|
687
|
+
* @param {Object} newLearnings - New learnings to merge
|
|
688
|
+
* @returns {{files_resolved: string[], strategies_used: Array, domain_changes: Object, phase_merges_completed: number}}
|
|
689
|
+
*/
|
|
690
|
+
function mergeLearningContexts(existing, newLearnings) {
|
|
691
|
+
if (!existing) return { ...newLearnings };
|
|
692
|
+
|
|
693
|
+
return {
|
|
694
|
+
files_resolved: [...(existing.files_resolved || []), ...(newLearnings.files_resolved || [])],
|
|
695
|
+
strategies_used: [...(existing.strategies_used || []), ...(newLearnings.strategies_used || [])],
|
|
696
|
+
domain_changes: { ...(existing.domain_changes || {}), ...(newLearnings.domain_changes || {}) },
|
|
697
|
+
phase_merges_completed: (existing.phase_merges_completed || 0) + (newLearnings.phase_merges_completed || 0),
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// ─── Section 7: CLI Command Handlers ─────────────────────────────────────────
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Orchestrate the full conflict resolution flow.
|
|
705
|
+
*
|
|
706
|
+
* 1. Detect conflicted files
|
|
707
|
+
* 2. For each file: get context, classify hunks, select strategy, resolve
|
|
708
|
+
* 3. Write resolved content to files
|
|
709
|
+
* 4. Run verification
|
|
710
|
+
* 5. If verification fails: roll back resolved files
|
|
711
|
+
* 6. If verification passes: record resolutions, build report
|
|
712
|
+
* 7. Output JSON summary
|
|
713
|
+
*
|
|
714
|
+
* @param {string} cwd - Working directory
|
|
715
|
+
* @param {string} phaseDir - Phase directory for context and report output
|
|
716
|
+
* @param {boolean} raw - Raw output mode
|
|
717
|
+
* @param {string|null} learningsFile - Path to accumulated learnings JSON file
|
|
718
|
+
*/
|
|
719
|
+
function cmdConflictAgentRun(cwd, phaseDir, raw, learningsFile) {
|
|
720
|
+
if (!phaseDir) error('--phase-dir required for conflict-agent run');
|
|
721
|
+
|
|
722
|
+
const absPhaseDir = path.isAbsolute(phaseDir) ? phaseDir : path.join(cwd, phaseDir);
|
|
723
|
+
|
|
724
|
+
// Load accumulated learnings from prior phase merges
|
|
725
|
+
let learnings = null;
|
|
726
|
+
if (learningsFile) {
|
|
727
|
+
try {
|
|
728
|
+
const learningsContent = safeReadFile(learningsFile);
|
|
729
|
+
if (learningsContent) {
|
|
730
|
+
learnings = JSON.parse(learningsContent);
|
|
731
|
+
}
|
|
732
|
+
} catch {
|
|
733
|
+
// Learnings file not parseable -- proceed without
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
const timestamp = new Date().toISOString();
|
|
737
|
+
|
|
738
|
+
// Step 1: Detect conflicted files
|
|
739
|
+
const diffResult = execGit(cwd, ['diff', '--name-only', '--diff-filter=U']);
|
|
740
|
+
const conflictedFiles = diffResult.exitCode === 0 && diffResult.stdout
|
|
741
|
+
? diffResult.stdout.split('\n').filter(f => f.trim())
|
|
742
|
+
: [];
|
|
743
|
+
|
|
744
|
+
if (conflictedFiles.length === 0) {
|
|
745
|
+
// No conflicts -- check for semantic conflicts in cleanly merged files
|
|
746
|
+
const mergeHeadResult = execGit(cwd, ['rev-parse', 'MERGE_HEAD']);
|
|
747
|
+
let semanticWarnings = [];
|
|
748
|
+
|
|
749
|
+
if (mergeHeadResult.exitCode === 0) {
|
|
750
|
+
// Get list of all files modified in the merge
|
|
751
|
+
const mergeFiles = execGit(cwd, ['diff', '--name-only', 'HEAD...MERGE_HEAD']);
|
|
752
|
+
if (mergeFiles.exitCode === 0 && mergeFiles.stdout) {
|
|
753
|
+
const cleanFiles = mergeFiles.stdout.split('\n').filter(f => f.trim());
|
|
754
|
+
semanticWarnings = detectSemanticConflicts(cwd, cleanFiles, null);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
output({
|
|
759
|
+
resolved_count: 0,
|
|
760
|
+
escalated_count: 0,
|
|
761
|
+
verified: true,
|
|
762
|
+
semantic_warnings: semanticWarnings.length,
|
|
763
|
+
report_path: null,
|
|
764
|
+
}, raw);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Step 2: For each file, get context, classify, select strategy, resolve
|
|
769
|
+
const fileResults = [];
|
|
770
|
+
const resolvedFilePaths = [];
|
|
771
|
+
const originalContents = {};
|
|
772
|
+
|
|
773
|
+
for (const filePath of conflictedFiles) {
|
|
774
|
+
const fullPath = path.join(cwd, filePath);
|
|
775
|
+
const fileContent = safeReadFile(fullPath);
|
|
776
|
+
if (!fileContent) continue;
|
|
777
|
+
|
|
778
|
+
// Save original for rollback
|
|
779
|
+
originalContents[filePath] = fileContent;
|
|
780
|
+
|
|
781
|
+
// Assemble plan context from phase dir
|
|
782
|
+
let planContext = [];
|
|
783
|
+
try {
|
|
784
|
+
const phaseFiles = fs.readdirSync(absPhaseDir);
|
|
785
|
+
const planFiles = phaseFiles.filter(f => f.match(/\d+-PLAN\.md$/i) || f === 'PLAN.md');
|
|
786
|
+
|
|
787
|
+
for (const planFile of planFiles) {
|
|
788
|
+
const planContent = safeReadFile(path.join(absPhaseDir, planFile));
|
|
789
|
+
if (!planContent) continue;
|
|
790
|
+
|
|
791
|
+
const fm = extractFrontmatter(planContent);
|
|
792
|
+
const planNumber = fm.plan || planFile.replace(/-PLAN\.md$/i, '');
|
|
793
|
+
|
|
794
|
+
const taskPattern = /<task\b[^>]*>([\s\S]*?)<\/task>/g;
|
|
795
|
+
let match;
|
|
796
|
+
while ((match = taskPattern.exec(planContent)) !== null) {
|
|
797
|
+
const taskBody = match[1];
|
|
798
|
+
const nameMatch = taskBody.match(/<name>([\s\S]*?)<\/name>/);
|
|
799
|
+
const filesMatch = taskBody.match(/<files>([\s\S]*?)<\/files>/);
|
|
800
|
+
const actionMatch = taskBody.match(/<action>([\s\S]*?)<\/action>/);
|
|
801
|
+
|
|
802
|
+
if (filesMatch) {
|
|
803
|
+
const filesList = filesMatch[1].split(/[\n,]/).map(f => f.trim()).filter(f => f);
|
|
804
|
+
const normalizedFilePath = filePath.replace(/^\.\//, '');
|
|
805
|
+
|
|
806
|
+
if (filesList.some(f => normalizedFilePath.includes(f.replace(/^\.\//, '')) || f.replace(/^\.\//, '').includes(normalizedFilePath))) {
|
|
807
|
+
planContext.push({
|
|
808
|
+
plan: planNumber,
|
|
809
|
+
task_name: nameMatch ? nameMatch[1].trim() : 'unnamed',
|
|
810
|
+
action_excerpt: actionMatch ? actionMatch[1].trim().slice(0, 200) : '',
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
} catch {
|
|
817
|
+
// Phase dir not readable
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// Classify hunks
|
|
821
|
+
const classifiedHunks = classifyConflictHunks(fileContent, planContext.length > 0 ? planContext : null);
|
|
822
|
+
|
|
823
|
+
// Select strategies
|
|
824
|
+
const strategies = selectResolutionStrategy(classifiedHunks, planContext, learnings);
|
|
825
|
+
|
|
826
|
+
// Resolve file content
|
|
827
|
+
const result = resolveFileContent(fileContent, strategies, null);
|
|
828
|
+
|
|
829
|
+
fileResults.push({
|
|
830
|
+
file: filePath,
|
|
831
|
+
hunks: classifiedHunks,
|
|
832
|
+
strategies,
|
|
833
|
+
result,
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
// Step 3: Write resolved content to file
|
|
837
|
+
if (!result.has_unresolved) {
|
|
838
|
+
fs.writeFileSync(fullPath, result.resolved_content, 'utf-8');
|
|
839
|
+
resolvedFilePaths.push(filePath);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Step 4: Run verification
|
|
844
|
+
const verification = verifyResolution(cwd, resolvedFilePaths);
|
|
845
|
+
|
|
846
|
+
if (!verification.passed) {
|
|
847
|
+
// Step 5: Roll back resolved files
|
|
848
|
+
for (const filePath of resolvedFilePaths) {
|
|
849
|
+
const fullPath = path.join(cwd, filePath);
|
|
850
|
+
if (originalContents[filePath]) {
|
|
851
|
+
fs.writeFileSync(fullPath, originalContents[filePath], 'utf-8');
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
output({
|
|
856
|
+
resolved_count: 0,
|
|
857
|
+
escalated_count: fileResults.reduce((acc, fr) => acc + (fr.result?.escalated || 0), 0),
|
|
858
|
+
verified: false,
|
|
859
|
+
verification_errors: verification.errors,
|
|
860
|
+
semantic_warnings: 0,
|
|
861
|
+
report_path: null,
|
|
862
|
+
}, raw);
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// Step 6: Record resolutions and build report
|
|
867
|
+
// Record each resolution via merge-conflicts resolved (internal function call)
|
|
868
|
+
for (const fr of fileResults) {
|
|
869
|
+
if (fr.result && !fr.result.has_unresolved) {
|
|
870
|
+
const resolutionJson = JSON.stringify({
|
|
871
|
+
strategy: fr.strategies.length === 1 ? fr.strategies[0].strategy : 'combined',
|
|
872
|
+
description: fr.strategies.map(s => s.reasoning).join('; '),
|
|
873
|
+
confidence: fr.strategies.reduce((min, s) => {
|
|
874
|
+
const order = { HIGH: 3, MEDIUM: 2, LOW: 1 };
|
|
875
|
+
return (order[s.confidence] || 0) < (order[min] || 0) ? s.confidence : min;
|
|
876
|
+
}, 'HIGH'),
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
// Write resolution record
|
|
880
|
+
const resolutionsPath = path.join(absPhaseDir, 'RESOLUTIONS.md');
|
|
881
|
+
let existing = safeReadFile(resolutionsPath);
|
|
882
|
+
if (!existing) {
|
|
883
|
+
const phaseName = path.basename(absPhaseDir);
|
|
884
|
+
existing = `# Merge Conflict Resolutions\n\n**Phase:** ${phaseName}\n**Created:** ${timestamp}\n\n---\n\n`;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
const res = JSON.parse(resolutionJson);
|
|
888
|
+
const entry = [
|
|
889
|
+
`### ${fr.file}\n`,
|
|
890
|
+
`- **Strategy:** ${res.strategy}`,
|
|
891
|
+
`- **Confidence:** ${res.confidence}`,
|
|
892
|
+
`- **Description:** ${res.description || 'No description provided'}`,
|
|
893
|
+
`- **Resolved:** ${timestamp}`,
|
|
894
|
+
'',
|
|
895
|
+
].join('\n');
|
|
896
|
+
|
|
897
|
+
fs.writeFileSync(resolutionsPath, existing + entry + '\n', 'utf-8');
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// Detect semantic conflicts in cleanly merged files
|
|
902
|
+
let semanticWarnings = [];
|
|
903
|
+
const mergeHeadResult = execGit(cwd, ['rev-parse', 'MERGE_HEAD']);
|
|
904
|
+
if (mergeHeadResult.exitCode === 0) {
|
|
905
|
+
const allDiff = execGit(cwd, ['diff', '--name-only', 'HEAD...MERGE_HEAD']);
|
|
906
|
+
if (allDiff.exitCode === 0 && allDiff.stdout) {
|
|
907
|
+
const allFiles = allDiff.stdout.split('\n').filter(f => f.trim());
|
|
908
|
+
const cleanFiles = allFiles.filter(f => !conflictedFiles.includes(f));
|
|
909
|
+
if (cleanFiles.length > 0) {
|
|
910
|
+
semanticWarnings = detectSemanticConflicts(cwd, cleanFiles, null);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// Get branch info for report
|
|
916
|
+
const branchResult = execGit(cwd, ['branch', '--show-current']);
|
|
917
|
+
const oursBranch = branchResult.exitCode === 0 ? branchResult.stdout : 'unknown';
|
|
918
|
+
let theirsBranch = 'unknown';
|
|
919
|
+
if (mergeHeadResult.exitCode === 0) {
|
|
920
|
+
const theirsRef = execGit(cwd, ['log', '-1', '--format=%D', 'MERGE_HEAD']);
|
|
921
|
+
if (theirsRef.exitCode === 0 && theirsRef.stdout) {
|
|
922
|
+
theirsBranch = theirsRef.stdout;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// Build report
|
|
927
|
+
const reportContent = buildResolutionReport(fileResults, {
|
|
928
|
+
branches: { ours: oursBranch, theirs: theirsBranch },
|
|
929
|
+
phase: path.basename(absPhaseDir),
|
|
930
|
+
timestamp,
|
|
931
|
+
semantic_warnings: semanticWarnings,
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
const reportPath = path.join(absPhaseDir, 'RESOLUTION-REPORT.md');
|
|
935
|
+
fs.writeFileSync(reportPath, reportContent, 'utf-8');
|
|
936
|
+
|
|
937
|
+
// Step 7: Output summary
|
|
938
|
+
const totalResolved = fileResults.reduce((acc, fr) => acc + (fr.result?.applied || 0), 0);
|
|
939
|
+
const totalEscalated = fileResults.reduce((acc, fr) => acc + (fr.result?.escalated || 0), 0);
|
|
940
|
+
|
|
941
|
+
// Extract learnings from resolution results for cascading context
|
|
942
|
+
const newLearnings = createLearningContext(fileResults);
|
|
943
|
+
|
|
944
|
+
output({
|
|
945
|
+
resolved_count: totalResolved,
|
|
946
|
+
escalated_count: totalEscalated,
|
|
947
|
+
verified: true,
|
|
948
|
+
semantic_warnings: semanticWarnings.length,
|
|
949
|
+
report_path: reportPath,
|
|
950
|
+
learnings: newLearnings,
|
|
951
|
+
}, raw);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Resolve a single file (used by escalation retry flow).
|
|
956
|
+
*
|
|
957
|
+
* @param {string} cwd - Working directory
|
|
958
|
+
* @param {string} filePath - Path to the conflicted file
|
|
959
|
+
* @param {string} phaseDir - Phase directory for context
|
|
960
|
+
* @param {boolean} raw - Raw output mode
|
|
961
|
+
* @param {string|null} hint - Optional user hint text for guiding resolution strategy
|
|
962
|
+
*/
|
|
963
|
+
function cmdConflictAgentResolveFile(cwd, filePath, phaseDir, raw, hint) {
|
|
964
|
+
if (!filePath) error('File path required');
|
|
965
|
+
if (!phaseDir) error('--phase-dir required for resolve-file command');
|
|
966
|
+
|
|
967
|
+
const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
|
|
968
|
+
const fileContent = safeReadFile(fullPath);
|
|
969
|
+
|
|
970
|
+
if (!fileContent) {
|
|
971
|
+
error('File not found: ' + filePath);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
if (!/^<{7}/m.test(fileContent)) {
|
|
975
|
+
error('File has no conflict markers: ' + filePath);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
const absPhaseDir = path.isAbsolute(phaseDir) ? phaseDir : path.join(cwd, phaseDir);
|
|
979
|
+
|
|
980
|
+
// Assemble plan context
|
|
981
|
+
let planContext = [];
|
|
982
|
+
try {
|
|
983
|
+
const phaseFiles = fs.readdirSync(absPhaseDir);
|
|
984
|
+
const planFiles = phaseFiles.filter(f => f.match(/\d+-PLAN\.md$/i) || f === 'PLAN.md');
|
|
985
|
+
|
|
986
|
+
for (const planFile of planFiles) {
|
|
987
|
+
const planContent = safeReadFile(path.join(absPhaseDir, planFile));
|
|
988
|
+
if (!planContent) continue;
|
|
989
|
+
|
|
990
|
+
const fm = extractFrontmatter(planContent);
|
|
991
|
+
const planNumber = fm.plan || planFile.replace(/-PLAN\.md$/i, '');
|
|
992
|
+
|
|
993
|
+
const taskPattern = /<task\b[^>]*>([\s\S]*?)<\/task>/g;
|
|
994
|
+
let match;
|
|
995
|
+
while ((match = taskPattern.exec(planContent)) !== null) {
|
|
996
|
+
const taskBody = match[1];
|
|
997
|
+
const nameMatch = taskBody.match(/<name>([\s\S]*?)<\/name>/);
|
|
998
|
+
const filesMatch = taskBody.match(/<files>([\s\S]*?)<\/files>/);
|
|
999
|
+
const actionMatch = taskBody.match(/<action>([\s\S]*?)<\/action>/);
|
|
1000
|
+
|
|
1001
|
+
if (filesMatch) {
|
|
1002
|
+
const filesList = filesMatch[1].split(/[\n,]/).map(f => f.trim()).filter(f => f);
|
|
1003
|
+
const normalizedFilePath = filePath.replace(/^\.\//, '');
|
|
1004
|
+
|
|
1005
|
+
if (filesList.some(f => normalizedFilePath.includes(f.replace(/^\.\//, '')) || f.replace(/^\.\//, '').includes(normalizedFilePath))) {
|
|
1006
|
+
planContext.push({
|
|
1007
|
+
plan: planNumber,
|
|
1008
|
+
task_name: nameMatch ? nameMatch[1].trim() : 'unnamed',
|
|
1009
|
+
action_excerpt: actionMatch ? actionMatch[1].trim().slice(0, 200) : '',
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
} catch {
|
|
1016
|
+
// Phase dir not readable
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// Classify and resolve
|
|
1020
|
+
const classifiedHunks = classifyConflictHunks(fileContent, planContext.length > 0 ? planContext : null);
|
|
1021
|
+
|
|
1022
|
+
// Augment plan context with user hint if provided
|
|
1023
|
+
let augmentedContext = planContext;
|
|
1024
|
+
if (hint && hint.trim()) {
|
|
1025
|
+
augmentedContext = [...planContext, {
|
|
1026
|
+
plan: 'user-hint',
|
|
1027
|
+
task_name: 'User escalation hint',
|
|
1028
|
+
action_excerpt: hint.trim(),
|
|
1029
|
+
}];
|
|
1030
|
+
}
|
|
1031
|
+
const strategies = selectResolutionStrategy(classifiedHunks, augmentedContext, null);
|
|
1032
|
+
const result = resolveFileContent(fileContent, strategies, null);
|
|
1033
|
+
|
|
1034
|
+
// Write resolved content
|
|
1035
|
+
if (!result.has_unresolved) {
|
|
1036
|
+
fs.writeFileSync(fullPath, result.resolved_content, 'utf-8');
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
output({
|
|
1040
|
+
file: filePath,
|
|
1041
|
+
hint_applied: !!hint,
|
|
1042
|
+
hunks: classifiedHunks.length,
|
|
1043
|
+
applied: result.applied,
|
|
1044
|
+
escalated: result.escalated,
|
|
1045
|
+
has_unresolved: result.has_unresolved,
|
|
1046
|
+
strategies: strategies.map(s => ({ strategy: s.strategy, confidence: s.confidence })),
|
|
1047
|
+
}, raw);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// ─── Exports ─────────────────────────────────────────────────────────────────
|
|
1051
|
+
|
|
1052
|
+
module.exports = {
|
|
1053
|
+
resolveConflicts: cmdConflictAgentRun,
|
|
1054
|
+
verifyResolution,
|
|
1055
|
+
detectSemanticConflicts,
|
|
1056
|
+
selectResolutionStrategy,
|
|
1057
|
+
resolveFileContent,
|
|
1058
|
+
buildResolutionReport,
|
|
1059
|
+
createLearningContext,
|
|
1060
|
+
mergeLearningContexts,
|
|
1061
|
+
cmdConflictAgentRun,
|
|
1062
|
+
cmdConflictAgentResolveFile,
|
|
1063
|
+
};
|