@aperant/framework 0.10.0 → 0.12.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 +236 -1
- package/agents/apt-planner.md +15 -0
- package/agents/apt-verifier.md +15 -10
- package/bin/apt-proof-video.mjs +256 -76
- package/dist/cli/commands/apply-recipe.d.mts +6 -0
- package/dist/cli/commands/apply-recipe.d.mts.map +1 -0
- package/dist/cli/commands/apply-recipe.mjs +98 -0
- package/dist/cli/commands/apply-recipe.mjs.map +1 -0
- package/dist/cli/commands/catch-up.d.mts +9 -0
- package/dist/cli/commands/catch-up.d.mts.map +1 -0
- package/dist/cli/commands/catch-up.mjs +64 -0
- package/dist/cli/commands/catch-up.mjs.map +1 -0
- package/dist/cli/commands/mobile-prepare.d.mts +53 -0
- package/dist/cli/commands/mobile-prepare.d.mts.map +1 -0
- package/dist/cli/commands/mobile-prepare.mjs +139 -0
- package/dist/cli/commands/mobile-prepare.mjs.map +1 -0
- package/dist/cli/commands/pr-review.d.mts +16 -4
- package/dist/cli/commands/pr-review.d.mts.map +1 -1
- package/dist/cli/commands/pr-review.mjs +30 -11
- package/dist/cli/commands/pr-review.mjs.map +1 -1
- package/dist/cli/commands/recipe-diff.d.mts +6 -0
- package/dist/cli/commands/recipe-diff.d.mts.map +1 -0
- package/dist/cli/commands/recipe-diff.mjs +75 -0
- package/dist/cli/commands/recipe-diff.mjs.map +1 -0
- package/dist/cli/commands/roadmap.d.mts.map +1 -1
- package/dist/cli/commands/roadmap.mjs +53 -0
- package/dist/cli/commands/roadmap.mjs.map +1 -1
- package/dist/cli/commands/route.d.mts.map +1 -1
- package/dist/cli/commands/route.mjs +92 -2
- package/dist/cli/commands/route.mjs.map +1 -1
- package/dist/cli/commands/verify-sandbox.d.mts +6 -0
- package/dist/cli/commands/verify-sandbox.d.mts.map +1 -0
- package/dist/cli/commands/verify-sandbox.mjs +200 -0
- package/dist/cli/commands/verify-sandbox.mjs.map +1 -0
- package/dist/cli/config/share-policy.d.mts.map +1 -1
- package/dist/cli/config/share-policy.mjs +1 -0
- 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 +1 -0
- package/dist/cli/config/upgrade-gitignore.mjs.map +1 -1
- package/dist/cli/consistency/parse-qa.d.mts.map +1 -1
- package/dist/cli/consistency/parse-qa.mjs +18 -4
- package/dist/cli/consistency/parse-qa.mjs.map +1 -1
- package/dist/cli/consistency/rules/r5-verdict-consistency.d.mts.map +1 -1
- package/dist/cli/consistency/rules/r5-verdict-consistency.mjs +11 -10
- package/dist/cli/consistency/rules/r5-verdict-consistency.mjs.map +1 -1
- package/dist/cli/coordination/catch-up.d.mts +26 -0
- package/dist/cli/coordination/catch-up.d.mts.map +1 -0
- package/dist/cli/coordination/catch-up.mjs +239 -0
- package/dist/cli/coordination/catch-up.mjs.map +1 -0
- package/dist/cli/coordination/last-seen.d.mts +45 -0
- package/dist/cli/coordination/last-seen.d.mts.map +1 -0
- package/dist/cli/coordination/last-seen.mjs +128 -0
- package/dist/cli/coordination/last-seen.mjs.map +1 -0
- package/dist/cli/coordination/store.d.mts +15 -0
- package/dist/cli/coordination/store.d.mts.map +1 -1
- package/dist/cli/coordination/store.mjs +16 -0
- package/dist/cli/coordination/store.mjs.map +1 -1
- package/dist/cli/coverage-check/user-outcomes.d.mts +7 -2
- package/dist/cli/coverage-check/user-outcomes.d.mts.map +1 -1
- package/dist/cli/coverage-check/user-outcomes.mjs +50 -17
- package/dist/cli/coverage-check/user-outcomes.mjs.map +1 -1
- package/dist/cli/dispatch.d.mts.map +1 -1
- package/dist/cli/dispatch.mjs +30 -0
- package/dist/cli/dispatch.mjs.map +1 -1
- package/dist/cli/gate/gates/verify-approved.d.mts +0 -48
- package/dist/cli/gate/gates/verify-approved.d.mts.map +1 -1
- package/dist/cli/gate/gates/verify-approved.mjs +23 -80
- package/dist/cli/gate/gates/verify-approved.mjs.map +1 -1
- package/dist/cli/git/default-branch.d.mts +51 -0
- package/dist/cli/git/default-branch.d.mts.map +1 -0
- package/dist/cli/git/default-branch.mjs +234 -0
- package/dist/cli/git/default-branch.mjs.map +1 -0
- package/dist/cli/git/identity.d.mts +3 -5
- package/dist/cli/git/identity.d.mts.map +1 -1
- package/dist/cli/git/identity.mjs +10 -4
- package/dist/cli/git/identity.mjs.map +1 -1
- package/dist/cli/install/legacy-paths.d.mts.map +1 -1
- package/dist/cli/install/legacy-paths.mjs +2 -0
- package/dist/cli/install/legacy-paths.mjs.map +1 -1
- package/dist/cli/install/mcp-server-specs.d.mts +6 -5
- package/dist/cli/install/mcp-server-specs.d.mts.map +1 -1
- package/dist/cli/install/mcp-server-specs.mjs +22 -4
- package/dist/cli/install/mcp-server-specs.mjs.map +1 -1
- package/dist/cli/roadmap/backend-adapter.d.mts +43 -0
- package/dist/cli/roadmap/backend-adapter.d.mts.map +1 -0
- package/dist/cli/roadmap/backend-adapter.mjs +222 -0
- package/dist/cli/roadmap/backend-adapter.mjs.map +1 -0
- package/dist/cli/util/runtime-capabilities.d.mts +7 -0
- package/dist/cli/util/runtime-capabilities.d.mts.map +1 -1
- package/dist/cli/util/runtime-capabilities.mjs +65 -0
- package/dist/cli/util/runtime-capabilities.mjs.map +1 -1
- package/dist/cli/verify-proof/manifest-validator.mjs +13 -0
- package/dist/cli/verify-proof/manifest-validator.mjs.map +1 -1
- package/dist/cli/verify-proof/resolver.d.mts +10 -7
- package/dist/cli/verify-proof/resolver.d.mts.map +1 -1
- package/dist/cli/verify-proof/resolver.mjs +28 -7
- package/dist/cli/verify-proof/resolver.mjs.map +1 -1
- package/dist/cli/verify-proof/sandbox/apply.d.mts +64 -0
- package/dist/cli/verify-proof/sandbox/apply.d.mts.map +1 -0
- package/dist/cli/verify-proof/sandbox/apply.mjs +379 -0
- package/dist/cli/verify-proof/sandbox/apply.mjs.map +1 -0
- package/dist/cli/verify-proof/sandbox/gate-predicate.d.mts +63 -0
- package/dist/cli/verify-proof/sandbox/gate-predicate.d.mts.map +1 -0
- package/dist/cli/verify-proof/sandbox/gate-predicate.mjs +115 -0
- package/dist/cli/verify-proof/sandbox/gate-predicate.mjs.map +1 -0
- package/dist/cli/verify-proof/sandbox/pattern-matcher.d.mts +57 -0
- package/dist/cli/verify-proof/sandbox/pattern-matcher.d.mts.map +1 -0
- package/dist/cli/verify-proof/sandbox/pattern-matcher.mjs +124 -0
- package/dist/cli/verify-proof/sandbox/pattern-matcher.mjs.map +1 -0
- package/dist/cli/verify-proof/sandbox/recipe-diff.d.mts +28 -0
- package/dist/cli/verify-proof/sandbox/recipe-diff.d.mts.map +1 -0
- package/dist/cli/verify-proof/sandbox/recipe-diff.mjs +190 -0
- package/dist/cli/verify-proof/sandbox/recipe-diff.mjs.map +1 -0
- package/dist/cli/verify-proof/sandbox/sandbox-config.d.mts +25 -0
- package/dist/cli/verify-proof/sandbox/sandbox-config.d.mts.map +1 -0
- package/dist/cli/verify-proof/sandbox/sandbox-config.mjs +85 -0
- package/dist/cli/verify-proof/sandbox/sandbox-config.mjs.map +1 -0
- package/dist/cli/verify-proof/sandbox/self-test.d.mts +39 -0
- package/dist/cli/verify-proof/sandbox/self-test.d.mts.map +1 -0
- package/dist/cli/verify-proof/sandbox/self-test.mjs +248 -0
- package/dist/cli/verify-proof/sandbox/self-test.mjs.map +1 -0
- package/dist/plugin/.claude-plugin/plugin.json +2 -1
- package/dist/plugin/agents/apt-planner.md +15 -0
- package/dist/plugin/agents/apt-verifier.md +15 -10
- package/dist/plugin/skills/apt/SKILL.md +29 -0
- package/dist/plugin/skills/apt-catch-up/SKILL.md +79 -0
- package/dist/plugin/skills/apt-plan/SKILL.md +23 -0
- package/dist/plugin/skills/apt-pr-review/SKILL.md +5 -4
- package/dist/plugin/skills/apt-run/SKILL.md +1 -1
- package/dist/plugin/skills/apt-setup/SKILL.md +132 -0
- package/dist/plugin/skills/apt-verify/SKILL.md +20 -15
- package/dist/plugin/skills/apt-verify-proof/SKILL.md +146 -11
- package/dist/proof-report.d.ts.map +1 -1
- package/dist/proof-report.js +4 -1
- package/dist/proof-report.js.map +1 -1
- package/dist/types/config.d.ts +42 -4
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/qa-scoring.d.ts +45 -9
- package/dist/types/qa-scoring.d.ts.map +1 -1
- package/dist/types/qa-scoring.js +34 -13
- package/dist/types/qa-scoring.js.map +1 -1
- package/dist/types/state.d.ts +7 -2
- package/dist/types/state.d.ts.map +1 -1
- package/dist/types/task-record.d.ts +10 -2
- package/dist/types/task-record.d.ts.map +1 -1
- package/drivers/mobile/README.md +40 -0
- package/drivers/mobile/driver.mjs +106 -0
- package/drivers/mobile/manifest.json +49 -0
- package/package.json +1 -1
- package/prompts/inbox-clarification.md +11 -0
- package/prompts/inbox-triage.md +20 -0
- package/prompts/qa_orchestrator_agentic.md +18 -17
- package/prompts/qa_reviewer.md +17 -15
- package/skills/apt/SKILL.md +29 -0
- package/skills/apt-catch-up/SKILL.md +79 -0
- package/skills/apt-plan/SKILL.md +23 -0
- package/skills/apt-pr-review/SKILL.md +5 -4
- package/skills/apt-run/SKILL.md +1 -1
- package/skills/apt-setup/SKILL.md +132 -0
- package/skills/apt-verifier.md +7 -4
- package/skills/apt-verify/SKILL.md +20 -15
- package/skills/apt-verify-proof/SKILL.md +146 -11
- package/src/cli/commands/apply-recipe.mjs +105 -0
- package/src/cli/commands/catch-up.mjs +67 -0
- package/src/cli/commands/mobile-prepare.mjs +151 -0
- package/src/cli/commands/pr-review.mjs +32 -11
- package/src/cli/commands/recipe-diff.mjs +81 -0
- package/src/cli/commands/roadmap.mjs +54 -0
- package/src/cli/commands/route.mjs +92 -1
- package/src/cli/commands/verify-sandbox.mjs +231 -0
- package/src/cli/config/share-policy.mjs +1 -0
- package/src/cli/config/upgrade-gitignore.mjs +1 -0
- package/src/cli/consistency/parse-qa.mjs +20 -4
- package/src/cli/consistency/rules/r5-verdict-consistency.mjs +11 -13
- package/src/cli/coordination/catch-up.mjs +231 -0
- package/src/cli/coordination/last-seen.mjs +131 -0
- package/src/cli/coordination/store.mjs +18 -0
- package/src/cli/coverage-check/user-outcomes.mjs +52 -17
- package/src/cli/dispatch.mjs +29 -0
- package/src/cli/gate/gates/verify-approved.mjs +22 -81
- package/src/cli/git/default-branch.mjs +250 -0
- package/src/cli/git/identity.mjs +9 -3
- package/src/cli/install/legacy-paths.mjs +2 -0
- package/src/cli/install/mcp-server-specs.mjs +24 -4
- package/src/cli/roadmap/backend-adapter.mjs +231 -0
- package/src/cli/util/runtime-capabilities.mjs +67 -0
- package/src/cli/verify-proof/manifest-validator.mjs +15 -0
- package/src/cli/verify-proof/resolver.mjs +27 -7
- package/src/cli/verify-proof/sandbox/apply.mjs +401 -0
- package/src/cli/verify-proof/sandbox/gate-predicate.mjs +126 -0
- package/src/cli/verify-proof/sandbox/pattern-matcher.mjs +127 -0
- package/src/cli/verify-proof/sandbox/recipe-diff.mjs +208 -0
- package/src/cli/verify-proof/sandbox/sandbox-config.mjs +82 -0
- package/src/cli/verify-proof/sandbox/sandbox-schema.json +33 -0
- package/src/cli/verify-proof/sandbox/self-test.mjs +265 -0
- package/templates/config.json +21 -1
- package/templates/proof-verification.md +27 -8
- package/workflows/verify-proof.md +376 -302
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commands/apply-recipe.mjs — `apt-tools apply-recipe <projectDir> [--confirm] [--accept-all] [--force]`.
|
|
3
|
+
*
|
|
4
|
+
* Wraps applyRecipe() at the CLI surface.
|
|
5
|
+
*
|
|
6
|
+
* --confirm switches from dry-run (the default) to live writes.
|
|
7
|
+
* --accept-all bypasses per-change confirmation (pipeline/scripted use).
|
|
8
|
+
* --force overrides the TOFU user_modified guard — overwrites files
|
|
9
|
+
* whose SHA diverges from the recipe's expected before value.
|
|
10
|
+
* Files containing the recipe's marker are still skipped.
|
|
11
|
+
*
|
|
12
|
+
* The two flags compose: `--confirm` alone enters live mode but defers
|
|
13
|
+
* per-change accept/skip to the caller — without `--accept-all` and
|
|
14
|
+
* without a stdin-driven prompt, every change is SKIPPED (fail-safe per
|
|
15
|
+
* ID-02). Adopters using the interactive apt-setup SKILL.md mini-flow
|
|
16
|
+
* walk each change one at a time before invoking the CLI with
|
|
17
|
+
* `--accept-all` once they've reviewed the diff. Adopters in CI /
|
|
18
|
+
* fan-out / scripted contexts pass both flags up front.
|
|
19
|
+
*
|
|
20
|
+
* Exit codes:
|
|
21
|
+
* 0 — applyRecipe completed (dry-run or confirmed)
|
|
22
|
+
* 1 — usage error / no-recipe-match / applyRecipe threw
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { parseFlags } from '../util/args.mjs'
|
|
26
|
+
import { err, exitWith, ok } from '../util/result.mjs'
|
|
27
|
+
import { loadRuntimeCapabilities } from '../util/runtime-capabilities.mjs'
|
|
28
|
+
import { applyRecipe } from '../verify-proof/sandbox/apply.mjs'
|
|
29
|
+
import { matchPatternForProject } from '../verify-proof/sandbox/pattern-matcher.mjs'
|
|
30
|
+
import { discoverBundledRecipes } from '../verify-proof/sandbox/sandbox-config.mjs'
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} projectDir
|
|
34
|
+
* @param {string[]} [extra]
|
|
35
|
+
*/
|
|
36
|
+
export async function cmdApplyRecipe(projectDir, extra = []) {
|
|
37
|
+
if (!projectDir) {
|
|
38
|
+
return err('Usage: apt-tools apply-recipe <project-dir> [--confirm] [--accept-all]')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const flags = parseFlags(extra)
|
|
42
|
+
const confirm = flags.has('confirm')
|
|
43
|
+
const acceptAll = flags.has('accept-all')
|
|
44
|
+
const force = flags.has('force')
|
|
45
|
+
|
|
46
|
+
const recipes = discoverBundledRecipes()
|
|
47
|
+
const runtime = loadRuntimeCapabilities(projectDir)
|
|
48
|
+
|
|
49
|
+
const explicitRecipeId = flags.get('recipe') ?? null
|
|
50
|
+
let recipeId
|
|
51
|
+
let matchReason
|
|
52
|
+
if (explicitRecipeId) {
|
|
53
|
+
recipeId = explicitRecipeId
|
|
54
|
+
matchReason = 'explicit'
|
|
55
|
+
} else {
|
|
56
|
+
const match = matchPatternForProject(projectDir, runtime, recipes)
|
|
57
|
+
recipeId = match.recipeId
|
|
58
|
+
matchReason = match.reason
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!recipeId) {
|
|
62
|
+
return exitWith(
|
|
63
|
+
{
|
|
64
|
+
status: 'error',
|
|
65
|
+
command: 'apply-recipe',
|
|
66
|
+
code: 'E_NO_RECIPE',
|
|
67
|
+
reason: matchReason,
|
|
68
|
+
runtime,
|
|
69
|
+
docs: 'docs/verify-proof-sandbox-patterns.md',
|
|
70
|
+
},
|
|
71
|
+
1,
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const result = await applyRecipe({
|
|
77
|
+
projectDir,
|
|
78
|
+
recipeId,
|
|
79
|
+
recipes,
|
|
80
|
+
confirm,
|
|
81
|
+
// AC8 + ID-02 — only auto-accept when the caller passed
|
|
82
|
+
// --accept-all explicitly. Without the flag the underlying
|
|
83
|
+
// applyRecipe defaults to skip (every change recorded as
|
|
84
|
+
// `user_declined`); callers driving per-change UX wrap the
|
|
85
|
+
// applier directly with their own confirmer.
|
|
86
|
+
perChangeConfirm: acceptAll ? async () => 'accept' : undefined,
|
|
87
|
+
force,
|
|
88
|
+
})
|
|
89
|
+
return ok({
|
|
90
|
+
status: 'ok',
|
|
91
|
+
command: 'apply-recipe',
|
|
92
|
+
project_dir: projectDir,
|
|
93
|
+
recipe_id: recipeId,
|
|
94
|
+
recipe_version: result.recipe_version,
|
|
95
|
+
confirm,
|
|
96
|
+
accept_all: acceptAll,
|
|
97
|
+
aborted: result.aborted,
|
|
98
|
+
writes: result.writes,
|
|
99
|
+
skips: result.skips,
|
|
100
|
+
audit_path: result.audit_path,
|
|
101
|
+
})
|
|
102
|
+
} catch (e) {
|
|
103
|
+
return err(`apply-recipe: ${e?.message ?? String(e)}`)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commands/catch-up.mjs — read-only re-view of the combined catch-up panel
|
|
3
|
+
* (LD-13). Backs the `/apt:catch-up` skill.
|
|
4
|
+
*
|
|
5
|
+
* Recompute-only contract: this command computes the SAME panel the
|
|
6
|
+
* interactive route call would show (Welcome-Back + gated Team-Change Digest)
|
|
7
|
+
* against the CURRENT Last-Seen Marker, but it NEVER advances the marker. A
|
|
8
|
+
* user who dismissed the auto-on-apt display can re-summon it without
|
|
9
|
+
* disturbing their baseline. Distinct from /apt:resume (full context restore).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { resolve } from 'node:path'
|
|
13
|
+
import { loadProjectConfig } from '../config/load.mjs'
|
|
14
|
+
import { resolveSharePolicy } from '../config/share-policy.mjs'
|
|
15
|
+
import { computeCatchUpPanel } from '../coordination/catch-up.mjs'
|
|
16
|
+
import { createCoordinationStore } from '../coordination/store.mjs'
|
|
17
|
+
import { resolveLocalDefaultBranch } from '../git/default-branch.mjs'
|
|
18
|
+
import { err, ok } from '../util/result.mjs'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} projectDir
|
|
22
|
+
* @returns {{ envelope: object, exitCode: number }}
|
|
23
|
+
*/
|
|
24
|
+
export function cmdCatchUp(projectDir) {
|
|
25
|
+
if (!projectDir) return err('Usage: apt-tools catch-up <project-dir>')
|
|
26
|
+
const targetDir = resolve(projectDir)
|
|
27
|
+
|
|
28
|
+
// Setup mirrors computeCatchUpForRoute in commands/route.mjs (advances marker).
|
|
29
|
+
const config = loadProjectConfig(targetDir)
|
|
30
|
+
if (!config) {
|
|
31
|
+
return ok({
|
|
32
|
+
status: 'ok',
|
|
33
|
+
command: 'catch-up',
|
|
34
|
+
catch_up: null,
|
|
35
|
+
reason: 'No .aperant/config.json — nothing to catch up on',
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const store = createCoordinationStore(targetDir)
|
|
40
|
+
const marker = store.readLastSeen()
|
|
41
|
+
const td = config?.collaboration?.team_digest
|
|
42
|
+
const vis = resolveSharePolicy(config).visibility
|
|
43
|
+
const needsDigest = td?.enabled !== false && (vis === 'team' || vis === 'oss')
|
|
44
|
+
const defaultBranch = needsDigest ? resolveLocalDefaultBranch(targetDir) : null
|
|
45
|
+
|
|
46
|
+
let catch_up = null
|
|
47
|
+
try {
|
|
48
|
+
catch_up = computeCatchUpPanel(targetDir, {
|
|
49
|
+
config,
|
|
50
|
+
now: Date.now(),
|
|
51
|
+
marker,
|
|
52
|
+
defaultBranch,
|
|
53
|
+
})
|
|
54
|
+
} catch (e) {
|
|
55
|
+
console.warn('[apt] catch-up:', e?.message ?? e)
|
|
56
|
+
catch_up = null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// NOTE: recompute-only — we deliberately do NOT call store.advanceLastSeen.
|
|
60
|
+
// The marker baseline is owned by the interactive route path (route.mjs).
|
|
61
|
+
return ok({
|
|
62
|
+
status: 'ok',
|
|
63
|
+
command: 'catch-up',
|
|
64
|
+
catch_up,
|
|
65
|
+
reason: catch_up ? 'Catch-up panel recomputed' : 'Nothing new since your last visit',
|
|
66
|
+
})
|
|
67
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commands/mobile-prepare.mjs — `apt-tools mobile prepare`.
|
|
3
|
+
*
|
|
4
|
+
* Owns the mobile app lifecycle for the verify-proof mobile surface
|
|
5
|
+
* (decisions D-03/D-04): build → install → launch → Metro → launch-readiness
|
|
6
|
+
* gate. This is the home for multi-minute orchestration that CANNOT live in a
|
|
7
|
+
* driver PreflightCheck (those are ≤5s, shell-free, single LocalCommand).
|
|
8
|
+
*
|
|
9
|
+
* The command is PLAN-producing: it resolves the ordered launch sequence and
|
|
10
|
+
* the readiness-gate contract and returns them in the envelope. The
|
|
11
|
+
* verify-proof workflow runner (which holds the host tool surface) executes
|
|
12
|
+
* the sequence, gates on the readiness signals, THEN hands UI verbs to the
|
|
13
|
+
* mobile driver. This mirrors the driver pattern (driver.mjs are adapters;
|
|
14
|
+
* the runner dispatches).
|
|
15
|
+
*
|
|
16
|
+
* Pure, unit-testable seams: `planLaunchSequence` + `isAppReady`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { loadRuntimeCapabilities } from '../util/runtime-capabilities.mjs'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {{ binary: string, args: string[] }} LocalCommand
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolve the ordered launch sequence for a (platform, mode, device).
|
|
27
|
+
*
|
|
28
|
+
* - mode 'expo' — `npx expo run:ios|run:android` builds, installs, launches,
|
|
29
|
+
* and starts Metro in one step (the common Expo/RN path).
|
|
30
|
+
* - mode 'native' — device-prep only (`xcrun simctl boot` / `adb
|
|
31
|
+
* wait-for-device`); the native build+install+launch is delegated (gradle /
|
|
32
|
+
* the XcodeBuildMCP escalation per D-02), out of scope for v1's planner.
|
|
33
|
+
*
|
|
34
|
+
* @param {{ platform?: string, mode?: string, device?: string }} [opts]
|
|
35
|
+
* @returns {LocalCommand[]}
|
|
36
|
+
*/
|
|
37
|
+
export function planLaunchSequence(opts = {}) {
|
|
38
|
+
const { platform, mode = 'expo', device } = opts
|
|
39
|
+
if (platform !== 'ios' && platform !== 'android') {
|
|
40
|
+
throw new Error(`planLaunchSequence: platform must be 'ios' or 'android', got "${platform}"`)
|
|
41
|
+
}
|
|
42
|
+
if (mode !== 'expo' && mode !== 'native') {
|
|
43
|
+
throw new Error(`planLaunchSequence: mode must be 'expo' or 'native', got "${mode}"`)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (mode === 'expo') {
|
|
47
|
+
const expoArgs = ['expo', platform === 'ios' ? 'run:ios' : 'run:android']
|
|
48
|
+
if (device) expoArgs.push('--device', device)
|
|
49
|
+
return [{ binary: 'npx', args: expoArgs }]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// native — device prep only.
|
|
53
|
+
if (platform === 'ios') {
|
|
54
|
+
return [{ binary: 'xcrun', args: ['simctl', 'boot', device ?? 'booted'] }]
|
|
55
|
+
}
|
|
56
|
+
return [{ binary: 'adb', args: device ? ['-s', device, 'wait-for-device'] : ['wait-for-device'] }]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Launch-readiness predicate (D-04). A successful screenshot alone is NOT
|
|
61
|
+
* enough — for RN/Expo it can capture a native splash, an Expo loading
|
|
62
|
+
* overlay, a redbox, or the dev-client launcher. ALL signals must hold before
|
|
63
|
+
* UI driving starts. For a pure-native app (no JS bundle) the caller passes
|
|
64
|
+
* `bundleConnected: true` and `launcherState: false`.
|
|
65
|
+
*
|
|
66
|
+
* @param {{ foregrounded?: boolean, bundleConnected?: boolean, redbox?: boolean, launcherState?: boolean }} [signals]
|
|
67
|
+
* @returns {boolean}
|
|
68
|
+
*/
|
|
69
|
+
export function isAppReady(signals) {
|
|
70
|
+
const s = signals && typeof signals === 'object' ? signals : {}
|
|
71
|
+
return (
|
|
72
|
+
s.foregrounded === true &&
|
|
73
|
+
s.bundleConnected === true &&
|
|
74
|
+
s.redbox !== true &&
|
|
75
|
+
s.launcherState !== true
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The readiness-gate contract surfaced to the workflow runner. */
|
|
80
|
+
const READINESS_GATE = {
|
|
81
|
+
description:
|
|
82
|
+
'App-ready gate (D-04). All signals must hold before UI verbs are dispatched to the mobile driver.',
|
|
83
|
+
signals: ['foregrounded', 'bundleConnected', 'no-redbox', 'no-launcher-state'],
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** @param {string} message */
|
|
87
|
+
function errorEnvelope(message) {
|
|
88
|
+
return { envelope: { status: 'error', command: 'mobile-prepare', error: message }, exitCode: 1 }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Parse `--platform <ios|android> [--mode <expo|native>] [--device <id>]`.
|
|
93
|
+
*
|
|
94
|
+
* @param {string[]} args
|
|
95
|
+
* @returns {{ platform?: string, mode?: string, device?: string }}
|
|
96
|
+
*/
|
|
97
|
+
function parseFlags(args) {
|
|
98
|
+
/** @type {{ platform?: string, mode?: string, device?: string }} */
|
|
99
|
+
const opts = {}
|
|
100
|
+
for (let i = 0; i < args.length; i++) {
|
|
101
|
+
if (args[i] === '--platform' && args[i + 1]) opts.platform = args[++i]
|
|
102
|
+
else if (args[i] === '--mode' && args[i + 1]) opts.mode = args[++i]
|
|
103
|
+
else if (args[i] === '--device' && args[i + 1]) opts.device = args[++i]
|
|
104
|
+
}
|
|
105
|
+
return opts
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* `apt-tools mobile prepare <project-dir> --platform <ios|android>
|
|
110
|
+
* [--mode <expo|native>] [--device <id>]`
|
|
111
|
+
*
|
|
112
|
+
* @param {string} projectDir
|
|
113
|
+
* @param {string[]} args
|
|
114
|
+
* @returns {{ envelope: object, exitCode: number }}
|
|
115
|
+
*/
|
|
116
|
+
export function cmdMobilePrepare(projectDir, args) {
|
|
117
|
+
const opts = parseFlags(args ?? [])
|
|
118
|
+
if (opts.platform !== 'ios' && opts.platform !== 'android') {
|
|
119
|
+
return errorEnvelope('--platform <ios|android> is required')
|
|
120
|
+
}
|
|
121
|
+
if (opts.mode && opts.mode !== 'expo' && opts.mode !== 'native') {
|
|
122
|
+
return errorEnvelope(`--mode must be 'expo' or 'native', got "${opts.mode}"`)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Infer mode from the project shape when not explicitly given (ties the
|
|
126
|
+
// detector to the launch story — an Expo/RN project launches via `expo run`).
|
|
127
|
+
let mode = opts.mode
|
|
128
|
+
if (!mode) {
|
|
129
|
+
const caps = loadRuntimeCapabilities(projectDir ?? '.')
|
|
130
|
+
mode = caps.is_expo || caps.is_react_native ? 'expo' : 'native'
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const launch_sequence = planLaunchSequence({ platform: opts.platform, mode, device: opts.device })
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
envelope: {
|
|
137
|
+
status: 'ok',
|
|
138
|
+
command: 'mobile-prepare',
|
|
139
|
+
platform: opts.platform,
|
|
140
|
+
mode,
|
|
141
|
+
device: opts.device ?? null,
|
|
142
|
+
launch_sequence,
|
|
143
|
+
readiness_gate: READINESS_GATE,
|
|
144
|
+
note:
|
|
145
|
+
'Plan only — the verify-proof workflow runner executes the launch sequence, gates on ' +
|
|
146
|
+
'the readiness signals, then hands UI verbs to the mobile driver. System prereqs ' +
|
|
147
|
+
'(idb/adb/Xcode/SDK) are validated by `apt-tools driver doctor` (D-09).',
|
|
148
|
+
},
|
|
149
|
+
exitCode: 0,
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -727,17 +727,30 @@ function parseGhPrView(raw) {
|
|
|
727
727
|
* the viewer's personal handle (e.g. `AndyMik90`). They never match, which
|
|
728
728
|
* previously caused every same-repo, owned PR to be mis-classified as
|
|
729
729
|
* `external` and silently disabled auto-fix. The third arg `baseRepoOwner`
|
|
730
|
-
* is the source of truth for "is this a fork PR?".
|
|
731
|
-
*
|
|
730
|
+
* is the source of truth for "is this a fork PR?".
|
|
731
|
+
*
|
|
732
|
+
* IMPORTANT (G44): "owned" means "a PR I authored," NOT "a branch in a repo I
|
|
733
|
+
* can push to." The owned test is strictly `gh.author.login === viewerLogin`
|
|
734
|
+
* — author IDENTITY, not `authorAssociation`. Previously a same-repo PR with
|
|
735
|
+
* `authorAssociation ∈ {OWNER, MEMBER, COLLABORATOR}` was classified `owned`,
|
|
736
|
+
* which meant every teammate's same-repo PR triggered the auto-fix-and-push
|
|
737
|
+
* pipeline on their branch. Now `authorAssociation` is retained only in the
|
|
738
|
+
* signal for human-readable tracing; it no longer gates the decision. The
|
|
739
|
+
* "is this mine" comparand is `gh.author?.login`, never `headRepoOwner`
|
|
740
|
+
* (comparing headRepoOwner to viewerLogin was the H1 bug — it never matches
|
|
741
|
+
* on an org-owned repo). Fork detection and author identity are independent:
|
|
742
|
+
* fork detection answers "did this come from a fork?"; author identity
|
|
743
|
+
* answers "did I write it?".
|
|
732
744
|
*
|
|
733
745
|
* @param {Record<string, any> | null | undefined} gh parsed `gh pr view` JSON (may have `authorAssociation` grafted in by the REST-fetch path)
|
|
734
|
-
* @param {string | null | undefined} viewerLogin personal login from `gh api user` (
|
|
735
|
-
* @param {string | null | undefined} [baseRepoOwner] the org/user that owns the repo the PR targets (
|
|
746
|
+
* @param {string | null | undefined} viewerLogin personal login from `gh api user` (compared to `gh.author.login` for the owned test)
|
|
747
|
+
* @param {string | null | undefined} [baseRepoOwner] the org/user that owns the repo the PR targets (fork-detection comparand)
|
|
736
748
|
* @returns {{ authorship_mode: 'owned' | 'external' | 'unknown', authorship_signal: string }}
|
|
737
749
|
*/
|
|
738
750
|
export function decideAuthorshipFromGh(gh, viewerLogin, baseRepoOwner = null) {
|
|
739
751
|
if (!gh) return { authorship_mode: 'unknown', authorship_signal: 'gh_payload_missing' }
|
|
740
752
|
const association = gh.authorAssociation ?? gh.author?.association ?? null
|
|
753
|
+
const author = gh.author?.login ?? null
|
|
741
754
|
const headRepoOwner =
|
|
742
755
|
gh.headRepository?.owner?.login ??
|
|
743
756
|
gh.headRepositoryOwner?.login ??
|
|
@@ -745,28 +758,36 @@ export function decideAuthorshipFromGh(gh, viewerLogin, baseRepoOwner = null) {
|
|
|
745
758
|
null
|
|
746
759
|
// Prefer baseRepoOwner when supplied; fall back to viewerLogin only for
|
|
747
760
|
// older callers that haven't been updated yet (e.g. legacy tests). The
|
|
748
|
-
// fallback preserves prior behavior for the personal-repo
|
|
761
|
+
// fallback preserves prior fork-detection behavior for the personal-repo
|
|
762
|
+
// case.
|
|
749
763
|
const compareOwner = baseRepoOwner ?? viewerLogin
|
|
750
764
|
const isFork = headRepoOwner && compareOwner && headRepoOwner !== compareOwner
|
|
751
765
|
|
|
752
|
-
const ownerLike = ['OWNER', 'MEMBER', 'COLLABORATOR']
|
|
753
766
|
const externalLike = ['NONE', 'CONTRIBUTOR', 'FIRST_TIME_CONTRIBUTOR', 'FIRST_TIMER']
|
|
754
767
|
|
|
755
|
-
|
|
768
|
+
// G44: owned ⟺ (not a fork) AND the PR was authored by the running viewer.
|
|
769
|
+
if (!isFork && author && viewerLogin && author === viewerLogin) {
|
|
756
770
|
return {
|
|
757
771
|
authorship_mode: 'owned',
|
|
758
|
-
authorship_signal: `
|
|
772
|
+
authorship_signal: `authored-by-viewer (author=${author}, viewer=${viewerLogin}), same-repo branch (head=${headRepoOwner ?? '?'}, base=${compareOwner ?? '?'}), authorAssociation=${association ?? 'null'}`,
|
|
759
773
|
}
|
|
760
774
|
}
|
|
761
|
-
|
|
775
|
+
// Forks, external-like associations, AND non-fork PRs authored by someone
|
|
776
|
+
// other than the viewer (a teammate's same-repo PR — the G44 case) all
|
|
777
|
+
// resolve to external (comment-only).
|
|
778
|
+
if (
|
|
779
|
+
isFork ||
|
|
780
|
+
(association && externalLike.includes(association)) ||
|
|
781
|
+
(!isFork && author && viewerLogin && author !== viewerLogin)
|
|
782
|
+
) {
|
|
762
783
|
return {
|
|
763
784
|
authorship_mode: 'external',
|
|
764
|
-
authorship_signal: `authorAssociation=${association}, fork=${Boolean(isFork)} (head=${headRepoOwner ?? '?'}, base=${compareOwner ?? '?'})`,
|
|
785
|
+
authorship_signal: `authorAssociation=${association ?? 'null'}, fork=${Boolean(isFork)}, author=${author ?? '?'}, viewer=${viewerLogin ?? '?'} (head=${headRepoOwner ?? '?'}, base=${compareOwner ?? '?'})`,
|
|
765
786
|
}
|
|
766
787
|
}
|
|
767
788
|
return {
|
|
768
789
|
authorship_mode: 'unknown',
|
|
769
|
-
authorship_signal: `authorAssociation=${association ?? 'null'}, fork=${Boolean(isFork)}`,
|
|
790
|
+
authorship_signal: `authorAssociation=${association ?? 'null'}, fork=${Boolean(isFork)}, author=${author ?? 'null'}`,
|
|
770
791
|
}
|
|
771
792
|
}
|
|
772
793
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commands/recipe-diff.mjs — `apt-tools recipe-diff <projectDir> [--format json|md]`.
|
|
3
|
+
*
|
|
4
|
+
* Loads bundled recipes, runs the pattern-matcher against the adopter's
|
|
5
|
+
* runtime + package.json to pick a recipe, then calls generateRecipeDiff
|
|
6
|
+
* to emit the Markdown body.
|
|
7
|
+
*
|
|
8
|
+
* Exit codes:
|
|
9
|
+
* 0 — success (envelope.status:'ok')
|
|
10
|
+
* 1 — no-recipe-match (envelope.status:'error', code:'E_NO_RECIPE')
|
|
11
|
+
*
|
|
12
|
+
* Output:
|
|
13
|
+
* default / --format json → JSON envelope to stdout
|
|
14
|
+
* --format md → raw Markdown body to stdout
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { parseFlags } from '../util/args.mjs'
|
|
18
|
+
import { err, exitWith, ok, rawText } from '../util/result.mjs'
|
|
19
|
+
import { loadRuntimeCapabilities } from '../util/runtime-capabilities.mjs'
|
|
20
|
+
import { matchPatternForProject } from '../verify-proof/sandbox/pattern-matcher.mjs'
|
|
21
|
+
import { generateRecipeDiff } from '../verify-proof/sandbox/recipe-diff.mjs'
|
|
22
|
+
import { discoverBundledRecipes } from '../verify-proof/sandbox/sandbox-config.mjs'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} projectDir
|
|
26
|
+
* @param {string[]} [extra]
|
|
27
|
+
*/
|
|
28
|
+
export function cmdRecipeDiff(projectDir, extra = []) {
|
|
29
|
+
if (!projectDir) return err('Usage: apt-tools recipe-diff <project-dir> [--format json|md]')
|
|
30
|
+
|
|
31
|
+
const flags = parseFlags(extra)
|
|
32
|
+
const formatFlag = flags.get('format') ?? 'json'
|
|
33
|
+
if (formatFlag !== 'json' && formatFlag !== 'md') {
|
|
34
|
+
return err(`recipe-diff: --format must be 'json' or 'md', got '${formatFlag}'`)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const recipes = discoverBundledRecipes()
|
|
38
|
+
const runtime = loadRuntimeCapabilities(projectDir)
|
|
39
|
+
|
|
40
|
+
// recipeId can be passed explicitly via --recipe; otherwise we match.
|
|
41
|
+
const explicitRecipeId = flags.get('recipe') ?? null
|
|
42
|
+
let recipeId
|
|
43
|
+
let matchReason
|
|
44
|
+
if (explicitRecipeId) {
|
|
45
|
+
recipeId = explicitRecipeId
|
|
46
|
+
matchReason = 'explicit'
|
|
47
|
+
} else {
|
|
48
|
+
const match = matchPatternForProject(projectDir, runtime, recipes)
|
|
49
|
+
recipeId = match.recipeId
|
|
50
|
+
matchReason = match.reason
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!recipeId) {
|
|
54
|
+
const envelope = {
|
|
55
|
+
status: 'error',
|
|
56
|
+
command: 'recipe-diff',
|
|
57
|
+
code: 'E_NO_RECIPE',
|
|
58
|
+
reason: matchReason,
|
|
59
|
+
runtime,
|
|
60
|
+
docs: 'docs/verify-proof-sandbox-patterns.md',
|
|
61
|
+
}
|
|
62
|
+
return exitWith(envelope, 1)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const diff = generateRecipeDiff({ projectDir, recipeId, recipes })
|
|
66
|
+
|
|
67
|
+
if (formatFlag === 'md') {
|
|
68
|
+
const exitCode = diff.envelope.status === 'ok' ? 0 : 1
|
|
69
|
+
return rawText(diff.body, exitCode)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return ok({
|
|
73
|
+
status: 'ok',
|
|
74
|
+
command: 'recipe-diff',
|
|
75
|
+
project_dir: projectDir,
|
|
76
|
+
recipe_id: recipeId,
|
|
77
|
+
match_reason: matchReason,
|
|
78
|
+
runtime,
|
|
79
|
+
...diff,
|
|
80
|
+
})
|
|
81
|
+
}
|
|
@@ -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 { dispatchRoadmapWrite } from '../roadmap/backend-adapter.mjs'
|
|
14
15
|
import { runConvexMirror } from '../roadmap/convex-mirror.mjs'
|
|
15
16
|
import {
|
|
16
17
|
loadRoadmapForScope as ioLoadRoadmapForScope,
|
|
@@ -197,6 +198,34 @@ async function cmdRoadmapImpl(subcommand, projectDir, extraArgs) {
|
|
|
197
198
|
return defaultScope
|
|
198
199
|
}
|
|
199
200
|
|
|
201
|
+
// Task 26-05-26 subtask 2.4 — `.aperant/config.json :: roadmap.backend`
|
|
202
|
+
// dispatch. Called at the top of each write-side subcommand. When the
|
|
203
|
+
// configured backend is `cloud-mcp` AND the dispatch succeeds (or fails
|
|
204
|
+
// with a structured error), we surface that envelope and short-circuit
|
|
205
|
+
// the existing markdown file-write path. When the backend is `markdown`
|
|
206
|
+
// (the Fast Path Guarantee default), the dispatcher returns
|
|
207
|
+
// `delegated:false` and the subcommand proceeds with the existing
|
|
208
|
+
// markdown writes verbatim — byte-identical to pre-2.4 behaviour.
|
|
209
|
+
async function maybeDelegate(subcommand, args) {
|
|
210
|
+
const fetchImpl =
|
|
211
|
+
cmdRoadmap._injectedFetch !== undefined ? cmdRoadmap._injectedFetch : undefined
|
|
212
|
+
const decision = await dispatchRoadmapWrite(targetDir, subcommand, args, { fetchImpl })
|
|
213
|
+
if (decision.delegated === false) return null
|
|
214
|
+
if (decision.status === 'error') {
|
|
215
|
+
return err(decision.error, decision.code === 401 ? 1 : 1, {
|
|
216
|
+
command: `roadmap-${subcommand}`,
|
|
217
|
+
backend: 'cloud-mcp',
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
return ok({
|
|
221
|
+
status: 'ok',
|
|
222
|
+
command: `roadmap-${subcommand}`,
|
|
223
|
+
backend: 'cloud-mcp',
|
|
224
|
+
delegated: true,
|
|
225
|
+
result: decision.result,
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
|
|
200
229
|
// C28 D-07 — cascade-null helper. The scanning of affected tasks is
|
|
201
230
|
// inlined at each destructive roadmap path so the scan + mutate can run
|
|
202
231
|
// under a single `withFileLock(state.json)` and close the TOCTOU window.
|
|
@@ -357,6 +386,9 @@ async function cmdRoadmapImpl(subcommand, projectDir, extraArgs) {
|
|
|
357
386
|
const scope = (flags.get('scope') || 'project').trim() || 'project'
|
|
358
387
|
if (!id || !title) throw new RoadmapUsageError('Required: --id <id> --title <title>')
|
|
359
388
|
|
|
389
|
+
const delegated = await maybeDelegate('add-milestone', { id, title, goal, scope })
|
|
390
|
+
if (delegated) return delegated
|
|
391
|
+
|
|
360
392
|
// Auto-init the scope if its roadmap file doesn't exist yet.
|
|
361
393
|
const { jsonPath } = scopePaths(scope)
|
|
362
394
|
if (!existsSync(jsonPath)) {
|
|
@@ -402,6 +434,15 @@ async function cmdRoadmapImpl(subcommand, projectDir, extraArgs) {
|
|
|
402
434
|
if (!milestoneId || !id || !title)
|
|
403
435
|
throw new RoadmapUsageError('Required: --milestone <id> --id <id> --title <title>')
|
|
404
436
|
|
|
437
|
+
const delegated = await maybeDelegate('add-phase', {
|
|
438
|
+
milestoneId,
|
|
439
|
+
id,
|
|
440
|
+
title,
|
|
441
|
+
description: flags.get('description') || '',
|
|
442
|
+
status: flags.get('status') || 'planned',
|
|
443
|
+
})
|
|
444
|
+
if (delegated) return delegated
|
|
445
|
+
|
|
405
446
|
const scope = resolveScope({ milestoneId })
|
|
406
447
|
const roadmap = loadRoadmap(scope)
|
|
407
448
|
const milestone = roadmap.milestones.find((m) => m.id === milestoneId)
|
|
@@ -445,6 +486,13 @@ async function cmdRoadmapImpl(subcommand, projectDir, extraArgs) {
|
|
|
445
486
|
if (!milestoneId || !phaseId || !status)
|
|
446
487
|
throw new RoadmapUsageError('Required: --milestone <id> --phase <id> --status <status>')
|
|
447
488
|
|
|
489
|
+
const delegated = await maybeDelegate('set-phase-status', {
|
|
490
|
+
milestoneId,
|
|
491
|
+
phaseId,
|
|
492
|
+
status,
|
|
493
|
+
})
|
|
494
|
+
if (delegated) return delegated
|
|
495
|
+
|
|
448
496
|
// C28 D-07 — `deleted` is the destructive path that triggers orphan
|
|
449
497
|
// block + cascade-null. All other statuses are non-destructive.
|
|
450
498
|
const validStatuses = ['planned', 'in-progress', 'completed', 'blocked', 'skipped', 'deleted']
|
|
@@ -602,6 +650,9 @@ async function cmdRoadmapImpl(subcommand, projectDir, extraArgs) {
|
|
|
602
650
|
const phaseId = flags.get('phase')
|
|
603
651
|
if (!milestoneId || !phaseId)
|
|
604
652
|
throw new RoadmapUsageError('Required: --milestone <id> --phase <id>')
|
|
653
|
+
|
|
654
|
+
const delegated = await maybeDelegate('delete-phase', { milestoneId, phaseId })
|
|
655
|
+
if (delegated) return delegated
|
|
605
656
|
const force = extraArgs.includes('--force')
|
|
606
657
|
const reason = flags.get('reason') || null
|
|
607
658
|
if (force && !reason) {
|
|
@@ -698,6 +749,9 @@ async function cmdRoadmapImpl(subcommand, projectDir, extraArgs) {
|
|
|
698
749
|
case 'delete-milestone': {
|
|
699
750
|
const milestoneId = flags.get('id') || flags.get('milestone')
|
|
700
751
|
if (!milestoneId) throw new RoadmapUsageError('Required: --id <milestone-id>')
|
|
752
|
+
|
|
753
|
+
const delegated = await maybeDelegate('delete-milestone', { milestoneId })
|
|
754
|
+
if (delegated) return delegated
|
|
701
755
|
const force = extraArgs.includes('--force')
|
|
702
756
|
const reason = flags.get('reason') || null
|
|
703
757
|
if (force && !reason) {
|