@aperant/framework 0.13.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.
Files changed (241) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/README.md +1 -1
  3. package/agents/apt-pr-review-i18n.md +190 -0
  4. package/agents/apt-pr-review-xplatform.md +189 -0
  5. package/dist/cli/commands/audit-branch-current.d.mts +27 -5
  6. package/dist/cli/commands/audit-branch-current.d.mts.map +1 -1
  7. package/dist/cli/commands/audit-branch-current.mjs +3 -3
  8. package/dist/cli/commands/audit-branch-current.mjs.map +1 -1
  9. package/dist/cli/commands/event.d.mts.map +1 -1
  10. package/dist/cli/commands/event.mjs +6 -4
  11. package/dist/cli/commands/event.mjs.map +1 -1
  12. package/dist/cli/commands/fan-out.d.mts.map +1 -1
  13. package/dist/cli/commands/fan-out.mjs +3 -1
  14. package/dist/cli/commands/fan-out.mjs.map +1 -1
  15. package/dist/cli/commands/features-audit.d.mts.map +1 -1
  16. package/dist/cli/commands/features-audit.mjs +6 -1
  17. package/dist/cli/commands/features-audit.mjs.map +1 -1
  18. package/dist/cli/commands/followups.d.mts +5 -0
  19. package/dist/cli/commands/followups.d.mts.map +1 -0
  20. package/dist/cli/commands/followups.mjs +266 -0
  21. package/dist/cli/commands/followups.mjs.map +1 -0
  22. package/dist/cli/commands/health-check.d.mts.map +1 -1
  23. package/dist/cli/commands/health-check.mjs +3 -1
  24. package/dist/cli/commands/health-check.mjs.map +1 -1
  25. package/dist/cli/commands/merge-detect.d.mts +29 -0
  26. package/dist/cli/commands/merge-detect.d.mts.map +1 -0
  27. package/dist/cli/commands/merge-detect.mjs +196 -0
  28. package/dist/cli/commands/merge-detect.mjs.map +1 -0
  29. package/dist/cli/commands/merge-integrate.d.mts +28 -0
  30. package/dist/cli/commands/merge-integrate.d.mts.map +1 -0
  31. package/dist/cli/commands/merge-integrate.mjs +383 -0
  32. package/dist/cli/commands/merge-integrate.mjs.map +1 -0
  33. package/dist/cli/commands/pr-review/detect-specialists.d.mts +58 -0
  34. package/dist/cli/commands/pr-review/detect-specialists.d.mts.map +1 -0
  35. package/dist/cli/commands/pr-review/detect-specialists.mjs +980 -0
  36. package/dist/cli/commands/pr-review/detect-specialists.mjs.map +1 -0
  37. package/dist/cli/commands/pr-review.d.mts.map +1 -1
  38. package/dist/cli/commands/pr-review.mjs +11 -2
  39. package/dist/cli/commands/pr-review.mjs.map +1 -1
  40. package/dist/cli/commands/precompact-hook.d.mts.map +1 -1
  41. package/dist/cli/commands/precompact-hook.mjs +10 -6
  42. package/dist/cli/commands/precompact-hook.mjs.map +1 -1
  43. package/dist/cli/commands/roadmap.d.mts.map +1 -1
  44. package/dist/cli/commands/roadmap.mjs +24 -11
  45. package/dist/cli/commands/roadmap.mjs.map +1 -1
  46. package/dist/cli/commands/seed.d.mts.map +1 -1
  47. package/dist/cli/commands/seed.mjs +3 -1
  48. package/dist/cli/commands/seed.mjs.map +1 -1
  49. package/dist/cli/commands/state.d.mts.map +1 -1
  50. package/dist/cli/commands/state.mjs +9 -3
  51. package/dist/cli/commands/state.mjs.map +1 -1
  52. package/dist/cli/commands/statusline.d.mts.map +1 -1
  53. package/dist/cli/commands/statusline.mjs +5 -1
  54. package/dist/cli/commands/statusline.mjs.map +1 -1
  55. package/dist/cli/commands/task.d.mts.map +1 -1
  56. package/dist/cli/commands/task.mjs +126 -23
  57. package/dist/cli/commands/task.mjs.map +1 -1
  58. package/dist/cli/commands/worktree.d.mts +1 -1
  59. package/dist/cli/commands/worktree.d.mts.map +1 -1
  60. package/dist/cli/commands/worktree.mjs +282 -83
  61. package/dist/cli/commands/worktree.mjs.map +1 -1
  62. package/dist/cli/config/post-merge-sweep.d.mts.map +1 -1
  63. package/dist/cli/config/post-merge-sweep.mjs +3 -2
  64. package/dist/cli/config/post-merge-sweep.mjs.map +1 -1
  65. package/dist/cli/config/share-policy.d.mts +3 -1
  66. package/dist/cli/config/share-policy.d.mts.map +1 -1
  67. package/dist/cli/config/share-policy.mjs +16 -3
  68. package/dist/cli/config/share-policy.mjs.map +1 -1
  69. package/dist/cli/config/upgrade-gitignore.d.mts.map +1 -1
  70. package/dist/cli/config/upgrade-gitignore.mjs +27 -5
  71. package/dist/cli/config/upgrade-gitignore.mjs.map +1 -1
  72. package/dist/cli/consistency/parse-qa.mjs +12 -1
  73. package/dist/cli/consistency/parse-qa.mjs.map +1 -1
  74. package/dist/cli/consistency/parse-review.d.mts +23 -0
  75. package/dist/cli/consistency/parse-review.d.mts.map +1 -1
  76. package/dist/cli/consistency/parse-review.mjs +36 -0
  77. package/dist/cli/consistency/parse-review.mjs.map +1 -1
  78. package/dist/cli/consistency/parse-spec.d.mts +16 -0
  79. package/dist/cli/consistency/parse-spec.d.mts.map +1 -1
  80. package/dist/cli/consistency/parse-spec.mjs +44 -0
  81. package/dist/cli/consistency/parse-spec.mjs.map +1 -1
  82. package/dist/cli/coordination/agent-identity.d.mts +5 -2
  83. package/dist/cli/coordination/agent-identity.d.mts.map +1 -1
  84. package/dist/cli/coordination/agent-identity.mjs +6 -4
  85. package/dist/cli/coordination/agent-identity.mjs.map +1 -1
  86. package/dist/cli/coordination/store.d.mts +1 -1
  87. package/dist/cli/dispatch.d.mts.map +1 -1
  88. package/dist/cli/dispatch.mjs +4 -0
  89. package/dist/cli/dispatch.mjs.map +1 -1
  90. package/dist/cli/followups/derive.d.mts +63 -0
  91. package/dist/cli/followups/derive.d.mts.map +1 -0
  92. package/dist/cli/followups/derive.mjs +115 -0
  93. package/dist/cli/followups/derive.mjs.map +1 -0
  94. package/dist/cli/followups/schema.d.mts +165 -0
  95. package/dist/cli/followups/schema.d.mts.map +1 -0
  96. package/dist/cli/followups/schema.mjs +251 -0
  97. package/dist/cli/followups/schema.mjs.map +1 -0
  98. package/dist/cli/gate/context.d.mts +1 -1
  99. package/dist/cli/gate/gates/followups-adjudicated.d.mts +69 -0
  100. package/dist/cli/gate/gates/followups-adjudicated.d.mts.map +1 -0
  101. package/dist/cli/gate/gates/followups-adjudicated.mjs +141 -0
  102. package/dist/cli/gate/gates/followups-adjudicated.mjs.map +1 -0
  103. package/dist/cli/gate/gates/index.mjs +1 -0
  104. package/dist/cli/gate/gates/index.mjs.map +1 -1
  105. package/dist/cli/git/materialize-env.d.mts +39 -0
  106. package/dist/cli/git/materialize-env.d.mts.map +1 -0
  107. package/dist/cli/git/materialize-env.mjs +217 -0
  108. package/dist/cli/git/materialize-env.mjs.map +1 -0
  109. package/dist/cli/help.d.mts.map +1 -1
  110. package/dist/cli/help.mjs +9 -1
  111. package/dist/cli/help.mjs.map +1 -1
  112. package/dist/cli/install/legacy-paths.d.mts.map +1 -1
  113. package/dist/cli/install/legacy-paths.mjs +4 -0
  114. package/dist/cli/install/legacy-paths.mjs.map +1 -1
  115. package/dist/cli/roadmap/lifecycle.d.mts.map +1 -1
  116. package/dist/cli/roadmap/lifecycle.mjs +4 -1
  117. package/dist/cli/roadmap/lifecycle.mjs.map +1 -1
  118. package/dist/cli/task/ids.d.mts +9 -0
  119. package/dist/cli/task/ids.d.mts.map +1 -1
  120. package/dist/cli/task/ids.mjs +9 -0
  121. package/dist/cli/task/ids.mjs.map +1 -1
  122. package/dist/cli/task/index-md.d.mts +5 -0
  123. package/dist/cli/task/index-md.d.mts.map +1 -1
  124. package/dist/cli/task/index-md.mjs +12 -4
  125. package/dist/cli/task/index-md.mjs.map +1 -1
  126. package/dist/cli/util/io.d.mts +6 -0
  127. package/dist/cli/util/io.d.mts.map +1 -1
  128. package/dist/cli/util/io.mjs +16 -1
  129. package/dist/cli/util/io.mjs.map +1 -1
  130. package/dist/cli/util/project.d.mts +7 -2
  131. package/dist/cli/util/project.d.mts.map +1 -1
  132. package/dist/cli/util/project.mjs +11 -14
  133. package/dist/cli/util/project.mjs.map +1 -1
  134. package/dist/cli/util/state-paths.d.mts +19 -0
  135. package/dist/cli/util/state-paths.d.mts.map +1 -0
  136. package/dist/cli/util/state-paths.mjs +45 -0
  137. package/dist/cli/util/state-paths.mjs.map +1 -0
  138. package/dist/plugin/.claude-plugin/plugin.json +4 -1
  139. package/dist/plugin/agents/apt-pr-review-i18n.md +190 -0
  140. package/dist/plugin/agents/apt-pr-review-xplatform.md +189 -0
  141. package/dist/plugin/skills/apt/SKILL.md +9 -3
  142. package/dist/plugin/skills/apt-close-task/SKILL.md +48 -0
  143. package/dist/plugin/skills/apt-debug/SKILL.md +19 -9
  144. package/dist/plugin/skills/apt-execute/SKILL.md +3 -1
  145. package/dist/plugin/skills/apt-fan-out/SKILL.md +65 -1
  146. package/dist/plugin/skills/apt-merge-conflict-resolution/SKILL.md +227 -0
  147. package/dist/plugin/skills/apt-pr-review/SCHEMA.md +4 -2
  148. package/dist/plugin/skills/apt-pr-review/SKILL.md +117 -29
  149. package/dist/plugin/skills/apt-quick/SKILL.md +24 -1
  150. package/dist/plugin/skills/apt-review/SKILL.md +6 -0
  151. package/dist/plugin/skills/apt-run/SKILL.md +1 -1
  152. package/dist/plugin/skills/apt-setup/SKILL.md +21 -3
  153. package/dist/plugin/skills/apt-ship/SKILL.md +32 -11
  154. package/dist/plugin/skills/apt-terminal/SKILL.md +8 -0
  155. package/dist/plugin/skills/apt-triage/SKILL.md +4 -0
  156. package/dist/plugin/skills/apt-verify/SKILL.md +24 -0
  157. package/dist/plugin/skills/apt-watch-ci/SKILL.md +3 -1
  158. package/dist/types/config.d.ts +33 -0
  159. package/dist/types/config.d.ts.map +1 -1
  160. package/package.json +1 -1
  161. package/prompts/conductor-framework-context.md +1 -1
  162. package/prompts/github/pr_i18n_agent.md +279 -0
  163. package/prompts/github/pr_xplatform_agent.md +283 -0
  164. package/prompts/inbox-clarification.md +18 -11
  165. package/skills/apt/SKILL.md +9 -3
  166. package/skills/apt-close-task/SKILL.md +48 -0
  167. package/skills/apt-debug/SKILL.md +19 -9
  168. package/skills/apt-execute/SKILL.md +3 -1
  169. package/skills/apt-fan-out/SKILL.md +65 -1
  170. package/skills/apt-merge-conflict-resolution/SKILL.md +227 -0
  171. package/skills/apt-pr-review/SCHEMA.md +4 -2
  172. package/skills/apt-pr-review/SKILL.md +117 -29
  173. package/skills/apt-quick/SKILL.md +24 -1
  174. package/skills/apt-review/SKILL.md +6 -0
  175. package/skills/apt-run/SKILL.md +1 -1
  176. package/skills/apt-setup/SKILL.md +21 -3
  177. package/skills/apt-ship/SKILL.md +32 -11
  178. package/skills/apt-terminal/SKILL.md +8 -0
  179. package/skills/apt-triage/SKILL.md +4 -0
  180. package/skills/apt-verify/SKILL.md +24 -0
  181. package/skills/apt-watch-ci/SKILL.md +3 -1
  182. package/src/cli/commands/audit-branch-current.mjs +3 -3
  183. package/src/cli/commands/event.mjs +6 -4
  184. package/src/cli/commands/fan-out.mjs +3 -1
  185. package/src/cli/commands/features-audit.mjs +6 -1
  186. package/src/cli/commands/followups.mjs +303 -0
  187. package/src/cli/commands/health-check.mjs +3 -1
  188. package/src/cli/commands/merge-detect.mjs +203 -0
  189. package/src/cli/commands/merge-integrate.mjs +392 -0
  190. package/src/cli/commands/pr-review/__fixtures__/android-strings/app/src/main/res/values-fr/strings.xml +3 -0
  191. package/src/cli/commands/pr-review/__fixtures__/babel-js/package.json +4 -0
  192. package/src/cli/commands/pr-review/__fixtures__/c-ifdef/src/main.c +4 -0
  193. package/src/cli/commands/pr-review/__fixtures__/ci-matrix/.github/workflows/ci.yml +10 -0
  194. package/src/cli/commands/pr-review/__fixtures__/cmake-win/CMakeLists.txt +5 -0
  195. package/src/cli/commands/pr-review/__fixtures__/diffs/add-arb.patch +9 -0
  196. package/src/cli/commands/pr-review/__fixtures__/diffs/ci-single-os-dep.patch +13 -0
  197. package/src/cli/commands/pr-review/__fixtures__/diffs/ci-single-os-list-item-dep.patch +17 -0
  198. package/src/cli/commands/pr-review/__fixtures__/diffs/lone-weak-t.patch +8 -0
  199. package/src/cli/commands/pr-review/__fixtures__/diffs/monorepo-b.patch +7 -0
  200. package/src/cli/commands/pr-review/__fixtures__/diffs/two-weak-xpl.patch +12 -0
  201. package/src/cli/commands/pr-review/__fixtures__/dropped-underscore/src/app.js +2 -0
  202. package/src/cli/commands/pr-review/__fixtures__/electron-app/package.json +4 -0
  203. package/src/cli/commands/pr-review/__fixtures__/i18n-locale-file/src/locales/fr.json +1 -0
  204. package/src/cli/commands/pr-review/__fixtures__/monorepo/packages/a/locales/fr.json +1 -0
  205. package/src/cli/commands/pr-review/__fixtures__/monorepo/packages/b/src/index.ts +1 -0
  206. package/src/cli/commands/pr-review/__fixtures__/py-babel/pyproject.toml +3 -0
  207. package/src/cli/commands/pr-review/__fixtures__/qt-i18n-dir/i18n/app_fr.ts +11 -0
  208. package/src/cli/commands/pr-review/__fixtures__/rust-cli/Cargo.toml +7 -0
  209. package/src/cli/commands/pr-review/__fixtures__/rust-cli/src/main.rs +1 -0
  210. package/src/cli/commands/pr-review/__fixtures__/ts-dir/ts/foo.ts +3 -0
  211. package/src/cli/commands/pr-review/detect-specialists.mjs +1023 -0
  212. package/src/cli/commands/pr-review.mjs +12 -2
  213. package/src/cli/commands/precompact-hook.mjs +10 -6
  214. package/src/cli/commands/roadmap.mjs +24 -11
  215. package/src/cli/commands/seed.mjs +3 -1
  216. package/src/cli/commands/state.mjs +9 -3
  217. package/src/cli/commands/statusline.mjs +5 -1
  218. package/src/cli/commands/task.mjs +135 -23
  219. package/src/cli/commands/worktree.mjs +298 -81
  220. package/src/cli/config/post-merge-sweep.mjs +3 -2
  221. package/src/cli/config/share-policy.mjs +16 -3
  222. package/src/cli/config/upgrade-gitignore.mjs +27 -5
  223. package/src/cli/consistency/parse-qa.mjs +11 -1
  224. package/src/cli/consistency/parse-review.mjs +36 -0
  225. package/src/cli/consistency/parse-spec.mjs +43 -0
  226. package/src/cli/coordination/agent-identity.mjs +6 -4
  227. package/src/cli/dispatch.mjs +4 -0
  228. package/src/cli/followups/derive.mjs +123 -0
  229. package/src/cli/followups/schema.mjs +264 -0
  230. package/src/cli/gate/gates/followups-adjudicated.mjs +158 -0
  231. package/src/cli/gate/gates/index.mjs +1 -0
  232. package/src/cli/git/materialize-env.mjs +212 -0
  233. package/src/cli/help.mjs +9 -1
  234. package/src/cli/install/legacy-paths.mjs +4 -0
  235. package/src/cli/roadmap/lifecycle.mjs +4 -1
  236. package/src/cli/task/ids.mjs +9 -0
  237. package/src/cli/task/index-md.mjs +12 -4
  238. package/src/cli/util/io.mjs +15 -1
  239. package/src/cli/util/project.mjs +11 -14
  240. package/src/cli/util/state-paths.mjs +45 -0
  241. package/prompts/followup_planner.md +0 -399
@@ -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,3 @@
1
+ <resources>
2
+ <string name="hello">Bonjour</string>
3
+ </resources>
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "webapp",
3
+ "devDependencies": { "@babel/core": "^7.0.0", "babel-loader": "^9.0.0" }
4
+ }
@@ -0,0 +1,4 @@
1
+ #include <stdio.h>
2
+ #ifdef _WIN32
3
+ void f() {}
4
+ #endif
@@ -0,0 +1,10 @@
1
+ name: ci
2
+ on: [push]
3
+ jobs:
4
+ test:
5
+ strategy:
6
+ matrix:
7
+ os: [ubuntu-latest, macos-latest, windows-latest]
8
+ runs-on: ${{ matrix.os }}
9
+ steps:
10
+ - run: echo hi
@@ -0,0 +1,5 @@
1
+ cmake_minimum_required(VERSION 3.10)
2
+ project(demo)
3
+ if(WIN32)
4
+ message("win")
5
+ endif()
@@ -0,0 +1,9 @@
1
+ diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb
2
+ new file mode 100644
3
+ index 0000000..1111111
4
+ --- /dev/null
5
+ +++ b/lib/l10n/app_fr.arb
6
+ @@ -0,0 +1,3 @@
7
+ +{
8
+ + "hello": "Bonjour"
9
+ +}
@@ -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
@@ -0,0 +1,8 @@
1
+ diff --git a/src/app.ts b/src/app.ts
2
+ index 1111111..2222222 100644
3
+ --- a/src/app.ts
4
+ +++ b/src/app.ts
5
+ @@ -1,2 +1,3 @@
6
+ export function greet() {
7
+ + return t('hello');
8
+ }
@@ -0,0 +1,7 @@
1
+ diff --git a/packages/b/src/index.ts b/packages/b/src/index.ts
2
+ index 1111111..2222222 100644
3
+ --- a/packages/b/src/index.ts
4
+ +++ b/packages/b/src/index.ts
5
+ @@ -1,1 +1,2 @@
6
+ export const x = 1;
7
+ +export const y = 2;
@@ -0,0 +1,12 @@
1
+ diff --git a/scripts/build.js b/scripts/build.js
2
+ index 1111111..2222222 100644
3
+ --- a/scripts/build.js
4
+ +++ b/scripts/build.js
5
+ @@ -1,1 +1,2 @@
6
+ +if (process.platform === 'win32') {}
7
+ diff --git a/scripts/postinstall.js b/scripts/postinstall.js
8
+ index 3333333..4444444 100644
9
+ --- a/scripts/postinstall.js
10
+ +++ b/scripts/postinstall.js
11
+ @@ -1,1 +1,2 @@
12
+ +const p = os.platform();
@@ -0,0 +1,2 @@
1
+ const x = _(value).map(f);
2
+ const y = __name__;
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "myapp",
3
+ "dependencies": { "electron": "^30.0.0" }
4
+ }
@@ -0,0 +1,3 @@
1
+ [project]
2
+ name = "myflaskapp"
3
+ dependencies = ["flask", "flask-babel"]
@@ -0,0 +1,11 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!DOCTYPE TS>
3
+ <TS version="2.1" language="fr_FR">
4
+ <context>
5
+ <name>MainWindow</name>
6
+ <message>
7
+ <source>Hello</source>
8
+ <translation>Bonjour</translation>
9
+ </message>
10
+ </context>
11
+ </TS>
@@ -0,0 +1,7 @@
1
+ [package]
2
+ name = "mycli"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+
6
+ [dependencies]
7
+ clap = "4"
@@ -0,0 +1 @@
1
+ fn main() { println!("hello"); }
@@ -0,0 +1,3 @@
1
+ export function foo(): string {
2
+ return 'plain typescript, not a Qt translation'
3
+ }