@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.
Files changed (241) hide show
  1. package/CHANGELOG.md +89 -3
  2. package/README.md +11 -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 +13 -6
  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 +138 -138
  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,158 @@
1
+ /**
2
+ * @internal
3
+ * G45 followups-adjudicated — blocking gate for `apt:ship` (full-pipeline only).
4
+ *
5
+ * The deterministic part the deferring agent can't talk past: shipping is
6
+ * blocked until every open follow-up row carries an adjudication and every
7
+ * load-bearing row is fixed. The ledger derivation also forces adoption —
8
+ * a spec.md with `## Out of Scope` bullets but no followups.json fails.
9
+ *
10
+ * Fails when:
11
+ * (a) spec.md has Out of Scope bullets but followups.json is absent
12
+ * (adoption forcing function),
13
+ * (b) the derived expected universe has rows missing from the ledger,
14
+ * (c) any `status: 'open'` row lacks `adjudication`,
15
+ * (d) any `verdict: 'load-bearing'` row has `status !== 'fixed'`.
16
+ *
17
+ * Skips when:
18
+ * - spec.md is absent, OR
19
+ * - there are no expected candidates AND no ledger.
20
+ *
21
+ * Fast Path Guarantee (ID-12): `applies_to_profile` gates this to
22
+ * full-pipeline (QUICK/DEBUG → lite → skip). The exemption is regression-
23
+ * locked by followups-quick-exempt.test.ts (ST7).
24
+ *
25
+ * Bypass: only the existing audited `--force --reason` (G42 Phase 1 pattern).
26
+ */
27
+
28
+ import { existsSync, readFileSync } from 'node:fs'
29
+ import { join } from 'node:path'
30
+
31
+ import { parseOutOfScope } from '../../consistency/parse-spec.mjs'
32
+ import { computeMissingExpected, deriveExpectedCandidates } from '../../followups/derive.mjs'
33
+ import {
34
+ listLoadBearingUnfixed,
35
+ listUnadjudicatedOpen,
36
+ parseFollowups,
37
+ } from '../../followups/schema.mjs'
38
+ import { readJsonIfExists } from '../../util/io.mjs'
39
+ import { registerGate } from '../registry.mjs'
40
+
41
+ function rowSummary(r) {
42
+ return {
43
+ id: r.id,
44
+ title: r.title ?? null,
45
+ status: r.status ?? null,
46
+ verdict: r.adjudication?.verdict ?? null,
47
+ }
48
+ }
49
+
50
+ const gate = {
51
+ id: 'followups-adjudicated',
52
+ description:
53
+ 'followups.json covers every derived candidate, every open row is adjudicated, and every load-bearing row is fixed (skipped if spec.md absent or no candidates + no ledger)',
54
+ severity: 'blocking',
55
+ skills: ['ship'],
56
+ applies_to_profile: (ctx) => ctx.profile === 'full-pipeline',
57
+ async check(ctx) {
58
+ const specPath = join(ctx.taskDir, 'spec.md')
59
+ const followupsPath = join(ctx.taskDir, 'followups.json')
60
+ const reviewPath = join(ctx.taskDir, 'review.json')
61
+ const qaPath = join(ctx.taskDir, 'qa_signoff.json')
62
+ const inputs = [specPath, followupsPath, reviewPath, qaPath]
63
+
64
+ if (!existsSync(specPath)) {
65
+ return {
66
+ status: 'skip',
67
+ reason: 'spec.md not present — skipping followups-adjudicated',
68
+ inputs,
69
+ }
70
+ }
71
+
72
+ let specContent
73
+ try {
74
+ specContent = readFileSync(specPath, 'utf-8')
75
+ } catch (err) {
76
+ // existsSync passed but the read failed (TOCTOU race, permission
77
+ // error). Return a structured fail like the sibling gates
78
+ // (review-clean, verify-approved) rather than throwing out of the
79
+ // gate runner — important in the concurrent-worktree environment.
80
+ return {
81
+ status: 'fail',
82
+ reason: `spec.md could not be read: ${err.message}`,
83
+ inputs,
84
+ }
85
+ }
86
+ const oosBullets = parseOutOfScope(specContent)
87
+ const review = readJsonIfExists(reviewPath)
88
+ const qa = readJsonIfExists(qaPath)
89
+ const candidates = deriveExpectedCandidates({ specContent, review, qa })
90
+ const ledgerDoc = readJsonIfExists(followupsPath)
91
+ const ledgerPresent = ledgerDoc !== null
92
+
93
+ // Skip when there is nothing to track and no ledger to validate.
94
+ if (candidates.length === 0 && !ledgerPresent) {
95
+ return {
96
+ status: 'skip',
97
+ reason: 'no expected follow-up candidates and no ledger — nothing to adjudicate',
98
+ inputs,
99
+ }
100
+ }
101
+
102
+ // (a) Out of Scope bullets exist but no ledger — adoption forcing function.
103
+ if (oosBullets.length > 0 && !ledgerPresent) {
104
+ return {
105
+ status: 'fail',
106
+ reason: `spec.md has ${oosBullets.length} Out of Scope bullet(s) but followups.json is absent — run \`apt-tools followups check\` and seed the ledger`,
107
+ evidence: { out_of_scope_bullets: oosBullets.map((b) => b.text) },
108
+ inputs,
109
+ }
110
+ }
111
+
112
+ const parsed = parseFollowups(ledgerDoc ?? { rows: [] })
113
+
114
+ // (b) Missing expected rows.
115
+ const missing = computeMissingExpected(candidates, parsed.rows)
116
+ if (missing.length > 0) {
117
+ return {
118
+ status: 'fail',
119
+ reason: `${missing.length} expected follow-up row(s) missing from followups.json`,
120
+ evidence: {
121
+ missing_expected: missing.map((m) => ({ id: m.id, title: m.title, origin: m.origin })),
122
+ },
123
+ inputs,
124
+ }
125
+ }
126
+
127
+ // (c) Open rows lacking adjudication.
128
+ const unadjudicated = listUnadjudicatedOpen(parsed)
129
+ if (unadjudicated.length > 0) {
130
+ return {
131
+ status: 'fail',
132
+ reason: `${unadjudicated.length} open follow-up row(s) lack adjudication`,
133
+ evidence: { unadjudicated_open: unadjudicated.map(rowSummary) },
134
+ inputs,
135
+ }
136
+ }
137
+
138
+ // (d) Load-bearing rows not fixed.
139
+ const loadBearing = listLoadBearingUnfixed(parsed)
140
+ if (loadBearing.length > 0) {
141
+ return {
142
+ status: 'fail',
143
+ reason: `${loadBearing.length} load-bearing follow-up row(s) not fixed`,
144
+ evidence: { load_bearing_unfixed: loadBearing.map(rowSummary) },
145
+ inputs,
146
+ }
147
+ }
148
+
149
+ return {
150
+ status: 'pass',
151
+ reason: `followups.json adjudicated (${parsed.rows.length} row(s), ${candidates.length} expected candidate(s))`,
152
+ inputs,
153
+ }
154
+ },
155
+ }
156
+
157
+ registerGate(gate)
158
+ export default gate
@@ -16,6 +16,7 @@ import './plan-exists.mjs'
16
16
  import './all-subtasks-complete.mjs'
17
17
  import './verify-approved.mjs'
18
18
  import './review-clean.mjs'
19
+ import './followups-adjudicated.mjs'
19
20
  import './gitignore-in-sync.mjs'
20
21
  import './consistency-check.mjs'
21
22
  import './tdd-iron-law.mjs'
@@ -0,0 +1,212 @@
1
+ /**
2
+ * git/materialize-env.mjs — framework-CLI env materialization seam (ID-06).
3
+ *
4
+ * Subtask 2 of `build-unified-per-project-env-secrets-ma_16-06-26` (#354). The
5
+ * third worktree-create seam: after `apt-tools worktree create --task` adds a
6
+ * worktree, write the project's stored env file blobs verbatim into it so a CLI
7
+ * verify-in-worktree run has the same env as the main checkout.
8
+ *
9
+ * This is a standalone `.mjs` mirror of the TS core `materializeProjectEnv`
10
+ * (the framework CLI cannot import the compiled `@aperant/core` helper). It is
11
+ * deliberately self-contained + best-effort: every failure path returns a
12
+ * `{ materialized: 0, ... }` envelope instead of throwing, so worktree creation
13
+ * (the parent op) is never aborted by an env hiccup.
14
+ *
15
+ * libSQL `project_env` schema (subtask 1) — keyed `(project_id, rel_path)`:
16
+ * project_id TEXT, rel_path TEXT, contents TEXT [may be safeStorage-encrypted
17
+ * on the desktop write path], classification TEXT, fingerprint TEXT, updated_at TEXT.
18
+ *
19
+ * Encryption caveat: the desktop write path wraps `contents` with Electron's
20
+ * safeStorage `CryptoProvider`, which a standalone Node CLI cannot decrypt. We
21
+ * write whatever the column holds — plaintext in self-hosted/CLI contexts
22
+ * (`CryptoProvider` absent), opaque otherwise. That mirrors the libSQL
23
+ * backend's own plaintext-fallback robustness and is the documented framework
24
+ * limitation; the desktop seams (pipeline + terminal) decrypt via the store.
25
+ */
26
+
27
+ import { createHash } from 'node:crypto'
28
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
29
+ import { homedir } from 'node:os'
30
+ import { dirname, isAbsolute, join, resolve, sep } from 'node:path'
31
+
32
+ /** Resolve the shared libSQL DB path: APERANT_DB_PATH override → ~/.aperant/data.db. */
33
+ function resolveDbPath() {
34
+ const raw = process.env.APERANT_DB_PATH
35
+ if (raw && raw.trim().length > 0) return raw
36
+ return join(homedir(), '.aperant', 'data.db')
37
+ }
38
+
39
+ /**
40
+ * Reject a rel_path that is absolute or escapes the worktree root BEFORE any
41
+ * write (the traversal guard, mirroring the core `assertPathContained` choke
42
+ * point). Returns the resolved absolute target, or null when unsafe.
43
+ *
44
+ * Exported so the security-sensitive guard can be unit-tested directly: the
45
+ * libSQL-backed `materializeProjectEnvCli` can only no-op in environments where
46
+ * `@libsql/client` is unresolvable (the framework test runner), so the pure
47
+ * guard is the testable load-bearing surface.
48
+ */
49
+ export function resolveContainedTarget(worktreePath, relPath) {
50
+ if (!relPath || isAbsolute(relPath)) return null
51
+ const base = resolve(worktreePath)
52
+ const target = resolve(base, relPath)
53
+ if (target !== base && !target.startsWith(base + sep)) return null
54
+ return target
55
+ }
56
+
57
+ /** sha256 hex of a file's verbatim text — drift comparison only (ID-07). */
58
+ function fingerprint(contents) {
59
+ return createHash('sha256').update(contents, 'utf-8').digest('hex')
60
+ }
61
+
62
+ /** Lazily open the shared libSQL DB, or null when absent / @libsql unavailable. */
63
+ async function openDb() {
64
+ const dbPath = resolveDbPath()
65
+ if (!existsSync(dbPath)) return null
66
+ let createClient
67
+ try {
68
+ ;({ createClient } = await import('@libsql/client'))
69
+ } catch {
70
+ // @libsql/client not installed in this consumer — best-effort no-op.
71
+ return null
72
+ }
73
+ try {
74
+ return createClient({ url: `file:${dbPath}` })
75
+ } catch {
76
+ return null
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Resolve the libSQL `projects.id` for a given project root path. The desktop /
82
+ * self-hosted app keys `project_env` rows by the project's stored id, NOT its
83
+ * path; the framework CLI only knows the path, so we match it against the
84
+ * `projects.path` column. Returns null when the project is unknown to libSQL
85
+ * (e.g. a fresh repo never opened in the app) — the caller treats that as
86
+ * "no env to materialize", never an error.
87
+ */
88
+ export async function resolveProjectIdFromPath(client, projectPath) {
89
+ if (!client) return null
90
+ const wanted = resolve(projectPath)
91
+ try {
92
+ const result = await client.execute({ sql: 'SELECT id, path FROM projects', args: [] })
93
+ for (const row of result.rows) {
94
+ if (row.path != null && resolve(String(row.path)) === wanted) {
95
+ return String(row.id)
96
+ }
97
+ }
98
+ } catch {
99
+ // projects table absent or unreadable — no project id.
100
+ }
101
+ return null
102
+ }
103
+
104
+ /**
105
+ * Read every `project_env` row for a project from an open libSQL client.
106
+ * Returns `[]` (never throws) when the table is absent or projectId is null.
107
+ *
108
+ * @returns Promise<Array<{ relPath, contents, classification, fingerprint }>>
109
+ */
110
+ async function readProjectEnvRows(client, projectId) {
111
+ if (!client || !projectId) return []
112
+ try {
113
+ const result = await client.execute({
114
+ sql: 'SELECT rel_path, contents, classification, fingerprint FROM project_env WHERE project_id = ? ORDER BY rel_path ASC',
115
+ args: [projectId],
116
+ })
117
+ return result.rows.map((row) => ({
118
+ relPath: String(row.rel_path),
119
+ contents: row.contents == null ? '' : String(row.contents),
120
+ classification: String(row.classification),
121
+ fingerprint: String(row.fingerprint),
122
+ }))
123
+ } catch {
124
+ // Missing table (project_env never created) or read error — no-op.
125
+ return []
126
+ }
127
+ }
128
+
129
+ /** Close a libSQL client best-effort (never throws). */
130
+ function closeDb(client) {
131
+ try {
132
+ client?.close?.()
133
+ } catch {
134
+ /* best-effort */
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Materialize the project's stored env blobs verbatim into a worktree.
140
+ *
141
+ * @param worktreePath absolute path of the target worktree
142
+ * @param projectPath absolute path of the main project root (used to resolve
143
+ * the libSQL project id via `projects.path`)
144
+ * @param explicitProjectId optional override when the caller already knows the id
145
+ * @returns { materialized, rejected, errored } counts — never throws.
146
+ */
147
+ export async function materializeProjectEnvCli(worktreePath, projectPath, explicitProjectId) {
148
+ const client = await openDb()
149
+ try {
150
+ const projectId = explicitProjectId || (await resolveProjectIdFromPath(client, projectPath))
151
+ const rows = await readProjectEnvRows(client, projectId)
152
+ let materialized = 0
153
+ let rejected = 0
154
+ let errored = 0
155
+
156
+ for (const row of rows) {
157
+ const target = resolveContainedTarget(worktreePath, row.relPath)
158
+ if (!target) {
159
+ rejected += 1
160
+ continue
161
+ }
162
+ try {
163
+ mkdirSync(dirname(target), { recursive: true })
164
+ writeFileSync(target, row.contents, 'utf-8')
165
+ materialized += 1
166
+ } catch {
167
+ errored += 1
168
+ }
169
+ }
170
+
171
+ return { materialized, rejected, errored }
172
+ } finally {
173
+ closeDb(client)
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Detect drift for an already-materialized worktree (ID-07): compare each
179
+ * stored blob's fingerprint against the on-disk file. Returns per-file verdicts;
180
+ * `sync-env` simply re-runs `materializeProjectEnvCli` to overwrite.
181
+ *
182
+ * @returns { hasDrift, files: [{ relPath, status }] } — never throws.
183
+ */
184
+ export async function detectEnvDriftCli(worktreePath, projectPath, explicitProjectId) {
185
+ const client = await openDb()
186
+ try {
187
+ const projectId = explicitProjectId || (await resolveProjectIdFromPath(client, projectPath))
188
+ const rows = await readProjectEnvRows(client, projectId)
189
+ const files = []
190
+ for (const row of rows) {
191
+ const target = resolveContainedTarget(worktreePath, row.relPath)
192
+ if (!target || !existsSync(target)) {
193
+ files.push({ relPath: row.relPath, status: 'missing' })
194
+ continue
195
+ }
196
+ let onDisk
197
+ try {
198
+ onDisk = readFileSync(target, 'utf-8')
199
+ } catch {
200
+ files.push({ relPath: row.relPath, status: 'missing' })
201
+ continue
202
+ }
203
+ files.push({
204
+ relPath: row.relPath,
205
+ status: fingerprint(onDisk) === row.fingerprint ? 'in-sync' : 'drifted',
206
+ })
207
+ }
208
+ return { hasDrift: files.some((f) => f.status !== 'in-sync'), files }
209
+ } finally {
210
+ closeDb(client)
211
+ }
212
+ }
package/src/cli/help.mjs CHANGED
@@ -19,6 +19,9 @@ Commands:
19
19
  config upgrade-layout <dir> [--dry-run] Migrate .aperant/ folder layout to C36 shape (moves + rewrites gitignore)
20
20
  audit privacy <dir> [--scrub] C35: diff tracked .aperant/* vs share policy (exit 2 on violations; --scrub prints filter-repo commands + checklist, never executes)
21
21
  audit visibility-signals <dir> C35: structured JSON signals for /apt:setup's visibility mini-flow
22
+ merge detect <dir> [--base <ref>] [--head <ref>] Trial-merge mergeability probe (reuses audit branch-current detection); emits {mergeable, conflicts_preview?} for apt:merge-conflict-resolution
23
+ merge markers <dir> Read on-disk conflict markers from the unmerged worktree so the agent can resolve them inline; emits {conflicted, files:[{path,content}]}
24
+ merge integrate <dir> --tasks <ids> | --batch <id> [--base <ref>] Serialize a fan-out batch into base: order tasks (topological on depends_on, else stable), probe each PR's mergeability (reuses merge detect), emit an integration report for apt:fan-out §5
22
25
  state-update <dir> --field --value Update a state field
23
26
  detect-project <project-dir> Scan project for init classification
24
27
  init-state <dir> --config <json> Write config + state after init flow
@@ -74,9 +77,10 @@ Commands:
74
77
 
75
78
  standards <project-dir> [--profile <name>] [--json] [--no-color] C20: zero-orchestration read-only bundle (constitution + profile + feature registry + design tokens + taste)
76
79
 
77
- worktree create <dir> --subtask <id> --branch <name> Create isolated worktree for a subtask
80
+ worktree create <dir> --subtask <id> --branch <name> Create isolated worktree for a subtask (also materializes the project's stored env blobs into the new worktree, #354)
78
81
  worktree remove <dir> --subtask <id> [--force] Remove a subtask worktree
79
82
  worktree list <dir> List active worktrees under .aperant/worktrees/
83
+ worktree sync-env <dir> --task <id> [--scope <s>] [--project-id <id>] Re-materialize the project's stored env blobs into an existing task worktree (store is source of truth — overwrites, no merge)
80
84
 
81
85
  detect-worktree <project-dir> Check if running inside a git worktree
82
86
  template <name> Output a template (agents-greenfield, agents-brownfield, config, state, gitignore)
@@ -131,6 +135,10 @@ Commands:
131
135
  ci-watch start|stop|status|tick <dir> [--pr <N>] [...] /apt:watch-ci primitives — see SKILL.md for full flag matrix
132
136
  ci-watch sweep <dir> --orphaned [--dry-run] BUG-027: enumerate ci-watches state files, query gh per PR, remove terminal-state (MERGED|CLOSED) entries (manual GC; rate-limited)
133
137
 
138
+ followups check <dir> --task-id <id> G45: derive the expected follow-up universe (Out of Scope ∪ raw review rows ∪ qa findings) + report missing/unadjudicated/unfixed/undispositioned rows; exit 1 when the ledger is dishonest
139
+ followups disposition <dir> --task-id <id> --row <row-id> --kind <filed_task|executed_commit|parked_note|dropped_by_user> --ref <ref> [--reason <text>] G45: flip a ledger row to dispositioned (atomic; dropped_by_user requires --reason)
140
+ followups policy <dir> --task-id <id> G45: effective disposition policy (ask|file|chain + max_chain_depth) from config.followups when set, else autonomy-derived
141
+
134
142
  Flags:
135
143
  --version Print version and exit
136
144
  --help Print usage and exit
@@ -102,6 +102,7 @@ export const LEGACY_INSTALL_PATHS = Object.freeze({
102
102
  'agents/apt-fan-out.md',
103
103
  'agents/apt-handoff.md',
104
104
  'agents/apt-improve.md',
105
+ 'agents/apt-merge-conflict-resolution.md',
105
106
  'agents/apt-mockup.md',
106
107
  'agents/apt-pause.md',
107
108
  'agents/apt-personas.md',
@@ -146,6 +147,7 @@ export const LEGACY_INSTALL_PATHS = Object.freeze({
146
147
  'commands/apt-fan-out.md',
147
148
  'commands/apt-handoff.md',
148
149
  'commands/apt-improve.md',
150
+ 'commands/apt-merge-conflict-resolution.md',
149
151
  'commands/apt-mockup.md',
150
152
  'commands/apt-pause.md',
151
153
  'commands/apt-personas.md',
@@ -180,6 +182,7 @@ export const LEGACY_INSTALL_PATHS = Object.freeze({
180
182
  'agents/apt-planner.md',
181
183
  'agents/apt-pr-review-codebase-fit.md',
182
184
  'agents/apt-pr-review-fixer.md',
185
+ 'agents/apt-pr-review-i18n.md',
183
186
  'agents/apt-pr-review-logic.md',
184
187
  'agents/apt-pr-review-performance.md',
185
188
  'agents/apt-pr-review-quality.md',
@@ -187,6 +190,7 @@ export const LEGACY_INSTALL_PATHS = Object.freeze({
187
190
  'agents/apt-pr-review-self-reviewer.md',
188
191
  'agents/apt-pr-review-test-integrity.md',
189
192
  'agents/apt-pr-review-validator.md',
193
+ 'agents/apt-pr-review-xplatform.md',
190
194
  'agents/apt-researcher.md',
191
195
  'agents/apt-reviewer.md',
192
196
  'agents/apt-team-docs-narrator.md',
@@ -12,6 +12,7 @@
12
12
 
13
13
  import { existsSync, readFileSync } from 'node:fs'
14
14
  import { join } from 'node:path'
15
+ import { resolveMainRepoRoot } from '../util/state-paths.mjs'
15
16
  import { loadRoadmapForScope, saveRoadmapForScope } from './io.mjs'
16
17
  import { applyRollup } from './rollup.mjs'
17
18
  import { syncRoadmapInProcess } from './sync.mjs'
@@ -78,7 +79,9 @@ export function applyLifecyclePhaseSideEffect(targetDir, task, spec, reasonPrefi
78
79
  if (spec.to === 'completed') {
79
80
  let stateBlob = opts.stateBlob
80
81
  if (!stateBlob) {
81
- const statePath = join(targetDir, '.aperant', 'state.json')
82
+ // state-fork-fix: ephemeral sibling-count read resolves to the main
83
+ // board (the committable roadmapDir read above stays worktree-local).
84
+ const statePath = join(resolveMainRepoRoot(targetDir), '.aperant', 'state.json')
82
85
  if (existsSync(statePath)) {
83
86
  try {
84
87
  stateBlob = JSON.parse(readFileSync(statePath, 'utf-8'))
@@ -45,6 +45,15 @@ export function generateTaskId(description, config, now = new Date()) {
45
45
  return `${slug}_${date}`
46
46
  }
47
47
 
48
+ /**
49
+ * Build the `.aperant/tasks/<id>/` directory path.
50
+ *
51
+ * state-fork-fix: `tasks/` is EPHEMERAL, so callers pass the resolved main-repo
52
+ * state root (`resolveMainRepoRoot(projectDir)`) as `projectDir` — the redirect
53
+ * stays visible at each call site rather than being hidden inside this builder.
54
+ * `resolve()` is idempotent on an already-absolute root, so passing the resolved
55
+ * root is safe.
56
+ */
48
57
  export function getTaskDir(projectDir, taskId) {
49
58
  return join(resolve(projectDir), '.aperant', 'tasks', taskId)
50
59
  }
@@ -6,14 +6,21 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
6
6
  import { join } from 'node:path'
7
7
  import { loadMergedProjectConfig } from '../config/load.mjs'
8
8
  import { withFileLock } from '../util/fs-lock.mjs'
9
+ import { resolveMainRepoRoot } from '../util/state-paths.mjs'
9
10
  import { formatDateDisplay, formatTimeDisplay } from './format.mjs'
10
11
 
11
12
  /**
12
13
  * Regenerate .aperant/tasks/INDEX.md from state.json.
13
14
  * One line per task — lightweight project task management view.
15
+ *
16
+ * state-fork-fix: both the state.json READ and the tasks/INDEX.md WRITE are
17
+ * ephemeral, so they resolve to the MAIN repo from any worktree cwd. Config
18
+ * (loadMergedProjectConfig below) stays on the raw targetDir — config.json /
19
+ * config.local.json are committable + per-device, not the ephemeral board.
14
20
  */
15
21
  export function regenerateTaskIndex(targetDir) {
16
- const statePath = join(targetDir, '.aperant', 'state.json')
22
+ const stateRoot = resolveMainRepoRoot(targetDir)
23
+ const statePath = join(stateRoot, '.aperant', 'state.json')
17
24
  if (!existsSync(statePath)) return
18
25
 
19
26
  // AUDIT-001: read through the merged loader so per-device
@@ -90,14 +97,15 @@ export function regenerateTaskIndex(targetDir) {
90
97
 
91
98
  lines.push('', `*Auto-generated by apt-tools \u2014 ${new Date().toISOString()}*`, '')
92
99
 
93
- const indexPath = join(targetDir, '.aperant', 'tasks', 'INDEX.md')
94
- mkdirSync(join(targetDir, '.aperant', 'tasks'), { recursive: true })
100
+ const indexPath = join(stateRoot, '.aperant', 'tasks', 'INDEX.md')
101
+ mkdirSync(join(stateRoot, '.aperant', 'tasks'), { recursive: true })
95
102
  // Lock-protected — C28 R4. Concurrent regenerators must not interleave.
103
+ // Lock root moves with the file (state-fork-fix ID-06).
96
104
  withFileLock(
97
105
  indexPath,
98
106
  () => {
99
107
  writeFileSync(indexPath, lines.join('\n'), 'utf-8')
100
108
  },
101
- { projectRoot: targetDir },
109
+ { projectRoot: stateRoot },
102
110
  )
103
111
  }
@@ -5,7 +5,7 @@
5
5
  * match the originals byte-for-byte; the CLI snapshot harness enforces this.
6
6
  */
7
7
 
8
- import { readFileSync } from 'node:fs'
8
+ import { existsSync, readFileSync } from 'node:fs'
9
9
  import { dirname, resolve } from 'node:path'
10
10
  import { fileURLToPath } from 'node:url'
11
11
 
@@ -17,6 +17,20 @@ export { appendJsonl, atomicWriteJson, atomicWriteText } from './atomic-write.mj
17
17
  const __filename = fileURLToPath(import.meta.url)
18
18
  const __dirname = dirname(__filename)
19
19
 
20
+ /**
21
+ * Read + JSON-parse a file if it exists, returning null when the file is
22
+ * absent OR unparseable (malformed JSON is a non-fatal "no document" signal
23
+ * for the callers that use this — gates / followups CLI). Never throws.
24
+ */
25
+ export function readJsonIfExists(path) {
26
+ if (!existsSync(path)) return null
27
+ try {
28
+ return JSON.parse(readFileSync(path, 'utf-8'))
29
+ } catch {
30
+ return null
31
+ }
32
+ }
33
+
20
34
  /**
21
35
  * Read framework package.json and return a "<name> <version>" identity string.
22
36
  * Resolves relative to this module's location so the CLI keeps its own
@@ -9,7 +9,7 @@
9
9
 
10
10
  import { existsSync, mkdirSync } from 'node:fs'
11
11
  import { dirname, join, resolve } from 'node:path'
12
- import { detectWorktree } from '../git/worktree-detect.mjs'
12
+ import { resolveMainRepoRoot } from './state-paths.mjs'
13
13
 
14
14
  /**
15
15
  * Walk upward from the target path until we find a `.aperant/` directory.
@@ -28,19 +28,16 @@ export function findProjectRootFromPath(filePath) {
28
28
  }
29
29
 
30
30
  /**
31
- * Get the shared lock directory — uses git common dir so locks coordinate
32
- * across worktrees. Falls back to local .aperant/locks/ if not in git.
31
+ * Get the shared lock directory — resolves to the main repo's .aperant/locks/
32
+ * from any worktree so locks coordinate across worktrees. Falls back to the
33
+ * resolved local .aperant/locks/ if not in git.
34
+ *
35
+ * This is a SEPARATE policy wrapper over the shared resolveMainRepoRoot
36
+ * mechanism (state-fork-fix ID-03) — locks are short-lived and do NOT follow
37
+ * resolveStateDir's Phase-2 home relocation, so the two stay distinct.
33
38
  */
34
39
  export function getSharedLockDir(projectDir) {
35
- const wt = detectWorktree(projectDir)
36
- if (wt.is_worktree && wt.main_repo_root) {
37
- // Use main repo's .aperant/locks/ so all worktrees share the same locks
38
- const sharedDir = join(wt.main_repo_root, '.aperant', 'locks')
39
- mkdirSync(sharedDir, { recursive: true })
40
- return sharedDir
41
- }
42
- // Not a worktree — use local
43
- const localDir = join(resolve(projectDir), '.aperant', 'locks')
44
- mkdirSync(localDir, { recursive: true })
45
- return localDir
40
+ const sharedDir = join(resolveMainRepoRoot(projectDir), '.aperant', 'locks')
41
+ mkdirSync(sharedDir, { recursive: true })
42
+ return sharedDir
46
43
  }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * state-paths.mjs — the single home for "resolve the ephemeral .aperant/
3
+ * board to the main repo from any worktree" (state-fork-fix, Phase 1).
4
+ *
5
+ * `resolveMainRepoRoot` is the ONE low-level MECHANISM. It replaces the three
6
+ * hand-rolled copies of the `wt.is_worktree && wt.main_repo_root` redirect in
7
+ * `getSharedLockDir` / `commands/event.mjs` / `coordination/agent-identity.mjs`.
8
+ * It is null-guarded: `detectWorktree` returns `main_repo_root: null` AND
9
+ * `worktree_root: null` on a non-git dir (git/worktree-detect.mjs:46-53), so we
10
+ * NEVER feed `null` into `join()` and NEVER throw on a non-git input.
11
+ *
12
+ * `resolveStateDir` is the thin POLICY wrapper scoped to `state.json` + `tasks/`
13
+ * only — the only wrapper Phase 2 repoints (to an XDG home). It is NOT used to
14
+ * build any committable path (`roadmap/`, `features/`, `config.json`, ...).
15
+ */
16
+
17
+ import { join, resolve } from 'node:path'
18
+ import { detectWorktree } from '../git/worktree-detect.mjs'
19
+
20
+ /**
21
+ * Resolve the main repo root from any cwd shape.
22
+ * - worktree cwd → the main repo root (so all worktrees share one board)
23
+ * - git, non-worktree → the git toplevel
24
+ * - non-git dir → resolve(projectDir) (null-guarded fallback, never null)
25
+ *
26
+ * @param {string} projectDir
27
+ * @returns {string}
28
+ */
29
+ export function resolveMainRepoRoot(projectDir) {
30
+ const wt = detectWorktree(projectDir)
31
+ if (wt.is_worktree && wt.main_repo_root) return wt.main_repo_root
32
+ if (wt.worktree_root) return wt.worktree_root
33
+ return resolve(projectDir)
34
+ }
35
+
36
+ /**
37
+ * The `.aperant` directory holding the ephemeral board (`state.json` + `tasks/`),
38
+ * resolved to the main repo from any cwd. Phase 1 body; Phase 2 repoints this.
39
+ *
40
+ * @param {string} projectDir
41
+ * @returns {string}
42
+ */
43
+ export function resolveStateDir(projectDir) {
44
+ return join(resolveMainRepoRoot(projectDir), '.aperant')
45
+ }