@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.
- package/CHANGELOG.md +87 -0
- package/README.md +1 -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 +9 -3
- 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 +1 -1
- 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
|
@@ -8,9 +8,9 @@ import { join, resolve } from 'node:path'
|
|
|
8
8
|
import { readLifecycleEventsForDay } from '../coordination/event-log.mjs'
|
|
9
9
|
import { validateConductorEvent } from '../coordination/event-schema.mjs'
|
|
10
10
|
import { createCoordinationStore } from '../coordination/store.mjs'
|
|
11
|
-
import { detectWorktree } from '../git/worktree-detect.mjs'
|
|
12
11
|
import { parseFlags } from '../util/args.mjs'
|
|
13
12
|
import { err, ok } from '../util/result.mjs'
|
|
13
|
+
import { resolveMainRepoRoot } from '../util/state-paths.mjs'
|
|
14
14
|
|
|
15
15
|
// QUA-003 / PRF-002 — single source of truth for valid event types.
|
|
16
16
|
// VALID_EVENT_TYPES is the authoritative list; CI_WATCH_EVENT_TYPES is the O(1)
|
|
@@ -88,9 +88,11 @@ export function cmdEvent(subcommand, projectDir, extraArgs) {
|
|
|
88
88
|
if (!projectDir) return err('Usage: apt-tools event ' + subcommand + ' <project-dir> [flags]')
|
|
89
89
|
|
|
90
90
|
const targetDir = resolve(projectDir)
|
|
91
|
-
// Use shared event dir (coordinates across worktrees
|
|
92
|
-
|
|
93
|
-
|
|
91
|
+
// Use shared event dir (coordinates across worktrees, same as locks). This is
|
|
92
|
+
// a SEPARATE policy wrapper over the shared resolveMainRepoRoot mechanism
|
|
93
|
+
// (state-fork-fix ID-03) — events do NOT follow resolveStateDir's Phase-2
|
|
94
|
+
// home relocation, so the resolution stays distinct from resolveStateDir.
|
|
95
|
+
const eventsBase = resolveMainRepoRoot(targetDir)
|
|
94
96
|
const eventsDir = join(eventsBase, '.aperant', 'events')
|
|
95
97
|
mkdirSync(eventsDir, { recursive: true })
|
|
96
98
|
|
|
@@ -14,6 +14,7 @@ import { existsSync, readFileSync } from 'node:fs'
|
|
|
14
14
|
import { join, resolve } from 'node:path'
|
|
15
15
|
import { parseFlags } from '../util/args.mjs'
|
|
16
16
|
import { err, ok } from '../util/result.mjs'
|
|
17
|
+
import { resolveMainRepoRoot } from '../util/state-paths.mjs'
|
|
17
18
|
|
|
18
19
|
const ID_PATTERN = /^[a-z0-9_-]+$/i
|
|
19
20
|
|
|
@@ -35,7 +36,8 @@ function parseIdList(raw) {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
function readState(targetDir) {
|
|
38
|
-
|
|
39
|
+
// state-fork-fix: ephemeral state read resolves to the main board.
|
|
40
|
+
const statePath = join(resolveMainRepoRoot(targetDir), '.aperant', 'state.json')
|
|
39
41
|
if (!existsSync(statePath)) return null
|
|
40
42
|
try {
|
|
41
43
|
return JSON.parse(readFileSync(statePath, 'utf-8'))
|
|
@@ -30,6 +30,7 @@ import { loadDocsPolicy, matchSurfaceToPolicy } from '../docs-audit/policy.mjs'
|
|
|
30
30
|
import { applyStubPatches, auditFeatureRegistry } from '../features/registry-audit.mjs'
|
|
31
31
|
import { parseFlags } from '../util/args.mjs'
|
|
32
32
|
import { err, ok } from '../util/result.mjs'
|
|
33
|
+
import { resolveMainRepoRoot } from '../util/state-paths.mjs'
|
|
33
34
|
|
|
34
35
|
const TERMINAL_PHASES = new Set(['completed', 'cancelled', 'closed', 'shipped', 'merged'])
|
|
35
36
|
|
|
@@ -138,7 +139,11 @@ function isWorktreeRoot(projectDir) {
|
|
|
138
139
|
}
|
|
139
140
|
|
|
140
141
|
function readActiveTasks(projectDir) {
|
|
141
|
-
|
|
142
|
+
// state-fork-fix: read the AUTHORITATIVE main board — readActiveTasks finds
|
|
143
|
+
// each task's worktree_path for the BUG-025b feature-write redirect, so it
|
|
144
|
+
// MUST read main or the redirect-target lookup itself forks. (The feature
|
|
145
|
+
// WRITE path / resolveWriteRoot stays untouched.)
|
|
146
|
+
const statePath = join(resolveMainRepoRoot(projectDir), '.aperant', 'state.json')
|
|
142
147
|
if (!existsSync(statePath)) return {}
|
|
143
148
|
try {
|
|
144
149
|
const parsed = JSON.parse(readFileSync(statePath, 'utf-8'))
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commands/followups.mjs — `apt-tools followups` (G45).
|
|
3
|
+
*
|
|
4
|
+
* The deterministic surface for the per-task follow-ups ledger
|
|
5
|
+
* ({task_dir}/followups.json). Three subcommands:
|
|
6
|
+
*
|
|
7
|
+
* apt-tools followups check <project-dir> --task-id <id>
|
|
8
|
+
* Derive the expected candidate universe (Out of Scope ∪ raw review rows ∪
|
|
9
|
+
* qa findings) and report what the ledger is missing / unadjudicated /
|
|
10
|
+
* unfixed / undispositioned. Exits 1 when the story is dishonest.
|
|
11
|
+
*
|
|
12
|
+
* apt-tools followups disposition <project-dir> --task-id <id> --row <row-id>
|
|
13
|
+
* --kind <filed_task|executed_commit|parked_note|dropped_by_user> --ref <ref>
|
|
14
|
+
* [--reason <text>]
|
|
15
|
+
* Flip a row to `dispositioned` with the validated disposition record
|
|
16
|
+
* (atomic write). `dropped_by_user` additionally requires --reason.
|
|
17
|
+
*
|
|
18
|
+
* apt-tools followups policy <project-dir> --task-id <id>
|
|
19
|
+
* Return the effective disposition policy (ask | file | chain +
|
|
20
|
+
* max_chain_depth) from config.followups when set, else autonomy-derived.
|
|
21
|
+
*
|
|
22
|
+
* Shared-root resolution + taskDir + taskRecord + config all come from
|
|
23
|
+
* buildGateContext so a worktree-resident invocation reads/writes the same
|
|
24
|
+
* followups.json the gates read (the L5 worktree concern, plan-approval.mjs).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
28
|
+
import { join, resolve } from 'node:path'
|
|
29
|
+
|
|
30
|
+
import { parseUserOutcomes } from '../coverage-check/user-outcomes.mjs'
|
|
31
|
+
import {
|
|
32
|
+
computeMissingExpected,
|
|
33
|
+
deriveExpectedCandidates,
|
|
34
|
+
validateOutcomeRefs,
|
|
35
|
+
} from '../followups/derive.mjs'
|
|
36
|
+
import {
|
|
37
|
+
DISPOSITION_KINDS,
|
|
38
|
+
listLoadBearingUnfixed,
|
|
39
|
+
listUnadjudicatedOpen,
|
|
40
|
+
listUndispositioned,
|
|
41
|
+
parseFollowups,
|
|
42
|
+
} from '../followups/schema.mjs'
|
|
43
|
+
import { buildGateContext } from '../gate/context.mjs'
|
|
44
|
+
import { parseFlags } from '../util/args.mjs'
|
|
45
|
+
import { atomicWriteJson, readJsonIfExists } from '../util/io.mjs'
|
|
46
|
+
import { err, exitWith, ok } from '../util/result.mjs'
|
|
47
|
+
|
|
48
|
+
const SUBCOMMANDS = new Set(['check', 'disposition', 'policy'])
|
|
49
|
+
|
|
50
|
+
function readTextIfExists(path) {
|
|
51
|
+
if (!existsSync(path)) return null
|
|
52
|
+
try {
|
|
53
|
+
return readFileSync(path, 'utf-8')
|
|
54
|
+
} catch {
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Entry: `apt-tools followups <subcommand> <project-dir> [flags]`.
|
|
61
|
+
*/
|
|
62
|
+
export function cmdFollowups(subcommand, projectDir, extraArgs) {
|
|
63
|
+
if (!subcommand || !SUBCOMMANDS.has(subcommand)) {
|
|
64
|
+
return err(
|
|
65
|
+
'Usage: apt-tools followups <check|disposition|policy> <project-dir> --task-id <id> [...]',
|
|
66
|
+
1,
|
|
67
|
+
{ command: 'followups' },
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
if (!projectDir) {
|
|
71
|
+
return err('project-dir is required', 1, { command: `followups-${subcommand}` })
|
|
72
|
+
}
|
|
73
|
+
const absProjectDir = resolve(projectDir)
|
|
74
|
+
if (!existsSync(absProjectDir)) {
|
|
75
|
+
return err(`project directory not found: ${absProjectDir}`, 1, {
|
|
76
|
+
command: `followups-${subcommand}`,
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const flags = parseFlags(extraArgs ?? [])
|
|
81
|
+
const taskId = flags.get('task-id')
|
|
82
|
+
if (typeof taskId !== 'string' || taskId.trim() === '') {
|
|
83
|
+
return err('--task-id <id> is required', 1, { command: `followups-${subcommand}` })
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let ctx
|
|
87
|
+
try {
|
|
88
|
+
ctx = buildGateContext(absProjectDir, taskId)
|
|
89
|
+
} catch (e) {
|
|
90
|
+
return err(`task not found: ${taskId} (${e?.message ?? String(e)})`, 1, {
|
|
91
|
+
command: `followups-${subcommand}`,
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (subcommand === 'check') return doCheck(ctx)
|
|
96
|
+
if (subcommand === 'disposition') return doDisposition(ctx, flags)
|
|
97
|
+
return doPolicy(ctx)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* `followups check` — the deterministic adjudication-readiness envelope (AC5).
|
|
102
|
+
*/
|
|
103
|
+
function doCheck(ctx) {
|
|
104
|
+
const followupsPath = join(ctx.taskDir, 'followups.json')
|
|
105
|
+
const specPath = join(ctx.taskDir, 'spec.md')
|
|
106
|
+
const reviewPath = join(ctx.taskDir, 'review.json')
|
|
107
|
+
const qaPath = join(ctx.taskDir, 'qa_signoff.json')
|
|
108
|
+
|
|
109
|
+
const specContent = readTextIfExists(specPath)
|
|
110
|
+
const review = readJsonIfExists(reviewPath)
|
|
111
|
+
const qa = readJsonIfExists(qaPath)
|
|
112
|
+
|
|
113
|
+
const candidates = deriveExpectedCandidates({ specContent, review, qa })
|
|
114
|
+
const ledgerDoc = readJsonIfExists(followupsPath)
|
|
115
|
+
const ledger_present = ledgerDoc !== null
|
|
116
|
+
|
|
117
|
+
const parsed = parseFollowups(ledgerDoc ?? { rows: [] })
|
|
118
|
+
const parsedOutcomes = parseUserOutcomes(specContent ?? '')
|
|
119
|
+
|
|
120
|
+
const missing_expected = computeMissingExpected(candidates, parsed.rows)
|
|
121
|
+
const invalid_outcome_refs = validateOutcomeRefs(parsed.rows, parsedOutcomes)
|
|
122
|
+
const unadjudicated_open = listUnadjudicatedOpen(parsed)
|
|
123
|
+
const load_bearing_unfixed = listLoadBearingUnfixed(parsed)
|
|
124
|
+
const undispositioned = listUndispositioned(parsed)
|
|
125
|
+
|
|
126
|
+
// Exit 1 when the ledger is absent while candidates exist (adoption forcing
|
|
127
|
+
// function), or when derivation/schema/outcome-ref validation fails. The
|
|
128
|
+
// unadjudicated / load-bearing / undispositioned lists are reported but do
|
|
129
|
+
// NOT fail `check` directly — those are gate concerns (ship gate / close
|
|
130
|
+
// gate) so an in-progress verify pass can iterate without `check` flapping.
|
|
131
|
+
const derivationFailure =
|
|
132
|
+
(!ledger_present && candidates.length > 0) ||
|
|
133
|
+
missing_expected.length > 0 ||
|
|
134
|
+
invalid_outcome_refs.length > 0 ||
|
|
135
|
+
parsed.schema_errors.length > 0
|
|
136
|
+
|
|
137
|
+
const envelope = {
|
|
138
|
+
status: derivationFailure ? 'fail' : 'ok',
|
|
139
|
+
command: 'followups-check',
|
|
140
|
+
task_id: ctx.taskId,
|
|
141
|
+
ledger_present,
|
|
142
|
+
expected_total: candidates.length,
|
|
143
|
+
ledger_rows: parsed.rows.length,
|
|
144
|
+
schema_errors: parsed.schema_errors,
|
|
145
|
+
missing_expected,
|
|
146
|
+
invalid_outcome_refs,
|
|
147
|
+
unadjudicated_open: unadjudicated_open.map(rowSummary),
|
|
148
|
+
load_bearing_unfixed: load_bearing_unfixed.map(rowSummary),
|
|
149
|
+
undispositioned: undispositioned.map(rowSummary),
|
|
150
|
+
}
|
|
151
|
+
return exitWith(envelope, derivationFailure ? 1 : 0)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* `followups disposition` — flip a row to dispositioned (AC7).
|
|
156
|
+
*/
|
|
157
|
+
function doDisposition(ctx, flags) {
|
|
158
|
+
const rowId = flags.get('row')
|
|
159
|
+
if (typeof rowId !== 'string' || rowId.trim() === '') {
|
|
160
|
+
return err('--row <row-id> is required', 1, { command: 'followups-disposition' })
|
|
161
|
+
}
|
|
162
|
+
const kind = flags.get('kind')
|
|
163
|
+
if (!DISPOSITION_KINDS.has(kind)) {
|
|
164
|
+
return err(`--kind must be one of: ${[...DISPOSITION_KINDS].join(', ')}`, 1, {
|
|
165
|
+
command: 'followups-disposition',
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
const ref = flags.get('ref')
|
|
169
|
+
if (typeof ref !== 'string' || ref.trim() === '') {
|
|
170
|
+
return err('--ref <ref> is required (non-empty)', 1, { command: 'followups-disposition' })
|
|
171
|
+
}
|
|
172
|
+
const reasonRaw = flags.get('reason')
|
|
173
|
+
const reason = typeof reasonRaw === 'string' ? reasonRaw : null
|
|
174
|
+
if (kind === 'dropped_by_user' && !reason) {
|
|
175
|
+
return err('--kind dropped_by_user requires --reason "<text>"', 1, {
|
|
176
|
+
command: 'followups-disposition',
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const followupsPath = join(ctx.taskDir, 'followups.json')
|
|
181
|
+
const ledgerDoc = readJsonIfExists(followupsPath)
|
|
182
|
+
if (ledgerDoc === null) {
|
|
183
|
+
return err(`followups.json not found for ${ctx.taskId}`, 1, {
|
|
184
|
+
command: 'followups-disposition',
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
const parsed = parseFollowups(ledgerDoc)
|
|
188
|
+
// Fail closed on schema errors: the write-back below re-serializes only the
|
|
189
|
+
// validated `parsed.rows`, so a single malformed row elsewhere in the ledger
|
|
190
|
+
// would be silently dropped when dispositioning a different row. Refuse and
|
|
191
|
+
// surface the errors so the operator fixes them first (no silent data loss).
|
|
192
|
+
if (parsed.schema_errors.length > 0) {
|
|
193
|
+
return err(
|
|
194
|
+
`followups.json has ${parsed.schema_errors.length} malformed row(s) — fix them before dispositioning (a write-back would drop them)`,
|
|
195
|
+
1,
|
|
196
|
+
{ command: 'followups-disposition', schema_errors: parsed.schema_errors },
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
const target = parsed.rows.find((r) => r.id === rowId)
|
|
200
|
+
if (!target) {
|
|
201
|
+
return err(`unknown row id: ${rowId}`, 1, {
|
|
202
|
+
command: 'followups-disposition',
|
|
203
|
+
known_rows: parsed.rows.map((r) => r.id),
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Atomic flip: re-serialize the full validated row set with the target's
|
|
208
|
+
// status + disposition updated. The on-disk shape is `{ rows: [...] }`.
|
|
209
|
+
const disposition = { kind, ref, ...(reason ? { reason } : {}) }
|
|
210
|
+
const nextRows = parsed.rows.map((r) =>
|
|
211
|
+
r.id === rowId ? { ...r, status: 'dispositioned', disposition } : r,
|
|
212
|
+
)
|
|
213
|
+
atomicWriteJson(followupsPath, { rows: nextRows })
|
|
214
|
+
|
|
215
|
+
return ok({
|
|
216
|
+
status: 'ok',
|
|
217
|
+
command: 'followups-disposition',
|
|
218
|
+
task_id: ctx.taskId,
|
|
219
|
+
row: rowId,
|
|
220
|
+
disposition,
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* `followups policy` — effective disposition policy (AC8 / ID-11).
|
|
226
|
+
*/
|
|
227
|
+
function doPolicy(ctx) {
|
|
228
|
+
const followupsCfg = ctx.config?.followups ?? {}
|
|
229
|
+
const explicit = followupsCfg.after_close
|
|
230
|
+
|
|
231
|
+
// Resolve the autonomy level: task record first, then config default, else 1.
|
|
232
|
+
const autonomy =
|
|
233
|
+
typeof ctx.taskRecord?.autonomy === 'number'
|
|
234
|
+
? ctx.taskRecord.autonomy
|
|
235
|
+
: typeof ctx.config?.autonomy?.default === 'number'
|
|
236
|
+
? ctx.config.autonomy.default
|
|
237
|
+
: 1
|
|
238
|
+
|
|
239
|
+
let after_close
|
|
240
|
+
let source
|
|
241
|
+
if (explicit === 'ask' || explicit === 'file' || explicit === 'chain') {
|
|
242
|
+
after_close = explicit
|
|
243
|
+
source = 'config'
|
|
244
|
+
} else {
|
|
245
|
+
after_close = autonomy <= 1 ? 'ask' : autonomy === 2 ? 'file' : 'chain'
|
|
246
|
+
source = 'autonomy'
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// max_chain_depth: default 1; clamp values > 1 to 1 with a warning (v1).
|
|
250
|
+
let max_chain_depth = 1
|
|
251
|
+
let clamp_warning = null
|
|
252
|
+
const rawDepth = followupsCfg.max_chain_depth
|
|
253
|
+
if (typeof rawDepth === 'number' && Number.isFinite(rawDepth)) {
|
|
254
|
+
if (rawDepth > 1) {
|
|
255
|
+
clamp_warning = `max_chain_depth ${rawDepth} clamped to 1 (v1 supports depth-1 only)`
|
|
256
|
+
} else if (rawDepth >= 1) {
|
|
257
|
+
max_chain_depth = 1
|
|
258
|
+
} else {
|
|
259
|
+
// depth < 1 means no chaining; degrade chain → file.
|
|
260
|
+
max_chain_depth = 0
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Anti-perpetual-motion: degrade chain → file when the CURRENT task's
|
|
265
|
+
// triage.json marks it as already a chained descendant at/over the cap.
|
|
266
|
+
const triagePath = join(ctx.taskDir, 'triage.json')
|
|
267
|
+
const triage = readJsonIfExists(triagePath)
|
|
268
|
+
const chainDepth = triage?.extra?.followups_chain?.depth
|
|
269
|
+
let degraded = false
|
|
270
|
+
if (after_close === 'chain' && typeof chainDepth === 'number' && chainDepth >= max_chain_depth) {
|
|
271
|
+
after_close = 'file'
|
|
272
|
+
degraded = true
|
|
273
|
+
}
|
|
274
|
+
// A 0 max_chain_depth (depth < 1 config) also degrades chain → file.
|
|
275
|
+
if (after_close === 'chain' && max_chain_depth < 1) {
|
|
276
|
+
after_close = 'file'
|
|
277
|
+
degraded = true
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const envelope = {
|
|
281
|
+
status: 'ok',
|
|
282
|
+
command: 'followups-policy',
|
|
283
|
+
task_id: ctx.taskId,
|
|
284
|
+
after_close,
|
|
285
|
+
max_chain_depth,
|
|
286
|
+
source,
|
|
287
|
+
autonomy,
|
|
288
|
+
}
|
|
289
|
+
if (clamp_warning) envelope.warning = clamp_warning
|
|
290
|
+
if (degraded) envelope.degraded_from_chain = true
|
|
291
|
+
return ok(envelope)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function rowSummary(r) {
|
|
295
|
+
return {
|
|
296
|
+
id: r.id,
|
|
297
|
+
title: r.title,
|
|
298
|
+
status: r.status,
|
|
299
|
+
class_proposed: r.class_proposed,
|
|
300
|
+
verdict: r.adjudication?.verdict ?? null,
|
|
301
|
+
disposition_kind: r.disposition?.kind ?? null,
|
|
302
|
+
}
|
|
303
|
+
}
|
|
@@ -9,6 +9,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
9
9
|
import { getSharedLockDir } from '../util/project.mjs'
|
|
10
10
|
import { err, ok } from '../util/result.mjs'
|
|
11
11
|
import { readSkillInstallState } from '../util/skill-installs.mjs'
|
|
12
|
+
import { resolveMainRepoRoot } from '../util/state-paths.mjs'
|
|
12
13
|
|
|
13
14
|
const __filename = fileURLToPath(import.meta.url)
|
|
14
15
|
const __dirname = dirname(__filename)
|
|
@@ -70,7 +71,8 @@ export function cmdHealthCheck(projectDir) {
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
// 2. Check for stale active_task in state.json
|
|
73
|
-
|
|
74
|
+
// state-fork-fix: state.json is EPHEMERAL — resolve to the main board.
|
|
75
|
+
const statePath = join(resolveMainRepoRoot(targetDir), '.aperant', 'state.json')
|
|
74
76
|
if (existsSync(statePath)) {
|
|
75
77
|
try {
|
|
76
78
|
const state = JSON.parse(readFileSync(statePath, 'utf-8'))
|
|
@@ -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 }
|