@aperant/framework 0.13.0 → 0.16.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 (245) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/README.md +1 -1
  3. package/agents/apt-pr-review-crossplatform.md +189 -0
  4. package/agents/apt-pr-review-i18n.md +190 -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/init.d.mts.map +1 -1
  26. package/dist/cli/commands/init.mjs +30 -1
  27. package/dist/cli/commands/init.mjs.map +1 -1
  28. package/dist/cli/commands/merge-detect.d.mts +29 -0
  29. package/dist/cli/commands/merge-detect.d.mts.map +1 -0
  30. package/dist/cli/commands/merge-detect.mjs +196 -0
  31. package/dist/cli/commands/merge-detect.mjs.map +1 -0
  32. package/dist/cli/commands/merge-integrate.d.mts +28 -0
  33. package/dist/cli/commands/merge-integrate.d.mts.map +1 -0
  34. package/dist/cli/commands/merge-integrate.mjs +383 -0
  35. package/dist/cli/commands/merge-integrate.mjs.map +1 -0
  36. package/dist/cli/commands/pr-review/detect-specialists.d.mts +58 -0
  37. package/dist/cli/commands/pr-review/detect-specialists.d.mts.map +1 -0
  38. package/dist/cli/commands/pr-review/detect-specialists.mjs +984 -0
  39. package/dist/cli/commands/pr-review/detect-specialists.mjs.map +1 -0
  40. package/dist/cli/commands/pr-review.d.mts.map +1 -1
  41. package/dist/cli/commands/pr-review.mjs +11 -2
  42. package/dist/cli/commands/pr-review.mjs.map +1 -1
  43. package/dist/cli/commands/precompact-hook.d.mts.map +1 -1
  44. package/dist/cli/commands/precompact-hook.mjs +10 -6
  45. package/dist/cli/commands/precompact-hook.mjs.map +1 -1
  46. package/dist/cli/commands/roadmap.d.mts.map +1 -1
  47. package/dist/cli/commands/roadmap.mjs +24 -11
  48. package/dist/cli/commands/roadmap.mjs.map +1 -1
  49. package/dist/cli/commands/seed.d.mts.map +1 -1
  50. package/dist/cli/commands/seed.mjs +3 -1
  51. package/dist/cli/commands/seed.mjs.map +1 -1
  52. package/dist/cli/commands/state.d.mts.map +1 -1
  53. package/dist/cli/commands/state.mjs +9 -3
  54. package/dist/cli/commands/state.mjs.map +1 -1
  55. package/dist/cli/commands/statusline.d.mts.map +1 -1
  56. package/dist/cli/commands/statusline.mjs +5 -1
  57. package/dist/cli/commands/statusline.mjs.map +1 -1
  58. package/dist/cli/commands/task.d.mts.map +1 -1
  59. package/dist/cli/commands/task.mjs +126 -23
  60. package/dist/cli/commands/task.mjs.map +1 -1
  61. package/dist/cli/commands/worktree.d.mts +1 -1
  62. package/dist/cli/commands/worktree.d.mts.map +1 -1
  63. package/dist/cli/commands/worktree.mjs +282 -83
  64. package/dist/cli/commands/worktree.mjs.map +1 -1
  65. package/dist/cli/config/post-merge-sweep.d.mts.map +1 -1
  66. package/dist/cli/config/post-merge-sweep.mjs +3 -2
  67. package/dist/cli/config/post-merge-sweep.mjs.map +1 -1
  68. package/dist/cli/config/share-policy.d.mts +3 -1
  69. package/dist/cli/config/share-policy.d.mts.map +1 -1
  70. package/dist/cli/config/share-policy.mjs +16 -3
  71. package/dist/cli/config/share-policy.mjs.map +1 -1
  72. package/dist/cli/config/upgrade-gitignore.d.mts.map +1 -1
  73. package/dist/cli/config/upgrade-gitignore.mjs +27 -5
  74. package/dist/cli/config/upgrade-gitignore.mjs.map +1 -1
  75. package/dist/cli/consistency/parse-qa.mjs +12 -1
  76. package/dist/cli/consistency/parse-qa.mjs.map +1 -1
  77. package/dist/cli/consistency/parse-review.d.mts +23 -0
  78. package/dist/cli/consistency/parse-review.d.mts.map +1 -1
  79. package/dist/cli/consistency/parse-review.mjs +36 -0
  80. package/dist/cli/consistency/parse-review.mjs.map +1 -1
  81. package/dist/cli/consistency/parse-spec.d.mts +16 -0
  82. package/dist/cli/consistency/parse-spec.d.mts.map +1 -1
  83. package/dist/cli/consistency/parse-spec.mjs +44 -0
  84. package/dist/cli/consistency/parse-spec.mjs.map +1 -1
  85. package/dist/cli/coordination/agent-identity.d.mts +5 -2
  86. package/dist/cli/coordination/agent-identity.d.mts.map +1 -1
  87. package/dist/cli/coordination/agent-identity.mjs +6 -4
  88. package/dist/cli/coordination/agent-identity.mjs.map +1 -1
  89. package/dist/cli/coordination/store.d.mts +1 -1
  90. package/dist/cli/dispatch.d.mts.map +1 -1
  91. package/dist/cli/dispatch.mjs +4 -0
  92. package/dist/cli/dispatch.mjs.map +1 -1
  93. package/dist/cli/followups/derive.d.mts +63 -0
  94. package/dist/cli/followups/derive.d.mts.map +1 -0
  95. package/dist/cli/followups/derive.mjs +115 -0
  96. package/dist/cli/followups/derive.mjs.map +1 -0
  97. package/dist/cli/followups/schema.d.mts +165 -0
  98. package/dist/cli/followups/schema.d.mts.map +1 -0
  99. package/dist/cli/followups/schema.mjs +251 -0
  100. package/dist/cli/followups/schema.mjs.map +1 -0
  101. package/dist/cli/gate/context.d.mts +1 -1
  102. package/dist/cli/gate/gates/followups-adjudicated.d.mts +69 -0
  103. package/dist/cli/gate/gates/followups-adjudicated.d.mts.map +1 -0
  104. package/dist/cli/gate/gates/followups-adjudicated.mjs +141 -0
  105. package/dist/cli/gate/gates/followups-adjudicated.mjs.map +1 -0
  106. package/dist/cli/gate/gates/index.mjs +1 -0
  107. package/dist/cli/gate/gates/index.mjs.map +1 -1
  108. package/dist/cli/git/materialize-env.d.mts +39 -0
  109. package/dist/cli/git/materialize-env.d.mts.map +1 -0
  110. package/dist/cli/git/materialize-env.mjs +217 -0
  111. package/dist/cli/git/materialize-env.mjs.map +1 -0
  112. package/dist/cli/help.d.mts.map +1 -1
  113. package/dist/cli/help.mjs +9 -1
  114. package/dist/cli/help.mjs.map +1 -1
  115. package/dist/cli/install/legacy-paths.d.mts.map +1 -1
  116. package/dist/cli/install/legacy-paths.mjs +5 -0
  117. package/dist/cli/install/legacy-paths.mjs.map +1 -1
  118. package/dist/cli/roadmap/lifecycle.d.mts.map +1 -1
  119. package/dist/cli/roadmap/lifecycle.mjs +4 -1
  120. package/dist/cli/roadmap/lifecycle.mjs.map +1 -1
  121. package/dist/cli/task/ids.d.mts +9 -0
  122. package/dist/cli/task/ids.d.mts.map +1 -1
  123. package/dist/cli/task/ids.mjs +9 -0
  124. package/dist/cli/task/ids.mjs.map +1 -1
  125. package/dist/cli/task/index-md.d.mts +5 -0
  126. package/dist/cli/task/index-md.d.mts.map +1 -1
  127. package/dist/cli/task/index-md.mjs +12 -4
  128. package/dist/cli/task/index-md.mjs.map +1 -1
  129. package/dist/cli/util/io.d.mts +6 -0
  130. package/dist/cli/util/io.d.mts.map +1 -1
  131. package/dist/cli/util/io.mjs +16 -1
  132. package/dist/cli/util/io.mjs.map +1 -1
  133. package/dist/cli/util/project.d.mts +7 -2
  134. package/dist/cli/util/project.d.mts.map +1 -1
  135. package/dist/cli/util/project.mjs +11 -14
  136. package/dist/cli/util/project.mjs.map +1 -1
  137. package/dist/cli/util/state-paths.d.mts +19 -0
  138. package/dist/cli/util/state-paths.d.mts.map +1 -0
  139. package/dist/cli/util/state-paths.mjs +45 -0
  140. package/dist/cli/util/state-paths.mjs.map +1 -0
  141. package/dist/plugin/.claude-plugin/plugin.json +4 -1
  142. package/dist/plugin/agents/apt-pr-review-crossplatform.md +189 -0
  143. package/dist/plugin/agents/apt-pr-review-i18n.md +190 -0
  144. package/dist/plugin/skills/apt/SKILL.md +9 -3
  145. package/dist/plugin/skills/apt-close-task/SKILL.md +48 -0
  146. package/dist/plugin/skills/apt-debug/SKILL.md +19 -9
  147. package/dist/plugin/skills/apt-execute/SKILL.md +3 -1
  148. package/dist/plugin/skills/apt-fan-out/SKILL.md +65 -1
  149. package/dist/plugin/skills/apt-merge-conflict-resolution/SKILL.md +227 -0
  150. package/dist/plugin/skills/apt-pr-review/SCHEMA.md +4 -2
  151. package/dist/plugin/skills/apt-pr-review/SKILL.md +117 -29
  152. package/dist/plugin/skills/apt-quick/SKILL.md +24 -1
  153. package/dist/plugin/skills/apt-review/SKILL.md +6 -0
  154. package/dist/plugin/skills/apt-run/SKILL.md +1 -1
  155. package/dist/plugin/skills/apt-setup/SKILL.md +21 -3
  156. package/dist/plugin/skills/apt-ship/SKILL.md +32 -11
  157. package/dist/plugin/skills/apt-terminal/SKILL.md +8 -0
  158. package/dist/plugin/skills/apt-triage/SKILL.md +4 -0
  159. package/dist/plugin/skills/apt-verify/SKILL.md +24 -0
  160. package/dist/plugin/skills/apt-watch-ci/SKILL.md +3 -1
  161. package/dist/types/config.d.ts +33 -0
  162. package/dist/types/config.d.ts.map +1 -1
  163. package/package.json +1 -1
  164. package/prompts/conductor-framework-context.md +1 -1
  165. package/prompts/github/pr_crossplatform_agent.md +283 -0
  166. package/prompts/github/pr_i18n_agent.md +279 -0
  167. package/prompts/inbox-clarification.md +18 -11
  168. package/skills/apt/SKILL.md +9 -3
  169. package/skills/apt-close-task/SKILL.md +48 -0
  170. package/skills/apt-debug/SKILL.md +19 -9
  171. package/skills/apt-execute/SKILL.md +3 -1
  172. package/skills/apt-fan-out/SKILL.md +65 -1
  173. package/skills/apt-merge-conflict-resolution/SKILL.md +227 -0
  174. package/skills/apt-pr-review/SCHEMA.md +4 -2
  175. package/skills/apt-pr-review/SKILL.md +117 -29
  176. package/skills/apt-quick/SKILL.md +24 -1
  177. package/skills/apt-review/SKILL.md +6 -0
  178. package/skills/apt-run/SKILL.md +1 -1
  179. package/skills/apt-setup/SKILL.md +21 -3
  180. package/skills/apt-ship/SKILL.md +32 -11
  181. package/skills/apt-terminal/SKILL.md +8 -0
  182. package/skills/apt-triage/SKILL.md +4 -0
  183. package/skills/apt-verify/SKILL.md +24 -0
  184. package/skills/apt-watch-ci/SKILL.md +3 -1
  185. package/src/cli/commands/audit-branch-current.mjs +3 -3
  186. package/src/cli/commands/event.mjs +6 -4
  187. package/src/cli/commands/fan-out.mjs +3 -1
  188. package/src/cli/commands/features-audit.mjs +6 -1
  189. package/src/cli/commands/followups.mjs +303 -0
  190. package/src/cli/commands/health-check.mjs +3 -1
  191. package/src/cli/commands/init.mjs +29 -0
  192. package/src/cli/commands/merge-detect.mjs +203 -0
  193. package/src/cli/commands/merge-integrate.mjs +392 -0
  194. package/src/cli/commands/pr-review/__fixtures__/android-strings/app/src/main/res/values-fr/strings.xml +3 -0
  195. package/src/cli/commands/pr-review/__fixtures__/babel-js/package.json +4 -0
  196. package/src/cli/commands/pr-review/__fixtures__/c-ifdef/src/main.c +4 -0
  197. package/src/cli/commands/pr-review/__fixtures__/ci-matrix/.github/workflows/ci.yml +10 -0
  198. package/src/cli/commands/pr-review/__fixtures__/cmake-win/CMakeLists.txt +5 -0
  199. package/src/cli/commands/pr-review/__fixtures__/diffs/add-arb.patch +9 -0
  200. package/src/cli/commands/pr-review/__fixtures__/diffs/ci-single-os-dep.patch +13 -0
  201. package/src/cli/commands/pr-review/__fixtures__/diffs/ci-single-os-list-item-dep.patch +17 -0
  202. package/src/cli/commands/pr-review/__fixtures__/diffs/lone-weak-t.patch +8 -0
  203. package/src/cli/commands/pr-review/__fixtures__/diffs/monorepo-b.patch +7 -0
  204. package/src/cli/commands/pr-review/__fixtures__/diffs/two-weak-xpl.patch +12 -0
  205. package/src/cli/commands/pr-review/__fixtures__/dropped-underscore/src/app.js +2 -0
  206. package/src/cli/commands/pr-review/__fixtures__/electron-app/package.json +4 -0
  207. package/src/cli/commands/pr-review/__fixtures__/i18n-locale-file/src/locales/fr.json +1 -0
  208. package/src/cli/commands/pr-review/__fixtures__/monorepo/packages/a/locales/fr.json +1 -0
  209. package/src/cli/commands/pr-review/__fixtures__/monorepo/packages/b/src/index.ts +1 -0
  210. package/src/cli/commands/pr-review/__fixtures__/py-babel/pyproject.toml +3 -0
  211. package/src/cli/commands/pr-review/__fixtures__/qt-i18n-dir/i18n/app_fr.ts +11 -0
  212. package/src/cli/commands/pr-review/__fixtures__/rust-cli/Cargo.toml +7 -0
  213. package/src/cli/commands/pr-review/__fixtures__/rust-cli/src/main.rs +1 -0
  214. package/src/cli/commands/pr-review/__fixtures__/ts-dir/ts/foo.ts +3 -0
  215. package/src/cli/commands/pr-review/detect-specialists.mjs +1029 -0
  216. package/src/cli/commands/pr-review.mjs +12 -2
  217. package/src/cli/commands/precompact-hook.mjs +10 -6
  218. package/src/cli/commands/roadmap.mjs +24 -11
  219. package/src/cli/commands/seed.mjs +3 -1
  220. package/src/cli/commands/state.mjs +9 -3
  221. package/src/cli/commands/statusline.mjs +5 -1
  222. package/src/cli/commands/task.mjs +135 -23
  223. package/src/cli/commands/worktree.mjs +298 -81
  224. package/src/cli/config/post-merge-sweep.mjs +3 -2
  225. package/src/cli/config/share-policy.mjs +16 -3
  226. package/src/cli/config/upgrade-gitignore.mjs +27 -5
  227. package/src/cli/consistency/parse-qa.mjs +11 -1
  228. package/src/cli/consistency/parse-review.mjs +36 -0
  229. package/src/cli/consistency/parse-spec.mjs +43 -0
  230. package/src/cli/coordination/agent-identity.mjs +6 -4
  231. package/src/cli/dispatch.mjs +4 -0
  232. package/src/cli/followups/derive.mjs +123 -0
  233. package/src/cli/followups/schema.mjs +264 -0
  234. package/src/cli/gate/gates/followups-adjudicated.mjs +158 -0
  235. package/src/cli/gate/gates/index.mjs +1 -0
  236. package/src/cli/git/materialize-env.mjs +212 -0
  237. package/src/cli/help.mjs +9 -1
  238. package/src/cli/install/legacy-paths.mjs +5 -0
  239. package/src/cli/roadmap/lifecycle.mjs +4 -1
  240. package/src/cli/task/ids.mjs +9 -0
  241. package/src/cli/task/index-md.mjs +12 -4
  242. package/src/cli/util/io.mjs +15 -1
  243. package/src/cli/util/project.mjs +11 -14
  244. package/src/cli/util/state-paths.mjs +45 -0
  245. package/prompts/followup_planner.md +0 -399
@@ -10,12 +10,14 @@
10
10
  import { execFileSync, execSync, spawnSync } from 'node:child_process'
11
11
  import { existsSync, mkdirSync, readdirSync, readFileSync } from 'node:fs'
12
12
  import { join, resolve } from 'node:path'
13
- import { getTaskIsolationConfig } from '../config/load.mjs'
13
+ import { getTaskIsolationConfig, loadProjectConfig } from '../config/load.mjs'
14
+ import { materializeProjectEnvCli } from '../git/materialize-env.mjs'
14
15
  import { detectWorktree } from '../git/worktree-detect.mjs'
15
16
  import { parseFlags } from '../util/args.mjs'
16
17
  import { resolveAptToolsBin } from '../util/bin-path.mjs'
17
18
  import { getSharedLockDir } from '../util/project.mjs'
18
19
  import { err, ok, UsageError } from '../util/result.mjs'
20
+ import { resolveMainRepoRoot } from '../util/state-paths.mjs'
19
21
 
20
22
  /**
21
23
  * Sentinel error class for helper-level usage failures inside cmdWorktree.
@@ -28,6 +30,98 @@ class WorktreeUsageError extends UsageError {
28
30
  }
29
31
  }
30
32
 
33
+ /**
34
+ * Resolve the effective autonomy for a `worktree create` invocation (ID-09b).
35
+ * Layered fallback: a `--autonomy <n>` flag (forwarded by the task.mjs auto-hook)
36
+ * wins; else the project config's `autonomy.default`; else 1 (the conservative
37
+ * confirm-by-default level). A non-numeric / out-of-range flag is ignored.
38
+ * @param {Map<string, string>} flags
39
+ * @param {string} targetDir
40
+ * @returns {number}
41
+ */
42
+ function resolveCreateAutonomy(flags, targetDir) {
43
+ const raw = flags.get('autonomy')
44
+ if (raw !== undefined && raw !== null && raw !== '') {
45
+ const n = Number.parseInt(String(raw), 10)
46
+ if (Number.isFinite(n) && n >= 0 && n <= 3) return n
47
+ }
48
+ try {
49
+ const cfg = loadProjectConfig(targetDir)
50
+ const def = cfg?.autonomy?.default
51
+ if (Number.isFinite(def) && def >= 0 && def <= 3) return def
52
+ } catch {
53
+ /* config unreadable — fall through to the conservative default */
54
+ }
55
+ return 1
56
+ }
57
+
58
+ /**
59
+ * Resolve the current branch name (or 'HEAD' on detached/failure).
60
+ * @param {string} cwd
61
+ * @returns {string}
62
+ */
63
+ function currentBranchName(cwd) {
64
+ try {
65
+ return execSync('git rev-parse --abbrev-ref HEAD', { cwd, encoding: 'utf-8' }).trim() || 'HEAD'
66
+ } catch {
67
+ return 'HEAD'
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Main-detection chain — local refs only, no network (ID-02). Returns the
73
+ * detected default branch (e.g. `origin/main`, `main`, `master`) or null when
74
+ * the repo has no main/master ref anywhere. Extracted so the `current`-branch
75
+ * surfacing (ID-09b) can compare against the same detected default the
76
+ * `from_branch: main` path uses.
77
+ * @param {string} cwd
78
+ * @returns {string|null}
79
+ */
80
+ function detectDefaultBranch(cwd) {
81
+ const candidates = ['origin/HEAD', 'origin/main', 'origin/master', 'main', 'master']
82
+ for (const ref of candidates) {
83
+ try {
84
+ const out = execSync(`git rev-parse --verify ${ref}`, {
85
+ cwd,
86
+ encoding: 'utf-8',
87
+ stdio: ['ignore', 'pipe', 'pipe'],
88
+ }).trim()
89
+ if (out) {
90
+ // origin/HEAD is a symref — resolve it to e.g. `origin/main`
91
+ // so the base branch we hand to `git worktree add` is usable.
92
+ if (ref === 'origin/HEAD') {
93
+ try {
94
+ const sym = execSync('git symbolic-ref refs/remotes/origin/HEAD', {
95
+ cwd,
96
+ encoding: 'utf-8',
97
+ stdio: ['ignore', 'pipe', 'pipe'],
98
+ }).trim()
99
+ return sym.replace(/^refs\/remotes\//, '') || ref
100
+ } catch {
101
+ return ref
102
+ }
103
+ }
104
+ return ref
105
+ }
106
+ } catch {
107
+ // Ref doesn't exist — try the next one.
108
+ }
109
+ }
110
+ return null
111
+ }
112
+
113
+ /**
114
+ * Normalize a detected default branch to its bare branch name for comparison
115
+ * with the current branch (e.g. `origin/main` → `main`). The current branch is
116
+ * always a local bare name, so strip a single leading remote segment.
117
+ * @param {string|null} ref
118
+ * @returns {string|null}
119
+ */
120
+ function bareBranchName(ref) {
121
+ if (!ref) return null
122
+ return ref.replace(/^origin\//, '')
123
+ }
124
+
31
125
  export function cmdDetectWorktree(projectDir) {
32
126
  if (!projectDir) return err('Usage: apt-tools detect-worktree <project-dir>')
33
127
  const wt = detectWorktree(projectDir)
@@ -42,9 +136,9 @@ export function cmdDetectWorktree(projectDir) {
42
136
  })
43
137
  }
44
138
 
45
- export function cmdWorktree(subcommand, projectDir, extraArgs) {
139
+ export async function cmdWorktree(subcommand, projectDir, extraArgs) {
46
140
  if (!subcommand || !projectDir) {
47
- return err('Usage: apt-tools worktree <create|remove|list> <dir> [options]')
141
+ return err('Usage: apt-tools worktree <create|remove|list|sync-env> <dir> [options]')
48
142
  }
49
143
 
50
144
  const targetDir = resolve(projectDir)
@@ -56,13 +150,23 @@ export function cmdWorktree(subcommand, projectDir, extraArgs) {
56
150
  try {
57
151
  switch (subcommand) {
58
152
  case 'create':
59
- return doCreate(subcommand, targetDir, extraArgs, SAFE_ID_PATTERN, SAFE_BRANCH_PATTERN)
153
+ return await doCreate(
154
+ subcommand,
155
+ targetDir,
156
+ extraArgs,
157
+ SAFE_ID_PATTERN,
158
+ SAFE_BRANCH_PATTERN,
159
+ )
60
160
  case 'remove':
61
161
  return doRemove(subcommand, targetDir, extraArgs, SAFE_ID_PATTERN)
62
162
  case 'list':
63
163
  return doList(targetDir)
164
+ case 'sync-env':
165
+ return await doSyncEnv(targetDir, extraArgs, SAFE_ID_PATTERN)
64
166
  default:
65
- return err('Unknown worktree subcommand: ' + subcommand + '. Use create, remove, or list.')
167
+ return err(
168
+ 'Unknown worktree subcommand: ' + subcommand + '. Use create, remove, list, or sync-env.',
169
+ )
66
170
  }
67
171
  } catch (e) {
68
172
  if (e instanceof WorktreeUsageError) return err(e.message)
@@ -70,7 +174,7 @@ export function cmdWorktree(subcommand, projectDir, extraArgs) {
70
174
  }
71
175
  }
72
176
 
73
- function doCreate(_subcommand, targetDir, extraArgs, SAFE_ID_PATTERN, SAFE_BRANCH_PATTERN) {
177
+ async function doCreate(_subcommand, targetDir, extraArgs, SAFE_ID_PATTERN, SAFE_BRANCH_PATTERN) {
74
178
  const flags = parseFlags(extraArgs)
75
179
  const subtaskId = flags.get('subtask')
76
180
  const taskId = flags.get('task')
@@ -88,13 +192,21 @@ function doCreate(_subcommand, targetDir, extraArgs, SAFE_ID_PATTERN, SAFE_BRANC
88
192
  throw new WorktreeUsageError('Invalid task ID: must match [a-zA-Z0-9_-]. Got: ' + taskId)
89
193
  }
90
194
 
195
+ // state-fork-fix ID-09a: ANCHOR AT MAIN. Task-mode worktree creation can
196
+ // now run from inside a worktree (the sub-agent / dropped-cd model) — it
197
+ // retargets to the main repo instead of hard-erroring. The worktree path,
198
+ // the mkdirSync parent, the `git worktree add` cwd, AND the scope state-read
199
+ // ALL resolve off this one anchorRoot so they can never drift.
200
+ const anchorRoot = resolveMainRepoRoot(targetDir)
201
+
91
202
  // C28 — scope propagation into worktree path. If --scope is not
92
203
  // passed, look it up from state.json so external callers keep
93
- // working without knowing the scope.
204
+ // working without knowing the scope. Reads off anchorRoot (the main
205
+ // board), since an empty worktree board would mis-resolve the scope.
94
206
  let taskScope = flags.get('scope')
95
207
  if (!taskScope) {
96
208
  try {
97
- const sp = join(targetDir, '.aperant', 'state.json')
209
+ const sp = join(anchorRoot, '.aperant', 'state.json')
98
210
  if (existsSync(sp)) {
99
211
  const st = JSON.parse(readFileSync(sp, 'utf-8'))
100
212
  taskScope = st?.active_tasks?.[taskId]?.scope || 'project'
@@ -121,69 +233,59 @@ function doCreate(_subcommand, targetDir, extraArgs, SAFE_ID_PATTERN, SAFE_BRANC
121
233
  }
122
234
 
123
235
  // Resolve base branch: --from beats config beats default chain.
236
+ // state-fork-fix ID-09b: default to the repo DEFAULT branch and make a
237
+ // surprising non-default current base EXPLICIT. The discriminator is
238
+ // "did the user ask for `current` on THIS invocation (--from) vs. did it
239
+ // fall through from a `from_branch: current` config default?".
124
240
  const fromFlag = flags.get('from')
125
241
  const fromPref = fromFlag || isolation.from_branch || 'main'
242
+ // Resolve against the anchored MAIN repo (ID-09a) so the detected default
243
+ // + current branch are read from the same repo the worktree is added to.
126
244
  let baseBranch = fromPref
245
+ let baseBranchSource = null
246
+ let baseBranchNote = null
127
247
  if (fromPref === 'current' || fromPref === 'HEAD') {
128
- // Explicit opt-in to chaining onto the user's current branch (AC8).
129
- try {
130
- baseBranch =
131
- execSync('git rev-parse --abbrev-ref HEAD', {
132
- cwd: targetDir,
133
- encoding: 'utf-8',
134
- }).trim() || 'HEAD'
135
- } catch {
136
- baseBranch = 'HEAD'
137
- }
138
- } else if (fromPref === 'main') {
139
- // Main-detection chain — local refs only, no network (per ID-02).
140
- const candidates = ['origin/HEAD', 'origin/main', 'origin/master', 'main', 'master']
141
- let resolved = null
142
- for (const ref of candidates) {
143
- try {
144
- const out = execSync(`git rev-parse --verify ${ref}`, {
145
- cwd: targetDir,
146
- encoding: 'utf-8',
147
- stdio: ['ignore', 'pipe', 'pipe'],
148
- }).trim()
149
- if (out) {
150
- // origin/HEAD is a symref — resolve it to e.g. `origin/main`
151
- // so the base branch we hand to `git worktree add` is usable.
152
- if (ref === 'origin/HEAD') {
153
- try {
154
- const sym = execSync('git symbolic-ref refs/remotes/origin/HEAD', {
155
- cwd: targetDir,
156
- encoding: 'utf-8',
157
- stdio: ['ignore', 'pipe', 'pipe'],
158
- }).trim()
159
- resolved = sym.replace(/^refs\/remotes\//, '') || ref
160
- } catch {
161
- resolved = ref
162
- }
163
- } else {
164
- resolved = ref
165
- }
166
- break
167
- }
168
- } catch {
169
- // Ref doesn't exist — try the next one.
248
+ // `current` = the INVOKING context's branch (the worktree/cwd you are
249
+ // on), NOT the main repo's — that is the branch the user would
250
+ // silently stack onto. Worktrees share refs, so this branch is still
251
+ // resolvable from anchorRoot when `git worktree add` runs.
252
+ const cur = currentBranchName(targetDir)
253
+ baseBranch = cur
254
+ const detectedDefault = bareBranchName(detectDefaultBranch(anchorRoot))
255
+ const isNonDefault = !!detectedDefault && cur !== detectedDefault && cur !== 'HEAD'
256
+ // EXPLICIT --from current/HEAD is intentional stacking → silent (AC13).
257
+ // A from_branch:'current' CONFIG default resolving to a non-default
258
+ // branch is the SURPRISING case that surfaces autonomy-aware.
259
+ const explicitCurrent = !!fromFlag
260
+ if (isNonDefault && !explicitCurrent) {
261
+ const autonomy = resolveCreateAutonomy(flags, targetDir)
262
+ if (autonomy <= 1) {
263
+ // Fail closed asking (C50/0.6.1 requires_input non-TTY contract):
264
+ // do NOT create until the caller re-invokes with explicit --from.
265
+ return ok({
266
+ status: 'requires_input',
267
+ command: 'worktree-create',
268
+ reason: 'non_default_base',
269
+ task_id: taskId,
270
+ scope: taskScope,
271
+ base_branch: cur,
272
+ default_branch: detectedDefault,
273
+ prompt: `base = ${cur} — fork from ${detectedDefault} instead? [${detectedDefault}/current]`,
274
+ })
170
275
  }
276
+ // autonomy 2-3 — proceed, but LOG the chosen non-default base.
277
+ baseBranchSource = 'current-non-default'
278
+ baseBranchNote = `base branch '${cur}' is not the repo default '${detectedDefault}' (from_branch: current config default) — proceeding at autonomy ${autonomy}`
171
279
  }
280
+ } else if (fromPref === 'main') {
281
+ const resolved = detectDefaultBranch(anchorRoot)
172
282
  if (resolved) {
173
283
  baseBranch = resolved
174
284
  } else {
175
285
  // Degenerate repo (no main/master ref anywhere). Fall back to
176
286
  // current HEAD so users in detached or greenfield repos still
177
287
  // get a working worktree instead of a hard error.
178
- try {
179
- baseBranch =
180
- execSync('git rev-parse --abbrev-ref HEAD', {
181
- cwd: targetDir,
182
- encoding: 'utf-8',
183
- }).trim() || 'HEAD'
184
- } catch {
185
- baseBranch = 'HEAD'
186
- }
288
+ baseBranch = currentBranchName(anchorRoot)
187
289
  }
188
290
  }
189
291
  // Any other explicit fromPref value (e.g. `origin/main`, `feature/foo`,
@@ -194,17 +296,12 @@ function doCreate(_subcommand, targetDir, extraArgs, SAFE_ID_PATTERN, SAFE_BRANC
194
296
  )
195
297
  }
196
298
 
197
- // Nested-worktree guard same rule as subtask mode.
198
- const wt = detectWorktree(targetDir)
199
- if (wt.is_worktree) {
200
- throw new WorktreeUsageError(
201
- 'Cannot create worktree: already inside a worktree at ' +
202
- wt.worktree_root +
203
- '. Nested worktrees are not supported.',
204
- )
205
- }
206
-
207
- const worktreePath = join(targetDir, '.aperant', 'worktrees', taskScope, taskId)
299
+ // state-fork-fix ID-09a: NO nested-worktree hard-error in task mode. The
300
+ // task worktree is always created as a SIBLING under the MAIN repo's
301
+ // .aperant/worktrees/<scope>/<id> — invoking from inside a worktree (or a
302
+ // subdir) retargets to main instead of refusing. (The legacy subtask-mode
303
+ // path below keeps its nested-worktree refusal back-compat only.)
304
+ const worktreePath = join(anchorRoot, '.aperant', 'worktrees', taskScope, taskId)
208
305
  if (existsSync(worktreePath)) {
209
306
  throw new WorktreeUsageError(
210
307
  'Worktree already exists at ' +
@@ -214,11 +311,11 @@ function doCreate(_subcommand, targetDir, extraArgs, SAFE_ID_PATTERN, SAFE_BRANC
214
311
  )
215
312
  }
216
313
 
217
- mkdirSync(join(targetDir, '.aperant', 'worktrees', taskScope), { recursive: true })
314
+ mkdirSync(join(anchorRoot, '.aperant', 'worktrees', taskScope), { recursive: true })
218
315
 
219
316
  try {
220
317
  execFileSync('git', ['worktree', 'add', worktreePath, '-b', taskBranch, baseBranch], {
221
- cwd: targetDir,
318
+ cwd: anchorRoot,
222
319
  encoding: 'utf-8',
223
320
  stdio: 'pipe',
224
321
  })
@@ -226,6 +323,22 @@ function doCreate(_subcommand, targetDir, extraArgs, SAFE_ID_PATTERN, SAFE_BRANC
226
323
  throw new WorktreeUsageError('Failed to create worktree: ' + (e.stderr || e.message).trim())
227
324
  }
228
325
 
326
+ // Materialize the project's stored env blobs into the fresh worktree
327
+ // (#354) — best-effort, traversal-guarded inside the helper, never aborts
328
+ // worktree creation. Surfaced on the envelope as `env_materialized`.
329
+ let envMaterialized = null
330
+ try {
331
+ envMaterialized = await materializeProjectEnvCli(
332
+ worktreePath,
333
+ targetDir,
334
+ flags.get('project-id'),
335
+ )
336
+ } catch {
337
+ // Defense in depth: the helper already swallows errors, but if it ever
338
+ // rejects we still must not fail the create.
339
+ envMaterialized = null
340
+ }
341
+
229
342
  // Best-effort push: honor --push flag or config.push_on_create.
230
343
  // Never abort on failure — users may have no network / no remote.
231
344
  const shouldPush = pushOptIn || isolation.push_on_create === true
@@ -259,10 +372,16 @@ function doCreate(_subcommand, targetDir, extraArgs, SAFE_ID_PATTERN, SAFE_BRANC
259
372
  // resolution (env override → process.argv[1] → monorepo →
260
373
  // consumer-install → $PATH). This file no longer reads
261
374
  // process.argv[1] directly.
375
+ // state-fork-fix ID-09a: pass anchorRoot (NOT raw targetDir) as the
376
+ // project-dir. With the nested-worktree guard removed, doCreate can
377
+ // run from inside a worktree, and worktree-install writes EPHEMERAL
378
+ // tasks/<id>/install/ logs + a stubbed $HOME + a detached --finalize
379
+ // re-spawn off its project-dir — anchoring at main keeps those off the
380
+ // forked worktree board, consistent with the main-anchored worktreePath.
262
381
  const wtInstallArgs = [
263
- resolveAptToolsBin(targetDir),
382
+ resolveAptToolsBin(anchorRoot),
264
383
  'worktree-install',
265
- targetDir,
384
+ anchorRoot,
266
385
  '--task',
267
386
  taskId,
268
387
  '--worktree',
@@ -334,6 +453,11 @@ function doCreate(_subcommand, targetDir, extraArgs, SAFE_ID_PATTERN, SAFE_BRANC
334
453
  }
335
454
  if (installBlock) createOut.install = installBlock
336
455
  if (installWarning) createOut.install_warning = installWarning
456
+ if (envMaterialized) createOut.env_materialized = envMaterialized
457
+ // ID-09b: when proceeding with a non-default current base at autonomy 2-3,
458
+ // LOG the chosen base on the success envelope.
459
+ if (baseBranchSource) createOut.base_branch_source = baseBranchSource
460
+ if (baseBranchNote) createOut.base_branch_note = baseBranchNote
337
461
  return ok(createOut)
338
462
  }
339
463
 
@@ -414,10 +538,14 @@ function doRemove(_subcommand, targetDir, extraArgs, SAFE_ID_PATTERN) {
414
538
  // C28 — resolve scope for {scope}/{task-id} path. Accept --scope,
415
539
  // else read state.json, else fall back to scanning the worktree
416
540
  // dir (handles orphaned worktrees whose task record is gone).
541
+ // state-fork-fix: anchor at main, matching doCreate (ID-09a).
542
+ const anchorRoot = resolveMainRepoRoot(targetDir)
543
+
417
544
  let taskScope = flags.get('scope') || null
418
545
  if (!taskScope) {
419
546
  try {
420
- const sp = join(targetDir, '.aperant', 'state.json')
547
+ // state-fork-fix: ephemeral scope read resolves to the main board.
548
+ const sp = join(anchorRoot, '.aperant', 'state.json')
421
549
  if (existsSync(sp)) {
422
550
  const st = JSON.parse(readFileSync(sp, 'utf-8'))
423
551
  taskScope = st?.active_tasks?.[taskId]?.scope || null
@@ -429,7 +557,7 @@ function doRemove(_subcommand, targetDir, extraArgs, SAFE_ID_PATTERN) {
429
557
  if (!taskScope) {
430
558
  // Scan scope subdirs for a match.
431
559
  try {
432
- const root = join(targetDir, '.aperant', 'worktrees')
560
+ const root = join(anchorRoot, '.aperant', 'worktrees')
433
561
  if (existsSync(root)) {
434
562
  for (const entry of readdirSync(root, { withFileTypes: true })) {
435
563
  if (entry.isDirectory() && existsSync(join(root, entry.name, taskId))) {
@@ -443,12 +571,15 @@ function doRemove(_subcommand, targetDir, extraArgs, SAFE_ID_PATTERN) {
443
571
  }
444
572
  }
445
573
  taskScope = taskScope || 'project'
574
+ if (!SAFE_ID_PATTERN.test(taskScope)) {
575
+ throw new WorktreeUsageError('Invalid scope: must match [a-zA-Z0-9_-]. Got: ' + taskScope)
576
+ }
446
577
 
447
- const worktreePath = join(targetDir, '.aperant', 'worktrees', taskScope, taskId)
578
+ const worktreePath = join(anchorRoot, '.aperant', 'worktrees', taskScope, taskId)
448
579
  const args = ['worktree', 'remove', worktreePath]
449
580
  if (force) args.push('--force')
450
581
  try {
451
- execFileSync('git', args, { cwd: targetDir, encoding: 'utf-8', stdio: 'pipe' })
582
+ execFileSync('git', args, { cwd: anchorRoot, encoding: 'utf-8', stdio: 'pipe' })
452
583
  } catch (e) {
453
584
  throw new WorktreeUsageError('Failed to remove worktree: ' + (e.stderr || e.message).trim())
454
585
  }
@@ -458,7 +589,7 @@ function doRemove(_subcommand, targetDir, extraArgs, SAFE_ID_PATTERN) {
458
589
  const taskBranch = `${isolation.branch_prefix || 'apt'}/${taskId}`
459
590
  try {
460
591
  execFileSync('git', ['branch', '-D', taskBranch], {
461
- cwd: targetDir,
592
+ cwd: anchorRoot,
462
593
  encoding: 'utf-8',
463
594
  stdio: 'pipe',
464
595
  })
@@ -507,10 +638,12 @@ function doRemove(_subcommand, targetDir, extraArgs, SAFE_ID_PATTERN) {
507
638
  }
508
639
 
509
640
  function doList(targetDir) {
641
+ // state-fork-fix: anchor at main so the prefix and git output share the same root.
642
+ const anchorRoot = resolveMainRepoRoot(targetDir)
510
643
  let rawOutput
511
644
  try {
512
645
  rawOutput = execSync('git worktree list --porcelain', {
513
- cwd: targetDir,
646
+ cwd: anchorRoot,
514
647
  encoding: 'utf-8',
515
648
  stdio: 'pipe',
516
649
  })
@@ -520,7 +653,7 @@ function doList(targetDir) {
520
653
 
521
654
  // Parse porcelain output — each worktree block separated by blank line
522
655
  const worktrees = []
523
- const aperantPrefix = join(targetDir, '.aperant', 'worktrees')
656
+ const aperantPrefix = join(anchorRoot, '.aperant', 'worktrees')
524
657
  const blocks = rawOutput.trim().split('\n\n')
525
658
  for (const block of blocks) {
526
659
  const lines = block.trim().split('\n')
@@ -562,3 +695,87 @@ function doList(targetDir) {
562
695
  worktrees: worktrees,
563
696
  })
564
697
  }
698
+
699
+ /**
700
+ * `apt-tools worktree sync-env <dir> --task <id> [--scope <scope>] [--project-id <id>]`
701
+ *
702
+ * Re-materialize the project's stored env blobs into an existing task worktree
703
+ * (ID-07). The store is the source of truth — sync-env OVERWRITES the worktree
704
+ * files; there is no two-way merge. Used for long-lived worktrees that drifted
705
+ * after the source env changed in the store.
706
+ */
707
+ async function doSyncEnv(targetDir, extraArgs, SAFE_ID_PATTERN) {
708
+ const flags = parseFlags(extraArgs)
709
+ const taskId = flags.get('task')
710
+ if (!taskId) {
711
+ throw new WorktreeUsageError(
712
+ 'Usage: apt-tools worktree sync-env <dir> --task <id> [--scope <scope>] [--project-id <id>]',
713
+ )
714
+ }
715
+ if (!SAFE_ID_PATTERN.test(taskId)) {
716
+ throw new WorktreeUsageError('Invalid task ID: must match [a-zA-Z0-9_-]. Got: ' + taskId)
717
+ }
718
+
719
+ // state-fork-fix: anchor at main, matching doCreate (ID-09a).
720
+ const anchorRoot = resolveMainRepoRoot(targetDir)
721
+
722
+ // Resolve scope the same way doRemove does: --scope, else state.json, else
723
+ // scan the worktrees dir for a matching task subdir.
724
+ let taskScope = flags.get('scope') || null
725
+ if (!taskScope) {
726
+ try {
727
+ // state-fork-fix: ephemeral scope read resolves to the main board.
728
+ const sp = join(anchorRoot, '.aperant', 'state.json')
729
+ if (existsSync(sp)) {
730
+ const st = JSON.parse(readFileSync(sp, 'utf-8'))
731
+ taskScope = st?.active_tasks?.[taskId]?.scope || null
732
+ }
733
+ } catch {
734
+ /* ignore */
735
+ }
736
+ }
737
+ if (!taskScope) {
738
+ try {
739
+ const root = join(anchorRoot, '.aperant', 'worktrees')
740
+ if (existsSync(root)) {
741
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
742
+ if (entry.isDirectory() && existsSync(join(root, entry.name, taskId))) {
743
+ taskScope = entry.name
744
+ break
745
+ }
746
+ }
747
+ }
748
+ } catch {
749
+ /* ignore */
750
+ }
751
+ }
752
+ taskScope = (taskScope || 'project').trim() || 'project'
753
+ // Validate scope BEFORE it is joined into the worktree path (mirrors
754
+ // doCreate). An unvalidated scope (e.g. `--scope ../../etc`) would let the
755
+ // resolved worktree root escape `.aperant/worktrees/`, and sync-env writes
756
+ // secret env blobs into that root — so reject anything outside [a-zA-Z0-9_-].
757
+ if (!SAFE_ID_PATTERN.test(taskScope)) {
758
+ throw new WorktreeUsageError('Invalid scope: must match [a-zA-Z0-9_-]. Got: ' + taskScope)
759
+ }
760
+
761
+ const worktreePath = join(anchorRoot, '.aperant', 'worktrees', taskScope, taskId)
762
+ if (!existsSync(worktreePath)) {
763
+ throw new WorktreeUsageError(
764
+ 'Worktree not found at ' +
765
+ worktreePath +
766
+ '. Create it first with: apt-tools worktree create <dir> --task ' +
767
+ taskId,
768
+ )
769
+ }
770
+
771
+ const result = await materializeProjectEnvCli(worktreePath, targetDir, flags.get('project-id'))
772
+
773
+ return ok({
774
+ status: 'ok',
775
+ command: 'worktree-sync-env',
776
+ task_id: taskId,
777
+ scope: taskScope,
778
+ worktree_path: worktreePath,
779
+ env_materialized: result,
780
+ })
781
+ }
@@ -37,6 +37,7 @@ import { spawn } from 'node:child_process'
37
37
  import { closeSync, existsSync, openSync, readFileSync, statSync, writeSync } from 'node:fs'
38
38
  import { dirname, join, resolve } from 'node:path'
39
39
  import { fileURLToPath } from 'node:url'
40
+ import { resolveMainRepoRoot } from '../util/state-paths.mjs'
40
41
 
41
42
  /**
42
43
  * Hot-path commands whose stderr feeds back into LLM-facing routers /
@@ -82,7 +83,7 @@ function getCooldownMs() {
82
83
  * @internal
83
84
  */
84
85
  export function readShippedCandidates(projectRoot) {
85
- const statePath = join(projectRoot, '.aperant', 'state.json')
86
+ const statePath = join(resolveMainRepoRoot(projectRoot), '.aperant', 'state.json')
86
87
  if (!existsSync(statePath)) return null
87
88
  let state
88
89
  try {
@@ -110,7 +111,7 @@ export function readShippedCandidates(projectRoot) {
110
111
  * @internal
111
112
  */
112
113
  export function readPendingNarrationCount(projectRoot) {
113
- const statePath = join(projectRoot, '.aperant', 'state.json')
114
+ const statePath = join(resolveMainRepoRoot(projectRoot), '.aperant', 'state.json')
114
115
  if (!existsSync(statePath)) return 0
115
116
  try {
116
117
  const state = JSON.parse(readFileSync(statePath, 'utf-8'))
@@ -23,7 +23,9 @@
23
23
  * visibility: 'solo' | 'team' | 'oss', // preset (unused after resolution
24
24
  * // except for diagnostics)
25
25
  * roadmap: boolean, // .aperant/roadmap/** *.json
26
- * tasks: boolean, // .aperant/tasks/ *\/ **
26
+ * tasks: boolean, // .aperant/tasks/ *\/ ** AND
27
+ * // .aperant/quick/ *\/ ** (quick is
28
+ * // a task track, shares this toggle)
27
29
  * team: { roster: boolean }, // .aperant/team.json
28
30
  * decisions: boolean, // .aperant/decisions/ *.md
29
31
  * digests: boolean // .aperant/digests/ *.md (resolved
@@ -263,8 +265,11 @@ export function buildPolicyIgnoreMatcher(policy) {
263
265
  return { ignored: true, class: 'ephemeral', reason: 'ephemeral' }
264
266
  }
265
267
 
266
- // Derived — always ignored (two-way write = drift).
267
- if (rest === 'tasks/INDEX.md' || rest === 'decisions/INDEX.md') {
268
+ // Derived — always ignored (two-way write = drift). `quick/INDEX.md` is
269
+ // the QUICK-task analogue of `tasks/INDEX.md`: a regenerated markdown
270
+ // index whose visibility follows share.tasks via the per-record rule
271
+ // below, so the index itself is never tracked.
272
+ if (rest === 'tasks/INDEX.md' || rest === 'decisions/INDEX.md' || rest === 'quick/INDEX.md') {
268
273
  return { ignored: true, class: 'derived', reason: 'derived' }
269
274
  }
270
275
  if (/^roadmap\/.*ROADMAP\.md$/.test(rest)) {
@@ -311,6 +316,14 @@ export function buildPolicyIgnoreMatcher(policy) {
311
316
  ? { ignored: false, class: 'shared-source', reason: null }
312
317
  : { ignored: true, class: 'shared-source', reason: 'share.tasks=false' }
313
318
  }
319
+ // QUICK-task records (quick/<task-id>/TASK.md, task.json, evidence/**)
320
+ // follow share.tasks — quick is a task track, not a separate share class.
321
+ // `quick/INDEX.md` is handled above as derived.
322
+ if (/^quick\/[^/]+\/.+/.test(rest)) {
323
+ return policy.tasks
324
+ ? { ignored: false, class: 'shared-source', reason: null }
325
+ : { ignored: true, class: 'shared-source', reason: 'share.tasks=false' }
326
+ }
314
327
 
315
328
  // Everything else under .aperant/ falls through to ignored (deny by
316
329
  // default) — matches the template's leading `*` rule. Classed as