@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.
- package/CHANGELOG.md +106 -0
- package/README.md +1 -1
- package/agents/apt-pr-review-crossplatform.md +189 -0
- package/agents/apt-pr-review-i18n.md +190 -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/init.d.mts.map +1 -1
- package/dist/cli/commands/init.mjs +30 -1
- package/dist/cli/commands/init.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 +984 -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 +5 -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-crossplatform.md +189 -0
- package/dist/plugin/agents/apt-pr-review-i18n.md +190 -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_crossplatform_agent.md +283 -0
- package/prompts/github/pr_i18n_agent.md +279 -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/init.mjs +29 -0
- 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 +1029 -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 +5 -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
|
@@ -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(
|
|
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(
|
|
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(
|
|
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
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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(
|
|
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:
|
|
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(
|
|
382
|
+
resolveAptToolsBin(anchorRoot),
|
|
264
383
|
'worktree-install',
|
|
265
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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
|