@aperant/framework 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +87 -0
- package/README.md +1 -1
- package/agents/apt-pr-review-i18n.md +190 -0
- package/agents/apt-pr-review-xplatform.md +189 -0
- package/dist/cli/commands/audit-branch-current.d.mts +27 -5
- package/dist/cli/commands/audit-branch-current.d.mts.map +1 -1
- package/dist/cli/commands/audit-branch-current.mjs +3 -3
- package/dist/cli/commands/audit-branch-current.mjs.map +1 -1
- package/dist/cli/commands/event.d.mts.map +1 -1
- package/dist/cli/commands/event.mjs +6 -4
- package/dist/cli/commands/event.mjs.map +1 -1
- package/dist/cli/commands/fan-out.d.mts.map +1 -1
- package/dist/cli/commands/fan-out.mjs +3 -1
- package/dist/cli/commands/fan-out.mjs.map +1 -1
- package/dist/cli/commands/features-audit.d.mts.map +1 -1
- package/dist/cli/commands/features-audit.mjs +6 -1
- package/dist/cli/commands/features-audit.mjs.map +1 -1
- package/dist/cli/commands/followups.d.mts +5 -0
- package/dist/cli/commands/followups.d.mts.map +1 -0
- package/dist/cli/commands/followups.mjs +266 -0
- package/dist/cli/commands/followups.mjs.map +1 -0
- package/dist/cli/commands/health-check.d.mts.map +1 -1
- package/dist/cli/commands/health-check.mjs +3 -1
- package/dist/cli/commands/health-check.mjs.map +1 -1
- package/dist/cli/commands/merge-detect.d.mts +29 -0
- package/dist/cli/commands/merge-detect.d.mts.map +1 -0
- package/dist/cli/commands/merge-detect.mjs +196 -0
- package/dist/cli/commands/merge-detect.mjs.map +1 -0
- package/dist/cli/commands/merge-integrate.d.mts +28 -0
- package/dist/cli/commands/merge-integrate.d.mts.map +1 -0
- package/dist/cli/commands/merge-integrate.mjs +383 -0
- package/dist/cli/commands/merge-integrate.mjs.map +1 -0
- package/dist/cli/commands/pr-review/detect-specialists.d.mts +58 -0
- package/dist/cli/commands/pr-review/detect-specialists.d.mts.map +1 -0
- package/dist/cli/commands/pr-review/detect-specialists.mjs +980 -0
- package/dist/cli/commands/pr-review/detect-specialists.mjs.map +1 -0
- package/dist/cli/commands/pr-review.d.mts.map +1 -1
- package/dist/cli/commands/pr-review.mjs +11 -2
- package/dist/cli/commands/pr-review.mjs.map +1 -1
- package/dist/cli/commands/precompact-hook.d.mts.map +1 -1
- package/dist/cli/commands/precompact-hook.mjs +10 -6
- package/dist/cli/commands/precompact-hook.mjs.map +1 -1
- package/dist/cli/commands/roadmap.d.mts.map +1 -1
- package/dist/cli/commands/roadmap.mjs +24 -11
- package/dist/cli/commands/roadmap.mjs.map +1 -1
- package/dist/cli/commands/seed.d.mts.map +1 -1
- package/dist/cli/commands/seed.mjs +3 -1
- package/dist/cli/commands/seed.mjs.map +1 -1
- package/dist/cli/commands/state.d.mts.map +1 -1
- package/dist/cli/commands/state.mjs +9 -3
- package/dist/cli/commands/state.mjs.map +1 -1
- package/dist/cli/commands/statusline.d.mts.map +1 -1
- package/dist/cli/commands/statusline.mjs +5 -1
- package/dist/cli/commands/statusline.mjs.map +1 -1
- package/dist/cli/commands/task.d.mts.map +1 -1
- package/dist/cli/commands/task.mjs +126 -23
- package/dist/cli/commands/task.mjs.map +1 -1
- package/dist/cli/commands/worktree.d.mts +1 -1
- package/dist/cli/commands/worktree.d.mts.map +1 -1
- package/dist/cli/commands/worktree.mjs +282 -83
- package/dist/cli/commands/worktree.mjs.map +1 -1
- package/dist/cli/config/post-merge-sweep.d.mts.map +1 -1
- package/dist/cli/config/post-merge-sweep.mjs +3 -2
- package/dist/cli/config/post-merge-sweep.mjs.map +1 -1
- package/dist/cli/config/share-policy.d.mts +3 -1
- package/dist/cli/config/share-policy.d.mts.map +1 -1
- package/dist/cli/config/share-policy.mjs +16 -3
- package/dist/cli/config/share-policy.mjs.map +1 -1
- package/dist/cli/config/upgrade-gitignore.d.mts.map +1 -1
- package/dist/cli/config/upgrade-gitignore.mjs +27 -5
- package/dist/cli/config/upgrade-gitignore.mjs.map +1 -1
- package/dist/cli/consistency/parse-qa.mjs +12 -1
- package/dist/cli/consistency/parse-qa.mjs.map +1 -1
- package/dist/cli/consistency/parse-review.d.mts +23 -0
- package/dist/cli/consistency/parse-review.d.mts.map +1 -1
- package/dist/cli/consistency/parse-review.mjs +36 -0
- package/dist/cli/consistency/parse-review.mjs.map +1 -1
- package/dist/cli/consistency/parse-spec.d.mts +16 -0
- package/dist/cli/consistency/parse-spec.d.mts.map +1 -1
- package/dist/cli/consistency/parse-spec.mjs +44 -0
- package/dist/cli/consistency/parse-spec.mjs.map +1 -1
- package/dist/cli/coordination/agent-identity.d.mts +5 -2
- package/dist/cli/coordination/agent-identity.d.mts.map +1 -1
- package/dist/cli/coordination/agent-identity.mjs +6 -4
- package/dist/cli/coordination/agent-identity.mjs.map +1 -1
- package/dist/cli/coordination/store.d.mts +1 -1
- package/dist/cli/dispatch.d.mts.map +1 -1
- package/dist/cli/dispatch.mjs +4 -0
- package/dist/cli/dispatch.mjs.map +1 -1
- package/dist/cli/followups/derive.d.mts +63 -0
- package/dist/cli/followups/derive.d.mts.map +1 -0
- package/dist/cli/followups/derive.mjs +115 -0
- package/dist/cli/followups/derive.mjs.map +1 -0
- package/dist/cli/followups/schema.d.mts +165 -0
- package/dist/cli/followups/schema.d.mts.map +1 -0
- package/dist/cli/followups/schema.mjs +251 -0
- package/dist/cli/followups/schema.mjs.map +1 -0
- package/dist/cli/gate/context.d.mts +1 -1
- package/dist/cli/gate/gates/followups-adjudicated.d.mts +69 -0
- package/dist/cli/gate/gates/followups-adjudicated.d.mts.map +1 -0
- package/dist/cli/gate/gates/followups-adjudicated.mjs +141 -0
- package/dist/cli/gate/gates/followups-adjudicated.mjs.map +1 -0
- package/dist/cli/gate/gates/index.mjs +1 -0
- package/dist/cli/gate/gates/index.mjs.map +1 -1
- package/dist/cli/git/materialize-env.d.mts +39 -0
- package/dist/cli/git/materialize-env.d.mts.map +1 -0
- package/dist/cli/git/materialize-env.mjs +217 -0
- package/dist/cli/git/materialize-env.mjs.map +1 -0
- package/dist/cli/help.d.mts.map +1 -1
- package/dist/cli/help.mjs +9 -1
- package/dist/cli/help.mjs.map +1 -1
- package/dist/cli/install/legacy-paths.d.mts.map +1 -1
- package/dist/cli/install/legacy-paths.mjs +4 -0
- package/dist/cli/install/legacy-paths.mjs.map +1 -1
- package/dist/cli/roadmap/lifecycle.d.mts.map +1 -1
- package/dist/cli/roadmap/lifecycle.mjs +4 -1
- package/dist/cli/roadmap/lifecycle.mjs.map +1 -1
- package/dist/cli/task/ids.d.mts +9 -0
- package/dist/cli/task/ids.d.mts.map +1 -1
- package/dist/cli/task/ids.mjs +9 -0
- package/dist/cli/task/ids.mjs.map +1 -1
- package/dist/cli/task/index-md.d.mts +5 -0
- package/dist/cli/task/index-md.d.mts.map +1 -1
- package/dist/cli/task/index-md.mjs +12 -4
- package/dist/cli/task/index-md.mjs.map +1 -1
- package/dist/cli/util/io.d.mts +6 -0
- package/dist/cli/util/io.d.mts.map +1 -1
- package/dist/cli/util/io.mjs +16 -1
- package/dist/cli/util/io.mjs.map +1 -1
- package/dist/cli/util/project.d.mts +7 -2
- package/dist/cli/util/project.d.mts.map +1 -1
- package/dist/cli/util/project.mjs +11 -14
- package/dist/cli/util/project.mjs.map +1 -1
- package/dist/cli/util/state-paths.d.mts +19 -0
- package/dist/cli/util/state-paths.d.mts.map +1 -0
- package/dist/cli/util/state-paths.mjs +45 -0
- package/dist/cli/util/state-paths.mjs.map +1 -0
- package/dist/plugin/.claude-plugin/plugin.json +4 -1
- package/dist/plugin/agents/apt-pr-review-i18n.md +190 -0
- package/dist/plugin/agents/apt-pr-review-xplatform.md +189 -0
- package/dist/plugin/skills/apt/SKILL.md +9 -3
- package/dist/plugin/skills/apt-close-task/SKILL.md +48 -0
- package/dist/plugin/skills/apt-debug/SKILL.md +19 -9
- package/dist/plugin/skills/apt-execute/SKILL.md +3 -1
- package/dist/plugin/skills/apt-fan-out/SKILL.md +65 -1
- package/dist/plugin/skills/apt-merge-conflict-resolution/SKILL.md +227 -0
- package/dist/plugin/skills/apt-pr-review/SCHEMA.md +4 -2
- package/dist/plugin/skills/apt-pr-review/SKILL.md +117 -29
- package/dist/plugin/skills/apt-quick/SKILL.md +24 -1
- package/dist/plugin/skills/apt-review/SKILL.md +6 -0
- package/dist/plugin/skills/apt-run/SKILL.md +1 -1
- package/dist/plugin/skills/apt-setup/SKILL.md +21 -3
- package/dist/plugin/skills/apt-ship/SKILL.md +32 -11
- package/dist/plugin/skills/apt-terminal/SKILL.md +8 -0
- package/dist/plugin/skills/apt-triage/SKILL.md +4 -0
- package/dist/plugin/skills/apt-verify/SKILL.md +24 -0
- package/dist/plugin/skills/apt-watch-ci/SKILL.md +3 -1
- package/dist/types/config.d.ts +33 -0
- package/dist/types/config.d.ts.map +1 -1
- package/package.json +1 -1
- package/prompts/conductor-framework-context.md +1 -1
- package/prompts/github/pr_i18n_agent.md +279 -0
- package/prompts/github/pr_xplatform_agent.md +283 -0
- package/prompts/inbox-clarification.md +18 -11
- package/skills/apt/SKILL.md +9 -3
- package/skills/apt-close-task/SKILL.md +48 -0
- package/skills/apt-debug/SKILL.md +19 -9
- package/skills/apt-execute/SKILL.md +3 -1
- package/skills/apt-fan-out/SKILL.md +65 -1
- package/skills/apt-merge-conflict-resolution/SKILL.md +227 -0
- package/skills/apt-pr-review/SCHEMA.md +4 -2
- package/skills/apt-pr-review/SKILL.md +117 -29
- package/skills/apt-quick/SKILL.md +24 -1
- package/skills/apt-review/SKILL.md +6 -0
- package/skills/apt-run/SKILL.md +1 -1
- package/skills/apt-setup/SKILL.md +21 -3
- package/skills/apt-ship/SKILL.md +32 -11
- package/skills/apt-terminal/SKILL.md +8 -0
- package/skills/apt-triage/SKILL.md +4 -0
- package/skills/apt-verify/SKILL.md +24 -0
- package/skills/apt-watch-ci/SKILL.md +3 -1
- package/src/cli/commands/audit-branch-current.mjs +3 -3
- package/src/cli/commands/event.mjs +6 -4
- package/src/cli/commands/fan-out.mjs +3 -1
- package/src/cli/commands/features-audit.mjs +6 -1
- package/src/cli/commands/followups.mjs +303 -0
- package/src/cli/commands/health-check.mjs +3 -1
- package/src/cli/commands/merge-detect.mjs +203 -0
- package/src/cli/commands/merge-integrate.mjs +392 -0
- package/src/cli/commands/pr-review/__fixtures__/android-strings/app/src/main/res/values-fr/strings.xml +3 -0
- package/src/cli/commands/pr-review/__fixtures__/babel-js/package.json +4 -0
- package/src/cli/commands/pr-review/__fixtures__/c-ifdef/src/main.c +4 -0
- package/src/cli/commands/pr-review/__fixtures__/ci-matrix/.github/workflows/ci.yml +10 -0
- package/src/cli/commands/pr-review/__fixtures__/cmake-win/CMakeLists.txt +5 -0
- package/src/cli/commands/pr-review/__fixtures__/diffs/add-arb.patch +9 -0
- package/src/cli/commands/pr-review/__fixtures__/diffs/ci-single-os-dep.patch +13 -0
- package/src/cli/commands/pr-review/__fixtures__/diffs/ci-single-os-list-item-dep.patch +17 -0
- package/src/cli/commands/pr-review/__fixtures__/diffs/lone-weak-t.patch +8 -0
- package/src/cli/commands/pr-review/__fixtures__/diffs/monorepo-b.patch +7 -0
- package/src/cli/commands/pr-review/__fixtures__/diffs/two-weak-xpl.patch +12 -0
- package/src/cli/commands/pr-review/__fixtures__/dropped-underscore/src/app.js +2 -0
- package/src/cli/commands/pr-review/__fixtures__/electron-app/package.json +4 -0
- package/src/cli/commands/pr-review/__fixtures__/i18n-locale-file/src/locales/fr.json +1 -0
- package/src/cli/commands/pr-review/__fixtures__/monorepo/packages/a/locales/fr.json +1 -0
- package/src/cli/commands/pr-review/__fixtures__/monorepo/packages/b/src/index.ts +1 -0
- package/src/cli/commands/pr-review/__fixtures__/py-babel/pyproject.toml +3 -0
- package/src/cli/commands/pr-review/__fixtures__/qt-i18n-dir/i18n/app_fr.ts +11 -0
- package/src/cli/commands/pr-review/__fixtures__/rust-cli/Cargo.toml +7 -0
- package/src/cli/commands/pr-review/__fixtures__/rust-cli/src/main.rs +1 -0
- package/src/cli/commands/pr-review/__fixtures__/ts-dir/ts/foo.ts +3 -0
- package/src/cli/commands/pr-review/detect-specialists.mjs +1023 -0
- package/src/cli/commands/pr-review.mjs +12 -2
- package/src/cli/commands/precompact-hook.mjs +10 -6
- package/src/cli/commands/roadmap.mjs +24 -11
- package/src/cli/commands/seed.mjs +3 -1
- package/src/cli/commands/state.mjs +9 -3
- package/src/cli/commands/statusline.mjs +5 -1
- package/src/cli/commands/task.mjs +135 -23
- package/src/cli/commands/worktree.mjs +298 -81
- package/src/cli/config/post-merge-sweep.mjs +3 -2
- package/src/cli/config/share-policy.mjs +16 -3
- package/src/cli/config/upgrade-gitignore.mjs +27 -5
- package/src/cli/consistency/parse-qa.mjs +11 -1
- package/src/cli/consistency/parse-review.mjs +36 -0
- package/src/cli/consistency/parse-spec.mjs +43 -0
- package/src/cli/coordination/agent-identity.mjs +6 -4
- package/src/cli/dispatch.mjs +4 -0
- package/src/cli/followups/derive.mjs +123 -0
- package/src/cli/followups/schema.mjs +264 -0
- package/src/cli/gate/gates/followups-adjudicated.mjs +158 -0
- package/src/cli/gate/gates/index.mjs +1 -0
- package/src/cli/git/materialize-env.mjs +212 -0
- package/src/cli/help.mjs +9 -1
- package/src/cli/install/legacy-paths.mjs +4 -0
- package/src/cli/roadmap/lifecycle.mjs +4 -1
- package/src/cli/task/ids.mjs +9 -0
- package/src/cli/task/index-md.mjs +12 -4
- package/src/cli/util/io.mjs +15 -1
- package/src/cli/util/project.mjs +11 -14
- package/src/cli/util/state-paths.mjs +45 -0
- package/prompts/followup_planner.md +0 -399
|
@@ -42,6 +42,7 @@ import { parseFlags } from '../util/args.mjs'
|
|
|
42
42
|
import { withFileLock } from '../util/fs-lock.mjs'
|
|
43
43
|
import { emitInitPrStateHint } from '../util/init-hints.mjs'
|
|
44
44
|
import { err, exitWith, ok, UsageError } from '../util/result.mjs'
|
|
45
|
+
import { cmdDetectSpecialists } from './pr-review/detect-specialists.mjs'
|
|
45
46
|
import { cmdPrReviewAuditFixer } from './pr-review-audit-fixer.mjs'
|
|
46
47
|
|
|
47
48
|
// C56 B4 — helper for self-staging committable pr-review writes. The
|
|
@@ -1083,7 +1084,7 @@ function handleSetAuthorshipOverride(root, flags) {
|
|
|
1083
1084
|
export function cmdPrReview(subcommand, projectDir, extraArgs) {
|
|
1084
1085
|
if (!subcommand)
|
|
1085
1086
|
return err(
|
|
1086
|
-
'Usage: apt-tools pr-review <init-pr|acquire-lock|release-lock|start-round|close-round|get-state|detect-authorship|record-comment|set-authorship-override|audit-fixer> <project-dir> [flags]',
|
|
1087
|
+
'Usage: apt-tools pr-review <init-pr|acquire-lock|release-lock|start-round|close-round|get-state|detect-authorship|detect-specialists|record-comment|set-authorship-override|audit-fixer> <project-dir> [flags]',
|
|
1087
1088
|
)
|
|
1088
1089
|
|
|
1089
1090
|
// audit-fixer has its own argv shape (accepts --help in projectDir
|
|
@@ -1096,6 +1097,15 @@ export function cmdPrReview(subcommand, projectDir, extraArgs) {
|
|
|
1096
1097
|
return cmdPrReviewAuditFixer(projectDir, extraArgs)
|
|
1097
1098
|
}
|
|
1098
1099
|
|
|
1100
|
+
// detect-specialists runs over an ARBITRARY project dir (the PR's worktree,
|
|
1101
|
+
// or a unit-test fixture tree) that need NOT contain a `.aperant/` dir —
|
|
1102
|
+
// the detector reads `.aperant/config.json` best-effort but does not
|
|
1103
|
+
// require it. Delegate BEFORE the .aperant existence guard, like
|
|
1104
|
+
// audit-fixer.
|
|
1105
|
+
if (subcommand === 'detect-specialists') {
|
|
1106
|
+
return cmdDetectSpecialists(projectDir, extraArgs)
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1099
1109
|
if (!projectDir) return err(`Usage: apt-tools pr-review ${subcommand} <project-dir> [flags]`)
|
|
1100
1110
|
|
|
1101
1111
|
const root = resolve(projectDir)
|
|
@@ -1124,7 +1134,7 @@ export function cmdPrReview(subcommand, projectDir, extraArgs) {
|
|
|
1124
1134
|
return handleSetAuthorshipOverride(root, flags)
|
|
1125
1135
|
default:
|
|
1126
1136
|
return err(
|
|
1127
|
-
`Unknown pr-review subcommand: ${subcommand}. Available: init-pr, acquire-lock, release-lock, start-round, close-round, get-state, detect-authorship, record-comment, set-authorship-override, audit-fixer`,
|
|
1137
|
+
`Unknown pr-review subcommand: ${subcommand}. Available: init-pr, acquire-lock, release-lock, start-round, close-round, get-state, detect-authorship, detect-specialists, record-comment, set-authorship-override, audit-fixer`,
|
|
1128
1138
|
)
|
|
1129
1139
|
}
|
|
1130
1140
|
} catch (e) {
|
|
@@ -11,6 +11,7 @@ import { existsSync, readFileSync } from 'node:fs'
|
|
|
11
11
|
import { join, resolve } from 'node:path'
|
|
12
12
|
import { mergeClaudeHook } from '../install/claude-hooks.mjs'
|
|
13
13
|
import { rawText } from '../util/result.mjs'
|
|
14
|
+
import { resolveMainRepoRoot } from '../util/state-paths.mjs'
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Install or merge the apt-tools PreCompact hook into
|
|
@@ -93,7 +94,10 @@ export function cmdPrecompactHook(projectDir) {
|
|
|
93
94
|
return rawText('', 0)
|
|
94
95
|
}
|
|
95
96
|
|
|
96
|
-
|
|
97
|
+
// state-fork-fix: state.json + tasks/ are EPHEMERAL — resolve to the main
|
|
98
|
+
// board so a post-compact agent in a worktree reads the authoritative tasks.
|
|
99
|
+
const stateRoot = resolveMainRepoRoot(targetDir)
|
|
100
|
+
const statePath = join(stateRoot, '.aperant', 'state.json')
|
|
97
101
|
if (!existsSync(statePath)) {
|
|
98
102
|
return rawText('', 0)
|
|
99
103
|
}
|
|
@@ -138,11 +142,11 @@ export function cmdPrecompactHook(projectDir) {
|
|
|
138
142
|
lines.push(`Active tasks (${taskIds.length}):`)
|
|
139
143
|
|
|
140
144
|
for (const [taskId, task] of Object.entries(activeTasks)) {
|
|
141
|
-
// Resolve task_dir deterministically
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
const taskDir = join('.aperant', 'tasks', taskId)
|
|
145
|
+
// Resolve task_dir deterministically — the task object does not store its
|
|
146
|
+
// own directory path; it's always derived from id. state-fork-fix: tasks/
|
|
147
|
+
// is EPHEMERAL, so anchor at the main-repo state root (cwd-independent AND
|
|
148
|
+
// points at the real spec/plan/progress, not a worktree's empty board).
|
|
149
|
+
const taskDir = join(stateRoot, '.aperant', 'tasks', taskId)
|
|
146
150
|
// Real schema from cmdTask create: description, lifecycle_phase,
|
|
147
151
|
// progress.{subtasks_total, subtasks_completed, current_subtask}.
|
|
148
152
|
// Title/status are fallbacks for legacy or hand-edited state.
|
|
@@ -30,6 +30,7 @@ import { parseFlags } from '../util/args.mjs'
|
|
|
30
30
|
import { withFileLock } from '../util/fs-lock.mjs'
|
|
31
31
|
import { atomicWriteJson } from '../util/io.mjs'
|
|
32
32
|
import { err, exitWith, ok, UsageError } from '../util/result.mjs'
|
|
33
|
+
import { resolveMainRepoRoot } from '../util/state-paths.mjs'
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
36
|
* Sentinel thrown by inner helpers (`loadRoadmap`, `resolveScope`, etc.) so
|
|
@@ -60,6 +61,15 @@ export async function cmdRoadmap(subcommand, projectDir, extraArgs) {
|
|
|
60
61
|
|
|
61
62
|
async function cmdRoadmapImpl(subcommand, projectDir, extraArgs) {
|
|
62
63
|
const targetDir = resolve(projectDir)
|
|
64
|
+
// state-fork-fix ID-01/ID-05/ID-06: committable + ephemeral path-builds
|
|
65
|
+
// INTERLEAVE in this file. `roadmapRoot` (milestones.json + ROADMAP.md +
|
|
66
|
+
// SEEDS.md), seeds.json, and the per-scope sinks are COMMITTABLE and MUST
|
|
67
|
+
// stay worktree-local on `targetDir` so they ride the PR branch. Only the
|
|
68
|
+
// EPHEMERAL state.json reads/writes + their withFileLock roots redirect to
|
|
69
|
+
// the main repo via `stateRoot` — including the deliberately behavior-CHANGING
|
|
70
|
+
// seeds-count read (ID-05: from a worktree the SEEDS.md counts now reflect the
|
|
71
|
+
// authoritative main board, not a forked worktree's zeros).
|
|
72
|
+
const stateRoot = resolveMainRepoRoot(projectDir)
|
|
63
73
|
updateTeamMember(targetDir)
|
|
64
74
|
const roadmapRoot = join(targetDir, '.aperant', 'roadmap')
|
|
65
75
|
const flags = parseFlags(extraArgs)
|
|
@@ -332,7 +342,10 @@ async function cmdRoadmapImpl(subcommand, projectDir, extraArgs) {
|
|
|
332
342
|
}
|
|
333
343
|
let state = null
|
|
334
344
|
try {
|
|
335
|
-
|
|
345
|
+
// ID-05 behavior-CHANGING redirect: seeds-count read resolves to
|
|
346
|
+
// the authoritative MAIN board so renderSeedsMd reports real
|
|
347
|
+
// project-wide counts even when render ran from a worktree.
|
|
348
|
+
const statePath = join(stateRoot, '.aperant', 'state.json')
|
|
336
349
|
state = existsSync(statePath) ? JSON.parse(readFileSync(statePath, 'utf-8')) : null
|
|
337
350
|
} catch {
|
|
338
351
|
/* state.json malformed — render with zero task counts */
|
|
@@ -526,7 +539,7 @@ async function cmdRoadmapImpl(subcommand, projectDir, extraArgs) {
|
|
|
526
539
|
// same state.json lock so a concurrent task create can't slip a new
|
|
527
540
|
// reference in between the check and the roadmap mutation.
|
|
528
541
|
if (status === 'deleted') {
|
|
529
|
-
const statePathLocal = join(
|
|
542
|
+
const statePathLocal = join(stateRoot, '.aperant', 'state.json')
|
|
530
543
|
let cascadedCount = 0
|
|
531
544
|
let blockedAffected = null
|
|
532
545
|
if (existsSync(statePathLocal)) {
|
|
@@ -563,7 +576,7 @@ async function cmdRoadmapImpl(subcommand, projectDir, extraArgs) {
|
|
|
563
576
|
cascadedCount = freshAffected.length
|
|
564
577
|
}
|
|
565
578
|
},
|
|
566
|
-
{ projectRoot:
|
|
579
|
+
{ projectRoot: stateRoot },
|
|
567
580
|
)
|
|
568
581
|
}
|
|
569
582
|
if (blockedAffected) {
|
|
@@ -577,7 +590,7 @@ async function cmdRoadmapImpl(subcommand, projectDir, extraArgs) {
|
|
|
577
590
|
1,
|
|
578
591
|
)
|
|
579
592
|
}
|
|
580
|
-
if (cascadedCount > 0) regenerateTaskIndex(
|
|
593
|
+
if (cascadedCount > 0) regenerateTaskIndex(stateRoot)
|
|
581
594
|
// Remove the phase entirely when status=deleted.
|
|
582
595
|
milestone.phases = milestone.phases.filter((p) => p.id !== phaseId)
|
|
583
596
|
saveRoadmap(scope, roadmap)
|
|
@@ -676,7 +689,7 @@ async function cmdRoadmapImpl(subcommand, projectDir, extraArgs) {
|
|
|
676
689
|
|
|
677
690
|
// Atomic orphan-block check + cascade under the state.json lock to
|
|
678
691
|
// close the TOCTOU window between the check and the roadmap write.
|
|
679
|
-
const stPath = join(
|
|
692
|
+
const stPath = join(stateRoot, '.aperant', 'state.json')
|
|
680
693
|
let cascadedCount = 0
|
|
681
694
|
let blockedAffected = null
|
|
682
695
|
if (existsSync(stPath)) {
|
|
@@ -713,7 +726,7 @@ async function cmdRoadmapImpl(subcommand, projectDir, extraArgs) {
|
|
|
713
726
|
cascadedCount = freshAffected.length
|
|
714
727
|
}
|
|
715
728
|
},
|
|
716
|
-
{ projectRoot:
|
|
729
|
+
{ projectRoot: stateRoot },
|
|
717
730
|
)
|
|
718
731
|
}
|
|
719
732
|
if (blockedAffected) {
|
|
@@ -727,7 +740,7 @@ async function cmdRoadmapImpl(subcommand, projectDir, extraArgs) {
|
|
|
727
740
|
1,
|
|
728
741
|
)
|
|
729
742
|
}
|
|
730
|
-
if (cascadedCount > 0) regenerateTaskIndex(
|
|
743
|
+
if (cascadedCount > 0) regenerateTaskIndex(stateRoot)
|
|
731
744
|
|
|
732
745
|
milestone.phases = milestone.phases.filter((p) => p.id !== phaseId)
|
|
733
746
|
saveRoadmap(scope, roadmap)
|
|
@@ -772,7 +785,7 @@ async function cmdRoadmapImpl(subcommand, projectDir, extraArgs) {
|
|
|
772
785
|
throw new RoadmapUsageError(`Milestone "${milestoneId}" not found in scope "${scope}"`)
|
|
773
786
|
|
|
774
787
|
// Atomic orphan-block check + cascade under the state.json lock.
|
|
775
|
-
const stPath = join(
|
|
788
|
+
const stPath = join(stateRoot, '.aperant', 'state.json')
|
|
776
789
|
let cascadedCount = 0
|
|
777
790
|
let blockedAffected = null
|
|
778
791
|
if (existsSync(stPath)) {
|
|
@@ -809,7 +822,7 @@ async function cmdRoadmapImpl(subcommand, projectDir, extraArgs) {
|
|
|
809
822
|
cascadedCount = freshAffected.length
|
|
810
823
|
}
|
|
811
824
|
},
|
|
812
|
-
{ projectRoot:
|
|
825
|
+
{ projectRoot: stateRoot },
|
|
813
826
|
)
|
|
814
827
|
}
|
|
815
828
|
if (blockedAffected) {
|
|
@@ -823,7 +836,7 @@ async function cmdRoadmapImpl(subcommand, projectDir, extraArgs) {
|
|
|
823
836
|
1,
|
|
824
837
|
)
|
|
825
838
|
}
|
|
826
|
-
if (cascadedCount > 0) regenerateTaskIndex(
|
|
839
|
+
if (cascadedCount > 0) regenerateTaskIndex(stateRoot)
|
|
827
840
|
|
|
828
841
|
// C34c — capture the phase ids before filtering so we can unlink each
|
|
829
842
|
// `.aperant/roadmap/{scope}/phases/{id}.json` and prevent the
|
|
@@ -851,7 +864,7 @@ async function cmdRoadmapImpl(subcommand, projectDir, extraArgs) {
|
|
|
851
864
|
// project root and grep for `<a id="<anchor>">`. For URL: unless
|
|
852
865
|
// --offline, HEAD via curl with a 5s timeout. Emits {status, checked, broken}.
|
|
853
866
|
const offline = extraArgs.includes('--offline')
|
|
854
|
-
const statePathLocal = join(
|
|
867
|
+
const statePathLocal = join(stateRoot, '.aperant', 'state.json')
|
|
855
868
|
const broken = []
|
|
856
869
|
let checked = 0
|
|
857
870
|
|
|
@@ -39,6 +39,7 @@ import { updateTeamMember } from '../team/team.mjs'
|
|
|
39
39
|
import { parseFlags } from '../util/args.mjs'
|
|
40
40
|
import { withFileLock } from '../util/fs-lock.mjs'
|
|
41
41
|
import { err, exitWith, ok } from '../util/result.mjs'
|
|
42
|
+
import { resolveMainRepoRoot } from '../util/state-paths.mjs'
|
|
42
43
|
|
|
43
44
|
/**
|
|
44
45
|
* Run validateSeeds against the current on-disk state.
|
|
@@ -65,7 +66,8 @@ function runValidation(targetDir) {
|
|
|
65
66
|
*/
|
|
66
67
|
function gatherSeedTasks(targetDir, refs) {
|
|
67
68
|
const out = { active: 0, completed: 0, total: 0, tasks: [] }
|
|
68
|
-
|
|
69
|
+
// state-fork-fix: ephemeral state read resolves to the main board.
|
|
70
|
+
const statePath = join(resolveMainRepoRoot(targetDir), '.aperant', 'state.json')
|
|
69
71
|
if (!existsSync(statePath)) return out
|
|
70
72
|
let state
|
|
71
73
|
try {
|
|
@@ -16,6 +16,7 @@ import { parseFlags } from '../util/args.mjs'
|
|
|
16
16
|
import { withFileLock } from '../util/fs-lock.mjs'
|
|
17
17
|
import { atomicWriteJson } from '../util/io.mjs'
|
|
18
18
|
import { err, exitWith, ok } from '../util/result.mjs'
|
|
19
|
+
import { resolveMainRepoRoot } from '../util/state-paths.mjs'
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Emit a human-readable preview to stderr when stdout is a TTY. Never throws;
|
|
@@ -38,7 +39,10 @@ export function cmdState(projectDir) {
|
|
|
38
39
|
if (!projectDir) return err('Usage: apt-tools state <project-dir>')
|
|
39
40
|
const targetDir = resolve(projectDir)
|
|
40
41
|
const aperantDir = join(targetDir, '.aperant')
|
|
41
|
-
|
|
42
|
+
// state-fork-fix: state.json is EPHEMERAL — resolve to the main board so
|
|
43
|
+
// `state .` from a worktree reads the authoritative board (the primary fork
|
|
44
|
+
// victim). config.json is COMMITTABLE — it stays worktree-local on aperantDir.
|
|
45
|
+
const statePath = join(resolveMainRepoRoot(projectDir), '.aperant', 'state.json')
|
|
42
46
|
const configPath = join(aperantDir, 'config.json')
|
|
43
47
|
|
|
44
48
|
if (!existsSync(aperantDir)) {
|
|
@@ -145,7 +149,9 @@ export function cmdStateUpdate(projectDir, args) {
|
|
|
145
149
|
if (!projectDir)
|
|
146
150
|
return err('Usage: apt-tools state-update <project-dir> --field <path> --value <json>')
|
|
147
151
|
const targetDir = resolve(projectDir)
|
|
148
|
-
|
|
152
|
+
// state-fork-fix: ephemeral state-update resolves + locks against the main board.
|
|
153
|
+
const stateRoot = resolveMainRepoRoot(projectDir)
|
|
154
|
+
const statePath = join(stateRoot, '.aperant', 'state.json')
|
|
149
155
|
|
|
150
156
|
if (!existsSync(statePath)) return err('No .aperant/state.json found.')
|
|
151
157
|
|
|
@@ -183,7 +189,7 @@ export function cmdStateUpdate(projectDir, args) {
|
|
|
183
189
|
|
|
184
190
|
atomicWriteJson(statePath, state)
|
|
185
191
|
},
|
|
186
|
-
{ projectRoot:
|
|
192
|
+
{ projectRoot: stateRoot },
|
|
187
193
|
)
|
|
188
194
|
|
|
189
195
|
return ok({ status: 'ok', command: 'state-update', field, value, projectDir: targetDir })
|
|
@@ -22,6 +22,7 @@ import { formatUpdateChips, readUpdateCache } from '../install/update-chips.mjs'
|
|
|
22
22
|
import { statusLine, supportsColor } from '../output-style/index.mjs'
|
|
23
23
|
import { parseFlags } from '../util/args.mjs'
|
|
24
24
|
import { err, ok, rawText } from '../util/result.mjs'
|
|
25
|
+
import { resolveMainRepoRoot } from '../util/state-paths.mjs'
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
28
|
* Normalize `active_tasks` from state.json across the legacy (single object)
|
|
@@ -143,7 +144,10 @@ export function cmdStatusline(projectDir, extraArgs = []) {
|
|
|
143
144
|
})
|
|
144
145
|
|
|
145
146
|
const aperantDir = join(targetDir, '.aperant')
|
|
146
|
-
|
|
147
|
+
// state-fork-fix: state.json is EPHEMERAL — resolve to the main board (aligns
|
|
148
|
+
// with the createCoordinationStore redirect below). The local aperantDir is
|
|
149
|
+
// kept only for the not-initialized existence check.
|
|
150
|
+
const statePath = join(resolveMainRepoRoot(targetDir), '.aperant', 'state.json')
|
|
147
151
|
|
|
148
152
|
// Read SessionStart worker cache once — chips prepend onto every render
|
|
149
153
|
// path (initialized or not).
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
} from '../config/load.mjs'
|
|
22
22
|
import { findMostRecentEventForTask } from '../coordination/event-log.mjs'
|
|
23
23
|
import { createCoordinationStore } from '../coordination/store.mjs'
|
|
24
|
+
import { listBlockingRowsForClose, parseFollowups } from '../followups/schema.mjs'
|
|
24
25
|
import { getGitIdentity } from '../git/identity.mjs'
|
|
25
26
|
import { detectWorktree } from '../git/worktree-detect.mjs'
|
|
26
27
|
import {
|
|
@@ -44,6 +45,7 @@ import { parseDuration } from '../util/duration.mjs'
|
|
|
44
45
|
import { withFileLock } from '../util/fs-lock.mjs'
|
|
45
46
|
import { atomicWriteJson } from '../util/io.mjs'
|
|
46
47
|
import { err, exitWith, ok } from '../util/result.mjs'
|
|
48
|
+
import { resolveMainRepoRoot } from '../util/state-paths.mjs'
|
|
47
49
|
import { cmdCiWatch } from './ci-watch.mjs'
|
|
48
50
|
|
|
49
51
|
export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
@@ -54,7 +56,14 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
54
56
|
if (!projectDir) return err(`Usage: apt-tools task ${subcommand} <project-dir> [flags]`)
|
|
55
57
|
|
|
56
58
|
const targetDir = resolve(projectDir)
|
|
57
|
-
|
|
59
|
+
// state-fork-fix ID-01/ID-04: ephemeral state (state.json + tasks/) resolves
|
|
60
|
+
// to the MAIN repo from any worktree cwd, so the task board never forks into
|
|
61
|
+
// a worktree's gitignored .aperant/. task.mjs touches ONLY ephemeral state
|
|
62
|
+
// (the mechanical exception in ID-01), so threading one stateRoot is correct.
|
|
63
|
+
// Committable roadmap path-builds in this file (the milestones.json reads)
|
|
64
|
+
// deliberately stay on the raw targetDir — they MUST ride the task branch.
|
|
65
|
+
const stateRoot = resolveMainRepoRoot(projectDir)
|
|
66
|
+
const statePath = join(stateRoot, '.aperant', 'state.json')
|
|
58
67
|
const flags = parseFlags(extraArgs)
|
|
59
68
|
|
|
60
69
|
// R8 — @internal CoordinationStore seam. V1 is a thin wrapper over the
|
|
@@ -167,8 +176,8 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
167
176
|
// but allows `.` and `..` as valid characters. A lone `..` id would
|
|
168
177
|
// escape via getTaskDir's join: `tasks/..` resolves to `.aperant/`.
|
|
169
178
|
if (explicitIdRaw) {
|
|
170
|
-
const tasksBase = resolve(join(
|
|
171
|
-
const candidateDir = resolve(getTaskDir(
|
|
179
|
+
const tasksBase = resolve(join(stateRoot, '.aperant', 'tasks'))
|
|
180
|
+
const candidateDir = resolve(getTaskDir(stateRoot, explicitIdRaw))
|
|
172
181
|
if (!candidateDir.startsWith(`${tasksBase}/`) && candidateDir !== tasksBase) {
|
|
173
182
|
return err('[apt-tools] Invalid task id: path escapes the tasks directory', 1, {
|
|
174
183
|
command: 'task-create',
|
|
@@ -213,7 +222,7 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
213
222
|
: []
|
|
214
223
|
const known = new Set([...activeIds, ...completedIds])
|
|
215
224
|
const isTaken = (candidate) =>
|
|
216
|
-
known.has(candidate) || existsSync(getTaskDir(
|
|
225
|
+
known.has(candidate) || existsSync(getTaskDir(stateRoot, candidate))
|
|
217
226
|
let candidate = base
|
|
218
227
|
let counter = 2
|
|
219
228
|
while (isTaken(candidate)) {
|
|
@@ -231,7 +240,7 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
231
240
|
|
|
232
241
|
// Create task directory under the lock so a concurrent
|
|
233
242
|
// run can't observe (and then re-pick) a half-created id.
|
|
234
|
-
taskDir = getTaskDir(
|
|
243
|
+
taskDir = getTaskDir(stateRoot, taskId)
|
|
235
244
|
mkdirSync(taskDir, { recursive: true })
|
|
236
245
|
for (const subdir of ['events', 'locks']) {
|
|
237
246
|
mkdirSync(join(taskDir, subdir), { recursive: true })
|
|
@@ -295,6 +304,10 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
295
304
|
if (isolation.from_branch && isolation.from_branch !== 'current') {
|
|
296
305
|
wtArgs.push('--from', isolation.from_branch)
|
|
297
306
|
}
|
|
307
|
+
// state-fork-fix ID-09b: forward the task's effective autonomy
|
|
308
|
+
// so `worktree create` can pick the 0-1 confirm vs. 2-3 log
|
|
309
|
+
// branch for a non-default `from_branch: current` base.
|
|
310
|
+
wtArgs.push('--autonomy', String(autonomy))
|
|
298
311
|
// Invoke inline so we capture the structured payload
|
|
299
312
|
// without shelling back out. We cheat by calling the
|
|
300
313
|
// worktree creator directly as a subprocess to keep
|
|
@@ -310,6 +323,19 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
310
323
|
)
|
|
311
324
|
if (createRes.status === 0 && createRes.stdout) {
|
|
312
325
|
const wtOut = JSON.parse(createRes.stdout)
|
|
326
|
+
// state-fork-fix ID-09b: a requires_input non-default-base
|
|
327
|
+
// return (autonomy <= 1) is NOT a created worktree — degrade
|
|
328
|
+
// it to a worktree_warning so task-create still SUCCEEDS
|
|
329
|
+
// (task created, worktree deferred) instead of recording a
|
|
330
|
+
// bogus undefined worktree_path. The operator re-runs
|
|
331
|
+
// `worktree create` with an explicit --from.
|
|
332
|
+
if (wtOut.status === 'requires_input' || wtOut.reason === 'non_default_base') {
|
|
333
|
+
state.active_tasks[taskId].worktree_warning =
|
|
334
|
+
`worktree deferred: non-default base — re-run worktree create with explicit --from (base would be '${wtOut.base_branch}', default '${wtOut.default_branch}')`
|
|
335
|
+
state.last_activity = new Date().toISOString()
|
|
336
|
+
atomicWriteJson(statePath, state)
|
|
337
|
+
return
|
|
338
|
+
}
|
|
313
339
|
state.active_tasks[taskId].worktree_path = wtOut.worktree_path
|
|
314
340
|
state.active_tasks[taskId].branch = wtOut.branch
|
|
315
341
|
state.active_tasks[taskId].base_branch = wtOut.base_branch
|
|
@@ -370,12 +396,12 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
370
396
|
state.last_activity = new Date().toISOString()
|
|
371
397
|
atomicWriteJson(statePath, state)
|
|
372
398
|
},
|
|
373
|
-
{ projectRoot:
|
|
399
|
+
{ projectRoot: stateRoot },
|
|
374
400
|
)
|
|
375
401
|
|
|
376
402
|
if (collisionOverflowErr) return collisionOverflowErr
|
|
377
403
|
|
|
378
|
-
regenerateTaskIndex(
|
|
404
|
+
regenerateTaskIndex(stateRoot)
|
|
379
405
|
|
|
380
406
|
// Team collaboration: register user + add to active board
|
|
381
407
|
updateTeamMember(targetDir)
|
|
@@ -517,7 +543,7 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
517
543
|
atomicWriteJson(statePath, freshState)
|
|
518
544
|
}
|
|
519
545
|
},
|
|
520
|
-
{ projectRoot:
|
|
546
|
+
{ projectRoot: stateRoot },
|
|
521
547
|
)
|
|
522
548
|
} catch {
|
|
523
549
|
/* best-effort — the in-memory `task` is already rewritten */
|
|
@@ -525,7 +551,7 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
525
551
|
}
|
|
526
552
|
}
|
|
527
553
|
|
|
528
|
-
const taskDir = getTaskDir(
|
|
554
|
+
const taskDir = getTaskDir(stateRoot, taskId)
|
|
529
555
|
const hasSpec = existsSync(join(taskDir, 'spec.md'))
|
|
530
556
|
const hasPlan = existsSync(join(taskDir, 'implementation_plan.json'))
|
|
531
557
|
const hasProgress = existsSync(join(taskDir, 'build-progress.json'))
|
|
@@ -549,6 +575,18 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
549
575
|
// runs fetch + ff-only instead.
|
|
550
576
|
const prMerged = flags.has('pr-merged-remotely')
|
|
551
577
|
|
|
578
|
+
// G45 — followups disposition gate. `--force --reason "<text>"`
|
|
579
|
+
// bypasses it with an audit event (G42 Phase 1 pattern). Reject
|
|
580
|
+
// --force without --reason, mirroring gate.mjs:88.
|
|
581
|
+
const closeForce = flags.get('force') === true
|
|
582
|
+
const closeReasonRaw = flags.get('reason')
|
|
583
|
+
const closeReason = typeof closeReasonRaw === 'string' ? closeReasonRaw : null
|
|
584
|
+
if (closeForce && !closeReason) {
|
|
585
|
+
return err(
|
|
586
|
+
'--force requires --reason "<explanation>" (the override is logged to the audit trail)',
|
|
587
|
+
)
|
|
588
|
+
}
|
|
589
|
+
|
|
552
590
|
if (!existsSync(statePath)) return err('No state.json found')
|
|
553
591
|
|
|
554
592
|
// withFileLock callback can produce either an error envelope (early
|
|
@@ -560,14 +598,16 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
560
598
|
() => {
|
|
561
599
|
const state = JSON.parse(readFileSync(statePath, 'utf-8'))
|
|
562
600
|
|
|
563
|
-
//
|
|
601
|
+
// Locate the task WITHOUT mutating yet — the G45 disposition
|
|
602
|
+
// gate (below) must run before any state mutation so a refused
|
|
603
|
+
// close leaves active_tasks intact.
|
|
564
604
|
let task = null
|
|
605
|
+
let taskInActiveMap = false
|
|
565
606
|
if (state.active_tasks?.[taskId]) {
|
|
566
607
|
task = state.active_tasks[taskId]
|
|
567
|
-
|
|
608
|
+
taskInActiveMap = true
|
|
568
609
|
} else if (state.active_task?.id === taskId) {
|
|
569
610
|
task = state.active_task
|
|
570
|
-
state.active_task = null
|
|
571
611
|
}
|
|
572
612
|
|
|
573
613
|
if (!task) {
|
|
@@ -575,6 +615,75 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
575
615
|
return
|
|
576
616
|
}
|
|
577
617
|
|
|
618
|
+
// G45 — followups disposition gate (ID-06 / ID-13). Runs at the
|
|
619
|
+
// TOP of the lock, before any state mutation, so close-merged
|
|
620
|
+
// (:1476 spawns `task close`) + the passive sweep inherit it.
|
|
621
|
+
// Skipped for verdict 'rejected' (abandoned work has moot
|
|
622
|
+
// follow-ups). Absent/malformed followups.json → no gating
|
|
623
|
+
// (byte-identical close; malformed is `followups check`'s
|
|
624
|
+
// problem, not close's). `--force --reason` bypasses with an
|
|
625
|
+
// audited task.close.followups-override event.
|
|
626
|
+
if (verdict !== 'rejected') {
|
|
627
|
+
const followupsPath = join(getTaskDir(stateRoot, taskId), 'followups.json')
|
|
628
|
+
let ledgerDoc = null
|
|
629
|
+
if (existsSync(followupsPath)) {
|
|
630
|
+
try {
|
|
631
|
+
ledgerDoc = JSON.parse(readFileSync(followupsPath, 'utf-8'))
|
|
632
|
+
} catch {
|
|
633
|
+
ledgerDoc = null // malformed → no gating
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (ledgerDoc !== null) {
|
|
637
|
+
const parsed = parseFollowups(ledgerDoc)
|
|
638
|
+
const blocking = listBlockingRowsForClose(parsed)
|
|
639
|
+
if (blocking.length > 0) {
|
|
640
|
+
const blockingSummary = blocking.map((r) => ({
|
|
641
|
+
id: r.id,
|
|
642
|
+
title: r.title ?? null,
|
|
643
|
+
status: r.status,
|
|
644
|
+
verdict: r.adjudication?.verdict ?? 'unadjudicated',
|
|
645
|
+
disposition_kind: r.disposition?.kind ?? null,
|
|
646
|
+
}))
|
|
647
|
+
if (!closeForce) {
|
|
648
|
+
closeError = err(
|
|
649
|
+
`Task "${taskId}" has ${blocking.length} open follow-up row(s) without a durable disposition — disposition them (\`apt-tools followups disposition\`) or re-run with --force --reason "<text>"`,
|
|
650
|
+
1,
|
|
651
|
+
{
|
|
652
|
+
command: 'task-close',
|
|
653
|
+
task_id: taskId,
|
|
654
|
+
blocking_followups: blockingSummary,
|
|
655
|
+
},
|
|
656
|
+
)
|
|
657
|
+
return
|
|
658
|
+
}
|
|
659
|
+
// Forced bypass — audit the override. `appendEvent` is
|
|
660
|
+
// best-effort (it swallows I/O errors so a full disk
|
|
661
|
+
// can't block a lifecycle transition). For this
|
|
662
|
+
// accountability event specifically, surface a stderr
|
|
663
|
+
// warning when the write does not land so the bypass
|
|
664
|
+
// is never SILENTLY unaudited — without re-introducing
|
|
665
|
+
// a hard block on the close.
|
|
666
|
+
const auditLogged = store.appendEvent({
|
|
667
|
+
op: 'task.close.followups-override',
|
|
668
|
+
task: taskId,
|
|
669
|
+
data: { rows: blockingSummary, reason: closeReason },
|
|
670
|
+
})
|
|
671
|
+
if (!auditLogged) {
|
|
672
|
+
process.stderr.write(
|
|
673
|
+
`[task-close] WARNING: forced followups override for "${taskId}" could not be written to the audit log — the bypass proceeded UNAUDITED.\n`,
|
|
674
|
+
)
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Gate passed (or bypassed) — now mutate.
|
|
681
|
+
if (taskInActiveMap) {
|
|
682
|
+
delete state.active_tasks[taskId]
|
|
683
|
+
} else {
|
|
684
|
+
state.active_task = null
|
|
685
|
+
}
|
|
686
|
+
|
|
578
687
|
if (!state.completed_tasks) state.completed_tasks = []
|
|
579
688
|
// C28 — preserve scope + lifecycle_phase on the completed row so
|
|
580
689
|
// INDEX.md can group completed tasks by scope and CompletedTaskRecord
|
|
@@ -591,7 +700,7 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
591
700
|
state.last_activity = new Date().toISOString()
|
|
592
701
|
atomicWriteJson(statePath, state)
|
|
593
702
|
|
|
594
|
-
regenerateTaskIndex(
|
|
703
|
+
regenerateTaskIndex(stateRoot)
|
|
595
704
|
|
|
596
705
|
// Team collaboration: remove from active board
|
|
597
706
|
updateTeamMember(targetDir)
|
|
@@ -742,7 +851,7 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
742
851
|
}
|
|
743
852
|
closeResult = ok(closeOut)
|
|
744
853
|
},
|
|
745
|
-
{ projectRoot:
|
|
854
|
+
{ projectRoot: stateRoot },
|
|
746
855
|
)
|
|
747
856
|
return closeError ?? closeResult
|
|
748
857
|
}
|
|
@@ -1054,7 +1163,7 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
1054
1163
|
}
|
|
1055
1164
|
}
|
|
1056
1165
|
|
|
1057
|
-
regenerateTaskIndex(
|
|
1166
|
+
regenerateTaskIndex(stateRoot)
|
|
1058
1167
|
|
|
1059
1168
|
// Team collaboration: sync active board with updated state
|
|
1060
1169
|
updateTeamMember(targetDir)
|
|
@@ -1084,7 +1193,7 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
1084
1193
|
}
|
|
1085
1194
|
updateResult = ok(updateOut)
|
|
1086
1195
|
},
|
|
1087
|
-
{ projectRoot:
|
|
1196
|
+
{ projectRoot: stateRoot },
|
|
1088
1197
|
)
|
|
1089
1198
|
return updateError ?? updateResult
|
|
1090
1199
|
}
|
|
@@ -1236,7 +1345,7 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
1236
1345
|
state.last_activity = new Date().toISOString()
|
|
1237
1346
|
atomicWriteJson(statePath, state)
|
|
1238
1347
|
|
|
1239
|
-
regenerateTaskIndex(
|
|
1348
|
+
regenerateTaskIndex(stateRoot)
|
|
1240
1349
|
updateTeamMember(targetDir)
|
|
1241
1350
|
store.upsertTeamStatus(taskId, {
|
|
1242
1351
|
description: task.description,
|
|
@@ -1253,7 +1362,7 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
1253
1362
|
task,
|
|
1254
1363
|
})
|
|
1255
1364
|
},
|
|
1256
|
-
{ projectRoot:
|
|
1365
|
+
{ projectRoot: stateRoot },
|
|
1257
1366
|
)
|
|
1258
1367
|
return moveError ?? moveResult
|
|
1259
1368
|
}
|
|
@@ -1272,7 +1381,10 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
1272
1381
|
const notes = flags.get('notes') || null
|
|
1273
1382
|
try {
|
|
1274
1383
|
const result = completeSubtask({
|
|
1275
|
-
|
|
1384
|
+
// state-fork-fix: tasks/<id>/ (plan + build-progress.json) is
|
|
1385
|
+
// EPHEMERAL — resolve to the main board so a complete-subtask
|
|
1386
|
+
// issued from a worktree updates the one authoritative task dir.
|
|
1387
|
+
projectDir: stateRoot,
|
|
1276
1388
|
taskId,
|
|
1277
1389
|
subtaskId: subtaskIdFlag,
|
|
1278
1390
|
commit,
|
|
@@ -1461,7 +1573,7 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
1461
1573
|
prUrlMutated = true
|
|
1462
1574
|
}
|
|
1463
1575
|
},
|
|
1464
|
-
{ projectRoot:
|
|
1576
|
+
{ projectRoot: stateRoot },
|
|
1465
1577
|
)
|
|
1466
1578
|
}
|
|
1467
1579
|
} catch {
|
|
@@ -1592,7 +1704,7 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
1592
1704
|
})
|
|
1593
1705
|
atomicWriteJson(statePath, fresh)
|
|
1594
1706
|
},
|
|
1595
|
-
{ projectRoot:
|
|
1707
|
+
{ projectRoot: stateRoot },
|
|
1596
1708
|
)
|
|
1597
1709
|
} catch (e) {
|
|
1598
1710
|
errors.push({
|
|
@@ -1647,7 +1759,7 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
1647
1759
|
}
|
|
1648
1760
|
remaining = ledger.length
|
|
1649
1761
|
},
|
|
1650
|
-
{ projectRoot:
|
|
1762
|
+
{ projectRoot: stateRoot },
|
|
1651
1763
|
)
|
|
1652
1764
|
|
|
1653
1765
|
return ok({
|
|
@@ -1712,7 +1824,7 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
1712
1824
|
skipped.push({ id: row.id, reason: 'worktree-active' })
|
|
1713
1825
|
continue
|
|
1714
1826
|
}
|
|
1715
|
-
const taskDir = getTaskDir(
|
|
1827
|
+
const taskDir = getTaskDir(stateRoot, row.id)
|
|
1716
1828
|
if (!existsSync(taskDir)) {
|
|
1717
1829
|
skipped.push({ id: row.id, reason: 'dir-missing' })
|
|
1718
1830
|
continue
|