@aperant/framework 0.7.3 → 0.8.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 +66 -0
- package/agents/apt-executor.md +12 -0
- package/agents/apt-team-docs-narrator.md +11 -0
- package/dist/cli/ci-watch/stop-matrix.d.mts +17 -4
- package/dist/cli/ci-watch/stop-matrix.d.mts.map +1 -1
- package/dist/cli/ci-watch/stop-matrix.mjs +41 -4
- package/dist/cli/ci-watch/stop-matrix.mjs.map +1 -1
- package/dist/cli/commands/fan-out.d.mts +2 -0
- package/dist/cli/commands/fan-out.d.mts.map +1 -0
- package/dist/cli/commands/fan-out.mjs +123 -0
- package/dist/cli/commands/fan-out.mjs.map +1 -0
- package/dist/cli/commands/modes.d.mts +22 -0
- package/dist/cli/commands/modes.d.mts.map +1 -1
- package/dist/cli/commands/modes.mjs +4 -1
- package/dist/cli/commands/modes.mjs.map +1 -1
- package/dist/cli/commands/pr-review-audit-fixer.d.mts.map +1 -1
- package/dist/cli/commands/pr-review-audit-fixer.mjs +86 -6
- package/dist/cli/commands/pr-review-audit-fixer.mjs.map +1 -1
- package/dist/cli/commands/roadmap.d.mts +1 -1
- package/dist/cli/commands/roadmap.d.mts.map +1 -1
- package/dist/cli/commands/roadmap.mjs +47 -4
- package/dist/cli/commands/roadmap.mjs.map +1 -1
- package/dist/cli/commands/task.d.mts.map +1 -1
- package/dist/cli/commands/task.mjs +15 -0
- package/dist/cli/commands/task.mjs.map +1 -1
- package/dist/cli/commands/validate-evidence.d.mts +1 -1
- package/dist/cli/commands/validate-evidence.d.mts.map +1 -1
- package/dist/cli/commands/validate-evidence.mjs +72 -47
- package/dist/cli/commands/validate-evidence.mjs.map +1 -1
- package/dist/cli/commands/wave.d.mts.map +1 -1
- package/dist/cli/commands/wave.mjs +91 -61
- package/dist/cli/commands/wave.mjs.map +1 -1
- package/dist/cli/commands/worktree.mjs +62 -3
- package/dist/cli/commands/worktree.mjs.map +1 -1
- package/dist/cli/config/load.mjs +1 -1
- package/dist/cli/config/load.mjs.map +1 -1
- package/dist/cli/config/parallelization.d.mts +14 -0
- package/dist/cli/config/parallelization.d.mts.map +1 -0
- package/dist/cli/config/parallelization.mjs +19 -0
- package/dist/cli/config/parallelization.mjs.map +1 -0
- package/dist/cli/coordination/agent-identity.d.mts.map +1 -1
- package/dist/cli/coordination/agent-identity.mjs +7 -1
- package/dist/cli/coordination/agent-identity.mjs.map +1 -1
- package/dist/cli/dispatch.d.mts.map +1 -1
- package/dist/cli/dispatch.mjs +2 -0
- package/dist/cli/dispatch.mjs.map +1 -1
- package/dist/cli/roadmap/convex-mirror.d.mts +21 -0
- package/dist/cli/roadmap/convex-mirror.d.mts.map +1 -0
- package/dist/cli/roadmap/convex-mirror.mjs +133 -0
- package/dist/cli/roadmap/convex-mirror.mjs.map +1 -0
- package/dist/cli/roadmap/lifecycle.d.mts +1 -1
- package/dist/cli/roadmap/lifecycle.d.mts.map +1 -1
- package/dist/cli/roadmap/lifecycle.mjs +54 -0
- package/dist/cli/roadmap/lifecycle.mjs.map +1 -1
- package/dist/cli/wfrun/unit-shape.d.mts +27 -0
- package/dist/cli/wfrun/unit-shape.d.mts.map +1 -0
- package/dist/cli/wfrun/unit-shape.mjs +38 -0
- package/dist/cli/wfrun/unit-shape.mjs.map +1 -0
- package/package.json +1 -1
- package/skills/apt-execute/SKILL.md +13 -0
- package/skills/apt-fan-out/SKILL.md +124 -0
- package/skills/apt-pr-review/SKILL.md +8 -1
- package/skills/apt-review/SKILL.md +4 -0
- package/skills/apt-run/SKILL.md +20 -5
- package/skills/apt-spar/SKILL.md +12 -9
- package/skills/apt-verify/SKILL.md +11 -0
- package/skills/apt-watch-ci/SKILL.md +2 -2
- package/src/cli/ci-watch/stop-matrix.mjs +44 -4
- package/src/cli/commands/fan-out.mjs +129 -0
- package/src/cli/commands/modes.mjs +4 -1
- package/src/cli/commands/pr-review-audit-fixer.mjs +82 -6
- package/src/cli/commands/roadmap.mjs +48 -4
- package/src/cli/commands/task.mjs +15 -0
- package/src/cli/commands/validate-evidence.mjs +75 -50
- package/src/cli/commands/wave.mjs +91 -58
- package/src/cli/commands/worktree.mjs +56 -3
- package/src/cli/config/load.mjs +1 -1
- package/src/cli/config/parallelization.mjs +18 -0
- package/src/cli/coordination/agent-identity.mjs +9 -1
- package/src/cli/dispatch.mjs +2 -0
- package/src/cli/roadmap/convex-mirror.mjs +131 -0
- package/src/cli/roadmap/lifecycle.mjs +48 -0
- package/src/cli/wfrun/unit-shape.mjs +39 -0
- package/templates/config.json +1 -1
package/skills/apt-run/SKILL.md
CHANGED
|
@@ -159,6 +159,10 @@ Execute the apt:execute workflow inline:
|
|
|
159
159
|
d. Commit atomically
|
|
160
160
|
e. Update subtask status to "completed"
|
|
161
161
|
3. Write build-progress.json
|
|
162
|
+
4. Flip lifecycle to `verifying` so Stage 3 finds the task in the correct from-state (idempotent; quick tasks no-op):
|
|
163
|
+
```bash
|
|
164
|
+
node packages/framework/bin/apt-tools.mjs task update . --id {task-id} --lifecycle-phase verifying
|
|
165
|
+
```
|
|
162
166
|
|
|
163
167
|
**Checkpoint state:** `build-progress.json` is written after every subtask. That file is the durable resume point — do NOT self-estimate context and do NOT voluntarily exit. Keep executing until the plan is complete or a risk gate stops you. If the harness auto-compacts mid-pipeline, re-read `implementation_plan.json` + `build-progress.json` and continue from the next pending subtask. The `--continue` flag exists for human-initiated resumes (`/apt:pause` + new session), not for agent-initiated bailouts.
|
|
164
168
|
|
|
@@ -189,6 +193,10 @@ Execute the apt:verify workflow inline:
|
|
|
189
193
|
6. Score on 4 dimensions (completeness, correctness, quality, coverage)
|
|
190
194
|
7. Fix all issues found — every severity, not just critical (up to 4 iterations: first 2 discovery, last 2 verification-only)
|
|
191
195
|
8. Write qa_signoff.json
|
|
196
|
+
9. If `status` is `approved` or `approved-with-notes`, flip lifecycle to `reviewing` so Stage 4 (or `/apt:ship` when review is skipped) finds the task in the correct from-state. Skip this step entirely on `rejected`:
|
|
197
|
+
```bash
|
|
198
|
+
node packages/framework/bin/apt-tools.mjs task update . --id {task-id} --lifecycle-phase reviewing
|
|
199
|
+
```
|
|
192
200
|
|
|
193
201
|
**Output:** qa_signoff.json with status
|
|
194
202
|
|
|
@@ -224,6 +232,8 @@ Execute the apt:review workflow inline (see `apt-review/SKILL.md`):
|
|
|
224
232
|
4. Update task documentation — review.json, qa_signoff.json (re-score if fixes changed quality), build-progress.json
|
|
225
233
|
5. Write review.json with final findings and assessment. The `docs_policy` at `.aperant/config.json.docs_policy` controls per-surface behavior; defaults in `packages/framework/templates/docs-policy-defaults.json`.
|
|
226
234
|
|
|
235
|
+
`lifecycle_phase` stays at `reviewing` after this stage — /apt:ship handles the next transition (`reviewing → shipped-pending-merge`).
|
|
236
|
+
|
|
227
237
|
**Output:** review.json (written to task directory)
|
|
228
238
|
|
|
229
239
|
**This is the task review, not the PR review.** It runs inline without sub-agents. For heavy multi-agent PR review, use `/apt:pr-review` separately.
|
|
@@ -269,11 +279,16 @@ node packages/framework/bin/apt-tools.mjs commit "pipeline: complete {task}" --f
|
|
|
269
279
|
|
|
270
280
|
## 8. Lifecycle transitions
|
|
271
281
|
|
|
272
|
-
Lifecycle phase transitions are handled by the child skills themselves
|
|
273
|
-
`apt:execute`
|
|
274
|
-
|
|
275
|
-
`
|
|
276
|
-
|
|
282
|
+
Lifecycle phase transitions are handled by the child skills themselves.
|
|
283
|
+
`apt:execute` flips `planned` → `in-progress` at start (lifecycle = `executing`)
|
|
284
|
+
and at success-end (lifecycle = `verifying`). `apt:verify` flips lifecycle =
|
|
285
|
+
`reviewing` on the approved verdict (skipped on `rejected`). `apt:review`
|
|
286
|
+
leaves lifecycle at `reviewing` — the from-state `/apt:ship` expects for its
|
|
287
|
+
atomic `reviewing → shipped-pending-merge` flip. `/apt:ship` then flips
|
|
288
|
+
`reviewing → shipped-pending-merge`, and `/apt:close-task` later flips
|
|
289
|
+
`shipped-pending-merge → completed` after the PR merges. The pipeline wrapper
|
|
290
|
+
does not need to record or drain anything beyond ensuring each child-skill
|
|
291
|
+
stage runs to its success-end.
|
|
277
292
|
|
|
278
293
|
</process>
|
|
279
294
|
</output>
|
package/skills/apt-spar/SKILL.md
CHANGED
|
@@ -90,7 +90,7 @@ Parse `$ARGUMENTS`. Extract:
|
|
|
90
90
|
`[apt:spar] --rounds clamped to 3 (hard cap, see SPAR-05)`
|
|
91
91
|
and proceed with N=3. If N<1, clamp to 1 and print:
|
|
92
92
|
`[apt:spar] --rounds clamped to 1 (minimum)`
|
|
93
|
-
- `--timeout-ms N` — per-partner Bash timeout in milliseconds, default
|
|
93
|
+
- `--timeout-ms N` — per-partner Bash timeout in milliseconds, default 300000 (5 min). Capped at 600000 (10 min, the Bash tool's own ceiling).
|
|
94
94
|
- `[topic]` — the remaining text after flags. If empty, frame the round from the **last N conversation turns** as the context.
|
|
95
95
|
|
|
96
96
|
Also read (best-effort, skip silently if missing):
|
|
@@ -135,7 +135,7 @@ Do NOT proceed; self-sparring is sycophancy by construction (the host cannot obj
|
|
|
135
135
|
2. **Rung 2 — Plugin shim** (`${CLAUDE_PLUGIN_ROOT}/scripts/<partner>-companion.mjs`). Codex only (claude / gemini have no plugin shim).
|
|
136
136
|
3. **Rung 3 — Raw CLI on `$PATH`** (`codex exec`, `claude -p`, `gemini -m`).
|
|
137
137
|
|
|
138
|
-
Walk the ladder in order; pick the first rung with `detected: true` (or `"runtime-host-only"` for MCP). **MCP rung has a hard timeout** — default
|
|
138
|
+
Walk the ladder in order; pick the first rung with `detected: true` (or `"runtime-host-only"` for MCP). **MCP rung has a hard timeout** — default 900000ms (15 min), configurable via `apt-spar.mcp_timeout_ms` in `.aperant/config.json`. The MCP rung is a structured tool call, NOT a Bash invocation, so the Bash tool's 600000ms ceiling does NOT apply — the MCP rung can carry heavy-research partner runs (e.g. Claude Code partner doing multiple minutes of Read/Grep/Bash) where the Bash rungs would hit the host ceiling. On MCP timeout, **automatically fall through to rung 2 or rung 3** — do NOT fail the whole spar. Treat MCP timeout as "rung unavailable for this invocation."
|
|
139
139
|
|
|
140
140
|
If all three rungs are unavailable (every rung `detected: false`, or MCP `runtime-host-only` timed out AND rungs 2+3 are `detected: false`), print:
|
|
141
141
|
|
|
@@ -163,13 +163,14 @@ You are the sparring partner in an apt:spar round. Your job:
|
|
|
163
163
|
- Identify the strongest objection.
|
|
164
164
|
- Cite specific evidence where possible (file:line, test output, doc reference).
|
|
165
165
|
- Be specific, not generic — "this might fail" is not a finding; "this fails when X because Y" is.
|
|
166
|
+
- After your main objection, provide NEXT_WEAKEST_POINT: one additional independent objection that would still matter if your first point is resolved. This populates the next round's frame.
|
|
166
167
|
```
|
|
167
168
|
|
|
168
169
|
The frame is one shot per round — no multi-turn conversation with the partner inside a single round.
|
|
169
170
|
|
|
170
171
|
## 5. Invoke Partner (with timeout)
|
|
171
172
|
|
|
172
|
-
**Wrap every partner Bash call with an explicit timeout** (default
|
|
173
|
+
**Wrap every partner Bash call with an explicit timeout** (default 300000ms (5 min), override via `--timeout-ms`). The empirical reason: heavy-research partner CLIs (especially Claude Code in `--with claude` mode) routinely spend multiple minutes doing genuine Read/Grep/Bash investigation per round, and a hung partner CLI must NOT stall the spar loop indefinitely.
|
|
173
174
|
|
|
174
175
|
The table below shows the canonical command shape per (partner × rung); the actual invocation for shell-based rungs (plugin shim, raw CLI) MUST use the heredoc pattern in the next subsection — never raw double-quoted interpolation. The MCP rung is a structured tool call — no shell quoting concern applies.
|
|
175
176
|
|
|
@@ -182,19 +183,19 @@ The table below shows the canonical command shape per (partner × rung); the act
|
|
|
182
183
|
|
|
183
184
|
| Partner | Rung | Command | Notes |
|
|
184
185
|
|---|---|---|---|
|
|
185
|
-
| codex | mcp | tool call: `mcp__codex-mcp__codex({prompt})` | Preferred.
|
|
186
|
+
| codex | mcp | tool call: `mcp__codex-mcp__codex({prompt})` | Preferred. 900000ms (15 min) timeout (configurable `apt-spar.mcp_timeout_ms`); on timeout fall through to plugin then CLI. |
|
|
186
187
|
| codex | plugin | `node "${CLAUDE_PLUGIN_ROOT}/scripts/codex-companion.mjs" task "$(cat <<'APT_SPAR_PROMPT_EOF' ... APT_SPAR_PROMPT_EOF\n)"` | **NO `--write`. NO `--effort` override.** Per SPAR-04 — spar is conversation, not patching. |
|
|
187
188
|
| codex | cli | `codex exec "$(cat <<'APT_SPAR_PROMPT_EOF' ... APT_SPAR_PROMPT_EOF\n)"` | Precedent: `apt-pr-review/SKILL.md:1084`. Verify with `codex --help`. |
|
|
188
|
-
| claude | mcp | tool call: `mcp__claude-code-mcp__claude_code({prompt})` | Preferred. Same
|
|
189
|
+
| claude | mcp | tool call: `mcp__claude-code-mcp__claude_code({prompt})` | Preferred. Same 900000ms (15 min) timeout + fallthrough rule. |
|
|
189
190
|
| claude | plugin | n/a | No plugin shim exists for Claude. Skip this rung. |
|
|
190
191
|
| claude | cli | `claude -p "$(cat <<'APT_SPAR_PROMPT_EOF' ... APT_SPAR_PROMPT_EOF\n)" --output-format text` | Symmetric. Claude has no `--write` equivalent — asymmetric flags are a smell. |
|
|
191
|
-
| gemini | mcp | tool call: `mcp__gemini-mcp__*({prompt})` | Preferred. Same
|
|
192
|
+
| gemini | mcp | tool call: `mcp__gemini-mcp__*({prompt})` | Preferred. Same 900000ms (15 min) timeout + fallthrough rule. Any `mcp__gemini-mcp__*` tool satisfies the rung. |
|
|
192
193
|
| gemini | plugin | n/a | No plugin shim exists for Gemini. Skip this rung. |
|
|
193
194
|
| gemini | cli | `gemini -m <model-from-router.llm.providers.gemini-cli> "$(cat <<'APT_SPAR_PROMPT_EOF' ... APT_SPAR_PROMPT_EOF\n)"` | Read the exact model name from `.aperant/config.json` at runtime — do not hardcode. |
|
|
194
195
|
|
|
195
|
-
Set the Bash tool's `timeout` parameter to `--timeout-ms` (default
|
|
196
|
+
Set the Bash tool's `timeout` parameter to `--timeout-ms` (default 300000).
|
|
196
197
|
|
|
197
|
-
**Timeout calibration.** The
|
|
198
|
+
**Timeout calibration.** The 5-min Bash-rung default and 15-min MCP-rung default are sized for heavy-research partner runs — modern partner CLIs commonly spend multiple minutes doing real Read/Grep/Bash investigation before producing a round response. The two defaults are intentionally asymmetric: the Bash rungs (plugin shim, raw CLI) are capped at the Claude Code Bash tool's 600000ms (10 min) host ceiling, while the MCP rung — a structured tool call rather than a Bash invocation — has no host ceiling and can carry the worst-case 15-min Claude Code partner. For tighter budgets (e.g. CI), pass `--timeout-ms` explicitly. The MCP-rung override is downward-only: if you observe consistent partner-CLI wedges (auth handshakes, hosted-API stalls, CI lanes wanting fast failure), set `apt-spar.mcp_timeout_ms` BELOW the 15-min default in `.aperant/config.json` (e.g. `120000` for a 2-min fast-fail budget) — raising it above 15 min is unnecessary because the default already covers the documented worst case. Future versions may introduce per-partner defaults.
|
|
198
199
|
|
|
199
200
|
**Prompt-quoting safety.** The prompt is user-controlled (it comes from `[topic]` and/or conversation context) and may legitimately contain double quotes, dollar signs, backticks, or `$(...)` substitution that bash would otherwise expand or break on. NEVER interpolate `<prompt>` directly inside a double-quoted shell argument. Instead, pipe via a here-document so the shell treats the body as opaque text:
|
|
200
201
|
|
|
@@ -284,7 +285,9 @@ After each round, print a verdict block (one of these four, exact format):
|
|
|
284
285
|
|
|
285
286
|
## 8. Loop Control (max 3 rounds)
|
|
286
287
|
|
|
287
|
-
- If `verdict=cede`
|
|
288
|
+
- If `verdict=cede` AND `--rounds 1` was explicit → terminate this round.
|
|
289
|
+
- If `verdict=cede` AND `--rounds >= 2` (default) → emit `next-round` regardless: convergence requires TWO consecutive rounds with the same disposition AND no materially new evidence. Round 1 alone cannot terminate as converged.
|
|
290
|
+
- The host's `next-round` payload for Round 2 MUST include: (a) the cited evidence that confirmed the partner's Round 1 point, (b) a request to challenge the host's cited evidence with the partner's NEXT_WEAKEST_POINT.
|
|
288
291
|
- If `verdict=hold` → only triggers `next-round` (partner gets the new evidence). There is no terminal `hold` convergence in a single round. If the round cap is reached with hold still active, the held position stands and the loop exits via the round-cap path below.
|
|
289
292
|
- If `verdict=next-round` → invoke the partner again with the new evidence the host just cited, go to Step 4.
|
|
290
293
|
- If the round counter hits **3 rounds** (or whatever `--rounds N` clamped to) → **terminate** with:
|
|
@@ -302,6 +302,17 @@ The `status` field is derived from the minimum dimension score:
|
|
|
302
302
|
node packages/framework/bin/apt-tools.mjs commit "qa: verification {status}" --files qa_signoff.json
|
|
303
303
|
```
|
|
304
304
|
|
|
305
|
+
### Mark lifecycle phase as `reviewing` (approved path only)
|
|
306
|
+
|
|
307
|
+
If `status` is `approved` or `approved-with-notes`, flip the task's lifecycle
|
|
308
|
+
phase so the next stage (`/apt:review`, or `/apt:ship` if review is skipped)
|
|
309
|
+
finds the task in the correct from-state. Skip this transition entirely on
|
|
310
|
+
`rejected` — the task stays in `verifying` until the user re-runs after fixes.
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
node packages/framework/bin/apt-tools.mjs task update . --id {task-id} --lifecycle-phase reviewing
|
|
314
|
+
```
|
|
315
|
+
|
|
305
316
|
## 8. Report
|
|
306
317
|
|
|
307
318
|
Output:
|
|
@@ -69,7 +69,7 @@ Watcher already running for PR {N} (held by sessionId={x}, pid={y}); exiting.
|
|
|
69
69
|
|
|
70
70
|
For the `--tick` mode, execute exactly the following sequence — once — inside this single skill invocation:
|
|
71
71
|
|
|
72
|
-
1. **Poll.** Call `gh pr checks <N> --json name,bucket,state,
|
|
72
|
+
1. **Poll.** Call `gh pr checks <N> --json name,bucket,state,link,workflow,completedAt` and `gh pr view <N> --json state,reviewDecision`. Combine both JSON outputs into a single file shaped `{checks: <pr-checks-json>, prMetadata: <pr-view-json>}` (for example with `jq -s '{checks: .[0], prMetadata: .[1]}'`) and save it as `{captured.json}` (any path under the worktree is fine). On any `gh` failure, omit the file and let the cmd's `tick` route to `infra-backoff` (it surfaces the same action when no payload is available). <!-- FRAMEWORK-BUG-016 (spec.md §D3): gh 2.65+'s `pr checks --json` schema dropped two legacy fields that the watcher previously requested; per-check status now lives in `state` (uppercase: SUCCESS / FAILURE / SKIPPED / IN_PROGRESS / …) and the failed-run URL lives in `link`. Do NOT re-introduce the legacy field names — gh exits non-zero ("Unknown JSON field") and the watcher falls into infra-backoff forever. -->
|
|
73
73
|
2. **Classify + state update.** Call `apt-tools ci-watch tick {worktree} --pr {N} --payload-file {captured.json}`. The cmd reads `{captured.json}` from disk and runs the classifier — it does NOT shell out to `gh` itself (that boundary stays with the orchestrator skill; see commands/ci-watch.mjs:15). The cmd internally:
|
|
74
74
|
- acquires the per-PR lock (BUSY → exit 2 → step §2);
|
|
75
75
|
- reads the per-PR state file;
|
|
@@ -111,7 +111,7 @@ In all terminal cases:
|
|
|
111
111
|
|
|
112
112
|
When `action === 'fix'` (or `diverged-rebased`/`diverged-bailed`):
|
|
113
113
|
|
|
114
|
-
1. **Fetch the failed-check log.** From the gh payload, find the failed check's `
|
|
114
|
+
1. **Fetch the failed-check log.** From the gh payload, find the failed check's `link` field (see FRAMEWORK-BUG-016 / §D3 for the gh 2.65+ field migration) and call `gh run view {runId} --log` to capture the log into a temp file.
|
|
115
115
|
2. **Shell out to the host CLI** to fix it. The watcher does NOT inline a custom fix prompt; it hands the worktree + failed-log to the same CLI binary that's running this skill (detected by `apt-tools host-detect`). Pseudocode:
|
|
116
116
|
|
|
117
117
|
```bash
|
|
@@ -5,11 +5,17 @@
|
|
|
5
5
|
* { action, counts_toward_budget, reason }
|
|
6
6
|
*
|
|
7
7
|
* No I/O, no fs, no gh CLI shell-outs — caller pre-fetches `checks` (the
|
|
8
|
-
* payload from `gh pr checks --json name,bucket,state,
|
|
8
|
+
* payload from `gh pr checks --json name,bucket,state,link,workflow,completedAt`)
|
|
9
9
|
* and `prMetadata` (`gh pr view --json state,reviewDecision`) and passes them
|
|
10
10
|
* in. The orchestrator in commands/ci-watch.mjs handles the side effects
|
|
11
11
|
* implied by the action.
|
|
12
12
|
*
|
|
13
|
+
* Primary per-check signal is `state` (uppercase per gh-CLI: SUCCESS,
|
|
14
|
+
* FAILURE, CANCELLED, TIMED_OUT, SKIPPED, IN_PROGRESS, QUEUED, PENDING,
|
|
15
|
+
* NEUTRAL). The legacy lowercase per-check field that gh dropped circa
|
|
16
|
+
* 2.65 is kept as a soft fallback for in-memory test fixtures only —
|
|
17
|
+
* production gh payloads no longer expose it (FRAMEWORK-BUG-016).
|
|
18
|
+
*
|
|
13
19
|
* Predicate order matters — short-circuits go first (merged/closed/green/cap
|
|
14
20
|
* fire before any per-check inspection). See spec.md §9 table for the full
|
|
15
21
|
* ordering.
|
|
@@ -31,8 +37,13 @@ const INFRA_FAILURE_CLASSES = new Set(['runner-oom', 'runner-cancelled', 'gh-5xx
|
|
|
31
37
|
/**
|
|
32
38
|
* @typedef {Object} CheckRow
|
|
33
39
|
* @property {string} [name]
|
|
34
|
-
* @property {string} [
|
|
35
|
-
*
|
|
40
|
+
* @property {string} [state] Primary per-check status (gh-CLI uppercase enum):
|
|
41
|
+
* 'SUCCESS' | 'FAILURE' | 'CANCELLED' | 'TIMED_OUT' | 'SKIPPED' |
|
|
42
|
+
* 'IN_PROGRESS' | 'QUEUED' | 'PENDING' | 'NEUTRAL'.
|
|
43
|
+
* @property {string} [conclusion] Legacy lowercase per-check field
|
|
44
|
+
* (deprecated — gh dropped it from `pr checks --json` schema circa 2.65).
|
|
45
|
+
* Retained only as a soft fallback for in-memory test fixtures
|
|
46
|
+
* (FRAMEWORK-BUG-016).
|
|
36
47
|
* @property {string} [bucket]
|
|
37
48
|
* @property {string} [failure_class] — optional pre-classified hint (test-only seam)
|
|
38
49
|
* @property {string} [log_fingerprint] — optional digest for identical-log detection
|
|
@@ -65,7 +76,16 @@ export function classify({ checks, state, prMetadata }) {
|
|
|
65
76
|
}
|
|
66
77
|
|
|
67
78
|
const failed = rows.filter((c) => isFailing(c))
|
|
68
|
-
|
|
79
|
+
// FRAMEWORK-BUG-016: `state` is gh-CLI's primary per-check field post-2.65.
|
|
80
|
+
// SKIPPED rows (workflow `if:` guards that didn't trip) are
|
|
81
|
+
// success-equivalent — they intentionally did not run and that is OK.
|
|
82
|
+
// Soft fallback to the legacy lowercase field keeps the in-memory test
|
|
83
|
+
// fixtures green (production gh payloads never hit it).
|
|
84
|
+
const isGreenRow = (c) =>
|
|
85
|
+
c.state === 'SUCCESS' ||
|
|
86
|
+
c.state === 'SKIPPED' ||
|
|
87
|
+
(c.state == null && c.conclusion === 'success')
|
|
88
|
+
const allGreen = rows.length > 0 && rows.every(isGreenRow)
|
|
69
89
|
|
|
70
90
|
// 3. All checks green.
|
|
71
91
|
if (allGreen) {
|
|
@@ -193,6 +213,26 @@ export function classify({ checks, state, prMetadata }) {
|
|
|
193
213
|
}
|
|
194
214
|
|
|
195
215
|
export function isFailing(check) {
|
|
216
|
+
// FRAMEWORK-BUG-016 / BUG-025: prefer the uppercase `state` field that gh
|
|
217
|
+
// 2.65+ exposes. SKIPPED / NEUTRAL / IN_PROGRESS / QUEUED / PENDING /
|
|
218
|
+
// SUCCESS are explicitly NOT red — SKIPPED in particular must never
|
|
219
|
+
// burn a fix attempt (workflow `if:` guards routinely skip jobs on PRs
|
|
220
|
+
// that don't trip them). The legacy lowercase field is a soft fallback
|
|
221
|
+
// for in-memory test fixtures only.
|
|
222
|
+
const s = check.state
|
|
223
|
+
if (
|
|
224
|
+
s === 'SUCCESS' ||
|
|
225
|
+
s === 'SKIPPED' ||
|
|
226
|
+
s === 'NEUTRAL' ||
|
|
227
|
+
s === 'IN_PROGRESS' ||
|
|
228
|
+
s === 'QUEUED' ||
|
|
229
|
+
s === 'PENDING'
|
|
230
|
+
) {
|
|
231
|
+
return false
|
|
232
|
+
}
|
|
233
|
+
if (s === 'FAILURE' || s === 'CANCELLED' || s === 'TIMED_OUT') {
|
|
234
|
+
return true
|
|
235
|
+
}
|
|
196
236
|
const c = check.conclusion
|
|
197
237
|
return c === 'failure' || c === 'cancelled' || c === 'timed_out' || c === 'action_required'
|
|
198
238
|
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commands/fan-out.mjs — FRAMEWORK-BUG-033 capability gap (ID-04 / ID-05).
|
|
3
|
+
*
|
|
4
|
+
* Single resolver helper for the `/apt:fan-out` SKILL body. Three input
|
|
5
|
+
* forms accepted (`--tasks`, `--phases`, `--unblocked`); all resolve to
|
|
6
|
+
* the canonical `{task_ids, source, batch_id}` shape. The actual
|
|
7
|
+
* orchestration loop (worktree create, worker spawn, polling) lives in
|
|
8
|
+
* the SKILL body — this CLI just materializes the batch.
|
|
9
|
+
*
|
|
10
|
+
* Subcommands: `resolve`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
14
|
+
import { join, resolve } from 'node:path'
|
|
15
|
+
import { parseFlags } from '../util/args.mjs'
|
|
16
|
+
import { err, ok } from '../util/result.mjs'
|
|
17
|
+
|
|
18
|
+
const ID_PATTERN = /^[a-z0-9_-]+$/i
|
|
19
|
+
|
|
20
|
+
function generateBatchId() {
|
|
21
|
+
const d = new Date()
|
|
22
|
+
const yyyy = d.getUTCFullYear()
|
|
23
|
+
const mm = String(d.getUTCMonth() + 1).padStart(2, '0')
|
|
24
|
+
const dd = String(d.getUTCDate()).padStart(2, '0')
|
|
25
|
+
const hex = Math.random().toString(16).slice(2, 8)
|
|
26
|
+
return `fanout-${yyyy}${mm}${dd}-${hex}`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseIdList(raw) {
|
|
30
|
+
if (typeof raw !== 'string') return []
|
|
31
|
+
return raw
|
|
32
|
+
.split(',')
|
|
33
|
+
.map((s) => s.trim())
|
|
34
|
+
.filter(Boolean)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readState(targetDir) {
|
|
38
|
+
const statePath = join(targetDir, '.aperant', 'state.json')
|
|
39
|
+
if (!existsSync(statePath)) return null
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(readFileSync(statePath, 'utf-8'))
|
|
42
|
+
} catch {
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function resolveFromPhases(state, phases) {
|
|
48
|
+
const wanted = new Set(phases)
|
|
49
|
+
const ids = []
|
|
50
|
+
for (const [id, task] of Object.entries(state?.active_tasks || {})) {
|
|
51
|
+
if (!task) continue
|
|
52
|
+
const key = `${task.milestone_id ?? ''}/${task.phase_id ?? ''}`
|
|
53
|
+
if (wanted.has(key) && task.lifecycle_phase !== 'closed') {
|
|
54
|
+
ids.push(id)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return ids
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function resolveUnblocked(state) {
|
|
61
|
+
const ids = []
|
|
62
|
+
for (const [id, task] of Object.entries(state?.active_tasks || {})) {
|
|
63
|
+
if (task?.lifecycle_phase === 'ready-for-agent') ids.push(id)
|
|
64
|
+
}
|
|
65
|
+
return ids
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function cmdFanOut(subcommand, projectDir, extraArgs) {
|
|
69
|
+
if (!subcommand) {
|
|
70
|
+
return err(
|
|
71
|
+
'Usage: apt-tools fan-out <resolve> <project-dir> [--tasks <ids> | --phases <m/p,m/p> | --unblocked]',
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
switch (subcommand) {
|
|
75
|
+
case 'resolve': {
|
|
76
|
+
if (!projectDir) {
|
|
77
|
+
return err(
|
|
78
|
+
'Usage: apt-tools fan-out resolve <project-dir> [--tasks <ids> | --phases <m/p,m/p> | --unblocked]',
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
const targetDir = resolve(projectDir)
|
|
82
|
+
const flags = parseFlags(extraArgs)
|
|
83
|
+
|
|
84
|
+
const hasTasks = flags.has('tasks')
|
|
85
|
+
const hasPhases = flags.has('phases')
|
|
86
|
+
const hasUnblocked = flags.has('unblocked')
|
|
87
|
+
|
|
88
|
+
const inputForms = [hasTasks, hasPhases, hasUnblocked].filter(Boolean).length
|
|
89
|
+
if (inputForms === 0) {
|
|
90
|
+
return err('One of --tasks, --phases, or --unblocked is required')
|
|
91
|
+
}
|
|
92
|
+
if (inputForms > 1) {
|
|
93
|
+
return err('Only one of --tasks, --phases, or --unblocked may be passed at a time')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let taskIds = []
|
|
97
|
+
let source = ''
|
|
98
|
+
if (hasTasks) {
|
|
99
|
+
taskIds = parseIdList(flags.get('tasks'))
|
|
100
|
+
source = 'tasks'
|
|
101
|
+
for (const id of taskIds) {
|
|
102
|
+
if (!ID_PATTERN.test(id)) {
|
|
103
|
+
return err(`Invalid task id in --tasks: "${id}" (must match /^[a-z0-9_-]+$/i)`)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} else if (hasPhases) {
|
|
107
|
+
const state = readState(targetDir)
|
|
108
|
+
if (!state) return err('No state.json found — cannot resolve --phases')
|
|
109
|
+
taskIds = resolveFromPhases(state, parseIdList(flags.get('phases')))
|
|
110
|
+
source = 'phases'
|
|
111
|
+
} else {
|
|
112
|
+
const state = readState(targetDir)
|
|
113
|
+
if (!state) return err('No state.json found — cannot resolve --unblocked')
|
|
114
|
+
taskIds = resolveUnblocked(state)
|
|
115
|
+
source = 'unblocked'
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return ok({
|
|
119
|
+
status: 'ok',
|
|
120
|
+
command: 'fan-out-resolve',
|
|
121
|
+
source,
|
|
122
|
+
task_ids: taskIds,
|
|
123
|
+
batch_id: generateBatchId(),
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
default:
|
|
127
|
+
return err(`Unknown fan-out subcommand: ${subcommand}. Available: resolve`)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -54,7 +54,7 @@ const FRONTMATTER_RE = /^---\s*([\s\S]*?)\s*---/
|
|
|
54
54
|
*
|
|
55
55
|
* @type {Record<string, string[]>}
|
|
56
56
|
*/
|
|
57
|
-
const SKILL_CONFIG_KEYS = {
|
|
57
|
+
export const SKILL_CONFIG_KEYS = {
|
|
58
58
|
'apt:execute': ['coding.tdd_default', 'tdd.iron_law'],
|
|
59
59
|
'apt:triage': [
|
|
60
60
|
'task_tracking.backend',
|
|
@@ -69,6 +69,9 @@ const SKILL_CONFIG_KEYS = {
|
|
|
69
69
|
'apt:ship': ['pipeline.auto_branch', 'pipeline.branch_prefix'],
|
|
70
70
|
'apt:setup': ['*'], // setup edits config — wildcard surface
|
|
71
71
|
'apt:run': ['parallelization.enabled', 'parallelization.max_agents'],
|
|
72
|
+
// FRAMEWORK-BUG-033 capability gap (AC10) — fan-out concurrency cap.
|
|
73
|
+
// Consumed by the /apt:fan-out SKILL body via cli/config/parallelization.mjs.
|
|
74
|
+
'apt:fan-out': ['parallelization.max_tasks', 'parallelization.max_agents'],
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
/**
|
|
@@ -147,13 +147,24 @@ export function parseFixesAppliedMarkdown(md) {
|
|
|
147
147
|
const fixesSection = extractTopLevelSection(md, 'Fixes Applied')
|
|
148
148
|
const selfReviewerSection = extractTopLevelSection(md, 'Fixes Applied by Self-Reviewer')
|
|
149
149
|
const skippedSection = extractTopLevelSection(md, 'Skipped')
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
150
|
+
const legacyFixerEntries = parseFixEntries(fixesSection)
|
|
151
|
+
const selfReviewerEntries = parseSelfReviewerFixEntries(selfReviewerSection)
|
|
152
|
+
// FRAMEWORK-BUG-035: additive `## Fix N` parser path. Scans the whole
|
|
153
|
+
// document — NOT a `## Fixes Applied` body — so it catches fixer reports
|
|
154
|
+
// shaped as numbered top-level blocks. Union, not substitution, per spec
|
|
155
|
+
// ID-03 so legacy `### {ID}` reports keep parsing unchanged.
|
|
156
|
+
const fixNEntries = parseFixNEntries(md)
|
|
157
|
+
const fixes = [...legacyFixerEntries, ...selfReviewerEntries, ...fixNEntries]
|
|
154
158
|
const skipped = parseSkippedEntries(skippedSection)
|
|
155
|
-
//
|
|
156
|
-
|
|
159
|
+
// Schema discriminator: when the ONLY entries came from the `## Fix N`
|
|
160
|
+
// parser path, label it `'fix-n'` so downstream code / debugging can
|
|
161
|
+
// tell which shape was on disk. Fall back to existing fixer / self-
|
|
162
|
+
// reviewer labels when those sections were present.
|
|
163
|
+
let schema
|
|
164
|
+
if (fixesSection) schema = 'fixer'
|
|
165
|
+
else if (selfReviewerSection) schema = 'self-reviewer'
|
|
166
|
+
else if (fixNEntries.length > 0) schema = 'fix-n'
|
|
167
|
+
else schema = 'fixer'
|
|
157
168
|
return { exists: true, fixes, skipped, schema }
|
|
158
169
|
}
|
|
159
170
|
|
|
@@ -237,6 +248,71 @@ function parseFixEntries(section) {
|
|
|
237
248
|
return entries
|
|
238
249
|
}
|
|
239
250
|
|
|
251
|
+
/**
|
|
252
|
+
* FRAMEWORK-BUG-035 — parse `## Fix N` top-level blocks into entries shaped
|
|
253
|
+
* like `parseFixEntries` output. The bug narrative documents `## Fix N`
|
|
254
|
+
* as the canonical shape going forward for fixes-applied-fixer-N.md, but
|
|
255
|
+
* the legacy `### {ID} — title` parser silently dropped every entry on
|
|
256
|
+
* those reports, surfacing `<none parsed>` and false-positive flipping
|
|
257
|
+
* the gate to `hallucinated`.
|
|
258
|
+
*
|
|
259
|
+
* This parser scans the WHOLE document (not a `## Fixes Applied` body)
|
|
260
|
+
* because the new shape uses `## Fix N` directly as top-level headers.
|
|
261
|
+
* Finding-ID extraction priority chain (per spec ID-04):
|
|
262
|
+
* 1. `- **Finding ID**:` field line
|
|
263
|
+
* 2. `- **Fix**:` field line
|
|
264
|
+
* 3. `- **ID**:` field line
|
|
265
|
+
* 4. synthetic `fix-N` fallback (matches `parseSelfReviewerFixEntries`'s
|
|
266
|
+
* `self-reviewer-fix-N` convention; the diff-intersect step in
|
|
267
|
+
* `decideVerdict` is the real check, not the id)
|
|
268
|
+
*
|
|
269
|
+
* Returns the same `{id, title, file, files, verification}` shape as
|
|
270
|
+
* `parseFixEntries` so downstream consumers see a uniform array.
|
|
271
|
+
*/
|
|
272
|
+
function parseFixNEntries(markdown) {
|
|
273
|
+
if (!markdown) return []
|
|
274
|
+
const entries = []
|
|
275
|
+
// `^## Fix N$` — top-level heading with the literal word "Fix" + a
|
|
276
|
+
// number. No trailing description in the header (the canonical shape
|
|
277
|
+
// per the BUG-034 narrative puts the description inside the body).
|
|
278
|
+
const headerRx = /^##\s+Fix\s+(\d+)\s*$/gm
|
|
279
|
+
const matches = [...markdown.matchAll(headerRx)]
|
|
280
|
+
for (let i = 0; i < matches.length; i++) {
|
|
281
|
+
const m = matches[i]
|
|
282
|
+
const start = m.index + m[0].length
|
|
283
|
+
// Terminate at the next top-level `## ` heading OR EOF. Mirrors
|
|
284
|
+
// `extractTopLevelSection`'s body slicing.
|
|
285
|
+
const rest = markdown.slice(start)
|
|
286
|
+
const nextTopRx = /\n##\s+/m
|
|
287
|
+
const nxt = nextTopRx.exec(rest)
|
|
288
|
+
const body = (nxt ? rest.slice(0, nxt.index) : rest).trim()
|
|
289
|
+
const findingId = extractFindingIdField(body) ?? `fix-${m[1]}`
|
|
290
|
+
const files = extractFileFields(body)
|
|
291
|
+
entries.push({
|
|
292
|
+
id: findingId,
|
|
293
|
+
title: null,
|
|
294
|
+
file: files[0] ?? null,
|
|
295
|
+
files,
|
|
296
|
+
verification: extractVerificationField(body),
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
return entries
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Extract the finding ID from a `## Fix N` block body via the priority
|
|
304
|
+
* chain documented in `parseFixNEntries`. Returns `null` if no field
|
|
305
|
+
* matched — caller falls back to the synthetic `fix-N` id.
|
|
306
|
+
*/
|
|
307
|
+
function extractFindingIdField(body) {
|
|
308
|
+
for (const key of ['Finding ID', 'Fix', 'ID']) {
|
|
309
|
+
const rx = new RegExp(`-\\s*\\*\\*${key}\\*\\*:\\s*([^\\n]+)`, 'i')
|
|
310
|
+
const m = rx.exec(body)
|
|
311
|
+
if (m) return m[1].trim()
|
|
312
|
+
}
|
|
313
|
+
return null
|
|
314
|
+
}
|
|
315
|
+
|
|
240
316
|
function parseSkippedEntries(section) {
|
|
241
317
|
if (!section) return []
|
|
242
318
|
const entries = []
|
|
@@ -11,6 +11,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSy
|
|
|
11
11
|
import { basename, join, resolve } from 'node:path'
|
|
12
12
|
import { selfStageCommittableWrites } from '../artifacts/self-stage.mjs'
|
|
13
13
|
import { getDefaultOwner, getGitIdentity } from '../git/identity.mjs'
|
|
14
|
+
import { runConvexMirror } from '../roadmap/convex-mirror.mjs'
|
|
14
15
|
import {
|
|
15
16
|
loadRoadmapForScope as ioLoadRoadmapForScope,
|
|
16
17
|
saveRoadmapForScope as ioSaveRoadmapForScope,
|
|
@@ -41,7 +42,7 @@ class RoadmapUsageError extends UsageError {
|
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
export function cmdRoadmap(subcommand, projectDir, extraArgs) {
|
|
45
|
+
export async function cmdRoadmap(subcommand, projectDir, extraArgs) {
|
|
45
46
|
if (!subcommand)
|
|
46
47
|
return err(
|
|
47
48
|
'Usage: apt-tools roadmap <init|render|add-milestone|add-phase|set-phase-status|rollup|delete-phase|delete-milestone|validate-refs|sync> <project-dir>',
|
|
@@ -49,14 +50,14 @@ export function cmdRoadmap(subcommand, projectDir, extraArgs) {
|
|
|
49
50
|
if (!projectDir) return err('Usage: apt-tools roadmap ' + subcommand + ' <project-dir>')
|
|
50
51
|
|
|
51
52
|
try {
|
|
52
|
-
return cmdRoadmapImpl(subcommand, projectDir, extraArgs)
|
|
53
|
+
return await cmdRoadmapImpl(subcommand, projectDir, extraArgs)
|
|
53
54
|
} catch (e) {
|
|
54
55
|
if (e instanceof RoadmapUsageError) return err(e.message)
|
|
55
56
|
throw e
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
function cmdRoadmapImpl(subcommand, projectDir, extraArgs) {
|
|
60
|
+
async function cmdRoadmapImpl(subcommand, projectDir, extraArgs) {
|
|
60
61
|
const targetDir = resolve(projectDir)
|
|
61
62
|
updateTeamMember(targetDir)
|
|
62
63
|
const roadmapRoot = join(targetDir, '.aperant', 'roadmap')
|
|
@@ -904,9 +905,52 @@ function cmdRoadmapImpl(subcommand, projectDir, extraArgs) {
|
|
|
904
905
|
})
|
|
905
906
|
}
|
|
906
907
|
|
|
908
|
+
case 'convex-mirror': {
|
|
909
|
+
// Slice 5 ST14 / AC10 / ID-06. POSTs the rendered narrator prose
|
|
910
|
+
// to the Next.js /api/roadmap/narrator-mirror endpoint. Markdown
|
|
911
|
+
// is canonical (ADR-0006); this call is the Convex mirror.
|
|
912
|
+
const scope = flags.get('scope')
|
|
913
|
+
const phaseId = flags.get('phase')
|
|
914
|
+
const anchorId = flags.get('anchor-id')
|
|
915
|
+
const prosePath = flags.get('prose-path')
|
|
916
|
+
const mergedPrRaw = flags.get('merged-pr')
|
|
917
|
+
if (!scope || !phaseId || !anchorId || !prosePath) {
|
|
918
|
+
return err(
|
|
919
|
+
'Usage: apt-tools roadmap convex-mirror <project-dir> --scope <s> --phase <p> --anchor-id <a> --prose-path <file> [--merged-pr <n>]',
|
|
920
|
+
)
|
|
921
|
+
}
|
|
922
|
+
let proseBody = ''
|
|
923
|
+
try {
|
|
924
|
+
proseBody = readFileSync(prosePath, 'utf-8')
|
|
925
|
+
} catch (e) {
|
|
926
|
+
return err(`prose-path read failed: ${e.message}`)
|
|
927
|
+
}
|
|
928
|
+
const mergedPr = mergedPrRaw ? Number(mergedPrRaw) : undefined
|
|
929
|
+
let result
|
|
930
|
+
try {
|
|
931
|
+
result = await runConvexMirror(targetDir, {
|
|
932
|
+
scope,
|
|
933
|
+
phaseId,
|
|
934
|
+
anchorId,
|
|
935
|
+
proseBody,
|
|
936
|
+
mergedPr,
|
|
937
|
+
})
|
|
938
|
+
} catch (e) {
|
|
939
|
+
return err(e.message)
|
|
940
|
+
}
|
|
941
|
+
if (result.status === 'error') {
|
|
942
|
+
return err(result.message ?? 'convex-mirror error')
|
|
943
|
+
}
|
|
944
|
+
return ok({
|
|
945
|
+
status: result.status,
|
|
946
|
+
command: 'roadmap-convex-mirror',
|
|
947
|
+
message: result.message,
|
|
948
|
+
})
|
|
949
|
+
}
|
|
950
|
+
|
|
907
951
|
default:
|
|
908
952
|
return err(
|
|
909
|
-
`Unknown roadmap subcommand: ${subcommand}. Expected: init, render, add-milestone, add-phase, set-phase-status, delete-phase, delete-milestone, validate-refs, sync`,
|
|
953
|
+
`Unknown roadmap subcommand: ${subcommand}. Expected: init, render, add-milestone, add-phase, set-phase-status, delete-phase, delete-milestone, validate-refs, sync, convex-mirror`,
|
|
910
954
|
)
|
|
911
955
|
}
|
|
912
956
|
}
|
|
@@ -264,6 +264,12 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
264
264
|
roadmap_ref_url: roadmapRefUrl,
|
|
265
265
|
depends_on: dependsOn,
|
|
266
266
|
outputs,
|
|
267
|
+
// FRAMEWORK-BUG-033 defect 2 — durable task→agent ownership.
|
|
268
|
+
// Defaults to null at create time; fan-out workers (or the
|
|
269
|
+
// `task update --assigned-to <agent-id>` path) populate it
|
|
270
|
+
// when they claim the task. Persists across short-lived CLI
|
|
271
|
+
// invocations unlike team-status/{agentId}.json (pid-keyed).
|
|
272
|
+
assigned_to: flags.has('assigned-to') ? flags.get('assigned-to') : null,
|
|
267
273
|
lifecycle_phase: 'planning',
|
|
268
274
|
history: [],
|
|
269
275
|
created_at: new Date().toISOString(),
|
|
@@ -704,6 +710,7 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
704
710
|
const result = applyLifecyclePhaseSideEffect(
|
|
705
711
|
targetDir,
|
|
706
712
|
{
|
|
713
|
+
id: task.id,
|
|
707
714
|
scope: task.scope || 'project',
|
|
708
715
|
milestone_id: task.milestone_id,
|
|
709
716
|
phase_id: task.phase_id,
|
|
@@ -933,6 +940,14 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
933
940
|
}
|
|
934
941
|
prUrlForCiWatch = url
|
|
935
942
|
}
|
|
943
|
+
// FRAMEWORK-BUG-033 defect 2 — durable task→agent ownership.
|
|
944
|
+
// Mutable owner field; fan-out workers write
|
|
945
|
+
// `fanout-${batch}-${task}` on claim, conductor reads to
|
|
946
|
+
// resolve crash recovery (which worker owned T1 when the
|
|
947
|
+
// conductor died?).
|
|
948
|
+
if (flags.has('assigned-to')) {
|
|
949
|
+
task.assigned_to = flags.get('assigned-to')
|
|
950
|
+
}
|
|
936
951
|
if (flags.has('subtasks-total'))
|
|
937
952
|
task.progress.subtasks_total = parseInt(flags.get('subtasks-total'), 10)
|
|
938
953
|
if (flags.has('subtasks-completed'))
|