@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
|
@@ -199,7 +199,7 @@ AskUserQuestion([
|
|
|
199
199
|
|
|
200
200
|
**Routing:**
|
|
201
201
|
- "Verification" → jump to **Step 2** (Batch 1 + Batch 2 only), then skip to **Step 4** (mapping) and **Step 5** (write)
|
|
202
|
-
- "Pipeline" → jump to **Step 3** (Batch 2: auto-branch, code review, TDD, PR Review Auto-Suggest, **Sparring Auto-Fire**) AND **Step 3b** (Batch 3 only, including Diagram Mode), then skip to **Step 4** and **Step 5**
|
|
202
|
+
- "Pipeline" → jump to **Step 3** (Batch 2: auto-branch, code review, TDD, PR Review Auto-Suggest, CI Watcher, **Sparring Auto-Fire**) AND **Step 3b** (Batch 3 only, including Diagram Mode), then skip to **Step 4** and **Step 5**
|
|
203
203
|
- "Orchestration" → jump to **Step 3c** (Batch 4 only), then skip to **Step 4** and **Step 5**
|
|
204
204
|
- "Team Collaboration" → jump to **Step 3d** (Batch 5 only), then skip to **Step 4** and **Step 5**
|
|
205
205
|
- "Visibility" → jump to **Step 3e** (Batch 6 only), then skip to **Step 4** and **Step 5**
|
|
@@ -412,6 +412,16 @@ AskUserQuestion([
|
|
|
412
412
|
{ label: "Ask me", description: "Prompt [Y/n] each time — choose per-PR." },
|
|
413
413
|
{ label: "Never", description: "Silent. Run /apt:pr-review manually if needed." }
|
|
414
414
|
]
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
question: "After /apt:ship opens a PR, run the post-ship CI watcher? It polls checks, auto-fixes the first red check and pushes, and can auto-merge once CI is green.",
|
|
418
|
+
header: "CI Watcher",
|
|
419
|
+
multiSelect: false,
|
|
420
|
+
options: [
|
|
421
|
+
{ label: "Watch & auto-fix (Recommended)", description: "Ship auto-arms the watcher: it polls CI, fixes the first red check + pushes, then stops at green for you to merge. Ticks automatically on cron-capable hosts (Claude Code/Pi); elsewhere you advance ticks manually with /apt:watch-ci --tick." },
|
|
422
|
+
{ label: "Watch, fix & auto-merge on green", description: "Same as above, plus auto-merges the PR (squash) once CI is green, then suggests /apt:close-task. Fully hands-off run." },
|
|
423
|
+
{ label: "Off", description: "Ship does not arm the watcher. Watch CI and merge yourself, or run /apt:watch-ci manually." }
|
|
424
|
+
]
|
|
415
425
|
}
|
|
416
426
|
])
|
|
417
427
|
```
|
|
@@ -728,8 +738,8 @@ AskUserQuestion([
|
|
|
728
738
|
options: [
|
|
729
739
|
{ label: "Auto", description: "Best available for your CLI" },
|
|
730
740
|
{ label: "Agent Team", description: "Uses Claude Code agent teams (requires CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1)" },
|
|
731
|
-
{ label: "Sub-Agents", description: "Spawns 6 parallel sub-agents for review (available on Claude Code and Codex)" },
|
|
732
|
-
{ label: "Sequential", description: "Runs 6 review passes inline, one at a time — works on any CLI" }
|
|
741
|
+
{ label: "Sub-Agents", description: "Spawns 6-8 parallel sub-agents for review (available on Claude Code and Codex)" },
|
|
742
|
+
{ label: "Sequential", description: "Runs 6-8 review passes inline, one at a time — works on any CLI" }
|
|
733
743
|
]
|
|
734
744
|
},
|
|
735
745
|
{
|
|
@@ -1378,6 +1388,13 @@ Map the user's selections to config values:
|
|
|
1378
1388
|
- "Ask me" → `pr_review.suggest_after_ship: "ask"`
|
|
1379
1389
|
- "Never" → `pr_review.suggest_after_ship: "never"`
|
|
1380
1390
|
|
|
1391
|
+
**CI Watcher (post-ship):** maps to the SHARED `.aperant/config.json` `ci_watch` block (team policy — NOT a per-device AUDIT-001 field, so it stays in shared `config.json`):
|
|
1392
|
+
- "Watch & auto-fix (Recommended)" → `ci_watch.after_ship: "auto"`, `ci_watch.auto_merge_on_green: false`
|
|
1393
|
+
- "Watch, fix & auto-merge on green" → `ci_watch.after_ship: "auto"`, `ci_watch.auto_merge_on_green: true`
|
|
1394
|
+
- "Off" → `ci_watch.after_ship: "never"`, `ci_watch.auto_merge_on_green: false`
|
|
1395
|
+
|
|
1396
|
+
The existing `ci_watch.merge_method` key (default `"squash"`) is preserved untouched — this question does not change it. On non-cron hosts, `after_ship: "auto"` still creates the watcher state and ship prints manual-tick instructions; the host-capability fallback already lives in `apt-ship/SKILL.md` §7.2, so the value is safe to store as team policy regardless of the setup-runner's host.
|
|
1397
|
+
|
|
1381
1398
|
**Sparring Auto-Fire (FRAMEWORK-RFC-001):**
|
|
1382
1399
|
- Phase 1 "No" → `spar.enabled: false`, ask Phase 3 (hint sub-picker)
|
|
1383
1400
|
- Phase 1 "Yes" → `spar.enabled: true`, ask Phase 2 (partner picker)
|
|
@@ -1522,6 +1539,7 @@ Display:
|
|
|
1522
1539
|
| Auto Branch | {Yes/No} |
|
|
1523
1540
|
| Code Review | {Yes/No} |
|
|
1524
1541
|
| TDD Iron Law | {Yes/No} |
|
|
1542
|
+
| CI Watcher | {Off / Watch & fix / Watch, fix & auto-merge} |
|
|
1525
1543
|
| Sparring Auto-Fire | {Off / On (codex), On (claude), On (gemini)} (hint: {Yes/No}) |
|
|
1526
1544
|
| Date Format | {DD-MM-YY / MM-DD-YY / YY-MM-DD} |
|
|
1527
1545
|
| Time Format | {24h / 12h} |
|
package/skills/apt-ship/SKILL.md
CHANGED
|
@@ -20,6 +20,7 @@ gates:
|
|
|
20
20
|
- constitution-read
|
|
21
21
|
- verify-approved
|
|
22
22
|
- review-clean
|
|
23
|
+
- followups-adjudicated
|
|
23
24
|
- consistency-check
|
|
24
25
|
- r-persona-alignment
|
|
25
26
|
- gitignore-in-sync
|
|
@@ -149,15 +150,18 @@ Capture `remote_tip_sha_pre_rebase` now — §6's lease uses it.
|
|
|
149
150
|
```
|
|
150
151
|
Proceed to §1. §6's push will use `--force-with-lease=refs/heads/<branch>:$remote_tip_sha_pre_rebase` because history was rewritten.
|
|
151
152
|
|
|
152
|
-
- **`behind > 0` AND `mergeable === false`** →
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
153
|
+
- **`behind > 0` AND `mergeable === false`** → the rebase would conflict. Default behavior is a hard STOP; AI resolution is **opt-in** via `pipeline.resolve_merge_conflicts` (default false) in `.aperant/config.json`.
|
|
154
|
+
|
|
155
|
+
- **Resolution opted in (`pipeline.resolve_merge_conflicts === true`)** → hand off to **`/apt:merge-conflict-resolution`** (`--base origin/{base_branch}`). That skill drives the rebase, resolves the conflicted files inline (reading the markers via `apt-tools merge markers`), and re-probes mergeability. On success it returns here — proceed to §1 with `--force-with-lease=refs/heads/<branch>:$remote_tip_sha_pre_rebase` (history was rewritten). On its non-convergence STOP, it aborts to a clean tree and hands back; fall through to the manual-STOP message below.
|
|
156
|
+
- **Resolution NOT opted in (default)** → STOP. Emit a structured error with the conflict preview verbatim:
|
|
157
|
+
```
|
|
158
|
+
[APT:ship] Cannot ship — task branch is behind origin/{base_branch} and the rebase would conflict.
|
|
159
|
+
Conflicts preview:
|
|
160
|
+
- {path1}
|
|
161
|
+
- {path2}
|
|
162
|
+
Resolve manually (rebase or merge in the worktree) or enable pipeline.resolve_merge_conflicts to use /apt:merge-conflict-resolution, then re-run /apt:ship.
|
|
163
|
+
```
|
|
164
|
+
Exit the skill — do NOT auto-resolve, do NOT proceed to §5 or §6.
|
|
161
165
|
|
|
162
166
|
- **`mergeable === null` AND `audit_warning === "git-version-too-old"`** → STOP with a friendlier message:
|
|
163
167
|
```
|
|
@@ -175,8 +179,8 @@ Capture `remote_tip_sha_pre_rebase` now — §6's lease uses it.
|
|
|
175
179
|
**Autonomy gating.** Read the active task's autonomy level from `.aperant/state.json` (`active_tasks[{task-id}].autonomy`, falling back to `autonomy.default`):
|
|
176
180
|
|
|
177
181
|
- **Autonomy 0–1 (step / guided)** — print the audit envelope to the operator and ask `Rebase now? [Y/n]` before executing the `git rebase` line. On `N`, exit the skill (user can rebase manually and re-invoke).
|
|
178
|
-
- **Autonomy 2 (auto-chain)** — proceed silently on the clean-rebase branch;
|
|
179
|
-
- **Autonomy 3 (YOLO)** — same as 2: silent on clean
|
|
182
|
+
- **Autonomy 2 (auto-chain)** — proceed silently on the clean-rebase branch; on conflict, hand off to `/apt:merge-conflict-resolution` when `pipeline.resolve_merge_conflicts` is enabled, else STOP.
|
|
183
|
+
- **Autonomy 3 (YOLO)** — same as 2: silent on clean; on conflict, AI-resolve via `/apt:merge-conflict-resolution` when opted in, else STOP. Even at YOLO the resolved branch still flows through PR → review → CI — apt:ship never auto-merges.
|
|
180
184
|
|
|
181
185
|
The captured `remote_tip_sha_pre_rebase` and `behind` integer are referenced verbatim in §6's push command. Treat them as read-only state for the remainder of the skill.
|
|
182
186
|
|
|
@@ -380,11 +384,21 @@ The body structure below is the full-pipeline template — unchanged from the pr
|
|
|
380
384
|
|
|
381
385
|
{If no review: "No code review was run for this task."}
|
|
382
386
|
|
|
387
|
+
## Follow-ups
|
|
388
|
+
|
|
389
|
+
{Read `{task_dir}/followups.json` and enumerate EVERY open/dispositioned row VERBATIM — never a count. One line per row:}
|
|
390
|
+
|
|
391
|
+
- [{status}] {title} — {adjudication.verdict ?? 'unadjudicated'} → {disposition.kind ?? 'pending'}
|
|
392
|
+
|
|
393
|
+
{If no followups.json: "No follow-ups recorded for this task."}
|
|
394
|
+
|
|
383
395
|
---
|
|
384
396
|
|
|
385
397
|
Built with [Aperant Framework](https://github.com/Mikalsen-AI/aperant-cloud-desktop/tree/main/packages/framework)
|
|
386
398
|
```
|
|
387
399
|
|
|
400
|
+
Counts hide work; the verbatim row list is the visibility contract (ID-16). The `followups-adjudicated` gate (Section 0) has already guaranteed every open row is adjudicated and every load-bearing row is fixed before you reach this section.
|
|
401
|
+
|
|
388
402
|
## 4. Determine Ship Mode
|
|
389
403
|
|
|
390
404
|
Read `.aperant/config.json` for `project_type`.
|
|
@@ -520,8 +534,15 @@ Review: logic={n} security={n} quality={n} architecture={n} performance={n}
|
|
|
520
534
|
Commits: {count}
|
|
521
535
|
Review findings: {count by severity or "none"}
|
|
522
536
|
Decisions traced: {count}
|
|
537
|
+
|
|
538
|
+
Follow-ups:
|
|
539
|
+
{Enumerate EVERY open/dispositioned row from {task_dir}/followups.json VERBATIM,
|
|
540
|
+
one per line — `- [{status}] {title} — {verdict} → {disposition.kind ?? 'pending'}`.
|
|
541
|
+
Never a count. "none" only when followups.json is absent or has zero rows.}
|
|
523
542
|
```
|
|
524
543
|
|
|
544
|
+
The report's Follow-ups block mirrors the PR-body `## Follow-ups` section (ID-16) — rows verbatim, never counts.
|
|
545
|
+
|
|
525
546
|
### 7.1 Post-ship /apt:pr-review hint
|
|
526
547
|
|
|
527
548
|
After printing the ship summary, conditionally emit a tip suggesting `/apt:pr-review` for a deep multi-agent review of the freshly-opened PR. The hint is controlled by `pr_review.suggest_after_ship` in `.aperant/config.json` (tri-state: `always` | `ask` | `never`; default when absent: `always`).
|
|
@@ -97,6 +97,14 @@ The response is JSON to stdout. On success:
|
|
|
97
97
|
|
|
98
98
|
`depth` is the new terminal's level (caller_depth + 1). Record `sessionId` in your task notes.
|
|
99
99
|
|
|
100
|
+
The worktree the spawned terminal opens in already has the project's stored env
|
|
101
|
+
blobs materialized (the per-project encrypted env store, #354) — worktree-create
|
|
102
|
+
copies each `.env*` blob verbatim (traversal-guarded, best-effort) so the sibling
|
|
103
|
+
terminal's CLI runs with the real env instead of crashing in no-cloud mode. If a
|
|
104
|
+
long-lived worktree's stored env later drifts, re-copy on demand with
|
|
105
|
+
`node packages/framework/bin/apt-tools.mjs worktree sync-env <dir> --task <id>`
|
|
106
|
+
(the store is the source of truth — sync-env overwrites, never merges).
|
|
107
|
+
|
|
100
108
|
## 4. Recognise the graceful-degrade error envelopes
|
|
101
109
|
|
|
102
110
|
These are **expected** error shapes — do not retry blindly. Each one tells you exactly which obligation discharged:
|
|
@@ -49,6 +49,10 @@ OUT-OF-SCOPE.md sub-files ported from
|
|
|
49
49
|
- An inbound task / bug / feature request needs status assignment
|
|
50
50
|
- Periodic review of `needs-triage` tasks
|
|
51
51
|
- Re-triage when new info arrives on a `needs-info` task
|
|
52
|
+
- A `new-task` follow-up disposition (G45) — `/apt:close-task` files an open
|
|
53
|
+
follow-up as a fresh task and enters it here at `needs-triage` via
|
|
54
|
+
`apt-tools triage init` (the configured backend mirrors it). This is a normal
|
|
55
|
+
entry point; the chained-task depth marker rides `triage.json` `extra.followups_chain`.
|
|
52
56
|
|
|
53
57
|
## When NOT to Use
|
|
54
58
|
|
|
@@ -266,6 +266,30 @@ for ITERATION in 1 2 3 4:
|
|
|
266
266
|
|
|
267
267
|
**Exhaustion:** If issues remain after 4 iterations, mark them in `qa_signoff.json` and report them. The verdict should reflect the remaining issues.
|
|
268
268
|
|
|
269
|
+
## 6.5 Follow-ups Adjudication (mandatory — G45)
|
|
270
|
+
|
|
271
|
+
This pass is **mandatory** on every full-pipeline verify. It is the owner of follow-up adjudication: `apt:verify` always runs in the pipeline (`apt:run` = plan → execute → verify → review), whereas `apt:verify-proof` is opt-in and therefore cannot be the sole owner. (When `verify-proof` runs later it MAY overwrite an adjudication with stronger evidence, `by: 'verify-proof'`.)
|
|
272
|
+
|
|
273
|
+
Run the deterministic check first:
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
node packages/framework/bin/apt-tools.mjs followups check . --task-id {task-id}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
The envelope returns `missing_expected[]` — the derived expected universe (parsed `## Out of Scope` bullets ∪ raw review rows ∪ qa findings, `status !== 'fixed'`) that the ledger does not yet cover. Create/sync `{task_dir}/followups.json` rows **mechanically** from those payloads:
|
|
280
|
+
|
|
281
|
+
- Append one row per `missing_expected[]` entry: `{ id, title, origin, class_proposed: <class_hint>, files: [], outcome_refs: [], status: 'open' }`. `class_proposed` is only a PROPOSAL (ID-03) — the verdict below is the authority; never let the class self-certify a deferral.
|
|
282
|
+
- Voluntary rows (origin `discuss | execute | ci`) may also be present; they are adjudicated the same way.
|
|
283
|
+
|
|
284
|
+
Then **adjudicate every open row**. For each, answer the single question:
|
|
285
|
+
|
|
286
|
+
> "Would the feature satisfy its stated `## User Outcomes` right now WITHOUT this follow-up?"
|
|
287
|
+
|
|
288
|
+
- If NO → `adjudication: { verdict: 'load-bearing', by: 'verify', outcome_ref: '<O-n>' }`. A load-bearing row blocks ship until it is fixed (flip `status: 'fixed'` once the work lands in this PR's fix loop) or bypassed via `--force --reason`.
|
|
289
|
+
- If YES → `adjudication: { verdict: 'deferrable', by: 'verify', outcome_ref: '<O-n> | null' }`. `outcome_ref` is the `O<n>` id (from `parseUserOutcomes`) the row relates to; it is nullable for refactor-only specs that declare no outcomes.
|
|
290
|
+
|
|
291
|
+
Re-run `followups check` until it is **derivation-clean** (exit 0 — no `missing_expected`, no `invalid_outcome_refs`, no `schema_errors`) BEFORE writing `qa_signoff.json`. The `followups-adjudicated` ship gate will block the PR otherwise.
|
|
292
|
+
|
|
269
293
|
## 7. Write QA Sign-off
|
|
270
294
|
|
|
271
295
|
Create `qa_signoff.json` using the `ScoredQASignoff` format (see `packages/framework/src/types/qa-scoring.ts`):
|
|
@@ -125,7 +125,9 @@ When `action === 'fix'` (or `diverged-rebased`/`diverged-bailed`):
|
|
|
125
125
|
5. **Emit event.** `apt-tools event append . --type ci-watch.fix-pushed --data '{pr, attempts, commit_sha}'`.
|
|
126
126
|
6. **Reschedule** per §3 step 4.
|
|
127
127
|
|
|
128
|
-
For `diverged-rebased`: the cmd already returned the verb; the orchestrator runs `git -C {worktree} fetch && git rebase origin/{base}` BEFORE the fix step. On conflict the action becomes `diverged-bailed
|
|
128
|
+
For `diverged-rebased`: the cmd already returned the verb; the orchestrator runs `git -C {worktree} fetch && git rebase origin/{base}` BEFORE the fix step. On conflict the action becomes `diverged-bailed`.
|
|
129
|
+
|
|
130
|
+
For `diverged-bailed`: default behavior is to exit with a PR comment summarizing the conflict. Self-heal is **opt-in** via `ci_watch.resolve_merge_conflicts` (default false) in `.aperant/config.json`. When enabled, instead of bailing, hand off to **`/apt:merge-conflict-resolution`** (`--base origin/{base}`) for the worktree; it resolves the rebase conflict inline and re-probes mergeability. On success, continue to the §5 fix-and-push step (`git push origin {branch}`) and reschedule per §3 — the watcher keeps running. On its non-convergence STOP (it aborts the rebase to a clean tree first), fall back to the bail-with-PR-comment path. The resolved branch still goes through CI; the watcher never auto-merges except via the existing config-gated `gh pr merge --auto`.
|
|
129
131
|
|
|
130
132
|
---
|
|
131
133
|
|
|
@@ -42,7 +42,7 @@ import { err, ok } from '../util/result.mjs'
|
|
|
42
42
|
* @param {string} cwd
|
|
43
43
|
* @param {string[]} args
|
|
44
44
|
*/
|
|
45
|
-
function gitTry(cwd, args) {
|
|
45
|
+
export function gitTry(cwd, args) {
|
|
46
46
|
try {
|
|
47
47
|
const stdout = execFileSync('git', args, {
|
|
48
48
|
cwd,
|
|
@@ -94,7 +94,7 @@ function parseBaseFlag(extraArgs) {
|
|
|
94
94
|
* @param {string} stdout
|
|
95
95
|
* @returns {string[]}
|
|
96
96
|
*/
|
|
97
|
-
function parseConflictPaths(stdout) {
|
|
97
|
+
export function parseConflictPaths(stdout) {
|
|
98
98
|
const paths = new Set()
|
|
99
99
|
for (const rawLine of stdout.split('\n')) {
|
|
100
100
|
const line = rawLine.replace(/\0/g, '')
|
|
@@ -125,7 +125,7 @@ function parseConflictPaths(stdout) {
|
|
|
125
125
|
* @param {string} stderr
|
|
126
126
|
* @returns {boolean}
|
|
127
127
|
*/
|
|
128
|
-
function isGitVersionTooOld(stderr) {
|
|
128
|
+
export function isGitVersionTooOld(stderr) {
|
|
129
129
|
return /unknown option|did you mean|usage: git merge-tree/i.test(stderr)
|
|
130
130
|
}
|
|
131
131
|
|
|
@@ -8,9 +8,9 @@ import { join, resolve } from 'node:path'
|
|
|
8
8
|
import { readLifecycleEventsForDay } from '../coordination/event-log.mjs'
|
|
9
9
|
import { validateConductorEvent } from '../coordination/event-schema.mjs'
|
|
10
10
|
import { createCoordinationStore } from '../coordination/store.mjs'
|
|
11
|
-
import { detectWorktree } from '../git/worktree-detect.mjs'
|
|
12
11
|
import { parseFlags } from '../util/args.mjs'
|
|
13
12
|
import { err, ok } from '../util/result.mjs'
|
|
13
|
+
import { resolveMainRepoRoot } from '../util/state-paths.mjs'
|
|
14
14
|
|
|
15
15
|
// QUA-003 / PRF-002 — single source of truth for valid event types.
|
|
16
16
|
// VALID_EVENT_TYPES is the authoritative list; CI_WATCH_EVENT_TYPES is the O(1)
|
|
@@ -88,9 +88,11 @@ export function cmdEvent(subcommand, projectDir, extraArgs) {
|
|
|
88
88
|
if (!projectDir) return err('Usage: apt-tools event ' + subcommand + ' <project-dir> [flags]')
|
|
89
89
|
|
|
90
90
|
const targetDir = resolve(projectDir)
|
|
91
|
-
// Use shared event dir (coordinates across worktrees
|
|
92
|
-
|
|
93
|
-
|
|
91
|
+
// Use shared event dir (coordinates across worktrees, same as locks). This is
|
|
92
|
+
// a SEPARATE policy wrapper over the shared resolveMainRepoRoot mechanism
|
|
93
|
+
// (state-fork-fix ID-03) — events do NOT follow resolveStateDir's Phase-2
|
|
94
|
+
// home relocation, so the resolution stays distinct from resolveStateDir.
|
|
95
|
+
const eventsBase = resolveMainRepoRoot(targetDir)
|
|
94
96
|
const eventsDir = join(eventsBase, '.aperant', 'events')
|
|
95
97
|
mkdirSync(eventsDir, { recursive: true })
|
|
96
98
|
|
|
@@ -14,6 +14,7 @@ import { existsSync, readFileSync } from 'node:fs'
|
|
|
14
14
|
import { join, resolve } from 'node:path'
|
|
15
15
|
import { parseFlags } from '../util/args.mjs'
|
|
16
16
|
import { err, ok } from '../util/result.mjs'
|
|
17
|
+
import { resolveMainRepoRoot } from '../util/state-paths.mjs'
|
|
17
18
|
|
|
18
19
|
const ID_PATTERN = /^[a-z0-9_-]+$/i
|
|
19
20
|
|
|
@@ -35,7 +36,8 @@ function parseIdList(raw) {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
function readState(targetDir) {
|
|
38
|
-
|
|
39
|
+
// state-fork-fix: ephemeral state read resolves to the main board.
|
|
40
|
+
const statePath = join(resolveMainRepoRoot(targetDir), '.aperant', 'state.json')
|
|
39
41
|
if (!existsSync(statePath)) return null
|
|
40
42
|
try {
|
|
41
43
|
return JSON.parse(readFileSync(statePath, 'utf-8'))
|
|
@@ -30,6 +30,7 @@ import { loadDocsPolicy, matchSurfaceToPolicy } from '../docs-audit/policy.mjs'
|
|
|
30
30
|
import { applyStubPatches, auditFeatureRegistry } from '../features/registry-audit.mjs'
|
|
31
31
|
import { parseFlags } from '../util/args.mjs'
|
|
32
32
|
import { err, ok } from '../util/result.mjs'
|
|
33
|
+
import { resolveMainRepoRoot } from '../util/state-paths.mjs'
|
|
33
34
|
|
|
34
35
|
const TERMINAL_PHASES = new Set(['completed', 'cancelled', 'closed', 'shipped', 'merged'])
|
|
35
36
|
|
|
@@ -138,7 +139,11 @@ function isWorktreeRoot(projectDir) {
|
|
|
138
139
|
}
|
|
139
140
|
|
|
140
141
|
function readActiveTasks(projectDir) {
|
|
141
|
-
|
|
142
|
+
// state-fork-fix: read the AUTHORITATIVE main board — readActiveTasks finds
|
|
143
|
+
// each task's worktree_path for the BUG-025b feature-write redirect, so it
|
|
144
|
+
// MUST read main or the redirect-target lookup itself forks. (The feature
|
|
145
|
+
// WRITE path / resolveWriteRoot stays untouched.)
|
|
146
|
+
const statePath = join(resolveMainRepoRoot(projectDir), '.aperant', 'state.json')
|
|
142
147
|
if (!existsSync(statePath)) return {}
|
|
143
148
|
try {
|
|
144
149
|
const parsed = JSON.parse(readFileSync(statePath, 'utf-8'))
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commands/followups.mjs — `apt-tools followups` (G45).
|
|
3
|
+
*
|
|
4
|
+
* The deterministic surface for the per-task follow-ups ledger
|
|
5
|
+
* ({task_dir}/followups.json). Three subcommands:
|
|
6
|
+
*
|
|
7
|
+
* apt-tools followups check <project-dir> --task-id <id>
|
|
8
|
+
* Derive the expected candidate universe (Out of Scope ∪ raw review rows ∪
|
|
9
|
+
* qa findings) and report what the ledger is missing / unadjudicated /
|
|
10
|
+
* unfixed / undispositioned. Exits 1 when the story is dishonest.
|
|
11
|
+
*
|
|
12
|
+
* apt-tools followups disposition <project-dir> --task-id <id> --row <row-id>
|
|
13
|
+
* --kind <filed_task|executed_commit|parked_note|dropped_by_user> --ref <ref>
|
|
14
|
+
* [--reason <text>]
|
|
15
|
+
* Flip a row to `dispositioned` with the validated disposition record
|
|
16
|
+
* (atomic write). `dropped_by_user` additionally requires --reason.
|
|
17
|
+
*
|
|
18
|
+
* apt-tools followups policy <project-dir> --task-id <id>
|
|
19
|
+
* Return the effective disposition policy (ask | file | chain +
|
|
20
|
+
* max_chain_depth) from config.followups when set, else autonomy-derived.
|
|
21
|
+
*
|
|
22
|
+
* Shared-root resolution + taskDir + taskRecord + config all come from
|
|
23
|
+
* buildGateContext so a worktree-resident invocation reads/writes the same
|
|
24
|
+
* followups.json the gates read (the L5 worktree concern, plan-approval.mjs).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
28
|
+
import { join, resolve } from 'node:path'
|
|
29
|
+
|
|
30
|
+
import { parseUserOutcomes } from '../coverage-check/user-outcomes.mjs'
|
|
31
|
+
import {
|
|
32
|
+
computeMissingExpected,
|
|
33
|
+
deriveExpectedCandidates,
|
|
34
|
+
validateOutcomeRefs,
|
|
35
|
+
} from '../followups/derive.mjs'
|
|
36
|
+
import {
|
|
37
|
+
DISPOSITION_KINDS,
|
|
38
|
+
listLoadBearingUnfixed,
|
|
39
|
+
listUnadjudicatedOpen,
|
|
40
|
+
listUndispositioned,
|
|
41
|
+
parseFollowups,
|
|
42
|
+
} from '../followups/schema.mjs'
|
|
43
|
+
import { buildGateContext } from '../gate/context.mjs'
|
|
44
|
+
import { parseFlags } from '../util/args.mjs'
|
|
45
|
+
import { atomicWriteJson, readJsonIfExists } from '../util/io.mjs'
|
|
46
|
+
import { err, exitWith, ok } from '../util/result.mjs'
|
|
47
|
+
|
|
48
|
+
const SUBCOMMANDS = new Set(['check', 'disposition', 'policy'])
|
|
49
|
+
|
|
50
|
+
function readTextIfExists(path) {
|
|
51
|
+
if (!existsSync(path)) return null
|
|
52
|
+
try {
|
|
53
|
+
return readFileSync(path, 'utf-8')
|
|
54
|
+
} catch {
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Entry: `apt-tools followups <subcommand> <project-dir> [flags]`.
|
|
61
|
+
*/
|
|
62
|
+
export function cmdFollowups(subcommand, projectDir, extraArgs) {
|
|
63
|
+
if (!subcommand || !SUBCOMMANDS.has(subcommand)) {
|
|
64
|
+
return err(
|
|
65
|
+
'Usage: apt-tools followups <check|disposition|policy> <project-dir> --task-id <id> [...]',
|
|
66
|
+
1,
|
|
67
|
+
{ command: 'followups' },
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
if (!projectDir) {
|
|
71
|
+
return err('project-dir is required', 1, { command: `followups-${subcommand}` })
|
|
72
|
+
}
|
|
73
|
+
const absProjectDir = resolve(projectDir)
|
|
74
|
+
if (!existsSync(absProjectDir)) {
|
|
75
|
+
return err(`project directory not found: ${absProjectDir}`, 1, {
|
|
76
|
+
command: `followups-${subcommand}`,
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const flags = parseFlags(extraArgs ?? [])
|
|
81
|
+
const taskId = flags.get('task-id')
|
|
82
|
+
if (typeof taskId !== 'string' || taskId.trim() === '') {
|
|
83
|
+
return err('--task-id <id> is required', 1, { command: `followups-${subcommand}` })
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let ctx
|
|
87
|
+
try {
|
|
88
|
+
ctx = buildGateContext(absProjectDir, taskId)
|
|
89
|
+
} catch (e) {
|
|
90
|
+
return err(`task not found: ${taskId} (${e?.message ?? String(e)})`, 1, {
|
|
91
|
+
command: `followups-${subcommand}`,
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (subcommand === 'check') return doCheck(ctx)
|
|
96
|
+
if (subcommand === 'disposition') return doDisposition(ctx, flags)
|
|
97
|
+
return doPolicy(ctx)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* `followups check` — the deterministic adjudication-readiness envelope (AC5).
|
|
102
|
+
*/
|
|
103
|
+
function doCheck(ctx) {
|
|
104
|
+
const followupsPath = join(ctx.taskDir, 'followups.json')
|
|
105
|
+
const specPath = join(ctx.taskDir, 'spec.md')
|
|
106
|
+
const reviewPath = join(ctx.taskDir, 'review.json')
|
|
107
|
+
const qaPath = join(ctx.taskDir, 'qa_signoff.json')
|
|
108
|
+
|
|
109
|
+
const specContent = readTextIfExists(specPath)
|
|
110
|
+
const review = readJsonIfExists(reviewPath)
|
|
111
|
+
const qa = readJsonIfExists(qaPath)
|
|
112
|
+
|
|
113
|
+
const candidates = deriveExpectedCandidates({ specContent, review, qa })
|
|
114
|
+
const ledgerDoc = readJsonIfExists(followupsPath)
|
|
115
|
+
const ledger_present = ledgerDoc !== null
|
|
116
|
+
|
|
117
|
+
const parsed = parseFollowups(ledgerDoc ?? { rows: [] })
|
|
118
|
+
const parsedOutcomes = parseUserOutcomes(specContent ?? '')
|
|
119
|
+
|
|
120
|
+
const missing_expected = computeMissingExpected(candidates, parsed.rows)
|
|
121
|
+
const invalid_outcome_refs = validateOutcomeRefs(parsed.rows, parsedOutcomes)
|
|
122
|
+
const unadjudicated_open = listUnadjudicatedOpen(parsed)
|
|
123
|
+
const load_bearing_unfixed = listLoadBearingUnfixed(parsed)
|
|
124
|
+
const undispositioned = listUndispositioned(parsed)
|
|
125
|
+
|
|
126
|
+
// Exit 1 when the ledger is absent while candidates exist (adoption forcing
|
|
127
|
+
// function), or when derivation/schema/outcome-ref validation fails. The
|
|
128
|
+
// unadjudicated / load-bearing / undispositioned lists are reported but do
|
|
129
|
+
// NOT fail `check` directly — those are gate concerns (ship gate / close
|
|
130
|
+
// gate) so an in-progress verify pass can iterate without `check` flapping.
|
|
131
|
+
const derivationFailure =
|
|
132
|
+
(!ledger_present && candidates.length > 0) ||
|
|
133
|
+
missing_expected.length > 0 ||
|
|
134
|
+
invalid_outcome_refs.length > 0 ||
|
|
135
|
+
parsed.schema_errors.length > 0
|
|
136
|
+
|
|
137
|
+
const envelope = {
|
|
138
|
+
status: derivationFailure ? 'fail' : 'ok',
|
|
139
|
+
command: 'followups-check',
|
|
140
|
+
task_id: ctx.taskId,
|
|
141
|
+
ledger_present,
|
|
142
|
+
expected_total: candidates.length,
|
|
143
|
+
ledger_rows: parsed.rows.length,
|
|
144
|
+
schema_errors: parsed.schema_errors,
|
|
145
|
+
missing_expected,
|
|
146
|
+
invalid_outcome_refs,
|
|
147
|
+
unadjudicated_open: unadjudicated_open.map(rowSummary),
|
|
148
|
+
load_bearing_unfixed: load_bearing_unfixed.map(rowSummary),
|
|
149
|
+
undispositioned: undispositioned.map(rowSummary),
|
|
150
|
+
}
|
|
151
|
+
return exitWith(envelope, derivationFailure ? 1 : 0)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* `followups disposition` — flip a row to dispositioned (AC7).
|
|
156
|
+
*/
|
|
157
|
+
function doDisposition(ctx, flags) {
|
|
158
|
+
const rowId = flags.get('row')
|
|
159
|
+
if (typeof rowId !== 'string' || rowId.trim() === '') {
|
|
160
|
+
return err('--row <row-id> is required', 1, { command: 'followups-disposition' })
|
|
161
|
+
}
|
|
162
|
+
const kind = flags.get('kind')
|
|
163
|
+
if (!DISPOSITION_KINDS.has(kind)) {
|
|
164
|
+
return err(`--kind must be one of: ${[...DISPOSITION_KINDS].join(', ')}`, 1, {
|
|
165
|
+
command: 'followups-disposition',
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
const ref = flags.get('ref')
|
|
169
|
+
if (typeof ref !== 'string' || ref.trim() === '') {
|
|
170
|
+
return err('--ref <ref> is required (non-empty)', 1, { command: 'followups-disposition' })
|
|
171
|
+
}
|
|
172
|
+
const reasonRaw = flags.get('reason')
|
|
173
|
+
const reason = typeof reasonRaw === 'string' ? reasonRaw : null
|
|
174
|
+
if (kind === 'dropped_by_user' && !reason) {
|
|
175
|
+
return err('--kind dropped_by_user requires --reason "<text>"', 1, {
|
|
176
|
+
command: 'followups-disposition',
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const followupsPath = join(ctx.taskDir, 'followups.json')
|
|
181
|
+
const ledgerDoc = readJsonIfExists(followupsPath)
|
|
182
|
+
if (ledgerDoc === null) {
|
|
183
|
+
return err(`followups.json not found for ${ctx.taskId}`, 1, {
|
|
184
|
+
command: 'followups-disposition',
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
const parsed = parseFollowups(ledgerDoc)
|
|
188
|
+
// Fail closed on schema errors: the write-back below re-serializes only the
|
|
189
|
+
// validated `parsed.rows`, so a single malformed row elsewhere in the ledger
|
|
190
|
+
// would be silently dropped when dispositioning a different row. Refuse and
|
|
191
|
+
// surface the errors so the operator fixes them first (no silent data loss).
|
|
192
|
+
if (parsed.schema_errors.length > 0) {
|
|
193
|
+
return err(
|
|
194
|
+
`followups.json has ${parsed.schema_errors.length} malformed row(s) — fix them before dispositioning (a write-back would drop them)`,
|
|
195
|
+
1,
|
|
196
|
+
{ command: 'followups-disposition', schema_errors: parsed.schema_errors },
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
const target = parsed.rows.find((r) => r.id === rowId)
|
|
200
|
+
if (!target) {
|
|
201
|
+
return err(`unknown row id: ${rowId}`, 1, {
|
|
202
|
+
command: 'followups-disposition',
|
|
203
|
+
known_rows: parsed.rows.map((r) => r.id),
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Atomic flip: re-serialize the full validated row set with the target's
|
|
208
|
+
// status + disposition updated. The on-disk shape is `{ rows: [...] }`.
|
|
209
|
+
const disposition = { kind, ref, ...(reason ? { reason } : {}) }
|
|
210
|
+
const nextRows = parsed.rows.map((r) =>
|
|
211
|
+
r.id === rowId ? { ...r, status: 'dispositioned', disposition } : r,
|
|
212
|
+
)
|
|
213
|
+
atomicWriteJson(followupsPath, { rows: nextRows })
|
|
214
|
+
|
|
215
|
+
return ok({
|
|
216
|
+
status: 'ok',
|
|
217
|
+
command: 'followups-disposition',
|
|
218
|
+
task_id: ctx.taskId,
|
|
219
|
+
row: rowId,
|
|
220
|
+
disposition,
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* `followups policy` — effective disposition policy (AC8 / ID-11).
|
|
226
|
+
*/
|
|
227
|
+
function doPolicy(ctx) {
|
|
228
|
+
const followupsCfg = ctx.config?.followups ?? {}
|
|
229
|
+
const explicit = followupsCfg.after_close
|
|
230
|
+
|
|
231
|
+
// Resolve the autonomy level: task record first, then config default, else 1.
|
|
232
|
+
const autonomy =
|
|
233
|
+
typeof ctx.taskRecord?.autonomy === 'number'
|
|
234
|
+
? ctx.taskRecord.autonomy
|
|
235
|
+
: typeof ctx.config?.autonomy?.default === 'number'
|
|
236
|
+
? ctx.config.autonomy.default
|
|
237
|
+
: 1
|
|
238
|
+
|
|
239
|
+
let after_close
|
|
240
|
+
let source
|
|
241
|
+
if (explicit === 'ask' || explicit === 'file' || explicit === 'chain') {
|
|
242
|
+
after_close = explicit
|
|
243
|
+
source = 'config'
|
|
244
|
+
} else {
|
|
245
|
+
after_close = autonomy <= 1 ? 'ask' : autonomy === 2 ? 'file' : 'chain'
|
|
246
|
+
source = 'autonomy'
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// max_chain_depth: default 1; clamp values > 1 to 1 with a warning (v1).
|
|
250
|
+
let max_chain_depth = 1
|
|
251
|
+
let clamp_warning = null
|
|
252
|
+
const rawDepth = followupsCfg.max_chain_depth
|
|
253
|
+
if (typeof rawDepth === 'number' && Number.isFinite(rawDepth)) {
|
|
254
|
+
if (rawDepth > 1) {
|
|
255
|
+
clamp_warning = `max_chain_depth ${rawDepth} clamped to 1 (v1 supports depth-1 only)`
|
|
256
|
+
} else if (rawDepth >= 1) {
|
|
257
|
+
max_chain_depth = 1
|
|
258
|
+
} else {
|
|
259
|
+
// depth < 1 means no chaining; degrade chain → file.
|
|
260
|
+
max_chain_depth = 0
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Anti-perpetual-motion: degrade chain → file when the CURRENT task's
|
|
265
|
+
// triage.json marks it as already a chained descendant at/over the cap.
|
|
266
|
+
const triagePath = join(ctx.taskDir, 'triage.json')
|
|
267
|
+
const triage = readJsonIfExists(triagePath)
|
|
268
|
+
const chainDepth = triage?.extra?.followups_chain?.depth
|
|
269
|
+
let degraded = false
|
|
270
|
+
if (after_close === 'chain' && typeof chainDepth === 'number' && chainDepth >= max_chain_depth) {
|
|
271
|
+
after_close = 'file'
|
|
272
|
+
degraded = true
|
|
273
|
+
}
|
|
274
|
+
// A 0 max_chain_depth (depth < 1 config) also degrades chain → file.
|
|
275
|
+
if (after_close === 'chain' && max_chain_depth < 1) {
|
|
276
|
+
after_close = 'file'
|
|
277
|
+
degraded = true
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const envelope = {
|
|
281
|
+
status: 'ok',
|
|
282
|
+
command: 'followups-policy',
|
|
283
|
+
task_id: ctx.taskId,
|
|
284
|
+
after_close,
|
|
285
|
+
max_chain_depth,
|
|
286
|
+
source,
|
|
287
|
+
autonomy,
|
|
288
|
+
}
|
|
289
|
+
if (clamp_warning) envelope.warning = clamp_warning
|
|
290
|
+
if (degraded) envelope.degraded_from_chain = true
|
|
291
|
+
return ok(envelope)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function rowSummary(r) {
|
|
295
|
+
return {
|
|
296
|
+
id: r.id,
|
|
297
|
+
title: r.title,
|
|
298
|
+
status: r.status,
|
|
299
|
+
class_proposed: r.class_proposed,
|
|
300
|
+
verdict: r.adjudication?.verdict ?? null,
|
|
301
|
+
disposition_kind: r.disposition?.kind ?? null,
|
|
302
|
+
}
|
|
303
|
+
}
|
|
@@ -9,6 +9,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
9
9
|
import { getSharedLockDir } from '../util/project.mjs'
|
|
10
10
|
import { err, ok } from '../util/result.mjs'
|
|
11
11
|
import { readSkillInstallState } from '../util/skill-installs.mjs'
|
|
12
|
+
import { resolveMainRepoRoot } from '../util/state-paths.mjs'
|
|
12
13
|
|
|
13
14
|
const __filename = fileURLToPath(import.meta.url)
|
|
14
15
|
const __dirname = dirname(__filename)
|
|
@@ -70,7 +71,8 @@ export function cmdHealthCheck(projectDir) {
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
// 2. Check for stale active_task in state.json
|
|
73
|
-
|
|
74
|
+
// state-fork-fix: state.json is EPHEMERAL — resolve to the main board.
|
|
75
|
+
const statePath = join(resolveMainRepoRoot(targetDir), '.aperant', 'state.json')
|
|
74
76
|
if (existsSync(statePath)) {
|
|
75
77
|
try {
|
|
76
78
|
const state = JSON.parse(readFileSync(statePath, 'utf-8'))
|