@aperant/framework 0.11.0 → 0.12.1
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 +226 -29
- 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/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/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/validate-evidence.d.mts +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/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/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/design/frontmatter-schema.d.mts +8 -8
- package/dist/cli/dispatch.d.mts.map +1 -1
- package/dist/cli/dispatch.mjs +28 -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/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/personas/sidecar.d.mts +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/roadmap/lifecycle.d.mts +1 -1
- package/dist/cli/roadmap/rollup.d.mts +2 -2
- 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 +1 -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 +2 -2
- package/dist/plugin/skills/apt-plan/SKILL.md +23 -0
- package/dist/plugin/skills/apt-run/SKILL.md +1 -1
- package/dist/plugin/skills/apt-setup/SKILL.md +56 -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 +138 -138
- package/prompts/inbox-clarification.md +17 -0
- package/prompts/inbox-triage.md +21 -0
- package/prompts/qa_orchestrator_agentic.md +18 -17
- package/prompts/qa_reviewer.md +17 -15
- package/skills/apt/SKILL.md +2 -2
- package/skills/apt-plan/SKILL.md +23 -0
- package/skills/apt-run/SKILL.md +1 -1
- package/skills/apt-setup/SKILL.md +56 -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/mobile-prepare.mjs +151 -0
- package/src/cli/commands/recipe-diff.mjs +81 -0
- package/src/cli/commands/roadmap.mjs +54 -0
- package/src/cli/commands/verify-sandbox.mjs +231 -0
- package/src/cli/consistency/parse-qa.mjs +20 -4
- package/src/cli/consistency/rules/r5-verdict-consistency.mjs +11 -13
- package/src/cli/coverage-check/user-outcomes.mjs +52 -17
- package/src/cli/dispatch.mjs +27 -0
- package/src/cli/gate/gates/verify-approved.mjs +22 -81
- 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 +10 -1
- package/templates/proof-verification.md +27 -8
- package/workflows/verify-proof.md +376 -302
|
@@ -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) {
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commands/verify-sandbox.mjs — `apt-tools verify-sandbox <projectDir>`.
|
|
3
|
+
*
|
|
4
|
+
* Drives runSelfTest() then atomically writes
|
|
5
|
+
* .aperant/state/verify-proof-sandbox-verified.json with the v2-shaped
|
|
6
|
+
* verdict envelope. On win32, short-circuits to a platform_unsupported
|
|
7
|
+
* verdict WITHOUT spawning child processes (ID-06 v1 supported floor =
|
|
8
|
+
* darwin + linux).
|
|
9
|
+
*
|
|
10
|
+
* The state file is written for BOTH verified-true and verified-false
|
|
11
|
+
* outcomes (ID-02 — no silent state mutation). The sandbox gate at
|
|
12
|
+
* apt-verify-proof/SKILL.md Step 2c (subtask 5.1) reads this file.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { join, resolve } from 'node:path'
|
|
16
|
+
import { atomicWriteJson } from '../util/atomic-write.mjs'
|
|
17
|
+
import { withFileLock } from '../util/fs-lock.mjs'
|
|
18
|
+
import { err, exitWith, ok } from '../util/result.mjs'
|
|
19
|
+
import { loadRuntimeCapabilities } from '../util/runtime-capabilities.mjs'
|
|
20
|
+
import { matchPatternForProject } from '../verify-proof/sandbox/pattern-matcher.mjs'
|
|
21
|
+
import {
|
|
22
|
+
discoverBundledRecipes,
|
|
23
|
+
readSandboxConfig,
|
|
24
|
+
} from '../verify-proof/sandbox/sandbox-config.mjs'
|
|
25
|
+
import { runSelfTest } from '../verify-proof/sandbox/self-test.mjs'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {string} projectDir
|
|
29
|
+
* @param {string[]} [extra]
|
|
30
|
+
*/
|
|
31
|
+
export async function cmdVerifySandbox(projectDir, _extra = []) {
|
|
32
|
+
if (!projectDir) return err('Usage: apt-tools verify-sandbox <project-dir>')
|
|
33
|
+
|
|
34
|
+
const target = resolve(projectDir)
|
|
35
|
+
const platform = process.platform
|
|
36
|
+
const statePath = join(target, '.aperant', 'state', 'verify-proof-sandbox-verified.json')
|
|
37
|
+
const at = new Date().toISOString()
|
|
38
|
+
|
|
39
|
+
const host = {
|
|
40
|
+
platform,
|
|
41
|
+
arch: process.arch,
|
|
42
|
+
nodeVersion: process.version,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Resolve recipeId from config or runtime detection.
|
|
46
|
+
const sandboxCfg = readSandboxConfig(target)
|
|
47
|
+
const recipes = discoverBundledRecipes()
|
|
48
|
+
let recipeId = sandboxCfg?.recipeId ?? null
|
|
49
|
+
let recipeVersion = sandboxCfg?.recipeVersion ?? null
|
|
50
|
+
if (!recipeId) {
|
|
51
|
+
const runtime = loadRuntimeCapabilities(target)
|
|
52
|
+
const match = matchPatternForProject(target, runtime, recipes)
|
|
53
|
+
recipeId = match.recipeId
|
|
54
|
+
}
|
|
55
|
+
// Always populate recipeVersion from bundled recipes when absent — covers
|
|
56
|
+
// the case where config pins recipeId but omits recipeVersion (LOG-003).
|
|
57
|
+
if (recipeId && !recipeVersion) {
|
|
58
|
+
recipeVersion = recipes.find((r) => r.recipeId === recipeId)?.recipeVersion ?? null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// win32 short-circuit (AC12, ID-06 v1 floor).
|
|
62
|
+
if (platform === 'win32') {
|
|
63
|
+
const envelope = {
|
|
64
|
+
verified: false,
|
|
65
|
+
verdict: 'needs_human',
|
|
66
|
+
reason: 'platform_unsupported',
|
|
67
|
+
recipeId,
|
|
68
|
+
recipeVersion,
|
|
69
|
+
verifiedAt: at,
|
|
70
|
+
host,
|
|
71
|
+
}
|
|
72
|
+
const writeErr = writeStateFile(statePath, envelope)
|
|
73
|
+
if (writeErr) {
|
|
74
|
+
return exitWith(
|
|
75
|
+
{
|
|
76
|
+
status: 'error',
|
|
77
|
+
command: 'verify-sandbox',
|
|
78
|
+
code: 'E_STATE_WRITE_FAILED',
|
|
79
|
+
verdict: 'needs_human',
|
|
80
|
+
reason: 'state_write_failed',
|
|
81
|
+
cause: writeErr.message,
|
|
82
|
+
project_dir: target,
|
|
83
|
+
state_path: statePath,
|
|
84
|
+
},
|
|
85
|
+
1,
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
return ok({
|
|
89
|
+
status: 'ok',
|
|
90
|
+
command: 'verify-sandbox',
|
|
91
|
+
project_dir: target,
|
|
92
|
+
state_path: statePath,
|
|
93
|
+
...envelope,
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!recipeId) {
|
|
98
|
+
const envelope = {
|
|
99
|
+
verified: false,
|
|
100
|
+
verdict: 'needs_human',
|
|
101
|
+
reason: 'no_recipe_available',
|
|
102
|
+
recipeId: null,
|
|
103
|
+
recipeVersion: null,
|
|
104
|
+
verifiedAt: at,
|
|
105
|
+
host,
|
|
106
|
+
}
|
|
107
|
+
const writeErr = writeStateFile(statePath, envelope)
|
|
108
|
+
if (writeErr) {
|
|
109
|
+
return exitWith(
|
|
110
|
+
{
|
|
111
|
+
status: 'error',
|
|
112
|
+
command: 'verify-sandbox',
|
|
113
|
+
code: 'E_STATE_WRITE_FAILED',
|
|
114
|
+
verdict: 'needs_human',
|
|
115
|
+
reason: 'state_write_failed',
|
|
116
|
+
cause: writeErr.message,
|
|
117
|
+
project_dir: target,
|
|
118
|
+
state_path: statePath,
|
|
119
|
+
},
|
|
120
|
+
1,
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
return exitWith(
|
|
124
|
+
{
|
|
125
|
+
status: 'error',
|
|
126
|
+
command: 'verify-sandbox',
|
|
127
|
+
code: 'E_NO_RECIPE',
|
|
128
|
+
project_dir: target,
|
|
129
|
+
state_path: statePath,
|
|
130
|
+
...envelope,
|
|
131
|
+
},
|
|
132
|
+
1,
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let result
|
|
137
|
+
try {
|
|
138
|
+
result = await runSelfTest({ projectDir: target, recipeId, recipes })
|
|
139
|
+
} catch (e) {
|
|
140
|
+
const envelope = {
|
|
141
|
+
verified: false,
|
|
142
|
+
verdict: 'needs_human',
|
|
143
|
+
reason: 'self_test_failed_app_init_timeout',
|
|
144
|
+
recipeId,
|
|
145
|
+
recipeVersion,
|
|
146
|
+
verifiedAt: at,
|
|
147
|
+
host,
|
|
148
|
+
error: e?.message ?? String(e),
|
|
149
|
+
}
|
|
150
|
+
const writeErr = writeStateFile(statePath, envelope)
|
|
151
|
+
if (writeErr) {
|
|
152
|
+
return exitWith(
|
|
153
|
+
{
|
|
154
|
+
status: 'error',
|
|
155
|
+
command: 'verify-sandbox',
|
|
156
|
+
code: 'E_STATE_WRITE_FAILED',
|
|
157
|
+
verdict: 'needs_human',
|
|
158
|
+
reason: 'state_write_failed',
|
|
159
|
+
cause: writeErr.message,
|
|
160
|
+
project_dir: target,
|
|
161
|
+
state_path: statePath,
|
|
162
|
+
},
|
|
163
|
+
1,
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
return exitWith(
|
|
167
|
+
{
|
|
168
|
+
status: 'error',
|
|
169
|
+
command: 'verify-sandbox',
|
|
170
|
+
project_dir: target,
|
|
171
|
+
state_path: statePath,
|
|
172
|
+
...envelope,
|
|
173
|
+
},
|
|
174
|
+
1,
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const envelope = {
|
|
179
|
+
verified: result.verified,
|
|
180
|
+
verdict: result.verdict,
|
|
181
|
+
reason: result.reason,
|
|
182
|
+
recipeId,
|
|
183
|
+
recipeVersion,
|
|
184
|
+
verifiedAt: at,
|
|
185
|
+
host,
|
|
186
|
+
durationMs: result.durationMs,
|
|
187
|
+
}
|
|
188
|
+
const writeErr = writeStateFile(statePath, envelope)
|
|
189
|
+
if (writeErr) {
|
|
190
|
+
return exitWith(
|
|
191
|
+
{
|
|
192
|
+
status: 'error',
|
|
193
|
+
command: 'verify-sandbox',
|
|
194
|
+
code: 'E_STATE_WRITE_FAILED',
|
|
195
|
+
verdict: 'needs_human',
|
|
196
|
+
reason: 'state_write_failed',
|
|
197
|
+
cause: writeErr.message,
|
|
198
|
+
project_dir: target,
|
|
199
|
+
state_path: statePath,
|
|
200
|
+
},
|
|
201
|
+
1,
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const exitCode = result.verified ? 0 : 1
|
|
206
|
+
return exitWith(
|
|
207
|
+
{
|
|
208
|
+
status: result.verified ? 'ok' : 'error',
|
|
209
|
+
command: 'verify-sandbox',
|
|
210
|
+
project_dir: target,
|
|
211
|
+
state_path: statePath,
|
|
212
|
+
...envelope,
|
|
213
|
+
},
|
|
214
|
+
exitCode,
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Write the verdict envelope to the state file under a file lock.
|
|
220
|
+
* Returns null on success, or an Error if the lock or write fails.
|
|
221
|
+
*/
|
|
222
|
+
function writeStateFile(statePath, envelope) {
|
|
223
|
+
try {
|
|
224
|
+
withFileLock(statePath, () => {
|
|
225
|
+
atomicWriteJson(statePath, envelope)
|
|
226
|
+
})
|
|
227
|
+
return null
|
|
228
|
+
} catch (e) {
|
|
229
|
+
return e instanceof Error ? e : new Error(String(e))
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -7,13 +7,17 @@
|
|
|
7
7
|
* - flat findings: findings: [{severity}]
|
|
8
8
|
* - legacy `issues: [{severity}]`
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* v0.9.0 (verify-proof-v2-outcome-walker_27-05-26) — verdict closed set is
|
|
11
|
+
* `{approved, rejected, needs_human, needs-changes}`. `approved_with_notes`
|
|
12
|
+
* is DELETED (no shim) — reading a legacy `approved_with_notes` /
|
|
13
|
+
* `approved-with-notes` value throws a clear migration error pointing at
|
|
14
|
+
* the framework CHANGELOG entry. Anything else maps to `'unknown'`.
|
|
15
|
+
* Accepts legacy `status` as a fallback field name.
|
|
12
16
|
*
|
|
13
17
|
* Output: {verdict, findings: [{severity, title, status, file}]}
|
|
14
18
|
*/
|
|
15
19
|
|
|
16
|
-
const KNOWN_VERDICTS = new Set(['approved', '
|
|
20
|
+
const KNOWN_VERDICTS = new Set(['approved', 'rejected', 'needs-human', 'needs-changes'])
|
|
17
21
|
|
|
18
22
|
const SEVERITY_MAP = {
|
|
19
23
|
critical: 'critical',
|
|
@@ -56,7 +60,19 @@ function normalizeVerdict(v) {
|
|
|
56
60
|
const k = v.toLowerCase().trim()
|
|
57
61
|
// normalize common spellings
|
|
58
62
|
const canonical = k.replace(/_/g, '-')
|
|
59
|
-
|
|
63
|
+
// v0.9.0 migration: approved_with_notes / approved-with-notes is
|
|
64
|
+
// DELETED with no shim. Crash loudly so the consumer task is
|
|
65
|
+
// re-run, not silently re-mapped to `approved`.
|
|
66
|
+
if (canonical === 'approved-with-notes') {
|
|
67
|
+
throw new Error(
|
|
68
|
+
"qa_signoff.json carries the legacy verdict 'approved_with_notes' (or 'approved-with-notes'). " +
|
|
69
|
+
"This verdict is removed in framework v0.9.0+ (see packages/framework/CHANGELOG.md :: 'verify-proof v2 — outcome walker + two-gate verdict'). " +
|
|
70
|
+
'Re-run verify-proof on this task to mint a fresh qa_signoff.json with the new {approved, rejected, needs_human} enum.',
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
// Accept both snake_case and kebab-case for needs_human.
|
|
74
|
+
const needsHuman = canonical === 'needs-human' ? 'needs-human' : canonical
|
|
75
|
+
return KNOWN_VERDICTS.has(needsHuman) ? needsHuman : 'unknown'
|
|
60
76
|
}
|
|
61
77
|
|
|
62
78
|
function collectRaw(qa) {
|
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
* consistency/rules/r5-verdict-consistency.mjs — R5 (C15).
|
|
4
4
|
*
|
|
5
5
|
* Fails when:
|
|
6
|
-
* - qa.verdict === 'approved'
|
|
7
|
-
*
|
|
8
|
-
* - qa.verdict === 'rejected' AND qa has no findings at all AND review
|
|
9
|
-
* zero unresolved findings (rejected-without-evidence)
|
|
6
|
+
* - qa.verdict === 'approved' AND review has >=1 unresolved critical or
|
|
7
|
+
* major finding.
|
|
8
|
+
* - qa.verdict === 'rejected' AND qa has no findings at all AND review
|
|
9
|
+
* has zero unresolved findings (rejected-without-evidence).
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* v0.9.0 (verify-proof-v2-outcome-walker_27-05-26) — the legacy
|
|
12
|
+
* `approved-with-notes` branch is REMOVED. The verdict enum is the closed
|
|
13
|
+
* set `{approved, rejected, needs_human}`. Reading a legacy verdict
|
|
14
|
+
* crashes in parseQaSignoff before this rule executes.
|
|
13
15
|
*
|
|
14
16
|
* Severity: error. Stages: verify, ship.
|
|
15
17
|
*/
|
|
@@ -30,10 +32,7 @@ const rule = {
|
|
|
30
32
|
const unresolvedBlocking = reviewFindings.filter(
|
|
31
33
|
(f) => (f.severity === 'critical' || f.severity === 'major') && f.status !== 'fixed',
|
|
32
34
|
)
|
|
33
|
-
if (
|
|
34
|
-
(verdict === 'approved' || verdict === 'approved-with-notes') &&
|
|
35
|
-
unresolvedBlocking.length > 0
|
|
36
|
-
) {
|
|
35
|
+
if (verdict === 'approved' && unresolvedBlocking.length > 0) {
|
|
37
36
|
return {
|
|
38
37
|
status: 'fail',
|
|
39
38
|
drift: [
|
|
@@ -43,11 +42,10 @@ const rule = {
|
|
|
43
42
|
id: f.id,
|
|
44
43
|
title: f.title,
|
|
45
44
|
})),
|
|
46
|
-
reason:
|
|
47
|
-
'approved/approved-with-notes verdict with unresolved critical or major finding(s) (G42 Phase 1)',
|
|
45
|
+
reason: 'approved verdict with unresolved critical or major finding(s) (G42 Phase 1)',
|
|
48
46
|
},
|
|
49
47
|
],
|
|
50
|
-
reason: `approved
|
|
48
|
+
reason: `approved verdict contradicts ${unresolvedBlocking.length} unresolved critical/major finding(s)`,
|
|
51
49
|
}
|
|
52
50
|
}
|
|
53
51
|
if (verdict === 'rejected') {
|
|
@@ -33,20 +33,21 @@
|
|
|
33
33
|
* authoring contract.
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
|
-
const VALID_SURFACES = new Set(['electron', 'web', 'cli', 'api', 'electron+web'])
|
|
36
|
+
const VALID_SURFACES = new Set(['electron', 'web', 'cli', 'api', 'mobile', 'electron+web'])
|
|
37
37
|
|
|
38
38
|
const HEADING_RE = /^(#{1,6})\s+(.*)$/
|
|
39
39
|
const USER_OUTCOMES_HEADING_RE = /^user\s+outcomes\s*$/i
|
|
40
40
|
const EPIC_RE = /^\*\*Epic:\*\*\s+(.+)$/
|
|
41
41
|
const EMPTY_WITH_NOTE_RE = /^_No user-observable changes — pure refactor\._\s*$/
|
|
42
42
|
const OUTCOME_RE =
|
|
43
|
-
/^-\s+\*\*ON\*\*\s+\[(electron|web|cli|api|electron\+web)\]:\s+(.+?)(\s+\*\(requires\s+ON\s+(\d+)\)\*)?\s*$/
|
|
43
|
+
/^-\s+\*\*ON\*\*\s+\[(electron|web|cli|api|mobile|electron\+web)\]:\s+(.+?)(\s+\*\(requires\s+ON\s+(\d+)\)\*)?\s*$/
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
46
|
* @typedef {Object} UserOutcome
|
|
47
47
|
* @property {string} id — synthesized positional ID (O1, O2, ...)
|
|
48
|
-
* @property {('electron'|'web'|'cli'|'api'|'electron+web')} surface
|
|
48
|
+
* @property {('electron'|'web'|'cli'|'api'|'mobile'|'electron+web')} surface
|
|
49
49
|
* @property {string} text — outcome prose (sans the surface tag and requires suffix)
|
|
50
|
+
* @property {string[]} inferred — v0.9.0 — indented child bullets under the parent outcome (raw prose, no further parsing). Defaults to [] when no children are present.
|
|
50
51
|
* @property {number} [requires] — the digit captured from `*(requires ON N)*`, when present and valid
|
|
51
52
|
*/
|
|
52
53
|
|
|
@@ -165,24 +166,58 @@ export function parseUserOutcomes(specContent) {
|
|
|
165
166
|
return result
|
|
166
167
|
}
|
|
167
168
|
|
|
168
|
-
// Parse outcome rows in document order.
|
|
169
|
-
//
|
|
169
|
+
// Parse outcome rows in document order. v0.9.0 — walk the raw body lines
|
|
170
|
+
// (not the trimmed `outcomeLines`) so we preserve indentation of child
|
|
171
|
+
// bullets. A `**ON**` parent row is followed by zero or more indented
|
|
172
|
+
// child bullets (lines starting with at least 2 spaces + `- `); those
|
|
173
|
+
// children attach to the parent as `inferred: string[]` (raw prose, no
|
|
174
|
+
// further parsing). The children block ends at the next `**ON**` row,
|
|
175
|
+
// a non-indented line, or a section terminator (blank line followed by
|
|
176
|
+
// another non-bullet non-blank line is fine; the next `**ON**` always
|
|
177
|
+
// resets ownership).
|
|
170
178
|
/** @type {UserOutcome[]} */
|
|
171
179
|
const outcomes = []
|
|
172
180
|
const rawRequires = []
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
181
|
+
const CHILD_RE = /^(?: {2}|\t)+- (.+)$/
|
|
182
|
+
let currentOutcomeIndex = -1
|
|
183
|
+
for (let i = 0; i < bodyLines.length; i++) {
|
|
184
|
+
const rawLine = bodyLines[i]
|
|
185
|
+
const trimmed = rawLine.trim()
|
|
186
|
+
if (trimmed.length === 0) {
|
|
187
|
+
// Blank lines don't close the parent — children may resume after
|
|
188
|
+
// a blank line (loose-list markdown). The next non-blank line
|
|
189
|
+
// decides ownership.
|
|
190
|
+
continue
|
|
191
|
+
}
|
|
192
|
+
const onMatch = trimmed.match(OUTCOME_RE)
|
|
193
|
+
if (onMatch) {
|
|
194
|
+
const surface = /** @type {UserOutcome['surface']} */ (onMatch[1])
|
|
195
|
+
if (!VALID_SURFACES.has(surface)) {
|
|
196
|
+
currentOutcomeIndex = -1
|
|
197
|
+
continue
|
|
198
|
+
}
|
|
199
|
+
const text = onMatch[2].trim()
|
|
200
|
+
const id = 'O' + (outcomes.length + 1)
|
|
201
|
+
const outcome = /** @type {UserOutcome} */ ({ id, surface, text, inferred: [] })
|
|
202
|
+
if (onMatch[4]) {
|
|
203
|
+
const n = Number.parseInt(onMatch[4], 10)
|
|
204
|
+
rawRequires.push({ outcomeIndex: outcomes.length, requires: n })
|
|
205
|
+
}
|
|
206
|
+
outcomes.push(outcome)
|
|
207
|
+
currentOutcomeIndex = outcomes.length - 1
|
|
208
|
+
continue
|
|
209
|
+
}
|
|
210
|
+
// Non-`**ON**` non-blank line. If it's an indented child bullet AND
|
|
211
|
+
// we have a current outcome, attach it. Otherwise (e.g. Epic line,
|
|
212
|
+
// sentinel, stray prose), drop ownership.
|
|
213
|
+
const childMatch = rawLine.match(CHILD_RE)
|
|
214
|
+
if (childMatch && currentOutcomeIndex >= 0) {
|
|
215
|
+
outcomes[currentOutcomeIndex].inferred.push(childMatch[1].trim())
|
|
216
|
+
continue
|
|
184
217
|
}
|
|
185
|
-
|
|
218
|
+
// Non-indented, non-`**ON**` line — closes the current outcome's
|
|
219
|
+
// child block. (The next `**ON**` will start a new block.)
|
|
220
|
+
currentOutcomeIndex = -1
|
|
186
221
|
}
|
|
187
222
|
|
|
188
223
|
if (outcomes.length === 0) {
|
package/src/cli/dispatch.mjs
CHANGED
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
|
|
37
37
|
import { cmdActive } from './commands/active.mjs'
|
|
38
38
|
import { cmdAdr } from './commands/adr.mjs'
|
|
39
|
+
// cmdApplyRecipe — loaded lazily in the dispatch handler (Fast Path PRF-001)
|
|
39
40
|
import { cmdAudit } from './commands/audit.mjs'
|
|
40
41
|
import { cmdAuthorSkill } from './commands/author-skill.mjs'
|
|
41
42
|
import { cmdBootstrap } from './commands/bootstrap.mjs'
|
|
@@ -70,12 +71,14 @@ import { cmdInitState } from './commands/init-state.mjs'
|
|
|
70
71
|
import { cmdInstallFromSource } from './commands/install-from-source.mjs'
|
|
71
72
|
import { cmdLock } from './commands/lock.mjs'
|
|
72
73
|
import { cmdMigratePrReviews } from './commands/migrate-pr-reviews.mjs'
|
|
74
|
+
import { cmdMobilePrepare } from './commands/mobile-prepare.mjs'
|
|
73
75
|
import { cmdPersonas } from './commands/personas.mjs'
|
|
74
76
|
import { cmdPlanApproval } from './commands/plan-approval.mjs'
|
|
75
77
|
import { cmdPrReview } from './commands/pr-review.mjs'
|
|
76
78
|
import { cmdPrecompactHook } from './commands/precompact-hook.mjs'
|
|
77
79
|
import { cmdProductContext } from './commands/product-context.mjs'
|
|
78
80
|
import { cmdProfile } from './commands/profile.mjs'
|
|
81
|
+
// cmdRecipeDiff — loaded lazily in the dispatch handler (Fast Path PRF-001)
|
|
79
82
|
import { cmdReleaseNotes } from './commands/release-notes.mjs'
|
|
80
83
|
import { cmdRiskCheck } from './commands/risk-check.mjs'
|
|
81
84
|
import { cmdRoadmap } from './commands/roadmap.mjs'
|
|
@@ -98,6 +101,7 @@ import { cmdValidateEvidence } from './commands/validate-evidence.mjs'
|
|
|
98
101
|
import { cmdVariantGallery } from './commands/variant-gallery.mjs'
|
|
99
102
|
import { cmdVariantPick } from './commands/variant-pick.mjs'
|
|
100
103
|
import { cmdVariantPool } from './commands/variant-pool.mjs'
|
|
104
|
+
// cmdVerifySandbox — loaded lazily in the dispatch handler (Fast Path PRF-001)
|
|
101
105
|
import { cmdVitestDoctor } from './commands/vitest-doctor.mjs'
|
|
102
106
|
import { cmdWave } from './commands/wave.mjs'
|
|
103
107
|
import { cmdWfrun } from './commands/wfrun.mjs'
|
|
@@ -219,6 +223,18 @@ export const DISPATCH = {
|
|
|
219
223
|
'state-artifacts': (args) => wrap(cmdStateArtifacts)(args[1], args.slice(2)),
|
|
220
224
|
'detect-project': (args) => wrap(cmdDetectProject)(args[1]),
|
|
221
225
|
'detect-runtime': (args) => wrap(cmdDetectRuntime)(args[1], args.slice(2)),
|
|
226
|
+
'recipe-diff': async (args) => {
|
|
227
|
+
const { cmdRecipeDiff } = await import('./commands/recipe-diff.mjs')
|
|
228
|
+
return wrap(cmdRecipeDiff)(args[1], args.slice(2))
|
|
229
|
+
},
|
|
230
|
+
'apply-recipe': async (args) => {
|
|
231
|
+
const { cmdApplyRecipe } = await import('./commands/apply-recipe.mjs')
|
|
232
|
+
return wrap(cmdApplyRecipe)(args[1], args.slice(2))
|
|
233
|
+
},
|
|
234
|
+
'verify-sandbox': async (args) => {
|
|
235
|
+
const { cmdVerifySandbox } = await import('./commands/verify-sandbox.mjs')
|
|
236
|
+
return wrap(cmdVerifySandbox)(args[1], args.slice(2))
|
|
237
|
+
},
|
|
222
238
|
driver: (args) => {
|
|
223
239
|
// `apt-tools driver doctor <project-dir>` — only subcommand for now.
|
|
224
240
|
if (args[1] === 'doctor') return wrap(cmdDriverDoctor)(args[2], args.slice(3))
|
|
@@ -230,6 +246,17 @@ export const DISPATCH = {
|
|
|
230
246
|
exitCode: 1,
|
|
231
247
|
}))()
|
|
232
248
|
},
|
|
249
|
+
mobile: (args) => {
|
|
250
|
+
// `apt-tools mobile prepare <project-dir> --platform <ios|android> [--mode <expo|native>] [--device <id>]`
|
|
251
|
+
if (args[1] === 'prepare') return wrap(cmdMobilePrepare)(args[2], args.slice(3))
|
|
252
|
+
return wrap(() => ({
|
|
253
|
+
envelope: {
|
|
254
|
+
status: 'error',
|
|
255
|
+
error: `Usage: apt-tools mobile prepare <project-dir> --platform <ios|android>. Unknown subcommand: ${args[1] ?? '(none)'}`,
|
|
256
|
+
},
|
|
257
|
+
exitCode: 1,
|
|
258
|
+
}))()
|
|
259
|
+
},
|
|
233
260
|
'features-audit': (args) => wrap(cmdFeaturesAudit)(args[1], args.slice(2)),
|
|
234
261
|
'init-state': (args) => wrap(cmdInitState)(args[1], args.slice(2)),
|
|
235
262
|
'risk-check': (args) => wrap(cmdRiskCheck)(args[1], args.slice(2)),
|