@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.
- package/CHANGELOG.md +89 -3
- package/README.md +11 -1
- package/agents/apt-pr-review-i18n.md +190 -0
- package/agents/apt-pr-review-xplatform.md +189 -0
- package/dist/cli/commands/audit-branch-current.d.mts +27 -5
- package/dist/cli/commands/audit-branch-current.d.mts.map +1 -1
- package/dist/cli/commands/audit-branch-current.mjs +3 -3
- package/dist/cli/commands/audit-branch-current.mjs.map +1 -1
- package/dist/cli/commands/event.d.mts.map +1 -1
- package/dist/cli/commands/event.mjs +6 -4
- package/dist/cli/commands/event.mjs.map +1 -1
- package/dist/cli/commands/fan-out.d.mts.map +1 -1
- package/dist/cli/commands/fan-out.mjs +3 -1
- package/dist/cli/commands/fan-out.mjs.map +1 -1
- package/dist/cli/commands/features-audit.d.mts.map +1 -1
- package/dist/cli/commands/features-audit.mjs +6 -1
- package/dist/cli/commands/features-audit.mjs.map +1 -1
- package/dist/cli/commands/followups.d.mts +5 -0
- package/dist/cli/commands/followups.d.mts.map +1 -0
- package/dist/cli/commands/followups.mjs +266 -0
- package/dist/cli/commands/followups.mjs.map +1 -0
- package/dist/cli/commands/health-check.d.mts.map +1 -1
- package/dist/cli/commands/health-check.mjs +3 -1
- package/dist/cli/commands/health-check.mjs.map +1 -1
- package/dist/cli/commands/merge-detect.d.mts +29 -0
- package/dist/cli/commands/merge-detect.d.mts.map +1 -0
- package/dist/cli/commands/merge-detect.mjs +196 -0
- package/dist/cli/commands/merge-detect.mjs.map +1 -0
- package/dist/cli/commands/merge-integrate.d.mts +28 -0
- package/dist/cli/commands/merge-integrate.d.mts.map +1 -0
- package/dist/cli/commands/merge-integrate.mjs +383 -0
- package/dist/cli/commands/merge-integrate.mjs.map +1 -0
- package/dist/cli/commands/pr-review/detect-specialists.d.mts +58 -0
- package/dist/cli/commands/pr-review/detect-specialists.d.mts.map +1 -0
- package/dist/cli/commands/pr-review/detect-specialists.mjs +980 -0
- package/dist/cli/commands/pr-review/detect-specialists.mjs.map +1 -0
- package/dist/cli/commands/pr-review.d.mts.map +1 -1
- package/dist/cli/commands/pr-review.mjs +11 -2
- package/dist/cli/commands/pr-review.mjs.map +1 -1
- package/dist/cli/commands/precompact-hook.d.mts.map +1 -1
- package/dist/cli/commands/precompact-hook.mjs +10 -6
- package/dist/cli/commands/precompact-hook.mjs.map +1 -1
- package/dist/cli/commands/roadmap.d.mts.map +1 -1
- package/dist/cli/commands/roadmap.mjs +24 -11
- package/dist/cli/commands/roadmap.mjs.map +1 -1
- package/dist/cli/commands/seed.d.mts.map +1 -1
- package/dist/cli/commands/seed.mjs +3 -1
- package/dist/cli/commands/seed.mjs.map +1 -1
- package/dist/cli/commands/state.d.mts.map +1 -1
- package/dist/cli/commands/state.mjs +9 -3
- package/dist/cli/commands/state.mjs.map +1 -1
- package/dist/cli/commands/statusline.d.mts.map +1 -1
- package/dist/cli/commands/statusline.mjs +5 -1
- package/dist/cli/commands/statusline.mjs.map +1 -1
- package/dist/cli/commands/task.d.mts.map +1 -1
- package/dist/cli/commands/task.mjs +126 -23
- package/dist/cli/commands/task.mjs.map +1 -1
- package/dist/cli/commands/worktree.d.mts +1 -1
- package/dist/cli/commands/worktree.d.mts.map +1 -1
- package/dist/cli/commands/worktree.mjs +282 -83
- package/dist/cli/commands/worktree.mjs.map +1 -1
- package/dist/cli/config/post-merge-sweep.d.mts.map +1 -1
- package/dist/cli/config/post-merge-sweep.mjs +3 -2
- package/dist/cli/config/post-merge-sweep.mjs.map +1 -1
- package/dist/cli/config/share-policy.d.mts +3 -1
- package/dist/cli/config/share-policy.d.mts.map +1 -1
- package/dist/cli/config/share-policy.mjs +16 -3
- package/dist/cli/config/share-policy.mjs.map +1 -1
- package/dist/cli/config/upgrade-gitignore.d.mts.map +1 -1
- package/dist/cli/config/upgrade-gitignore.mjs +27 -5
- package/dist/cli/config/upgrade-gitignore.mjs.map +1 -1
- package/dist/cli/consistency/parse-qa.mjs +12 -1
- package/dist/cli/consistency/parse-qa.mjs.map +1 -1
- package/dist/cli/consistency/parse-review.d.mts +23 -0
- package/dist/cli/consistency/parse-review.d.mts.map +1 -1
- package/dist/cli/consistency/parse-review.mjs +36 -0
- package/dist/cli/consistency/parse-review.mjs.map +1 -1
- package/dist/cli/consistency/parse-spec.d.mts +16 -0
- package/dist/cli/consistency/parse-spec.d.mts.map +1 -1
- package/dist/cli/consistency/parse-spec.mjs +44 -0
- package/dist/cli/consistency/parse-spec.mjs.map +1 -1
- package/dist/cli/coordination/agent-identity.d.mts +5 -2
- package/dist/cli/coordination/agent-identity.d.mts.map +1 -1
- package/dist/cli/coordination/agent-identity.mjs +6 -4
- package/dist/cli/coordination/agent-identity.mjs.map +1 -1
- package/dist/cli/coordination/store.d.mts +1 -1
- package/dist/cli/dispatch.d.mts.map +1 -1
- package/dist/cli/dispatch.mjs +4 -0
- package/dist/cli/dispatch.mjs.map +1 -1
- package/dist/cli/followups/derive.d.mts +63 -0
- package/dist/cli/followups/derive.d.mts.map +1 -0
- package/dist/cli/followups/derive.mjs +115 -0
- package/dist/cli/followups/derive.mjs.map +1 -0
- package/dist/cli/followups/schema.d.mts +165 -0
- package/dist/cli/followups/schema.d.mts.map +1 -0
- package/dist/cli/followups/schema.mjs +251 -0
- package/dist/cli/followups/schema.mjs.map +1 -0
- package/dist/cli/gate/context.d.mts +1 -1
- package/dist/cli/gate/gates/followups-adjudicated.d.mts +69 -0
- package/dist/cli/gate/gates/followups-adjudicated.d.mts.map +1 -0
- package/dist/cli/gate/gates/followups-adjudicated.mjs +141 -0
- package/dist/cli/gate/gates/followups-adjudicated.mjs.map +1 -0
- package/dist/cli/gate/gates/index.mjs +1 -0
- package/dist/cli/gate/gates/index.mjs.map +1 -1
- package/dist/cli/git/materialize-env.d.mts +39 -0
- package/dist/cli/git/materialize-env.d.mts.map +1 -0
- package/dist/cli/git/materialize-env.mjs +217 -0
- package/dist/cli/git/materialize-env.mjs.map +1 -0
- package/dist/cli/help.d.mts.map +1 -1
- package/dist/cli/help.mjs +9 -1
- package/dist/cli/help.mjs.map +1 -1
- package/dist/cli/install/legacy-paths.d.mts.map +1 -1
- package/dist/cli/install/legacy-paths.mjs +4 -0
- package/dist/cli/install/legacy-paths.mjs.map +1 -1
- package/dist/cli/roadmap/lifecycle.d.mts.map +1 -1
- package/dist/cli/roadmap/lifecycle.mjs +4 -1
- package/dist/cli/roadmap/lifecycle.mjs.map +1 -1
- package/dist/cli/task/ids.d.mts +9 -0
- package/dist/cli/task/ids.d.mts.map +1 -1
- package/dist/cli/task/ids.mjs +9 -0
- package/dist/cli/task/ids.mjs.map +1 -1
- package/dist/cli/task/index-md.d.mts +5 -0
- package/dist/cli/task/index-md.d.mts.map +1 -1
- package/dist/cli/task/index-md.mjs +12 -4
- package/dist/cli/task/index-md.mjs.map +1 -1
- package/dist/cli/util/io.d.mts +6 -0
- package/dist/cli/util/io.d.mts.map +1 -1
- package/dist/cli/util/io.mjs +16 -1
- package/dist/cli/util/io.mjs.map +1 -1
- package/dist/cli/util/project.d.mts +7 -2
- package/dist/cli/util/project.d.mts.map +1 -1
- package/dist/cli/util/project.mjs +11 -14
- package/dist/cli/util/project.mjs.map +1 -1
- package/dist/cli/util/state-paths.d.mts +19 -0
- package/dist/cli/util/state-paths.d.mts.map +1 -0
- package/dist/cli/util/state-paths.mjs +45 -0
- package/dist/cli/util/state-paths.mjs.map +1 -0
- package/dist/plugin/.claude-plugin/plugin.json +4 -1
- package/dist/plugin/agents/apt-pr-review-i18n.md +190 -0
- package/dist/plugin/agents/apt-pr-review-xplatform.md +189 -0
- package/dist/plugin/skills/apt/SKILL.md +13 -6
- 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 +138 -138
- package/prompts/conductor-framework-context.md +1 -1
- package/prompts/github/pr_i18n_agent.md +279 -0
- package/prompts/github/pr_xplatform_agent.md +283 -0
- package/prompts/inbox-clarification.md +18 -11
- package/skills/apt/SKILL.md +9 -3
- package/skills/apt-close-task/SKILL.md +48 -0
- package/skills/apt-debug/SKILL.md +19 -9
- package/skills/apt-execute/SKILL.md +3 -1
- package/skills/apt-fan-out/SKILL.md +65 -1
- package/skills/apt-merge-conflict-resolution/SKILL.md +227 -0
- package/skills/apt-pr-review/SCHEMA.md +4 -2
- package/skills/apt-pr-review/SKILL.md +117 -29
- package/skills/apt-quick/SKILL.md +24 -1
- package/skills/apt-review/SKILL.md +6 -0
- package/skills/apt-run/SKILL.md +1 -1
- package/skills/apt-setup/SKILL.md +21 -3
- package/skills/apt-ship/SKILL.md +32 -11
- package/skills/apt-terminal/SKILL.md +8 -0
- package/skills/apt-triage/SKILL.md +4 -0
- package/skills/apt-verify/SKILL.md +24 -0
- package/skills/apt-watch-ci/SKILL.md +3 -1
- package/src/cli/commands/audit-branch-current.mjs +3 -3
- package/src/cli/commands/event.mjs +6 -4
- package/src/cli/commands/fan-out.mjs +3 -1
- package/src/cli/commands/features-audit.mjs +6 -1
- package/src/cli/commands/followups.mjs +303 -0
- package/src/cli/commands/health-check.mjs +3 -1
- package/src/cli/commands/merge-detect.mjs +203 -0
- package/src/cli/commands/merge-integrate.mjs +392 -0
- package/src/cli/commands/pr-review/__fixtures__/android-strings/app/src/main/res/values-fr/strings.xml +3 -0
- package/src/cli/commands/pr-review/__fixtures__/babel-js/package.json +4 -0
- package/src/cli/commands/pr-review/__fixtures__/c-ifdef/src/main.c +4 -0
- package/src/cli/commands/pr-review/__fixtures__/ci-matrix/.github/workflows/ci.yml +10 -0
- package/src/cli/commands/pr-review/__fixtures__/cmake-win/CMakeLists.txt +5 -0
- package/src/cli/commands/pr-review/__fixtures__/diffs/add-arb.patch +9 -0
- package/src/cli/commands/pr-review/__fixtures__/diffs/ci-single-os-dep.patch +13 -0
- package/src/cli/commands/pr-review/__fixtures__/diffs/ci-single-os-list-item-dep.patch +17 -0
- package/src/cli/commands/pr-review/__fixtures__/diffs/lone-weak-t.patch +8 -0
- package/src/cli/commands/pr-review/__fixtures__/diffs/monorepo-b.patch +7 -0
- package/src/cli/commands/pr-review/__fixtures__/diffs/two-weak-xpl.patch +12 -0
- package/src/cli/commands/pr-review/__fixtures__/dropped-underscore/src/app.js +2 -0
- package/src/cli/commands/pr-review/__fixtures__/electron-app/package.json +4 -0
- package/src/cli/commands/pr-review/__fixtures__/i18n-locale-file/src/locales/fr.json +1 -0
- package/src/cli/commands/pr-review/__fixtures__/monorepo/packages/a/locales/fr.json +1 -0
- package/src/cli/commands/pr-review/__fixtures__/monorepo/packages/b/src/index.ts +1 -0
- package/src/cli/commands/pr-review/__fixtures__/py-babel/pyproject.toml +3 -0
- package/src/cli/commands/pr-review/__fixtures__/qt-i18n-dir/i18n/app_fr.ts +11 -0
- package/src/cli/commands/pr-review/__fixtures__/rust-cli/Cargo.toml +7 -0
- package/src/cli/commands/pr-review/__fixtures__/rust-cli/src/main.rs +1 -0
- package/src/cli/commands/pr-review/__fixtures__/ts-dir/ts/foo.ts +3 -0
- package/src/cli/commands/pr-review/detect-specialists.mjs +1023 -0
- package/src/cli/commands/pr-review.mjs +12 -2
- package/src/cli/commands/precompact-hook.mjs +10 -6
- package/src/cli/commands/roadmap.mjs +24 -11
- package/src/cli/commands/seed.mjs +3 -1
- package/src/cli/commands/state.mjs +9 -3
- package/src/cli/commands/statusline.mjs +5 -1
- package/src/cli/commands/task.mjs +135 -23
- package/src/cli/commands/worktree.mjs +298 -81
- package/src/cli/config/post-merge-sweep.mjs +3 -2
- package/src/cli/config/share-policy.mjs +16 -3
- package/src/cli/config/upgrade-gitignore.mjs +27 -5
- package/src/cli/consistency/parse-qa.mjs +11 -1
- package/src/cli/consistency/parse-review.mjs +36 -0
- package/src/cli/consistency/parse-spec.mjs +43 -0
- package/src/cli/coordination/agent-identity.mjs +6 -4
- package/src/cli/dispatch.mjs +4 -0
- package/src/cli/followups/derive.mjs +123 -0
- package/src/cli/followups/schema.mjs +264 -0
- package/src/cli/gate/gates/followups-adjudicated.mjs +158 -0
- package/src/cli/gate/gates/index.mjs +1 -0
- package/src/cli/git/materialize-env.mjs +212 -0
- package/src/cli/help.mjs +9 -1
- package/src/cli/install/legacy-paths.mjs +4 -0
- package/src/cli/roadmap/lifecycle.mjs +4 -1
- package/src/cli/task/ids.mjs +9 -0
- package/src/cli/task/index-md.mjs +12 -4
- package/src/cli/util/io.mjs +15 -1
- package/src/cli/util/project.mjs +11 -14
- package/src/cli/util/state-paths.mjs +45 -0
- package/prompts/followup_planner.md +0 -399
|
@@ -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
|
-
|
|
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'))
|
package/src/cli/task/ids.mjs
CHANGED
|
@@ -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
|
|
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(
|
|
94
|
-
mkdirSync(join(
|
|
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:
|
|
109
|
+
{ projectRoot: stateRoot },
|
|
102
110
|
)
|
|
103
111
|
}
|
package/src/cli/util/io.mjs
CHANGED
|
@@ -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
|
package/src/cli/util/project.mjs
CHANGED
|
@@ -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 {
|
|
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 —
|
|
32
|
-
* across worktrees. Falls back to
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
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
|
+
}
|