@aperant/framework 0.13.0-next.0 → 0.15.0
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/CHANGELOG.md +89 -3
- package/README.md +11 -1
- package/agents/apt-pr-review-i18n.md +190 -0
- package/agents/apt-pr-review-xplatform.md +189 -0
- package/dist/cli/commands/audit-branch-current.d.mts +27 -5
- package/dist/cli/commands/audit-branch-current.d.mts.map +1 -1
- package/dist/cli/commands/audit-branch-current.mjs +3 -3
- package/dist/cli/commands/audit-branch-current.mjs.map +1 -1
- package/dist/cli/commands/event.d.mts.map +1 -1
- package/dist/cli/commands/event.mjs +6 -4
- package/dist/cli/commands/event.mjs.map +1 -1
- package/dist/cli/commands/fan-out.d.mts.map +1 -1
- package/dist/cli/commands/fan-out.mjs +3 -1
- package/dist/cli/commands/fan-out.mjs.map +1 -1
- package/dist/cli/commands/features-audit.d.mts.map +1 -1
- package/dist/cli/commands/features-audit.mjs +6 -1
- package/dist/cli/commands/features-audit.mjs.map +1 -1
- package/dist/cli/commands/followups.d.mts +5 -0
- package/dist/cli/commands/followups.d.mts.map +1 -0
- package/dist/cli/commands/followups.mjs +266 -0
- package/dist/cli/commands/followups.mjs.map +1 -0
- package/dist/cli/commands/health-check.d.mts.map +1 -1
- package/dist/cli/commands/health-check.mjs +3 -1
- package/dist/cli/commands/health-check.mjs.map +1 -1
- package/dist/cli/commands/merge-detect.d.mts +29 -0
- package/dist/cli/commands/merge-detect.d.mts.map +1 -0
- package/dist/cli/commands/merge-detect.mjs +196 -0
- package/dist/cli/commands/merge-detect.mjs.map +1 -0
- package/dist/cli/commands/merge-integrate.d.mts +28 -0
- package/dist/cli/commands/merge-integrate.d.mts.map +1 -0
- package/dist/cli/commands/merge-integrate.mjs +383 -0
- package/dist/cli/commands/merge-integrate.mjs.map +1 -0
- package/dist/cli/commands/pr-review/detect-specialists.d.mts +58 -0
- package/dist/cli/commands/pr-review/detect-specialists.d.mts.map +1 -0
- package/dist/cli/commands/pr-review/detect-specialists.mjs +980 -0
- package/dist/cli/commands/pr-review/detect-specialists.mjs.map +1 -0
- package/dist/cli/commands/pr-review.d.mts.map +1 -1
- package/dist/cli/commands/pr-review.mjs +11 -2
- package/dist/cli/commands/pr-review.mjs.map +1 -1
- package/dist/cli/commands/precompact-hook.d.mts.map +1 -1
- package/dist/cli/commands/precompact-hook.mjs +10 -6
- package/dist/cli/commands/precompact-hook.mjs.map +1 -1
- package/dist/cli/commands/roadmap.d.mts.map +1 -1
- package/dist/cli/commands/roadmap.mjs +24 -11
- package/dist/cli/commands/roadmap.mjs.map +1 -1
- package/dist/cli/commands/seed.d.mts.map +1 -1
- package/dist/cli/commands/seed.mjs +3 -1
- package/dist/cli/commands/seed.mjs.map +1 -1
- package/dist/cli/commands/state.d.mts.map +1 -1
- package/dist/cli/commands/state.mjs +9 -3
- package/dist/cli/commands/state.mjs.map +1 -1
- package/dist/cli/commands/statusline.d.mts.map +1 -1
- package/dist/cli/commands/statusline.mjs +5 -1
- package/dist/cli/commands/statusline.mjs.map +1 -1
- package/dist/cli/commands/task.d.mts.map +1 -1
- package/dist/cli/commands/task.mjs +126 -23
- package/dist/cli/commands/task.mjs.map +1 -1
- package/dist/cli/commands/worktree.d.mts +1 -1
- package/dist/cli/commands/worktree.d.mts.map +1 -1
- package/dist/cli/commands/worktree.mjs +282 -83
- package/dist/cli/commands/worktree.mjs.map +1 -1
- package/dist/cli/config/post-merge-sweep.d.mts.map +1 -1
- package/dist/cli/config/post-merge-sweep.mjs +3 -2
- package/dist/cli/config/post-merge-sweep.mjs.map +1 -1
- package/dist/cli/config/share-policy.d.mts +3 -1
- package/dist/cli/config/share-policy.d.mts.map +1 -1
- package/dist/cli/config/share-policy.mjs +16 -3
- package/dist/cli/config/share-policy.mjs.map +1 -1
- package/dist/cli/config/upgrade-gitignore.d.mts.map +1 -1
- package/dist/cli/config/upgrade-gitignore.mjs +27 -5
- package/dist/cli/config/upgrade-gitignore.mjs.map +1 -1
- package/dist/cli/consistency/parse-qa.mjs +12 -1
- package/dist/cli/consistency/parse-qa.mjs.map +1 -1
- package/dist/cli/consistency/parse-review.d.mts +23 -0
- package/dist/cli/consistency/parse-review.d.mts.map +1 -1
- package/dist/cli/consistency/parse-review.mjs +36 -0
- package/dist/cli/consistency/parse-review.mjs.map +1 -1
- package/dist/cli/consistency/parse-spec.d.mts +16 -0
- package/dist/cli/consistency/parse-spec.d.mts.map +1 -1
- package/dist/cli/consistency/parse-spec.mjs +44 -0
- package/dist/cli/consistency/parse-spec.mjs.map +1 -1
- package/dist/cli/coordination/agent-identity.d.mts +5 -2
- package/dist/cli/coordination/agent-identity.d.mts.map +1 -1
- package/dist/cli/coordination/agent-identity.mjs +6 -4
- package/dist/cli/coordination/agent-identity.mjs.map +1 -1
- package/dist/cli/coordination/store.d.mts +1 -1
- package/dist/cli/dispatch.d.mts.map +1 -1
- package/dist/cli/dispatch.mjs +4 -0
- package/dist/cli/dispatch.mjs.map +1 -1
- package/dist/cli/followups/derive.d.mts +63 -0
- package/dist/cli/followups/derive.d.mts.map +1 -0
- package/dist/cli/followups/derive.mjs +115 -0
- package/dist/cli/followups/derive.mjs.map +1 -0
- package/dist/cli/followups/schema.d.mts +165 -0
- package/dist/cli/followups/schema.d.mts.map +1 -0
- package/dist/cli/followups/schema.mjs +251 -0
- package/dist/cli/followups/schema.mjs.map +1 -0
- package/dist/cli/gate/context.d.mts +1 -1
- package/dist/cli/gate/gates/followups-adjudicated.d.mts +69 -0
- package/dist/cli/gate/gates/followups-adjudicated.d.mts.map +1 -0
- package/dist/cli/gate/gates/followups-adjudicated.mjs +141 -0
- package/dist/cli/gate/gates/followups-adjudicated.mjs.map +1 -0
- package/dist/cli/gate/gates/index.mjs +1 -0
- package/dist/cli/gate/gates/index.mjs.map +1 -1
- package/dist/cli/git/materialize-env.d.mts +39 -0
- package/dist/cli/git/materialize-env.d.mts.map +1 -0
- package/dist/cli/git/materialize-env.mjs +217 -0
- package/dist/cli/git/materialize-env.mjs.map +1 -0
- package/dist/cli/help.d.mts.map +1 -1
- package/dist/cli/help.mjs +9 -1
- package/dist/cli/help.mjs.map +1 -1
- package/dist/cli/install/legacy-paths.d.mts.map +1 -1
- package/dist/cli/install/legacy-paths.mjs +4 -0
- package/dist/cli/install/legacy-paths.mjs.map +1 -1
- package/dist/cli/roadmap/lifecycle.d.mts.map +1 -1
- package/dist/cli/roadmap/lifecycle.mjs +4 -1
- package/dist/cli/roadmap/lifecycle.mjs.map +1 -1
- package/dist/cli/task/ids.d.mts +9 -0
- package/dist/cli/task/ids.d.mts.map +1 -1
- package/dist/cli/task/ids.mjs +9 -0
- package/dist/cli/task/ids.mjs.map +1 -1
- package/dist/cli/task/index-md.d.mts +5 -0
- package/dist/cli/task/index-md.d.mts.map +1 -1
- package/dist/cli/task/index-md.mjs +12 -4
- package/dist/cli/task/index-md.mjs.map +1 -1
- package/dist/cli/util/io.d.mts +6 -0
- package/dist/cli/util/io.d.mts.map +1 -1
- package/dist/cli/util/io.mjs +16 -1
- package/dist/cli/util/io.mjs.map +1 -1
- package/dist/cli/util/project.d.mts +7 -2
- package/dist/cli/util/project.d.mts.map +1 -1
- package/dist/cli/util/project.mjs +11 -14
- package/dist/cli/util/project.mjs.map +1 -1
- package/dist/cli/util/state-paths.d.mts +19 -0
- package/dist/cli/util/state-paths.d.mts.map +1 -0
- package/dist/cli/util/state-paths.mjs +45 -0
- package/dist/cli/util/state-paths.mjs.map +1 -0
- package/dist/plugin/.claude-plugin/plugin.json +4 -1
- package/dist/plugin/agents/apt-pr-review-i18n.md +190 -0
- package/dist/plugin/agents/apt-pr-review-xplatform.md +189 -0
- package/dist/plugin/skills/apt/SKILL.md +13 -6
- package/dist/plugin/skills/apt-close-task/SKILL.md +48 -0
- package/dist/plugin/skills/apt-debug/SKILL.md +19 -9
- package/dist/plugin/skills/apt-execute/SKILL.md +3 -1
- package/dist/plugin/skills/apt-fan-out/SKILL.md +65 -1
- package/dist/plugin/skills/apt-merge-conflict-resolution/SKILL.md +227 -0
- package/dist/plugin/skills/apt-pr-review/SCHEMA.md +4 -2
- package/dist/plugin/skills/apt-pr-review/SKILL.md +117 -29
- package/dist/plugin/skills/apt-quick/SKILL.md +24 -1
- package/dist/plugin/skills/apt-review/SKILL.md +6 -0
- package/dist/plugin/skills/apt-run/SKILL.md +1 -1
- package/dist/plugin/skills/apt-setup/SKILL.md +21 -3
- package/dist/plugin/skills/apt-ship/SKILL.md +32 -11
- package/dist/plugin/skills/apt-terminal/SKILL.md +8 -0
- package/dist/plugin/skills/apt-triage/SKILL.md +4 -0
- package/dist/plugin/skills/apt-verify/SKILL.md +24 -0
- package/dist/plugin/skills/apt-watch-ci/SKILL.md +3 -1
- package/dist/types/config.d.ts +33 -0
- package/dist/types/config.d.ts.map +1 -1
- package/package.json +138 -138
- package/prompts/conductor-framework-context.md +1 -1
- package/prompts/github/pr_i18n_agent.md +279 -0
- package/prompts/github/pr_xplatform_agent.md +283 -0
- package/prompts/inbox-clarification.md +18 -11
- package/skills/apt/SKILL.md +9 -3
- package/skills/apt-close-task/SKILL.md +48 -0
- package/skills/apt-debug/SKILL.md +19 -9
- package/skills/apt-execute/SKILL.md +3 -1
- package/skills/apt-fan-out/SKILL.md +65 -1
- package/skills/apt-merge-conflict-resolution/SKILL.md +227 -0
- package/skills/apt-pr-review/SCHEMA.md +4 -2
- package/skills/apt-pr-review/SKILL.md +117 -29
- package/skills/apt-quick/SKILL.md +24 -1
- package/skills/apt-review/SKILL.md +6 -0
- package/skills/apt-run/SKILL.md +1 -1
- package/skills/apt-setup/SKILL.md +21 -3
- package/skills/apt-ship/SKILL.md +32 -11
- package/skills/apt-terminal/SKILL.md +8 -0
- package/skills/apt-triage/SKILL.md +4 -0
- package/skills/apt-verify/SKILL.md +24 -0
- package/skills/apt-watch-ci/SKILL.md +3 -1
- package/src/cli/commands/audit-branch-current.mjs +3 -3
- package/src/cli/commands/event.mjs +6 -4
- package/src/cli/commands/fan-out.mjs +3 -1
- package/src/cli/commands/features-audit.mjs +6 -1
- package/src/cli/commands/followups.mjs +303 -0
- package/src/cli/commands/health-check.mjs +3 -1
- package/src/cli/commands/merge-detect.mjs +203 -0
- package/src/cli/commands/merge-integrate.mjs +392 -0
- package/src/cli/commands/pr-review/__fixtures__/android-strings/app/src/main/res/values-fr/strings.xml +3 -0
- package/src/cli/commands/pr-review/__fixtures__/babel-js/package.json +4 -0
- package/src/cli/commands/pr-review/__fixtures__/c-ifdef/src/main.c +4 -0
- package/src/cli/commands/pr-review/__fixtures__/ci-matrix/.github/workflows/ci.yml +10 -0
- package/src/cli/commands/pr-review/__fixtures__/cmake-win/CMakeLists.txt +5 -0
- package/src/cli/commands/pr-review/__fixtures__/diffs/add-arb.patch +9 -0
- package/src/cli/commands/pr-review/__fixtures__/diffs/ci-single-os-dep.patch +13 -0
- package/src/cli/commands/pr-review/__fixtures__/diffs/ci-single-os-list-item-dep.patch +17 -0
- package/src/cli/commands/pr-review/__fixtures__/diffs/lone-weak-t.patch +8 -0
- package/src/cli/commands/pr-review/__fixtures__/diffs/monorepo-b.patch +7 -0
- package/src/cli/commands/pr-review/__fixtures__/diffs/two-weak-xpl.patch +12 -0
- package/src/cli/commands/pr-review/__fixtures__/dropped-underscore/src/app.js +2 -0
- package/src/cli/commands/pr-review/__fixtures__/electron-app/package.json +4 -0
- package/src/cli/commands/pr-review/__fixtures__/i18n-locale-file/src/locales/fr.json +1 -0
- package/src/cli/commands/pr-review/__fixtures__/monorepo/packages/a/locales/fr.json +1 -0
- package/src/cli/commands/pr-review/__fixtures__/monorepo/packages/b/src/index.ts +1 -0
- package/src/cli/commands/pr-review/__fixtures__/py-babel/pyproject.toml +3 -0
- package/src/cli/commands/pr-review/__fixtures__/qt-i18n-dir/i18n/app_fr.ts +11 -0
- package/src/cli/commands/pr-review/__fixtures__/rust-cli/Cargo.toml +7 -0
- package/src/cli/commands/pr-review/__fixtures__/rust-cli/src/main.rs +1 -0
- package/src/cli/commands/pr-review/__fixtures__/ts-dir/ts/foo.ts +3 -0
- package/src/cli/commands/pr-review/detect-specialists.mjs +1023 -0
- package/src/cli/commands/pr-review.mjs +12 -2
- package/src/cli/commands/precompact-hook.mjs +10 -6
- package/src/cli/commands/roadmap.mjs +24 -11
- package/src/cli/commands/seed.mjs +3 -1
- package/src/cli/commands/state.mjs +9 -3
- package/src/cli/commands/statusline.mjs +5 -1
- package/src/cli/commands/task.mjs +135 -23
- package/src/cli/commands/worktree.mjs +298 -81
- package/src/cli/config/post-merge-sweep.mjs +3 -2
- package/src/cli/config/share-policy.mjs +16 -3
- package/src/cli/config/upgrade-gitignore.mjs +27 -5
- package/src/cli/consistency/parse-qa.mjs +11 -1
- package/src/cli/consistency/parse-review.mjs +36 -0
- package/src/cli/consistency/parse-spec.mjs +43 -0
- package/src/cli/coordination/agent-identity.mjs +6 -4
- package/src/cli/dispatch.mjs +4 -0
- package/src/cli/followups/derive.mjs +123 -0
- package/src/cli/followups/schema.mjs +264 -0
- package/src/cli/gate/gates/followups-adjudicated.mjs +158 -0
- package/src/cli/gate/gates/index.mjs +1 -0
- package/src/cli/git/materialize-env.mjs +212 -0
- package/src/cli/help.mjs +9 -1
- package/src/cli/install/legacy-paths.mjs +4 -0
- package/src/cli/roadmap/lifecycle.mjs +4 -1
- package/src/cli/task/ids.mjs +9 -0
- package/src/cli/task/index-md.mjs +12 -4
- package/src/cli/util/io.mjs +15 -1
- package/src/cli/util/project.mjs +11 -14
- package/src/cli/util/state-paths.mjs +45 -0
- package/prompts/followup_planner.md +0 -399
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commands/merge-detect.mjs — conflict envelope + on-disk marker reader for
|
|
3
|
+
* apt:merge-conflict-resolution.
|
|
4
|
+
*
|
|
5
|
+
* Two read-only subcommands, both pure git inspection — NO history rewrite,
|
|
6
|
+
* NO `git merge`, NO `git rebase`, NO index mutation. The SKILL.md prose owns
|
|
7
|
+
* every state change; this primitive only OBSERVES.
|
|
8
|
+
*
|
|
9
|
+
* apt-tools merge detect <project-dir> [--base <ref>] [--head <ref>]
|
|
10
|
+
* Trial-merge probe via `git merge-tree --write-tree --no-messages`.
|
|
11
|
+
* Reuses the EXACT detection from audit-branch-current.mjs (`gitTry`,
|
|
12
|
+
* `parseConflictPaths`, `isGitVersionTooOld`) — no reinvented detection.
|
|
13
|
+
* --base defaults to `origin/main`; --head defaults to `HEAD`. Returns
|
|
14
|
+
* { mergeable, conflicts_preview?, audit_warning? }. `mergeable: null`
|
|
15
|
+
* on an unprobeable git (too old / invocation error) so the caller can
|
|
16
|
+
* fall back gracefully rather than asserting clean.
|
|
17
|
+
*
|
|
18
|
+
* apt-tools merge markers <project-dir>
|
|
19
|
+
* Lists files that carry on-disk conflict markers (the state AFTER a
|
|
20
|
+
* real `git merge`/`git rebase` left the worktree conflicted) and reads
|
|
21
|
+
* each file's full content so the executing agent can resolve the
|
|
22
|
+
* markers inline. Source of truth for "is the merge conflicted" is
|
|
23
|
+
* `git diff --name-only --diff-filter=U` (unmerged paths). Returns
|
|
24
|
+
* { conflicted, files: [{ path, content }] }. Empty `conflicted` means
|
|
25
|
+
* a clean tree — nothing to resolve.
|
|
26
|
+
*
|
|
27
|
+
* Why agent-inline (not a programmatic resolver): the framework CLI is
|
|
28
|
+
* self-contained ESM and MUST NOT import @aperant/core (it declares no such
|
|
29
|
+
* dep — see runtime-capabilities.mjs:5). The LLM driving the CLI reads the
|
|
30
|
+
* markers this command surfaces and resolves them, then re-probes via
|
|
31
|
+
* `detect`. See GitHub issue #390 for the architecture decision.
|
|
32
|
+
*
|
|
33
|
+
* @internal
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { readFileSync } from 'node:fs'
|
|
37
|
+
import { join, resolve } from 'node:path'
|
|
38
|
+
import { err, ok } from '../util/result.mjs'
|
|
39
|
+
import { gitTry, isGitVersionTooOld, parseConflictPaths } from './audit-branch-current.mjs'
|
|
40
|
+
import { mergeIntegrate } from './merge-integrate.mjs'
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Parse `--<name> <value>` (or `--<name>=<value>`) from extraArgs.
|
|
44
|
+
*
|
|
45
|
+
* @param {string[]} extraArgs
|
|
46
|
+
* @param {string} name
|
|
47
|
+
* @param {string} fallback
|
|
48
|
+
* @returns {string}
|
|
49
|
+
*/
|
|
50
|
+
function parseFlag(extraArgs, name, fallback) {
|
|
51
|
+
const long = `--${name}`
|
|
52
|
+
const eq = `--${name}=`
|
|
53
|
+
for (let i = 0; i < extraArgs.length; i++) {
|
|
54
|
+
const a = extraArgs[i]
|
|
55
|
+
if (a === long && i + 1 < extraArgs.length) return extraArgs[i + 1]
|
|
56
|
+
if (a.startsWith(eq)) return a.slice(eq.length)
|
|
57
|
+
}
|
|
58
|
+
return fallback
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* `apt-tools merge detect` — trial-merge mergeability probe.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} targetDir
|
|
65
|
+
* @param {string[]} extraArgs
|
|
66
|
+
* @returns {import('../util/result.mjs').CommandResult}
|
|
67
|
+
*/
|
|
68
|
+
function mergeDetect(targetDir, extraArgs) {
|
|
69
|
+
const base = parseFlag(extraArgs, 'base', 'origin/main')
|
|
70
|
+
const head = parseFlag(extraArgs, 'head', 'HEAD')
|
|
71
|
+
|
|
72
|
+
const probe = gitTry(targetDir, ['merge-tree', '--write-tree', '--no-messages', head, base])
|
|
73
|
+
|
|
74
|
+
/** @type {boolean | null} */
|
|
75
|
+
let mergeable
|
|
76
|
+
/** @type {string[] | undefined} */
|
|
77
|
+
let conflictsPreview
|
|
78
|
+
/** @type {string | undefined} */
|
|
79
|
+
let auditWarning
|
|
80
|
+
|
|
81
|
+
if (probe.ok) {
|
|
82
|
+
// Documented merge-tree exit-code semantics: 0 = clean.
|
|
83
|
+
mergeable = true
|
|
84
|
+
} else if (probe.code === 1 && probe.stdout.trim().length > 0) {
|
|
85
|
+
// 1 = conflict, BUT only when stdout carried the conflict report
|
|
86
|
+
// (the tree-OID line + per-stage paths). A bad ref also exits 1 with
|
|
87
|
+
// EMPTY stdout and the error on stderr (e.g. "<ref> - not something we
|
|
88
|
+
// can merge") — that is a probe failure, not a clean-merge conflict, so
|
|
89
|
+
// we fall through to the merge-tree-probe-failed branch below.
|
|
90
|
+
mergeable = false
|
|
91
|
+
conflictsPreview = parseConflictPaths(probe.stdout)
|
|
92
|
+
} else if (isGitVersionTooOld(probe.stderr)) {
|
|
93
|
+
mergeable = null
|
|
94
|
+
auditWarning = 'git-version-too-old'
|
|
95
|
+
} else {
|
|
96
|
+
// Other = invocation error (bad ref, not a repo, etc.).
|
|
97
|
+
mergeable = null
|
|
98
|
+
auditWarning = 'merge-tree-probe-failed'
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const envelope = {
|
|
102
|
+
status: 'ok',
|
|
103
|
+
command: 'merge-detect',
|
|
104
|
+
projectDir: targetDir,
|
|
105
|
+
base,
|
|
106
|
+
head,
|
|
107
|
+
mergeable,
|
|
108
|
+
}
|
|
109
|
+
if (conflictsPreview && conflictsPreview.length > 0) {
|
|
110
|
+
envelope.conflicts_preview = conflictsPreview
|
|
111
|
+
}
|
|
112
|
+
if (auditWarning) {
|
|
113
|
+
envelope.audit_warning = auditWarning
|
|
114
|
+
if (probe.stderr.trim()) {
|
|
115
|
+
envelope.warnings = [probe.stderr.trim().slice(0, 200)]
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return ok(envelope)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* `apt-tools merge markers` — read on-disk conflict markers from the unmerged
|
|
123
|
+
* worktree so the agent can resolve them inline.
|
|
124
|
+
*
|
|
125
|
+
* @param {string} targetDir
|
|
126
|
+
* @returns {import('../util/result.mjs').CommandResult}
|
|
127
|
+
*/
|
|
128
|
+
function mergeMarkers(targetDir) {
|
|
129
|
+
// Unmerged paths (stage > 0) are the authoritative "conflicted file" set.
|
|
130
|
+
const unmerged = gitTry(targetDir, ['diff', '--name-only', '--diff-filter=U'])
|
|
131
|
+
if (!unmerged.ok) {
|
|
132
|
+
return ok({
|
|
133
|
+
status: 'ok',
|
|
134
|
+
command: 'merge-markers',
|
|
135
|
+
projectDir: targetDir,
|
|
136
|
+
conflicted: [],
|
|
137
|
+
files: [],
|
|
138
|
+
warnings: [`git diff failed: ${unmerged.stderr.trim().slice(0, 200) || 'unknown error'}`],
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const conflicted = unmerged.stdout
|
|
143
|
+
.split('\n')
|
|
144
|
+
.map((l) => l.trim())
|
|
145
|
+
.filter((l) => l.length > 0)
|
|
146
|
+
|
|
147
|
+
const files = []
|
|
148
|
+
const warnings = []
|
|
149
|
+
for (const path of conflicted) {
|
|
150
|
+
try {
|
|
151
|
+
const content = readFileSync(join(targetDir, path), 'utf-8')
|
|
152
|
+
files.push({ path, content })
|
|
153
|
+
} catch (e) {
|
|
154
|
+
warnings.push(`could not read ${path}: ${e?.message ?? String(e)}`)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const envelope = {
|
|
159
|
+
status: 'ok',
|
|
160
|
+
command: 'merge-markers',
|
|
161
|
+
projectDir: targetDir,
|
|
162
|
+
conflicted,
|
|
163
|
+
files,
|
|
164
|
+
}
|
|
165
|
+
if (warnings.length > 0) envelope.warnings = warnings
|
|
166
|
+
return ok(envelope)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Dispatch an `apt-tools merge <subcommand>` call.
|
|
171
|
+
*
|
|
172
|
+
* @param {string} subcommand
|
|
173
|
+
* @param {string} projectDir
|
|
174
|
+
* @param {string[]} [extraArgs]
|
|
175
|
+
* @returns {import('../util/result.mjs').CommandResult}
|
|
176
|
+
*
|
|
177
|
+
* @internal
|
|
178
|
+
*/
|
|
179
|
+
export function cmdMerge(subcommand, projectDir, extraArgs = []) {
|
|
180
|
+
if (!subcommand) {
|
|
181
|
+
return err('Usage: apt-tools merge <detect|markers|integrate> <project-dir> [flags]')
|
|
182
|
+
}
|
|
183
|
+
if (!projectDir) {
|
|
184
|
+
return err(`Usage: apt-tools merge ${subcommand} <project-dir> [flags]`)
|
|
185
|
+
}
|
|
186
|
+
const targetDir = resolve(projectDir)
|
|
187
|
+
switch (subcommand) {
|
|
188
|
+
case 'detect':
|
|
189
|
+
return mergeDetect(targetDir, extraArgs)
|
|
190
|
+
case 'markers':
|
|
191
|
+
return mergeMarkers(targetDir)
|
|
192
|
+
case 'integrate':
|
|
193
|
+
return mergeIntegrate(targetDir, extraArgs)
|
|
194
|
+
default:
|
|
195
|
+
return err(
|
|
196
|
+
`Unknown merge subcommand: ${subcommand}. Expected 'detect', 'markers', or 'integrate'.`,
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Test-only exports for unit-level coverage (not part of the public CLI
|
|
202
|
+
// contract).
|
|
203
|
+
export { mergeDetect as __mergeDetectForTests, mergeMarkers as __mergeMarkersForTests }
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commands/merge-integrate.mjs — `apt-tools merge integrate` (GitHub issue #397).
|
|
3
|
+
*
|
|
4
|
+
* The fan-out PR-integration phase. After a fan-out run, N workers each opened
|
|
5
|
+
* an independent PR against the same base; nothing ordered or reconciled them.
|
|
6
|
+
* This command SERIALIZES that batch: it resolves the task → branch/PR set,
|
|
7
|
+
* computes a deterministic merge order (topological when `depends_on` edges
|
|
8
|
+
* exist, else stable by task id), and probes each branch's mergeability against
|
|
9
|
+
* the CURRENT base via the EXACT same `git merge-tree` detection the `detect`
|
|
10
|
+
* subcommand uses (#395). It emits a structured integration report the
|
|
11
|
+
* conductor renders and the `apt:merge-conflict-resolution` flow consumes for
|
|
12
|
+
* any conflicted branch.
|
|
13
|
+
*
|
|
14
|
+
* READ-ONLY, like `detect`/`markers`: NO `git merge`, NO `git rebase`, NO index
|
|
15
|
+
* mutation, NO `gh pr merge`. The SKILL.md prose owns every state change; this
|
|
16
|
+
* primitive only OBSERVES and ORDERS. Conflict resolution stays agent-inline —
|
|
17
|
+
* the framework CLI is self-contained ESM and MUST NOT import @aperant/core
|
|
18
|
+
* (see GitHub issue #390 for the architecture decision). When a branch probes
|
|
19
|
+
* `mergeable: false`, this command surfaces the conflict envelope (the worker's
|
|
20
|
+
* worktree + base ref + conflicts_preview) and the agent resolves it inline via
|
|
21
|
+
* `/apt:merge-conflict-resolution`, then re-runs `integrate` to re-probe.
|
|
22
|
+
*
|
|
23
|
+
* Reuse, not reinvent (#397):
|
|
24
|
+
* - detection → `gitTry` / `parseConflictPaths` / `isGitVersionTooOld`
|
|
25
|
+
* promoted in audit-branch-current.mjs (#395).
|
|
26
|
+
* - batch resolution → the same `--tasks`/`--batch` shape fan-out.mjs uses;
|
|
27
|
+
* task records read from the shared `.aperant/state.json`
|
|
28
|
+
* board (resolveMainRepoRoot — state-fork-fix).
|
|
29
|
+
* - dependency graph → `buildDependsOnMap` (graph/depends-on.mjs), which
|
|
30
|
+
* already normalizes `depends_on` edges to bare ids.
|
|
31
|
+
*
|
|
32
|
+
* apt-tools merge integrate <project-dir> --tasks <id,id,...> [--base <ref>]
|
|
33
|
+
* apt-tools merge integrate <project-dir> --batch <batch-id> [--base <ref>]
|
|
34
|
+
*
|
|
35
|
+
* --tasks Explicit canonical task-id list (what `fan-out resolve` returns and
|
|
36
|
+
* the conductor already holds). Primary form.
|
|
37
|
+
* --batch Recover the task-id list from the batch's worker rows
|
|
38
|
+
* `.aperant/team-status/fanout-<batch>-<task>.json` (the agent-id
|
|
39
|
+
* format fan-out.mjs spawns workers with: `fanout-${batch}-${task}`).
|
|
40
|
+
* --base Override the base ref every branch is probed against. When omitted,
|
|
41
|
+
* each task is probed against `origin/<its base_branch>` (falling back
|
|
42
|
+
* to `origin/main` when the record carries no base_branch).
|
|
43
|
+
*
|
|
44
|
+
* @internal
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
48
|
+
import { join, resolve } from 'node:path'
|
|
49
|
+
import { buildDependsOnMap } from '../graph/depends-on.mjs'
|
|
50
|
+
import { parseFlags } from '../util/args.mjs'
|
|
51
|
+
import { err, ok } from '../util/result.mjs'
|
|
52
|
+
import { resolveMainRepoRoot } from '../util/state-paths.mjs'
|
|
53
|
+
import { gitTry, isGitVersionTooOld, parseConflictPaths } from './audit-branch-current.mjs'
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Parse a PR number from a `pr_url` string (full GitHub URL or `#123` short
|
|
57
|
+
* form). Returns null if not parseable. Incidental metadata for the report —
|
|
58
|
+
* the integration order/probe never depends on it.
|
|
59
|
+
*
|
|
60
|
+
* @param {unknown} url
|
|
61
|
+
* @returns {number | null}
|
|
62
|
+
*/
|
|
63
|
+
function extractPrNumber(url) {
|
|
64
|
+
if (typeof url !== 'string') return null
|
|
65
|
+
const m1 = url.match(/\/pull\/(\d+)(?:[?#/]|$)/)
|
|
66
|
+
if (m1) return Number.parseInt(m1[1], 10)
|
|
67
|
+
const m2 = url.match(/#(\d+)$/)
|
|
68
|
+
if (m2) return Number.parseInt(m2[1], 10)
|
|
69
|
+
const m3 = url.match(/^(\d+)$/)
|
|
70
|
+
if (m3) return Number.parseInt(m3[1], 10)
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Read the shared ephemeral board (`state.json`). Anchors to the MAIN repo so
|
|
76
|
+
* it resolves identically from any worktree (state-fork-fix), matching how
|
|
77
|
+
* fan-out.mjs reads it.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} targetDir
|
|
80
|
+
* @returns {object | null}
|
|
81
|
+
*/
|
|
82
|
+
function readState(targetDir) {
|
|
83
|
+
const statePath = join(resolveMainRepoRoot(targetDir), '.aperant', 'state.json')
|
|
84
|
+
if (!existsSync(statePath)) return null
|
|
85
|
+
try {
|
|
86
|
+
return JSON.parse(readFileSync(statePath, 'utf-8'))
|
|
87
|
+
} catch {
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Recover a batch's task-id list from its worker rows. Fan-out spawns each
|
|
94
|
+
* worker with `APT_AGENT_ID=fanout-<batch>-<task>`, which materializes one
|
|
95
|
+
* `.aperant/team-status/fanout-<batch>-<task>.json` per worker. We list that
|
|
96
|
+
* directory and strip the shared prefix to recover the task ids.
|
|
97
|
+
*
|
|
98
|
+
* @param {string} targetDir
|
|
99
|
+
* @param {string} batchId
|
|
100
|
+
* @returns {string[]}
|
|
101
|
+
*/
|
|
102
|
+
function recoverBatchTaskIds(targetDir, batchId) {
|
|
103
|
+
const statusDir = join(resolveMainRepoRoot(targetDir), '.aperant', 'team-status')
|
|
104
|
+
if (!existsSync(statusDir)) return []
|
|
105
|
+
const prefix = `fanout-${batchId}-`
|
|
106
|
+
const ids = []
|
|
107
|
+
let entries
|
|
108
|
+
try {
|
|
109
|
+
entries = readdirSync(statusDir)
|
|
110
|
+
} catch {
|
|
111
|
+
return []
|
|
112
|
+
}
|
|
113
|
+
for (const name of entries) {
|
|
114
|
+
if (!name.endsWith('.json')) continue
|
|
115
|
+
const base = name.slice(0, -'.json'.length)
|
|
116
|
+
if (base.startsWith(prefix)) {
|
|
117
|
+
const taskId = base.slice(prefix.length)
|
|
118
|
+
if (taskId) ids.push(taskId)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return ids
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Order the batch's task ids. Topological (Kahn) over the batch SUBSET when any
|
|
126
|
+
* member has a `depends_on` edge to another member, else a stable alphabetical
|
|
127
|
+
* sort by task id (deterministic). A dependency cycle (or any unresolvable
|
|
128
|
+
* ordering) falls back to the stable order with a warning rather than throwing.
|
|
129
|
+
*
|
|
130
|
+
* Reuses `buildDependsOnMap` so the edge normalization matches the rest of the
|
|
131
|
+
* framework's graph handling exactly.
|
|
132
|
+
*
|
|
133
|
+
* @param {string[]} taskIds
|
|
134
|
+
* @param {object | null} state
|
|
135
|
+
* @returns {{ order: string[], strategy: 'topological' | 'stable', warnings: string[] }}
|
|
136
|
+
*/
|
|
137
|
+
function orderTasks(taskIds, state) {
|
|
138
|
+
const stable = [...taskIds].sort()
|
|
139
|
+
const inBatch = new Set(taskIds)
|
|
140
|
+
const warnings = []
|
|
141
|
+
|
|
142
|
+
const depMap = buildDependsOnMap(state || {})
|
|
143
|
+
// Restrict edges to within-batch dependencies — cross-batch deps don't gate
|
|
144
|
+
// the integration order (those PRs aren't part of this serialization).
|
|
145
|
+
const deps = new Map()
|
|
146
|
+
let edgeCount = 0
|
|
147
|
+
for (const id of taskIds) {
|
|
148
|
+
// buildDependsOnMap already normalizes deps to bare task_ids, so the
|
|
149
|
+
// entries are consumed directly (no re-parse — a re-parse would split a
|
|
150
|
+
// colon-bearing id and drop the constraint). De-dupe so a task that lists
|
|
151
|
+
// the same dependency twice doesn't overcount its Kahn indegree (which
|
|
152
|
+
// would strand it below zero-indegree and mis-flag a valid graph as a
|
|
153
|
+
// cycle).
|
|
154
|
+
const within = []
|
|
155
|
+
const seen = new Set()
|
|
156
|
+
for (const depId of depMap.get(id) || []) {
|
|
157
|
+
if (inBatch.has(depId) && depId !== id && !seen.has(depId)) {
|
|
158
|
+
seen.add(depId)
|
|
159
|
+
within.push(depId)
|
|
160
|
+
edgeCount++
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
deps.set(id, within)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (edgeCount === 0) {
|
|
167
|
+
return { order: stable, strategy: 'stable', warnings }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Kahn's algorithm — a task lands AFTER its dependencies (deps merge first).
|
|
171
|
+
const indegree = new Map(taskIds.map((id) => [id, 0]))
|
|
172
|
+
for (const id of taskIds) {
|
|
173
|
+
for (const _dep of deps.get(id) || []) {
|
|
174
|
+
indegree.set(id, (indegree.get(id) || 0) + 1)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Ready = zero remaining unmet deps. Process in stable order for determinism.
|
|
178
|
+
const ready = taskIds.filter((id) => (indegree.get(id) || 0) === 0).sort()
|
|
179
|
+
const order = []
|
|
180
|
+
while (ready.length > 0) {
|
|
181
|
+
const id = ready.shift()
|
|
182
|
+
order.push(id)
|
|
183
|
+
// Decrement indegree of every task that depends on `id`.
|
|
184
|
+
const newlyReady = []
|
|
185
|
+
for (const other of taskIds) {
|
|
186
|
+
if ((deps.get(other) || []).includes(id)) {
|
|
187
|
+
const d = (indegree.get(other) || 0) - 1
|
|
188
|
+
indegree.set(other, d)
|
|
189
|
+
if (d === 0) newlyReady.push(other)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
for (const r of newlyReady) ready.push(r)
|
|
193
|
+
// Re-sort the whole queue (not just the newly-ready tail) so ties across
|
|
194
|
+
// rounds resolve in strict alphabetical order — fully deterministic.
|
|
195
|
+
ready.sort()
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (order.length !== taskIds.length) {
|
|
199
|
+
// Cycle or unresolvable ordering — fall back to stable, deterministic.
|
|
200
|
+
warnings.push(
|
|
201
|
+
'dependency cycle detected among batch tasks — falling back to stable task-id order',
|
|
202
|
+
)
|
|
203
|
+
return { order: stable, strategy: 'stable', warnings }
|
|
204
|
+
}
|
|
205
|
+
return { order, strategy: 'topological', warnings }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Probe one task's branch against the base. Mirrors the `detect` subcommand's
|
|
210
|
+
* merge-tree semantics exactly (#395) — same gitTry call, same exit-code
|
|
211
|
+
* branching — so the integrate report and a standalone `merge detect` agree.
|
|
212
|
+
*
|
|
213
|
+
* @param {string} worktreeDir
|
|
214
|
+
* @param {string} base
|
|
215
|
+
* @param {string} head
|
|
216
|
+
* @returns {{ mergeable: boolean | null, conflicts_preview?: string[], audit_warning?: string }}
|
|
217
|
+
*/
|
|
218
|
+
function probeBranch(worktreeDir, base, head) {
|
|
219
|
+
const probe = gitTry(worktreeDir, ['merge-tree', '--write-tree', '--no-messages', head, base])
|
|
220
|
+
if (probe.ok) return { mergeable: true }
|
|
221
|
+
if (probe.code === 1 && probe.stdout.trim().length > 0) {
|
|
222
|
+
return { mergeable: false, conflicts_preview: parseConflictPaths(probe.stdout) }
|
|
223
|
+
}
|
|
224
|
+
if (isGitVersionTooOld(probe.stderr)) {
|
|
225
|
+
return { mergeable: null, audit_warning: 'git-version-too-old' }
|
|
226
|
+
}
|
|
227
|
+
return { mergeable: null, audit_warning: 'merge-tree-probe-failed' }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* `apt-tools merge integrate` — serialize a fan-out batch into base.
|
|
232
|
+
*
|
|
233
|
+
* @param {string} targetDir
|
|
234
|
+
* @param {string[]} extraArgs
|
|
235
|
+
* @returns {import('../util/result.mjs').CommandResult}
|
|
236
|
+
*/
|
|
237
|
+
export function mergeIntegrate(targetDir, extraArgs) {
|
|
238
|
+
const flags = parseFlags(extraArgs)
|
|
239
|
+
const hasTasks = flags.has('tasks')
|
|
240
|
+
const hasBatch = flags.has('batch')
|
|
241
|
+
|
|
242
|
+
if (!hasTasks && !hasBatch) {
|
|
243
|
+
return err('One of --tasks <id,id,...> or --batch <batch-id> is required for merge integrate')
|
|
244
|
+
}
|
|
245
|
+
if (hasTasks && hasBatch) {
|
|
246
|
+
return err('Pass only one of --tasks or --batch to merge integrate')
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const state = readState(targetDir)
|
|
250
|
+
|
|
251
|
+
let taskIds = []
|
|
252
|
+
let source = ''
|
|
253
|
+
if (hasTasks) {
|
|
254
|
+
taskIds = String(flags.get('tasks') || '')
|
|
255
|
+
.split(',')
|
|
256
|
+
.map((s) => s.trim())
|
|
257
|
+
.filter(Boolean)
|
|
258
|
+
source = 'tasks'
|
|
259
|
+
if (taskIds.length === 0) {
|
|
260
|
+
return err('--tasks resolved to an empty list')
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
const batchId = String(flags.get('batch') || '').trim()
|
|
264
|
+
if (!batchId) return err('--batch requires a batch id')
|
|
265
|
+
taskIds = recoverBatchTaskIds(targetDir, batchId)
|
|
266
|
+
source = 'batch'
|
|
267
|
+
if (taskIds.length === 0) {
|
|
268
|
+
return err(`No worker rows found for batch "${batchId}" — nothing to integrate`)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// parseFlags yields `true` for a bare `--base` with no following value; reject
|
|
273
|
+
// it rather than silently probing against the ref literal "true".
|
|
274
|
+
let baseOverride = null
|
|
275
|
+
if (flags.has('base')) {
|
|
276
|
+
const raw = flags.get('base')
|
|
277
|
+
if (raw === true || String(raw).trim() === '') {
|
|
278
|
+
return err('--base requires a ref value (e.g. --base origin/main)')
|
|
279
|
+
}
|
|
280
|
+
baseOverride = String(raw).trim()
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const { order, strategy, warnings } = orderTasks(taskIds, state)
|
|
284
|
+
|
|
285
|
+
const tasks = []
|
|
286
|
+
let merged = 0
|
|
287
|
+
let conflicted = 0
|
|
288
|
+
let needsHuman = 0
|
|
289
|
+
|
|
290
|
+
for (const id of order) {
|
|
291
|
+
const task = state?.active_tasks?.[id] || null
|
|
292
|
+
|
|
293
|
+
// Tasks with no shipped branch can't be integrated — record them so the
|
|
294
|
+
// conductor sees why, but never block the rest of the batch.
|
|
295
|
+
if (!task) {
|
|
296
|
+
needsHuman++
|
|
297
|
+
tasks.push({ task_id: id, status: 'unknown', reason: 'task not found in state.json' })
|
|
298
|
+
continue
|
|
299
|
+
}
|
|
300
|
+
const branch = typeof task.branch === 'string' ? task.branch : null
|
|
301
|
+
const prUrl = typeof task.pr_url === 'string' ? task.pr_url : null
|
|
302
|
+
const worktree =
|
|
303
|
+
typeof task.worktree_path === 'string' && existsSync(task.worktree_path)
|
|
304
|
+
? resolve(task.worktree_path)
|
|
305
|
+
: null
|
|
306
|
+
|
|
307
|
+
if (!prUrl) {
|
|
308
|
+
needsHuman++
|
|
309
|
+
tasks.push({
|
|
310
|
+
task_id: id,
|
|
311
|
+
branch,
|
|
312
|
+
status: 'skipped',
|
|
313
|
+
reason: 'no pr_url on task record (not shipped) — nothing to integrate',
|
|
314
|
+
})
|
|
315
|
+
continue
|
|
316
|
+
}
|
|
317
|
+
if (!worktree) {
|
|
318
|
+
needsHuman++
|
|
319
|
+
tasks.push({
|
|
320
|
+
task_id: id,
|
|
321
|
+
branch,
|
|
322
|
+
pr_url: prUrl,
|
|
323
|
+
pr_number: extractPrNumber(prUrl),
|
|
324
|
+
status: 'needs-human',
|
|
325
|
+
reason: 'worktree_path missing or no longer on disk — cannot probe mergeability',
|
|
326
|
+
})
|
|
327
|
+
continue
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const taskBaseBranch =
|
|
331
|
+
typeof task.base_branch === 'string' && task.base_branch ? task.base_branch : 'main'
|
|
332
|
+
const base = baseOverride || `origin/${taskBaseBranch}`
|
|
333
|
+
const probe = probeBranch(worktree, base, 'HEAD')
|
|
334
|
+
|
|
335
|
+
const entry = {
|
|
336
|
+
task_id: id,
|
|
337
|
+
branch,
|
|
338
|
+
pr_url: prUrl,
|
|
339
|
+
pr_number: extractPrNumber(prUrl),
|
|
340
|
+
worktree_path: worktree,
|
|
341
|
+
base,
|
|
342
|
+
mergeable: probe.mergeable,
|
|
343
|
+
}
|
|
344
|
+
if (probe.conflicts_preview && probe.conflicts_preview.length > 0) {
|
|
345
|
+
entry.conflicts_preview = probe.conflicts_preview
|
|
346
|
+
}
|
|
347
|
+
if (probe.audit_warning) entry.audit_warning = probe.audit_warning
|
|
348
|
+
|
|
349
|
+
if (probe.mergeable === true) {
|
|
350
|
+
merged++
|
|
351
|
+
entry.status = 'mergeable'
|
|
352
|
+
entry.action = 'advance'
|
|
353
|
+
} else if (probe.mergeable === false) {
|
|
354
|
+
conflicted++
|
|
355
|
+
entry.status = 'conflicted'
|
|
356
|
+
// Hand-off envelope for /apt:merge-conflict-resolution — the agent
|
|
357
|
+
// resolves these markers inline then re-runs `integrate` to re-probe.
|
|
358
|
+
entry.action = 'resolve-conflict'
|
|
359
|
+
entry.resolution = {
|
|
360
|
+
skill: 'apt:merge-conflict-resolution',
|
|
361
|
+
worktree_path: worktree,
|
|
362
|
+
base,
|
|
363
|
+
}
|
|
364
|
+
} else {
|
|
365
|
+
needsHuman++
|
|
366
|
+
entry.status = 'needs-human'
|
|
367
|
+
entry.action = 'probe-failed'
|
|
368
|
+
}
|
|
369
|
+
tasks.push(entry)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const envelope = {
|
|
373
|
+
status: 'ok',
|
|
374
|
+
command: 'merge-integrate',
|
|
375
|
+
projectDir: targetDir,
|
|
376
|
+
source,
|
|
377
|
+
order_strategy: strategy,
|
|
378
|
+
order,
|
|
379
|
+
summary: {
|
|
380
|
+
total: tasks.length,
|
|
381
|
+
mergeable: merged,
|
|
382
|
+
conflicted,
|
|
383
|
+
needs_human: needsHuman,
|
|
384
|
+
},
|
|
385
|
+
tasks,
|
|
386
|
+
}
|
|
387
|
+
if (warnings.length > 0) envelope.warnings = warnings
|
|
388
|
+
return ok(envelope)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Test-only export for unit-level coverage of the ordering core.
|
|
392
|
+
export { orderTasks as __orderTasksForTests }
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
|
|
2
|
+
new file mode 100644
|
|
3
|
+
index 0000000..1111111
|
|
4
|
+
--- /dev/null
|
|
5
|
+
+++ b/.github/workflows/ci.yml
|
|
6
|
+
@@ -0,0 +1,6 @@
|
|
7
|
+
+name: ci
|
|
8
|
+
+on: [push]
|
|
9
|
+
+jobs:
|
|
10
|
+
+ build:
|
|
11
|
+
+ runs-on: ubuntu-latest
|
|
12
|
+
+ steps:
|
|
13
|
+
+ - run: npm install windows-build-tools
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
|
|
2
|
+
new file mode 100644
|
|
3
|
+
index 0000000..2222222
|
|
4
|
+
--- /dev/null
|
|
5
|
+
+++ b/.github/workflows/ci.yml
|
|
6
|
+
@@ -0,0 +1,11 @@
|
|
7
|
+
+name: ci
|
|
8
|
+
+on: [push]
|
|
9
|
+
+jobs:
|
|
10
|
+
+ build:
|
|
11
|
+
+ runs-on: ubuntu-latest
|
|
12
|
+
+ steps:
|
|
13
|
+
+ - uses: actions/setup-node@v4
|
|
14
|
+
+ - run: npm install
|
|
15
|
+
+ packages:
|
|
16
|
+
+ - windows-build-tools
|
|
17
|
+
+ - macos-codesign-helper
|