@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,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task 26-05-26 subtask 2.4 — roadmap.backend dispatch adapter.
|
|
3
|
+
*
|
|
4
|
+
* Reads `.aperant/config.json :: roadmap.backend` and either delegates
|
|
5
|
+
* the subcommand to the Aperant Cloud MCP server (`'cloud-mcp'`) or
|
|
6
|
+
* returns a `{ delegated: false }` envelope so the calling subcommand
|
|
7
|
+
* proceeds with the existing markdown-canonical file-write path.
|
|
8
|
+
*
|
|
9
|
+
* Fast Path Guarantee: a missing `roadmap` block, a missing
|
|
10
|
+
* `roadmap.backend` key, or `backend: 'markdown'` ALL return
|
|
11
|
+
* `{ backend: 'markdown', delegated: false }` byte-identically — OSS
|
|
12
|
+
* framework users with zero cloud config see no behaviour change.
|
|
13
|
+
*
|
|
14
|
+
* Token resolution mirrors convex-mirror.mjs (token-source order ID-07):
|
|
15
|
+
* 1. process.env.MCP_ROADMAP_TOKEN
|
|
16
|
+
* 2. .aperant/team.json :: roadmapMcpToken
|
|
17
|
+
*
|
|
18
|
+
* Tool registration on the server side lands in subtask 2.3
|
|
19
|
+
* (apps/web/src/app/api/mcp/_lib/tools.ts). Until 2.3 ships, the
|
|
20
|
+
* adapter will issue the MCP call against whatever endpoint the user
|
|
21
|
+
* configured; a 404 / "unknown tool" response surfaces as a delegated
|
|
22
|
+
* error envelope rather than silently falling back to markdown.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
26
|
+
import { join } from 'node:path'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Load `.aperant/config.json` defensively. Returns `{}` for any read
|
|
30
|
+
* or parse failure so callers never throw — the Fast Path branch must
|
|
31
|
+
* hit even when the config file is missing or malformed.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} projectDir
|
|
34
|
+
* @returns {Record<string, unknown>}
|
|
35
|
+
*/
|
|
36
|
+
function loadConfig(projectDir) {
|
|
37
|
+
const p = join(projectDir, '.aperant', 'config.json')
|
|
38
|
+
if (!existsSync(p)) return {}
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(readFileSync(p, 'utf-8'))
|
|
41
|
+
return parsed && typeof parsed === 'object' ? parsed : {}
|
|
42
|
+
} catch {
|
|
43
|
+
return {}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Resolve the cloud-mcp bearer token via the documented order. Returns
|
|
49
|
+
* `null` when neither source has a non-empty string.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} projectDir
|
|
52
|
+
* @returns {string | null}
|
|
53
|
+
*/
|
|
54
|
+
function resolveToken(projectDir) {
|
|
55
|
+
const env = process.env.MCP_ROADMAP_TOKEN
|
|
56
|
+
if (typeof env === 'string' && env.length > 0) return env
|
|
57
|
+
const teamPath = join(projectDir, '.aperant', 'team.json')
|
|
58
|
+
if (!existsSync(teamPath)) return null
|
|
59
|
+
try {
|
|
60
|
+
const parsed = JSON.parse(readFileSync(teamPath, 'utf-8'))
|
|
61
|
+
if (typeof parsed.roadmapMcpToken === 'string' && parsed.roadmapMcpToken.length > 0) {
|
|
62
|
+
return parsed.roadmapMcpToken
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// Bad JSON — fall through to null
|
|
66
|
+
}
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Map a CLI subcommand to the MCP tool name registered in subtask 2.3.
|
|
72
|
+
* Read-side subcommands are intentionally absent — Phase 2 keeps reads
|
|
73
|
+
* markdown-only (cloud-mcp render is a Phase 3 follow-up per the plan).
|
|
74
|
+
*
|
|
75
|
+
* @param {string} subcommand
|
|
76
|
+
* @returns {string | null}
|
|
77
|
+
*/
|
|
78
|
+
function toolNameFor(subcommand) {
|
|
79
|
+
switch (subcommand) {
|
|
80
|
+
case 'add-milestone':
|
|
81
|
+
return 'roadmap.hierarchy.milestones.create'
|
|
82
|
+
case 'add-phase':
|
|
83
|
+
return 'roadmap.hierarchy.phases.create'
|
|
84
|
+
case 'set-phase-status':
|
|
85
|
+
return 'roadmap.hierarchy.phases.update'
|
|
86
|
+
case 'delete-phase':
|
|
87
|
+
return 'roadmap.hierarchy.phases.remove'
|
|
88
|
+
case 'delete-milestone':
|
|
89
|
+
return 'roadmap.hierarchy.milestones.remove'
|
|
90
|
+
default:
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Dispatch a roadmap write subcommand through the configured backend.
|
|
97
|
+
*
|
|
98
|
+
* Return shapes:
|
|
99
|
+
* - markdown (default) → `{ backend: 'markdown', delegated: false }`
|
|
100
|
+
* The caller proceeds with the existing markdown file-write path.
|
|
101
|
+
* - cloud-mcp success → `{ backend: 'cloud-mcp', delegated: true,
|
|
102
|
+
* result: <MCP response JSON> }`
|
|
103
|
+
* - cloud-mcp error → `{ backend: 'cloud-mcp', delegated: true,
|
|
104
|
+
* error: '<msg>', code?: <http>, status: 'error' }`
|
|
105
|
+
*
|
|
106
|
+
* The `error` branch is still `delegated: true` because the caller MUST
|
|
107
|
+
* NOT silently fall through to the markdown path when the user has
|
|
108
|
+
* opted into cloud-mcp — that would lose writes the user expected to
|
|
109
|
+
* land in Convex.
|
|
110
|
+
*
|
|
111
|
+
* @param {string} projectDir
|
|
112
|
+
* @param {string} subcommand
|
|
113
|
+
* @param {Record<string, unknown>} args
|
|
114
|
+
* @param {{ fetchImpl?: typeof fetch }} [opts]
|
|
115
|
+
* @returns {Promise<
|
|
116
|
+
* | { backend: 'markdown', delegated: false }
|
|
117
|
+
* | { backend: 'cloud-mcp', delegated: true, result: unknown }
|
|
118
|
+
* | { backend: 'cloud-mcp', delegated: true, status: 'error', error: string, code?: number }
|
|
119
|
+
* >}
|
|
120
|
+
*/
|
|
121
|
+
export async function dispatchRoadmapWrite(projectDir, subcommand, args, opts = {}) {
|
|
122
|
+
const config = loadConfig(projectDir)
|
|
123
|
+
const roadmapBlock = config.roadmap && typeof config.roadmap === 'object' ? config.roadmap : null
|
|
124
|
+
const backend =
|
|
125
|
+
roadmapBlock && typeof roadmapBlock.backend === 'string' ? roadmapBlock.backend : 'markdown'
|
|
126
|
+
|
|
127
|
+
if (backend !== 'cloud-mcp') {
|
|
128
|
+
return { backend: 'markdown', delegated: false }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const cloudMcp =
|
|
132
|
+
roadmapBlock && typeof roadmapBlock.cloudMcp === 'object' && roadmapBlock.cloudMcp !== null
|
|
133
|
+
? roadmapBlock.cloudMcp
|
|
134
|
+
: {}
|
|
135
|
+
const teamSlug = typeof cloudMcp.teamSlug === 'string' ? cloudMcp.teamSlug : null
|
|
136
|
+
if (!teamSlug || teamSlug.length === 0) {
|
|
137
|
+
return {
|
|
138
|
+
backend: 'cloud-mcp',
|
|
139
|
+
delegated: true,
|
|
140
|
+
status: 'error',
|
|
141
|
+
error: 'roadmap.cloudMcp.teamSlug required when roadmap.backend=cloud-mcp',
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const tool = toolNameFor(subcommand)
|
|
146
|
+
if (!tool) {
|
|
147
|
+
return {
|
|
148
|
+
backend: 'cloud-mcp',
|
|
149
|
+
delegated: true,
|
|
150
|
+
status: 'error',
|
|
151
|
+
error: `cloud-mcp backend does not handle subcommand "${subcommand}" (read-side stays markdown in Phase 2)`,
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const token = resolveToken(projectDir)
|
|
156
|
+
if (!token) {
|
|
157
|
+
return {
|
|
158
|
+
backend: 'cloud-mcp',
|
|
159
|
+
delegated: true,
|
|
160
|
+
status: 'error',
|
|
161
|
+
error: 'No roadmap MCP token (set MCP_ROADMAP_TOKEN or .aperant/team.json::roadmapMcpToken)',
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const baseUrl = (
|
|
166
|
+
typeof cloudMcp.mcpServerUrl === 'string' && cloudMcp.mcpServerUrl.length > 0
|
|
167
|
+
? cloudMcp.mcpServerUrl
|
|
168
|
+
: process.env.APERANT_BASE_URL || 'https://app.aperant.com'
|
|
169
|
+
).replace(/\/$/, '')
|
|
170
|
+
const url = `${baseUrl}/api/mcp/${encodeURIComponent(teamSlug)}`
|
|
171
|
+
|
|
172
|
+
// Streamable HTTP MCP body — tools/call envelope per the existing
|
|
173
|
+
// server-factory contract. The wire format mirrors what the
|
|
174
|
+
// MCP SDK clients send; the Aperant route handler dispatches on
|
|
175
|
+
// `params.name` to the matching tool registered in subtask 2.3.
|
|
176
|
+
const body = {
|
|
177
|
+
jsonrpc: '2.0',
|
|
178
|
+
id: 1,
|
|
179
|
+
method: 'tools/call',
|
|
180
|
+
params: {
|
|
181
|
+
name: tool,
|
|
182
|
+
arguments: args,
|
|
183
|
+
},
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const f = opts.fetchImpl ?? fetch
|
|
187
|
+
let res
|
|
188
|
+
try {
|
|
189
|
+
res = await f(url, {
|
|
190
|
+
method: 'POST',
|
|
191
|
+
headers: {
|
|
192
|
+
Authorization: `Bearer ${token}`,
|
|
193
|
+
'Content-Type': 'application/json',
|
|
194
|
+
Accept: 'application/json, text/event-stream',
|
|
195
|
+
},
|
|
196
|
+
body: JSON.stringify(body),
|
|
197
|
+
})
|
|
198
|
+
} catch (e) {
|
|
199
|
+
const msg = e instanceof Error ? e.message : String(e)
|
|
200
|
+
return {
|
|
201
|
+
backend: 'cloud-mcp',
|
|
202
|
+
delegated: true,
|
|
203
|
+
status: 'error',
|
|
204
|
+
error: `network: ${msg}`,
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (res.status >= 400) {
|
|
209
|
+
let detail = ''
|
|
210
|
+
try {
|
|
211
|
+
detail = await res.text()
|
|
212
|
+
} catch {
|
|
213
|
+
// ignore body-read failures
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
backend: 'cloud-mcp',
|
|
217
|
+
delegated: true,
|
|
218
|
+
status: 'error',
|
|
219
|
+
code: res.status,
|
|
220
|
+
error: detail || `http ${res.status}`,
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let parsed
|
|
225
|
+
try {
|
|
226
|
+
parsed = await res.json()
|
|
227
|
+
} catch {
|
|
228
|
+
parsed = null
|
|
229
|
+
}
|
|
230
|
+
return { backend: 'cloud-mcp', delegated: true, result: parsed }
|
|
231
|
+
}
|
|
@@ -21,6 +21,14 @@
|
|
|
21
21
|
* whose monorepo lives elsewhere (e.g. `packages/apps/*`) pass a
|
|
22
22
|
* custom `monorepo_globs`. Setting `monorepo_globs: []` disables the
|
|
23
23
|
* walk entirely (root-only escape hatch).
|
|
24
|
+
*
|
|
25
|
+
* Mobile verify-proof surface — `loadRuntimeCapabilities` ALSO detects
|
|
26
|
+
* pure-native iOS/Android projects (Swift `*.xcodeproj` / Kotlin
|
|
27
|
+
* `build.gradle`) that carry no JS deps, via a shallow fs check on the
|
|
28
|
+
* root + each monorepo dir (`detectNativeProject`). The pure
|
|
29
|
+
* `detectFromPackageJson` is intentionally NOT given fs access — native
|
|
30
|
+
* detection lives ONLY in the fs-walking `loadRuntimeCapabilities` so the
|
|
31
|
+
* pure-function contract holds (decision D-06).
|
|
24
32
|
*/
|
|
25
33
|
|
|
26
34
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
@@ -35,6 +43,10 @@ import { join, resolve } from 'node:path'
|
|
|
35
43
|
* @property {boolean} is_web_frontend
|
|
36
44
|
* @property {boolean} is_expo
|
|
37
45
|
* @property {boolean} is_react_native
|
|
46
|
+
* @property {boolean} [is_ios_native] — optional: always populated by the
|
|
47
|
+
* detector, but optional in the type so partial capability records built by
|
|
48
|
+
* other consumers (and pre-existing test fixtures) stay valid.
|
|
49
|
+
* @property {boolean} [is_android_native]
|
|
38
50
|
* @property {boolean} has_node
|
|
39
51
|
*/
|
|
40
52
|
|
|
@@ -48,6 +60,8 @@ function emptyCapabilities() {
|
|
|
48
60
|
is_web_frontend: false,
|
|
49
61
|
is_expo: false,
|
|
50
62
|
is_react_native: false,
|
|
63
|
+
is_ios_native: false,
|
|
64
|
+
is_android_native: false,
|
|
51
65
|
has_node: false,
|
|
52
66
|
}
|
|
53
67
|
}
|
|
@@ -134,6 +148,8 @@ export function loadRuntimeCapabilities(projectDir, opts) {
|
|
|
134
148
|
/* fall through with empty caps */
|
|
135
149
|
}
|
|
136
150
|
}
|
|
151
|
+
// Fold in pure-native detection on the root (covers a JS-less iOS/Android app).
|
|
152
|
+
rootCaps = orMergeCapabilities(rootCaps, detectNativeProject(root))
|
|
137
153
|
|
|
138
154
|
// Default to the canonical Aperant monorepo layout. Passing `[]`
|
|
139
155
|
// disables the walk entirely (root-only escape hatch per spec ID-05).
|
|
@@ -144,6 +160,9 @@ export function loadRuntimeCapabilities(projectDir, opts) {
|
|
|
144
160
|
for (const glob of globs) {
|
|
145
161
|
const matches = expandSimpleGlob(root, glob)
|
|
146
162
|
for (const dir of matches) {
|
|
163
|
+
// Native detection runs regardless of package.json — a pure-native
|
|
164
|
+
// iOS/Android app in apps/* may carry no package.json at all.
|
|
165
|
+
merged = orMergeCapabilities(merged, detectNativeProject(dir))
|
|
147
166
|
const childPkgPath = join(dir, 'package.json')
|
|
148
167
|
if (!existsSync(childPkgPath)) continue
|
|
149
168
|
try {
|
|
@@ -211,6 +230,54 @@ function orMergeCapabilities(a, b) {
|
|
|
211
230
|
is_web_frontend: a.is_web_frontend || b.is_web_frontend,
|
|
212
231
|
is_expo: a.is_expo || b.is_expo,
|
|
213
232
|
is_react_native: a.is_react_native || b.is_react_native,
|
|
233
|
+
is_ios_native: a.is_ios_native || b.is_ios_native,
|
|
234
|
+
is_android_native: a.is_android_native || b.is_android_native,
|
|
214
235
|
has_node: a.has_node || b.has_node,
|
|
215
236
|
}
|
|
216
237
|
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Shallow fs detection of a pure-native mobile project — a Swift/ObjC iOS
|
|
241
|
+
* app (`*.xcodeproj` / `*.xcworkspace`) or a Kotlin/Java Android app
|
|
242
|
+
* (`build.gradle[.kts]` / `settings.gradle[.kts]`). Checks `dir` itself and
|
|
243
|
+
* the conventional `ios/` + `android/` subdirs (the React Native / Expo
|
|
244
|
+
* prebuild layout). Returns a full capability record so it OR-merges with
|
|
245
|
+
* the package.json-derived caps.
|
|
246
|
+
*
|
|
247
|
+
* This is the ONLY fs-touching detection — `detectFromPackageJson` stays
|
|
248
|
+
* pure (D-06). Fails closed (treats unreadable dirs as absent).
|
|
249
|
+
*
|
|
250
|
+
* @param {string} dir
|
|
251
|
+
* @returns {RuntimeCapabilities}
|
|
252
|
+
*/
|
|
253
|
+
function detectNativeProject(dir) {
|
|
254
|
+
const caps = emptyCapabilities()
|
|
255
|
+
if (dirHasXcodeProject(dir) || dirHasXcodeProject(join(dir, 'ios'))) {
|
|
256
|
+
caps.is_ios_native = true
|
|
257
|
+
}
|
|
258
|
+
if (dirHasGradle(dir) || dirHasGradle(join(dir, 'android'))) {
|
|
259
|
+
caps.is_android_native = true
|
|
260
|
+
}
|
|
261
|
+
return caps
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** True when `dir` directly contains a `*.xcodeproj` or `*.xcworkspace`. */
|
|
265
|
+
function dirHasXcodeProject(dir) {
|
|
266
|
+
if (!existsSync(dir)) return false
|
|
267
|
+
try {
|
|
268
|
+
return readdirSync(dir).some((n) => n.endsWith('.xcodeproj') || n.endsWith('.xcworkspace'))
|
|
269
|
+
} catch {
|
|
270
|
+
return false
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** True when `dir` directly contains a Gradle build/settings file. */
|
|
275
|
+
function dirHasGradle(dir) {
|
|
276
|
+
if (!existsSync(dir)) return false
|
|
277
|
+
return (
|
|
278
|
+
existsSync(join(dir, 'build.gradle')) ||
|
|
279
|
+
existsSync(join(dir, 'build.gradle.kts')) ||
|
|
280
|
+
existsSync(join(dir, 'settings.gradle')) ||
|
|
281
|
+
existsSync(join(dir, 'settings.gradle.kts'))
|
|
282
|
+
)
|
|
283
|
+
}
|
|
@@ -172,6 +172,21 @@ function validateNode(value, schema, path, errors) {
|
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
|
+
// additionalProperties: false — reject any key not declared in `properties`.
|
|
176
|
+
if (
|
|
177
|
+
schema.additionalProperties === false &&
|
|
178
|
+
schema.properties &&
|
|
179
|
+
typeof schema.properties === 'object'
|
|
180
|
+
) {
|
|
181
|
+
for (const key of Object.keys(obj)) {
|
|
182
|
+
if (!(key in schema.properties)) {
|
|
183
|
+
errors.push({
|
|
184
|
+
path: `${path}/${key}`,
|
|
185
|
+
message: `additional property "${key}" is not allowed`,
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
175
190
|
}
|
|
176
191
|
|
|
177
192
|
// array validation
|
|
@@ -124,6 +124,8 @@ function matchesRuntime(manifest, runtime) {
|
|
|
124
124
|
if (stripped === 'nuxt' && runtime.is_nuxt) return true
|
|
125
125
|
if (stripped === 'expo' && runtime.is_expo) return true
|
|
126
126
|
if (stripped === 'react-native' && runtime.is_react_native) return true
|
|
127
|
+
if (stripped === 'ios' && runtime.is_ios_native) return true
|
|
128
|
+
if (stripped === 'android' && runtime.is_android_native) return true
|
|
127
129
|
if (stripped === 'node' && runtime.has_node) return true
|
|
128
130
|
if (stripped === 'cli' && runtime.has_node) return true
|
|
129
131
|
if (stripped === 'api' && runtime.has_node) return true
|
|
@@ -246,12 +248,15 @@ export function resolveDriver(opts) {
|
|
|
246
248
|
* Map a User Outcomes surface tag to the runtime-capability flag a driver
|
|
247
249
|
* must carry (via `matchesRuntime`) to be eligible for that surface.
|
|
248
250
|
*
|
|
249
|
-
* Closed surface set: `electron | web | cli | api | electron+web`.
|
|
251
|
+
* Closed surface set: `electron | web | cli | api | mobile | electron+web`.
|
|
250
252
|
* `electron+web` is NOT a single flag — it's a cross-surface tag the
|
|
251
|
-
* workflow runner expands into two single-surface dispatches.
|
|
253
|
+
* workflow runner expands into two single-surface dispatches. `mobile`
|
|
254
|
+
* maps to the synthetic `is_mobile` marker, which `maskRuntimeForSurface`
|
|
255
|
+
* expands to the four real mobile flags (the unified mobile-mcp driver
|
|
256
|
+
* targets ios/android/expo/react-native — decision D-08).
|
|
252
257
|
*
|
|
253
|
-
* @param {('electron'|'web'|'cli'|'api')} surface
|
|
254
|
-
* @returns {('is_electron'|'is_web_frontend'|'has_node')}
|
|
258
|
+
* @param {('electron'|'web'|'cli'|'api'|'mobile')} surface
|
|
259
|
+
* @returns {('is_electron'|'is_web_frontend'|'has_node'|'is_mobile')}
|
|
255
260
|
*/
|
|
256
261
|
export function surfaceToRequiredFlag(surface) {
|
|
257
262
|
switch (surface) {
|
|
@@ -262,6 +267,8 @@ export function surfaceToRequiredFlag(surface) {
|
|
|
262
267
|
case 'cli':
|
|
263
268
|
case 'api':
|
|
264
269
|
return 'has_node'
|
|
270
|
+
case 'mobile':
|
|
271
|
+
return 'is_mobile'
|
|
265
272
|
default:
|
|
266
273
|
throw new Error(`surfaceToRequiredFlag: unknown surface "${surface}"`)
|
|
267
274
|
}
|
|
@@ -292,7 +299,7 @@ export function surfaceToRequiredFlag(surface) {
|
|
|
292
299
|
* @param {Object} opts
|
|
293
300
|
* @param {DriverManifest[]} opts.drivers
|
|
294
301
|
* @param {RuntimeCapabilities} opts.runtime
|
|
295
|
-
* @param {Array<{id: string, surface: 'electron'|'web'|'cli'|'api'|'electron+web'}>} opts.outcomes
|
|
302
|
+
* @param {Array<{id: string, surface: 'electron'|'web'|'cli'|'api'|'mobile'|'electron+web'}>} opts.outcomes
|
|
296
303
|
* @param {string[]} [opts.override]
|
|
297
304
|
* @param {string[]} [opts.required_stability]
|
|
298
305
|
* @param {string[]} [opts.required_capabilities] — defaults to ['screenshot']
|
|
@@ -314,7 +321,9 @@ export function resolveDriversForOutcomes(opts) {
|
|
|
314
321
|
outcome.surface === 'electron+web' ? ['electron', 'web'] : [outcome.surface]
|
|
315
322
|
|
|
316
323
|
for (const surface of surfacesToDispatch) {
|
|
317
|
-
const flag = surfaceToRequiredFlag(
|
|
324
|
+
const flag = surfaceToRequiredFlag(
|
|
325
|
+
/** @type {'electron'|'web'|'cli'|'api'|'mobile'} */ (surface),
|
|
326
|
+
)
|
|
318
327
|
// Synthesize a per-surface runtime view: only the surface's
|
|
319
328
|
// required flag is on; every other flag is masked off so the
|
|
320
329
|
// scoring prefers per-surface drivers (otherwise, on a
|
|
@@ -347,7 +356,7 @@ export function resolveDriversForOutcomes(opts) {
|
|
|
347
356
|
* single surface during per-outcome dispatch.
|
|
348
357
|
*
|
|
349
358
|
* @param {RuntimeCapabilities} runtime
|
|
350
|
-
* @param {string} flag — one of: is_electron | is_web_frontend | has_node
|
|
359
|
+
* @param {string} flag — one of: is_electron | is_web_frontend | has_node | is_mobile
|
|
351
360
|
* @returns {RuntimeCapabilities}
|
|
352
361
|
*/
|
|
353
362
|
function maskRuntimeForSurface(runtime, flag) {
|
|
@@ -360,6 +369,8 @@ function maskRuntimeForSurface(runtime, flag) {
|
|
|
360
369
|
is_web_frontend: false,
|
|
361
370
|
is_expo: false,
|
|
362
371
|
is_react_native: false,
|
|
372
|
+
is_ios_native: false,
|
|
373
|
+
is_android_native: false,
|
|
363
374
|
has_node: false,
|
|
364
375
|
}
|
|
365
376
|
// Only flip the surface's flag on if the project actually carries it
|
|
@@ -373,5 +384,14 @@ function maskRuntimeForSurface(runtime, flag) {
|
|
|
373
384
|
masked.is_nuxt = runtime.is_nuxt
|
|
374
385
|
}
|
|
375
386
|
if (flag === 'has_node') masked.has_node = runtime.has_node
|
|
387
|
+
// `is_mobile` is a synthetic surface marker — expand it to the four real
|
|
388
|
+
// mobile flags so the unified mobile-mcp driver (targets ios/android/
|
|
389
|
+
// expo/react-native) matches whichever the project actually carries.
|
|
390
|
+
if (flag === 'is_mobile') {
|
|
391
|
+
masked.is_ios_native = runtime.is_ios_native
|
|
392
|
+
masked.is_android_native = runtime.is_android_native
|
|
393
|
+
masked.is_expo = runtime.is_expo
|
|
394
|
+
masked.is_react_native = runtime.is_react_native
|
|
395
|
+
}
|
|
376
396
|
return masked
|
|
377
397
|
}
|