@aperant/framework 0.9.0 → 0.10.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 +68 -0
- package/agents/apt-verifier.md +6 -3
- package/dist/cli/commands/init.d.mts.map +1 -1
- package/dist/cli/commands/init.mjs +17 -1
- package/dist/cli/commands/init.mjs.map +1 -1
- package/dist/cli/consistency/parse-review.mjs +5 -3
- package/dist/cli/consistency/parse-review.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 +10 -8
- package/dist/cli/consistency/rules/r5-verdict-consistency.mjs.map +1 -1
- package/dist/cli/gate/gates/review-clean.d.mts.map +1 -1
- package/dist/cli/gate/gates/review-clean.mjs +4 -2
- package/dist/cli/gate/gates/review-clean.mjs.map +1 -1
- package/dist/cli/install/mcp-provision.d.mts +26 -0
- package/dist/cli/install/mcp-provision.d.mts.map +1 -0
- package/dist/cli/install/mcp-provision.mjs +281 -0
- package/dist/cli/install/mcp-provision.mjs.map +1 -0
- package/dist/cli/install/mcp-server-specs.d.mts +80 -0
- package/dist/cli/install/mcp-server-specs.d.mts.map +1 -0
- package/dist/cli/install/mcp-server-specs.mjs +102 -0
- package/dist/cli/install/mcp-server-specs.mjs.map +1 -0
- package/dist/cli/install/runtime-detect.d.mts +1 -0
- package/dist/cli/install/runtime-detect.d.mts.map +1 -1
- package/dist/cli/install/runtime-detect.mjs +5 -0
- package/dist/cli/install/runtime-detect.mjs.map +1 -1
- package/dist/cli/install/toml-merge.d.mts +40 -0
- package/dist/cli/install/toml-merge.d.mts.map +1 -0
- package/dist/cli/install/toml-merge.mjs +134 -0
- package/dist/cli/install/toml-merge.mjs.map +1 -0
- package/dist/cli/util/events-path.d.mts +8 -0
- package/dist/cli/util/events-path.d.mts.map +1 -0
- package/dist/cli/util/events-path.mjs +23 -0
- package/dist/cli/util/events-path.mjs.map +1 -0
- package/dist/cli/util/runtime-capabilities.d.mts +30 -0
- package/dist/cli/util/runtime-capabilities.d.mts.map +1 -0
- package/dist/cli/util/runtime-capabilities.mjs +214 -0
- package/dist/cli/util/runtime-capabilities.mjs.map +1 -0
- package/dist/cli/verify-proof/audit.d.mts +2 -7
- package/dist/cli/verify-proof/audit.d.mts.map +1 -1
- package/dist/cli/verify-proof/audit.mjs +11 -17
- package/dist/cli/verify-proof/audit.mjs.map +1 -1
- package/dist/cli/verify-proof/runtime-detect.d.mts +2 -29
- package/dist/cli/verify-proof/runtime-detect.d.mts.map +1 -1
- package/dist/cli/verify-proof/runtime-detect.mjs +8 -203
- package/dist/cli/verify-proof/runtime-detect.mjs.map +1 -1
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/agents/apt-verifier.md +6 -3
- package/dist/plugin/skills/apt-pr-review/SKILL.md +54 -0
- package/dist/plugin/skills/apt-verify/SKILL.md +1 -1
- package/dist/plugin/skills/apt-verify-proof/SKILL.md +25 -1
- package/drivers/browser/skill/agent-browser/SKILL.md +51 -0
- package/drivers/tauri/README.md +107 -0
- package/drivers/tauri/driver.mjs +88 -0
- package/drivers/tauri/manifest.json +37 -0
- package/drivers/tauri/scripts/tauri-mcp.sh +41 -0
- package/package.json +1 -1
- package/skills/apt-pr-review/SKILL.md +54 -0
- package/skills/apt-verify/SKILL.md +1 -1
- package/skills/apt-verify-proof/SKILL.md +25 -1
- package/src/cli/commands/init.mjs +19 -0
- package/src/cli/consistency/parse-review.mjs +5 -3
- package/src/cli/consistency/rules/r5-verdict-consistency.mjs +14 -9
- package/src/cli/gate/gates/review-clean.mjs +4 -2
- package/src/cli/install/mcp-provision.mjs +293 -0
- package/src/cli/install/mcp-server-specs.mjs +101 -0
- package/src/cli/install/runtime-detect.mjs +9 -0
- package/src/cli/install/toml-merge.mjs +139 -0
- package/src/cli/util/events-path.mjs +24 -0
- package/src/cli/util/runtime-capabilities.mjs +216 -0
- package/src/cli/verify-proof/audit.mjs +11 -17
- package/src/cli/verify-proof/runtime-detect.mjs +11 -204
- package/templates/config.json +2 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"driverId": "tauri",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"targets": ["tauri@>=1"],
|
|
5
|
+
"capabilities": [
|
|
6
|
+
"click",
|
|
7
|
+
"type",
|
|
8
|
+
"key",
|
|
9
|
+
"navigate",
|
|
10
|
+
"screenshot",
|
|
11
|
+
"assert_visible",
|
|
12
|
+
"assert_text",
|
|
13
|
+
"wait_for_idle",
|
|
14
|
+
"execute_js",
|
|
15
|
+
"dump_state"
|
|
16
|
+
],
|
|
17
|
+
"evidence_capabilities": ["screenshot", "dom_dump", "capture_logs"],
|
|
18
|
+
"frameworkApiRange": "^1.0.0",
|
|
19
|
+
"priority": 85,
|
|
20
|
+
"stability": "ga",
|
|
21
|
+
"trust": {
|
|
22
|
+
"tier": "first_party_signed",
|
|
23
|
+
"publisher": {
|
|
24
|
+
"name": "Aperant",
|
|
25
|
+
"npm_scope": "@aperant",
|
|
26
|
+
"github_org": "Mikalsen-AI",
|
|
27
|
+
"verified": true
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"security": {
|
|
31
|
+
"sandbox_profile": "workspace_write",
|
|
32
|
+
"network": "deny",
|
|
33
|
+
"requires_user_approval": false
|
|
34
|
+
},
|
|
35
|
+
"audit": { "log_invocations": true, "log_manifest_hash": true },
|
|
36
|
+
"transport": { "mcp_server_id": "tauri" }
|
|
37
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Launches the tauri-plugin-mcp-server with the project's .env injected.
|
|
3
|
+
# Generated by `apt-tools init` (Aperant Framework) and referenced from the
|
|
4
|
+
# host CLI's MCP config as: { command: "bash", args: ["./scripts/tauri-mcp.sh"] }
|
|
5
|
+
#
|
|
6
|
+
# This launcher exists because the tauri-plugin-mcp-server npm bin ships
|
|
7
|
+
# WITHOUT a shebang, so `npx -y tauri-plugin-mcp-server` runs the .js as a
|
|
8
|
+
# shell script and fails. We invoke the bin via `node` with a stable path.
|
|
9
|
+
#
|
|
10
|
+
# Idempotent: this file is never overwritten once present (edit it freely).
|
|
11
|
+
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
|
|
14
|
+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
15
|
+
|
|
16
|
+
# Load .env (gitignored) WITHOUT executing it. We do NOT dot-source the file
|
|
17
|
+
# (that would run any `$(...)`, backticks, or function defs it contains as
|
|
18
|
+
# shell). Instead parse it line-by-line: skip blank/comment lines, and export
|
|
19
|
+
# only well-formed `KEY=VALUE` assignments whose key is a valid shell name.
|
|
20
|
+
if [[ -f "$ROOT/.env" ]]; then
|
|
21
|
+
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
22
|
+
# Strip a leading `export ` prefix if present.
|
|
23
|
+
line="${line#export }"
|
|
24
|
+
# Skip blank lines and comments.
|
|
25
|
+
[[ -z "$line" || "$line" == \#* ]] && continue
|
|
26
|
+
# Only accept lines of the form NAME=VALUE (bash-style identifier name).
|
|
27
|
+
if [[ "$line" =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; then
|
|
28
|
+
key="${line%%=*}"
|
|
29
|
+
value="${line#*=}"
|
|
30
|
+
export "$key=$value"
|
|
31
|
+
fi
|
|
32
|
+
done < "$ROOT/.env"
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
export TAURI_MCP_CONNECTION_TYPE="${TAURI_MCP_CONNECTION_TYPE:-ipc}"
|
|
36
|
+
export TAURI_MCP_IPC_PATH="${TAURI_MCP_IPC_PATH:-$HOME/.{{PROJECT_SOCKET}}-mcp.sock}"
|
|
37
|
+
|
|
38
|
+
# Install the transport package as a devDependency first:
|
|
39
|
+
# pnpm add -D tauri-plugin-mcp-server
|
|
40
|
+
# and add the tauri-plugin-mcp Rust plugin to your app (see drivers/tauri/README.md).
|
|
41
|
+
exec node "$ROOT/node_modules/tauri-plugin-mcp-server/build/index.js"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aperant/framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "AI coding framework — composable skills for planning, executing, verifying, and reviewing code with any LLM provider. Works as Claude Code commands, Codex tasks, or programmatically.",
|
|
5
5
|
"author": "Mikalsen AI <hello@mikalsen.ai>",
|
|
6
6
|
"type": "module",
|
|
@@ -469,6 +469,55 @@ fi
|
|
|
469
469
|
|
|
470
470
|
After this point, the per-round artifact root is `.aperant/pr-reviews/pr-${PR_NUMBER}/rounds/${ROUND}/` (the `${REVIEW_ID}` is still used for the git worktree branch and temporary files). Subsequent `git diff` invocations within the round use `${PINNED_HEAD_SHA}` as the head ref.
|
|
471
471
|
|
|
472
|
+
### Step 4c: Mergeability Pre-Check (PR reviews only)
|
|
473
|
+
|
|
474
|
+
**Why this exists.** The review diff is `git diff {BASE}...{HEAD}` (three-dot — from the *merge-base*), so it shows the PR's own changes relative to where it branched. It is **blind to drift on `{BASE}` since then** and therefore says nothing about whether the PR still merges cleanly. A PR can pass a glowing review and be unmergeable. This step closes that gap: it is **informational, never blocking** (the review still runs in full), but a conflicting/stale state MUST be surfaced so the reader doesn't trust a review of a diff that no longer applies.
|
|
475
|
+
|
|
476
|
+
```bash
|
|
477
|
+
# PR-mode only. Query GitHub's authoritative mergeability + measure drift.
|
|
478
|
+
if [ -n "$PR_NUMBER" ]; then
|
|
479
|
+
MERGE_JSON=$(gh pr view ${PR_NUMBER} --json mergeable,mergeStateStatus,baseRefName 2>/dev/null)
|
|
480
|
+
MERGEABLE=$(echo "$MERGE_JSON" | jq -r '.mergeable // "UNKNOWN"')
|
|
481
|
+
MERGE_STATE=$(echo "$MERGE_JSON" | jq -r '.mergeStateStatus // "UNKNOWN"')
|
|
482
|
+
BASE_REF=$(echo "$MERGE_JSON" | jq -r '.baseRefName // "main"')
|
|
483
|
+
git fetch origin "${BASE_REF}" 2>/dev/null
|
|
484
|
+
BEHIND=$(git rev-list --count ${PINNED_HEAD_SHA}..origin/${BASE_REF} 2>/dev/null || echo "?")
|
|
485
|
+
AHEAD=$(git rev-list --count origin/${BASE_REF}..${PINNED_HEAD_SHA} 2>/dev/null || echo "?")
|
|
486
|
+
|
|
487
|
+
# If GitHub reports a conflict (or hasn't computed yet), enumerate the conflicting
|
|
488
|
+
# files locally without touching any working tree (git >= 2.38):
|
|
489
|
+
CONFLICT_FILES=""
|
|
490
|
+
if [ "$MERGEABLE" = "CONFLICTING" ] || [ "$MERGE_STATE" = "DIRTY" ]; then
|
|
491
|
+
CONFLICT_FILES=$(git merge-tree --write-tree --name-only origin/${BASE_REF} ${PINNED_HEAD_SHA} 2>/dev/null \
|
|
492
|
+
| tail -n +2 | sed '/^$/d' | grep '/' )
|
|
493
|
+
fi
|
|
494
|
+
fi
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
Record the result in `metadata.json` under a `mergeability` key:
|
|
498
|
+
|
|
499
|
+
```json
|
|
500
|
+
{
|
|
501
|
+
"mergeability": {
|
|
502
|
+
"mergeable": "CONFLICTING | MERGEABLE | UNKNOWN",
|
|
503
|
+
"merge_state_status": "DIRTY | CLEAN | BLOCKED | BEHIND | ...",
|
|
504
|
+
"base_ref": "main",
|
|
505
|
+
"commits_behind_base": 101,
|
|
506
|
+
"commits_ahead_base": 91,
|
|
507
|
+
"conflicting_files": ["apps/.../App.tsx", "..."]
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
**How to use the result:**
|
|
513
|
+
|
|
514
|
+
- `MERGEABLE` ∈ {`CONFLICTING`} OR `MERGE_STATE` = `DIRTY` → the PR has conflicts. Surface a **⚠️ Mergeability** block in the Phase 5 intermediate report AND the Phase 8 final report, listing `commits_behind_base` and the `conflicting_files`. In **external / comment-only** mode, include the same block in the Phase 6b comment. State plainly that the branch needs a rebase/merge from `{BASE}` and that **the reviewed diff may be stale** with respect to current `{BASE}`.
|
|
515
|
+
- `MERGE_STATE` = `BEHIND` (mergeable but behind) → note the drift (`commits_behind_base`) as a softer advisory; no conflict, but the diff predates recent `{BASE}` commits.
|
|
516
|
+
- `MERGEABLE` = `UNKNOWN` (GitHub hasn't finished computing, common right after a push) → record `UNKNOWN`; optionally re-poll once after a few seconds. Do not block.
|
|
517
|
+
- `MERGEABLE` = `MERGEABLE`, `MERGE_STATE` = `CLEAN` → record it and move on silently.
|
|
518
|
+
|
|
519
|
+
This step never changes the review's findings or assessment — a conflicting PR with zero code findings is still `approve` on its own merits. Mergeability is reported as a **separate** signal so the reader knows whether the approval can actually land.
|
|
520
|
+
|
|
472
521
|
### Step 5: Read All Changed Files
|
|
473
522
|
|
|
474
523
|
For every file in the changed file list, read it in full using the Read tool **from the worktree**:
|
|
@@ -1058,6 +1107,10 @@ Review Synthesis (Before Auto-Fix)
|
|
|
1058
1107
|
Auto-Checks: {PASS/FAIL}
|
|
1059
1108
|
lint: {P/F} | typecheck: {P/F} | tests: {P/F}
|
|
1060
1109
|
|
|
1110
|
+
Mergeability (PR reviews — from Step 4c): {MERGEABLE/CONFLICTING/UNKNOWN}
|
|
1111
|
+
{if CONFLICTING/DIRTY: "⚠️ conflicts with {BASE} — {behind} commits behind — needs rebase — reviewed diff may be stale"}
|
|
1112
|
+
{if conflicting_files: list them}
|
|
1113
|
+
|
|
1061
1114
|
Validated Findings:
|
|
1062
1115
|
CRITICAL: {N}
|
|
1063
1116
|
HIGH: {N}
|
|
@@ -1630,6 +1683,7 @@ Output the human-readable summary:
|
|
|
1630
1683
|
|
|
1631
1684
|
Review ID: {REVIEW_ID}
|
|
1632
1685
|
Assessment: {APPROVE / REQUEST CHANGES / COMMENT}
|
|
1686
|
+
Mergeability: {MERGEABLE / CONFLICTING (N behind {BASE}, needs rebase) / UNKNOWN} # PR reviews — from Step 4c / metadata.json.mergeability
|
|
1633
1687
|
|
|
1634
1688
|
Scores:
|
|
1635
1689
|
Security: {score}/10
|
|
@@ -60,7 +60,7 @@ Before spawning any verifier subagent or running scoring against acceptance crit
|
|
|
60
60
|
</locked_decisions>
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
Verifiers apply this digest when interpreting "completeness" and "correctness" — if a decision explicitly deferred a behavior, its absence is NOT a verification failure.
|
|
63
|
+
Verifiers apply this digest when interpreting "completeness" and "correctness" — if a decision explicitly deferred a behavior AND the deferred finding has severity below critical or major, its absence is NOT a verification failure. A deferred critical or major finding IS a Completeness gap — score accordingly and note it in the issues list.
|
|
64
64
|
</decision_ledger_inject>
|
|
65
65
|
|
|
66
66
|
<process>
|
|
@@ -20,6 +20,7 @@ gates: []
|
|
|
20
20
|
config_keys:
|
|
21
21
|
- verification.open_report
|
|
22
22
|
- verification.report_formats
|
|
23
|
+
- verification.provision_mcp
|
|
23
24
|
---
|
|
24
25
|
|
|
25
26
|
# Proof Verification
|
|
@@ -104,13 +105,36 @@ disable the walk entirely (root-only behaviour, backwards compat).
|
|
|
104
105
|
The `verification.runtimes[]` override stays the final escape hatch
|
|
105
106
|
and is now usually unnecessary because the walk runs by default.
|
|
106
107
|
|
|
107
|
-
### Bundled drivers (
|
|
108
|
+
### Bundled drivers (5)
|
|
108
109
|
|
|
109
110
|
- **browser** (priority=100, ga) — Next.js / vanilla web frontends.
|
|
110
111
|
- **electron** (priority=90, ga) — Electron desktop apps.
|
|
112
|
+
- **tauri** (priority=85, ga) — Tauri desktop apps.
|
|
111
113
|
- **cli** (priority=60, ga) — headless CI, output-shape assertions.
|
|
112
114
|
- **api** (priority=50, ga) — HTTP / JSON-API regression tests.
|
|
113
115
|
|
|
116
|
+
### Install-time MCP provisioning (verification.provision_mcp)
|
|
117
|
+
|
|
118
|
+
`apt-tools init` (and `/apt:update`, which routes through the same path)
|
|
119
|
+
auto-provisions the MCP transport a driver needs into each SELECTED host
|
|
120
|
+
CLI's config, so verify-proof works at the prompt with no manual MCP
|
|
121
|
+
setup. It detects the project runtime and, per host:
|
|
122
|
+
|
|
123
|
+
- **claude** → JSON-merges the server into the project-root `.mcp.json`
|
|
124
|
+
(`mcpServers.<id>`).
|
|
125
|
+
- **codex** → upserts a `[mcp_servers.<id>]` table into the project-root
|
|
126
|
+
`.codex/config.toml`.
|
|
127
|
+
|
|
128
|
+
Electron and Tauri projects get the matching MCP server; web frontends
|
|
129
|
+
get the browser driver's declared `agent-browser` skill copied into the
|
|
130
|
+
host skill dir (`.claude/skills/` / `.agents/skills/`) — its `agent-browser`
|
|
131
|
+
CLI is a documented one-time prereq, the same class as the
|
|
132
|
+
`electron-mcp-server` / `tauri-plugin-mcp-server` npm packages (see each
|
|
133
|
+
driver's README). All writes are **TOFU/non-clobbering**: a pre-existing
|
|
134
|
+
server id, config table, or skill file is never overwritten. Opt out with
|
|
135
|
+
`verification.provision_mcp: false` (default `true`) — then init/update
|
|
136
|
+
writes no host MCP config and no skill.
|
|
137
|
+
|
|
114
138
|
### Driver authors
|
|
115
139
|
|
|
116
140
|
Third-party driver authors should run `apt-tools driver doctor .` before
|
|
@@ -32,6 +32,7 @@ import { deriveSidecar } from '../bootstrap/product-context-sidecar.mjs'
|
|
|
32
32
|
import { assertConfigWriteAllowed } from '../config/guard.mjs'
|
|
33
33
|
import {
|
|
34
34
|
AUDIT_001_LOCAL_FIELDS,
|
|
35
|
+
loadMergedProjectConfig,
|
|
35
36
|
migrateDeferredFieldsToLocalConfig,
|
|
36
37
|
writeLocalConfigField,
|
|
37
38
|
} from '../config/load.mjs'
|
|
@@ -42,6 +43,7 @@ import { detectPublicRemote } from '../git/remote.mjs'
|
|
|
42
43
|
import { detectArchetype } from '../install/archetype.mjs'
|
|
43
44
|
import { installKernel } from '../install/install-kernel.mjs'
|
|
44
45
|
import { MANIFEST_FILENAME } from '../install/manifest.mjs'
|
|
46
|
+
import { provisionMcpConfig } from '../install/mcp-provision.mjs'
|
|
45
47
|
import {
|
|
46
48
|
discoverCanonicalFiles,
|
|
47
49
|
runInstall,
|
|
@@ -1304,6 +1306,23 @@ export async function cmdInit(projectDir, extraArgs = []) {
|
|
|
1304
1306
|
if (envelope?.__earlyExit) {
|
|
1305
1307
|
return envelope.__earlyExit
|
|
1306
1308
|
}
|
|
1309
|
+
|
|
1310
|
+
// Auto-provision host MCP-server config + the agent-browser skill for
|
|
1311
|
+
// the detected runtime (C38). Runs on BOTH the fresh install AND the
|
|
1312
|
+
// no-drift /apt:update upgrade path (both reach finishInit). finishInit
|
|
1313
|
+
// has already written .aperant/config.json from the template, so the
|
|
1314
|
+
// merged config carries verification.provision_mcp (default true) — the
|
|
1315
|
+
// opt-out gate. TOFU/non-clobbering; never throws.
|
|
1316
|
+
const mergedConfig = loadMergedProjectConfig(installTarget)
|
|
1317
|
+
const mcpProvision = provisionMcpConfig({
|
|
1318
|
+
targetDir: installTarget,
|
|
1319
|
+
selectedRuntimes: effectiveAnswers.runtimes,
|
|
1320
|
+
config: mergedConfig,
|
|
1321
|
+
frameworkRoot: canonicalRoot,
|
|
1322
|
+
version,
|
|
1323
|
+
})
|
|
1324
|
+
envelope.mcp_provision = mcpProvision
|
|
1325
|
+
|
|
1307
1326
|
if (interactiveTty) {
|
|
1308
1327
|
await renderOutro(effectiveAnswers, results, version, kernelResult)
|
|
1309
1328
|
// TTY mode: envelope was persisted to disk + suppressed from stdout
|
|
@@ -97,9 +97,11 @@ function normalizeStatus(f) {
|
|
|
97
97
|
if (STATUS_MAP[s]) return STATUS_MAP[s]
|
|
98
98
|
// BUG-009: reviewer-skill writes `deferred-per-spec`, `deferred-followup`,
|
|
99
99
|
// etc. for findings that are intentionally not being fixed in the
|
|
100
|
-
// current scope. Collapse the variant prefix back to canonical
|
|
101
|
-
//
|
|
102
|
-
//
|
|
100
|
+
// current scope. Collapse the variant prefix back to canonical `deferred`.
|
|
101
|
+
// Note: for blocking severities (critical/major), `deferred` no longer
|
|
102
|
+
// clears review-clean or R5 (G42 Phase 1). Only `status: "fixed"` clears
|
|
103
|
+
// those gates. Non-blocking severities (suggestion/note) are dropped by
|
|
104
|
+
// normalizeSeverity and never reach the blocking filter.
|
|
103
105
|
if (s.startsWith('deferred-')) return 'deferred'
|
|
104
106
|
}
|
|
105
107
|
return 'open'
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
* consistency/rules/r5-verdict-consistency.mjs — R5 (C15).
|
|
4
4
|
*
|
|
5
5
|
* Fails when:
|
|
6
|
-
* - qa.verdict === 'approved' AND review has >=1
|
|
6
|
+
* - qa.verdict === 'approved' or 'approved-with-notes' AND review has >=1
|
|
7
|
+
* unresolved critical or major finding
|
|
7
8
|
* - qa.verdict === 'rejected' AND qa has no findings at all AND review has
|
|
8
9
|
* zero unresolved findings (rejected-without-evidence)
|
|
9
10
|
*
|
|
10
|
-
* `approved-with-notes`
|
|
11
|
-
*
|
|
11
|
+
* `approved-with-notes` no longer escapes unresolved critical or major
|
|
12
|
+
* findings (G42 Phase 1). It still passes for lower-severity issues.
|
|
12
13
|
*
|
|
13
14
|
* Severity: error. Stages: verify, ship.
|
|
14
15
|
*/
|
|
@@ -26,23 +27,27 @@ const rule = {
|
|
|
26
27
|
}
|
|
27
28
|
const verdict = parsed.qa.verdict ?? 'unknown'
|
|
28
29
|
const reviewFindings = parsed?.review?.findings ?? []
|
|
29
|
-
const
|
|
30
|
-
(f) => f.severity === 'critical' && f.status !== 'fixed',
|
|
30
|
+
const unresolvedBlocking = reviewFindings.filter(
|
|
31
|
+
(f) => (f.severity === 'critical' || f.severity === 'major') && f.status !== 'fixed',
|
|
31
32
|
)
|
|
32
|
-
if (
|
|
33
|
+
if (
|
|
34
|
+
(verdict === 'approved' || verdict === 'approved-with-notes') &&
|
|
35
|
+
unresolvedBlocking.length > 0
|
|
36
|
+
) {
|
|
33
37
|
return {
|
|
34
38
|
status: 'fail',
|
|
35
39
|
drift: [
|
|
36
40
|
{
|
|
37
41
|
verdict,
|
|
38
|
-
blockers:
|
|
42
|
+
blockers: unresolvedBlocking.map((f) => ({
|
|
39
43
|
id: f.id,
|
|
40
44
|
title: f.title,
|
|
41
45
|
})),
|
|
42
|
-
reason:
|
|
46
|
+
reason:
|
|
47
|
+
'approved/approved-with-notes verdict with unresolved critical or major finding(s) (G42 Phase 1)',
|
|
43
48
|
},
|
|
44
49
|
],
|
|
45
|
-
reason: `approved verdict contradicts ${
|
|
50
|
+
reason: `approved/approved-with-notes verdict contradicts ${unresolvedBlocking.length} unresolved critical/major finding(s)`,
|
|
46
51
|
}
|
|
47
52
|
}
|
|
48
53
|
if (verdict === 'rejected') {
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* shapes the reviewer skill writes: `fixed: true`, `resolved: true`,
|
|
9
9
|
* `status: "fixed"` (string), and `status: "deferred-*"` variants
|
|
10
10
|
* (BUG-009). Fails if any post-normalization finding is severity
|
|
11
|
-
* critical|major AND status is
|
|
11
|
+
* critical|major AND status is not `fixed`. `deferred` no longer clears
|
|
12
|
+
* blocking findings (G42 Phase 1 — only `--force --reason` override may
|
|
13
|
+
* bypass blocking severity deferrals).
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
16
|
import { existsSync, readFileSync } from 'node:fs'
|
|
@@ -55,7 +57,7 @@ const gate = {
|
|
|
55
57
|
// hole and picks up `deferred-*` variants for free.
|
|
56
58
|
const { findings: normalized } = parseReview(doc)
|
|
57
59
|
const unresolved = normalized.filter(
|
|
58
|
-
(f) => POST_NORM_BLOCKING.has(f.severity) && f.status !== 'fixed'
|
|
60
|
+
(f) => POST_NORM_BLOCKING.has(f.severity) && f.status !== 'fixed',
|
|
59
61
|
)
|
|
60
62
|
if (unresolved.length > 0) {
|
|
61
63
|
return {
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* install/mcp-provision.mjs — TOFU install-time MCP-server + skill provisioner.
|
|
3
|
+
*
|
|
4
|
+
* Called from cmdInit (the only call site — the QUICK fast-path never invokes
|
|
5
|
+
* init, so the Fast Path Guarantee is structurally untouched). Detects the
|
|
6
|
+
* project's runtime, then for each selected host CLI that supports the action:
|
|
7
|
+
*
|
|
8
|
+
* - kind 'mcp-server' (electron/tauri) → write the server into the host's
|
|
9
|
+
* MCP config (claude → .mcp.json JSON merge; codex → .codex/config.toml
|
|
10
|
+
* TOML upsert).
|
|
11
|
+
* - kind 'skill' (agent-browser) → copy the vendored SKILL.md into the
|
|
12
|
+
* host's skill dir (claude → .claude/skills/; codex → .agents/skills/).
|
|
13
|
+
*
|
|
14
|
+
* Every write is TOFU/idempotent (ID-05): if the target server id / table /
|
|
15
|
+
* skill file already exists, SKIP (never diff/merge/overwrite) — this is the
|
|
16
|
+
* must-not-clobber invariant for a hand-authored electron
|
|
17
|
+
* SCREENSHOT_ENCRYPTION_KEY (US-3) and a user-edited agent-browser skill
|
|
18
|
+
* (US-6). Each action appends one audit line to .aperant/events/{date}.jsonl.
|
|
19
|
+
*
|
|
20
|
+
* Gated behind config.verification.provision_mcp (default true) so users can
|
|
21
|
+
* opt out. Never throws — wraps the body in try/catch → {status:'error'}.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs'
|
|
25
|
+
import { basename, dirname, join, resolve } from 'node:path'
|
|
26
|
+
import { appendJsonl, atomicWriteJson, atomicWriteText } from '../util/atomic-write.mjs'
|
|
27
|
+
import { dailyEventsPath } from '../util/events-path.mjs'
|
|
28
|
+
import { capabilityToProvisionIds, PROVISION_SPECS } from './mcp-server-specs.mjs'
|
|
29
|
+
import { loadRuntimeCapabilities } from './runtime-detect.mjs'
|
|
30
|
+
import { upsertMcpServerToml } from './toml-merge.mjs'
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Per-host writer registry (ID-07, ID-08). v1 = claude + codex. The MCP
|
|
34
|
+
* config path AND skill dir are EXPLICIT per host — neither is derived from
|
|
35
|
+
* the runtime's installRoot (Codex's MCP path .codex/config.toml diverges
|
|
36
|
+
* from its skill dir .agents/skills/). Adding gemini/opencode later = one
|
|
37
|
+
* entry each.
|
|
38
|
+
*
|
|
39
|
+
* @type {Record<string, {mcpFormat: 'json'|'toml', mcpConfigPath: string, skillDir: string}>}
|
|
40
|
+
*/
|
|
41
|
+
const HOST_PROVISION_WRITERS = {
|
|
42
|
+
claude: { mcpFormat: 'json', mcpConfigPath: '.mcp.json', skillDir: '.claude/skills' },
|
|
43
|
+
codex: { mcpFormat: 'toml', mcpConfigPath: '.codex/config.toml', skillDir: '.agents/skills' },
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Substitute the per-project socket basename into the tauri launcher
|
|
48
|
+
* template, deriving uniqueness from the project dir basename.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} template
|
|
51
|
+
* @param {string} targetDir
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
function renderTauriLauncher(template, targetDir) {
|
|
55
|
+
const slug =
|
|
56
|
+
basename(resolve(targetDir))
|
|
57
|
+
.toLowerCase()
|
|
58
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
59
|
+
.replace(/^-+|-+$/g, '') || 'tauri'
|
|
60
|
+
return template.replaceAll('{{PROJECT_SOCKET}}', slug)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Write the electron/tauri MCP server into a Claude `.mcp.json`. JSON merge:
|
|
65
|
+
* read-or-{}, ensure mcpServers, SKIP if the server id already exists, else
|
|
66
|
+
* insert {type:'stdio', command, args, env?} preserving all sibling keys.
|
|
67
|
+
*
|
|
68
|
+
* TOFU non-clobber (ID-05): if the existing file fails to parse OR parses to
|
|
69
|
+
* a non-plain-object root (array / null / primitive), do NOT overwrite it —
|
|
70
|
+
* return `skipped_unsafe` with a reason so the caller can audit it and leave
|
|
71
|
+
* the file byte-identical. (`typeof [] === 'object'` is why arrays must be
|
|
72
|
+
* excluded explicitly: a `.mcp.json` containing `[]` would otherwise pass the
|
|
73
|
+
* object guard, drop the user's content on stringify, and report `written`.)
|
|
74
|
+
*
|
|
75
|
+
* @param {string} configPath — absolute path to .mcp.json
|
|
76
|
+
* @param {string} serverId
|
|
77
|
+
* @param {import('./mcp-server-specs.mjs').McpServerSpec} spec
|
|
78
|
+
* @returns {{action: 'written'|'skipped'|'skipped_unsafe', reason?: string}}
|
|
79
|
+
*/
|
|
80
|
+
function writeClaudeMcpServer(configPath, serverId, spec) {
|
|
81
|
+
let config = {}
|
|
82
|
+
if (existsSync(configPath)) {
|
|
83
|
+
try {
|
|
84
|
+
config = JSON.parse(readFileSync(configPath, 'utf-8'))
|
|
85
|
+
} catch {
|
|
86
|
+
// Malformed JSON: overwriting would erase every hand-authored MCP
|
|
87
|
+
// server. Leave the file untouched and surface it.
|
|
88
|
+
return { action: 'skipped_unsafe', reason: 'malformed_json' }
|
|
89
|
+
}
|
|
90
|
+
if (config === null || typeof config !== 'object' || Array.isArray(config)) {
|
|
91
|
+
// Non-plain-object root (array / null / primitive): same non-clobber
|
|
92
|
+
// rule — never overwrite content we cannot safely merge into.
|
|
93
|
+
return { action: 'skipped_unsafe', reason: 'non_object_root' }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (config.mcpServers === null || typeof config.mcpServers !== 'object') {
|
|
97
|
+
config.mcpServers = {}
|
|
98
|
+
}
|
|
99
|
+
// TOFU: never clobber a hand-authored server id.
|
|
100
|
+
if (Object.hasOwn(config.mcpServers, serverId)) return { action: 'skipped' }
|
|
101
|
+
|
|
102
|
+
const entry = { type: 'stdio', command: spec.command, args: spec.args ?? [] }
|
|
103
|
+
if (spec.env && Object.keys(spec.env).length > 0) entry.env = spec.env
|
|
104
|
+
config.mcpServers[serverId] = entry
|
|
105
|
+
atomicWriteJson(configPath, config)
|
|
106
|
+
return { action: 'written' }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Write the electron/tauri MCP server into a Codex `.codex/config.toml`.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} configPath — absolute path to .codex/config.toml
|
|
113
|
+
* @param {string} serverId
|
|
114
|
+
* @param {import('./mcp-server-specs.mjs').McpServerSpec} spec
|
|
115
|
+
* @returns {{action: 'written'|'skipped'}}
|
|
116
|
+
*/
|
|
117
|
+
function writeCodexMcpServer(configPath, serverId, spec) {
|
|
118
|
+
const existing = existsSync(configPath) ? readFileSync(configPath, 'utf-8') : ''
|
|
119
|
+
const { content, action } = upsertMcpServerToml(existing, serverId, {
|
|
120
|
+
command: spec.command,
|
|
121
|
+
args: spec.args ?? [],
|
|
122
|
+
...(spec.env && Object.keys(spec.env).length > 0 ? { env: spec.env } : {}),
|
|
123
|
+
})
|
|
124
|
+
if (action === 'written') atomicWriteText(configPath, content)
|
|
125
|
+
return { action }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Copy the vendored skill into a host skill dir. TOFU: never overwrite an
|
|
130
|
+
* existing agent-browser/SKILL.md (a user may have edited it).
|
|
131
|
+
*
|
|
132
|
+
* @param {string} skillDestPath — absolute <skillDir>/<skillId>/SKILL.md
|
|
133
|
+
* @param {string} vendoredSkillPath — absolute source path
|
|
134
|
+
* @returns {'written'|'skipped'}
|
|
135
|
+
*/
|
|
136
|
+
function copySkill(skillDestPath, vendoredSkillPath) {
|
|
137
|
+
if (existsSync(skillDestPath)) return 'skipped'
|
|
138
|
+
mkdirSync(dirname(skillDestPath), { recursive: true })
|
|
139
|
+
// copyFileSync is fine here — atomicWriteText would re-read; the dest is
|
|
140
|
+
// guaranteed absent (TOFU check above) so there is no clobber risk.
|
|
141
|
+
copyFileSync(vendoredSkillPath, skillDestPath)
|
|
142
|
+
return 'written'
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Provision MCP servers + skills for the selected host CLIs.
|
|
147
|
+
*
|
|
148
|
+
* @param {Object} opts
|
|
149
|
+
* @param {string} opts.targetDir — project root being installed into
|
|
150
|
+
* @param {string[]} opts.selectedRuntimes — the runtimes the user selected
|
|
151
|
+
* @param {object} opts.config — the effective merged project config
|
|
152
|
+
* @param {string} opts.frameworkRoot — absolute path to packages/framework (for vendored skill resolution)
|
|
153
|
+
* @returns {{status: 'ok'|'disabled'|'error', writes?: Array<{host:string, spec_id:string, kind:string, action:string}>, reason?: string}}
|
|
154
|
+
*/
|
|
155
|
+
export function provisionMcpConfig(opts) {
|
|
156
|
+
const targetDir = resolve(opts.targetDir)
|
|
157
|
+
const config = opts.config ?? {}
|
|
158
|
+
const selected = Array.isArray(opts.selectedRuntimes) ? opts.selectedRuntimes : []
|
|
159
|
+
const frameworkRoot = opts.frameworkRoot
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
// (1) Opt-out gate (default true).
|
|
163
|
+
if (config?.verification?.provision_mcp === false) {
|
|
164
|
+
appendEvent(targetDir, 'mcp-provision.disabled', {
|
|
165
|
+
reason: 'verification.provision_mcp=false',
|
|
166
|
+
})
|
|
167
|
+
return { status: 'disabled' }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// (2) Detect runtime → derive spec ids.
|
|
171
|
+
const runtimeCaps = loadRuntimeCapabilities(targetDir)
|
|
172
|
+
const specIds = capabilityToProvisionIds(runtimeCaps)
|
|
173
|
+
|
|
174
|
+
/** @type {Array<{host:string, spec_id:string, kind:string, action:string}>} */
|
|
175
|
+
const writes = []
|
|
176
|
+
|
|
177
|
+
// Iterate every (selected host, spec) pair.
|
|
178
|
+
for (const host of selected) {
|
|
179
|
+
const writer = HOST_PROVISION_WRITERS[host]
|
|
180
|
+
if (!writer) {
|
|
181
|
+
// Selected host with no writer (gemini/opencode/... in v1) — record
|
|
182
|
+
// once as unsupported_host, no write.
|
|
183
|
+
writes.push({ host, spec_id: '*', kind: '*', action: 'unsupported_host' })
|
|
184
|
+
appendEvent(targetDir, 'mcp-provision.unsupported_host', { host })
|
|
185
|
+
continue
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
for (const specId of specIds) {
|
|
189
|
+
const spec = PROVISION_SPECS[specId]
|
|
190
|
+
if (!spec) continue
|
|
191
|
+
|
|
192
|
+
if (spec.kind === 'mcp-server') {
|
|
193
|
+
const configPath = join(targetDir, writer.mcpConfigPath)
|
|
194
|
+
const { action, reason } =
|
|
195
|
+
writer.mcpFormat === 'json'
|
|
196
|
+
? writeClaudeMcpServer(configPath, specId, spec)
|
|
197
|
+
: writeCodexMcpServer(configPath, specId, spec)
|
|
198
|
+
writes.push({ host, spec_id: specId, kind: 'mcp-server', action })
|
|
199
|
+
appendEvent(targetDir, `mcp-provision.${action}`, {
|
|
200
|
+
host,
|
|
201
|
+
spec_id: specId,
|
|
202
|
+
config_path: writer.mcpConfigPath,
|
|
203
|
+
...(reason ? { reason } : {}),
|
|
204
|
+
})
|
|
205
|
+
// (5) tauri needs its launcher script — only provision it once the
|
|
206
|
+
// MCP config entry is actually written, so a TOFU-skipped or
|
|
207
|
+
// skipped_unsafe config never leaves an orphan launcher + a
|
|
208
|
+
// misleading audit line.
|
|
209
|
+
if (spec.requiresLauncherScript && frameworkRoot && action === 'written') {
|
|
210
|
+
provisionTauriLauncher(targetDir, frameworkRoot, writes)
|
|
211
|
+
}
|
|
212
|
+
} else if (spec.kind === 'skill') {
|
|
213
|
+
if (!frameworkRoot) continue
|
|
214
|
+
const vendoredSkillPath = resolve(frameworkRoot, spec.vendoredSkillRelPath)
|
|
215
|
+
const skillDestPath = join(targetDir, writer.skillDir, spec.skillId, 'SKILL.md')
|
|
216
|
+
const action = copySkill(skillDestPath, vendoredSkillPath)
|
|
217
|
+
writes.push({ host, spec_id: specId, kind: 'skill', action })
|
|
218
|
+
appendEvent(
|
|
219
|
+
targetDir,
|
|
220
|
+
`skill-provision.${action}`,
|
|
221
|
+
{
|
|
222
|
+
host,
|
|
223
|
+
spec_id: specId,
|
|
224
|
+
skill_dir: writer.skillDir,
|
|
225
|
+
},
|
|
226
|
+
'skill_provision',
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return { status: 'ok', writes }
|
|
233
|
+
} catch (e) {
|
|
234
|
+
return { status: 'error', reason: e instanceof Error ? e.message : String(e) }
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Copy the tauri launcher template into the project's scripts/ dir (TOFU).
|
|
240
|
+
* Records its own audit line; appends to `writes` with a synthetic host.
|
|
241
|
+
*
|
|
242
|
+
* @param {string} targetDir
|
|
243
|
+
* @param {string} frameworkRoot
|
|
244
|
+
* @param {Array<{host:string, spec_id:string, kind:string, action:string}>} writes
|
|
245
|
+
*/
|
|
246
|
+
function provisionTauriLauncher(targetDir, frameworkRoot, writes) {
|
|
247
|
+
const destPath = join(targetDir, 'scripts', 'tauri-mcp.sh')
|
|
248
|
+
// Already recorded a launcher write/skip for this run? Only provision once.
|
|
249
|
+
if (writes.some((w) => w.kind === 'launcher')) return
|
|
250
|
+
if (existsSync(destPath)) {
|
|
251
|
+
writes.push({ host: '-', spec_id: 'tauri', kind: 'launcher', action: 'skipped' })
|
|
252
|
+
appendEvent(targetDir, 'mcp-provision.skipped', {
|
|
253
|
+
host: '-',
|
|
254
|
+
spec_id: 'tauri',
|
|
255
|
+
launcher: 'scripts/tauri-mcp.sh',
|
|
256
|
+
})
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
const templatePath = resolve(frameworkRoot, 'drivers/tauri/scripts/tauri-mcp.sh')
|
|
260
|
+
const template = readFileSync(templatePath, 'utf-8')
|
|
261
|
+
atomicWriteText(destPath, renderTauriLauncher(template, targetDir))
|
|
262
|
+
writes.push({ host: '-', spec_id: 'tauri', kind: 'launcher', action: 'written' })
|
|
263
|
+
appendEvent(targetDir, 'mcp-provision.written', {
|
|
264
|
+
host: '-',
|
|
265
|
+
spec_id: 'tauri',
|
|
266
|
+
launcher: 'scripts/tauri-mcp.sh',
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Append one audit line to .aperant/events/{date}.jsonl, mirroring the
|
|
272
|
+
* daily-events shape. `kind` discriminates the provisioning channel:
|
|
273
|
+
* `mcp_provision` for MCP-server + launcher events, `skill_provision` for
|
|
274
|
+
* skill-copy events.
|
|
275
|
+
*
|
|
276
|
+
* @param {string} targetDir
|
|
277
|
+
* @param {string} op — e.g. mcp-provision.written, skill-provision.skipped
|
|
278
|
+
* @param {Record<string, unknown>} [extra]
|
|
279
|
+
* @param {string} [kind] — audit channel (default `mcp_provision`)
|
|
280
|
+
*/
|
|
281
|
+
function appendEvent(targetDir, op, extra = {}, kind = 'mcp_provision') {
|
|
282
|
+
try {
|
|
283
|
+
const path = dailyEventsPath(targetDir)
|
|
284
|
+
appendJsonl(path, {
|
|
285
|
+
ts: new Date().toISOString(),
|
|
286
|
+
kind,
|
|
287
|
+
op,
|
|
288
|
+
...extra,
|
|
289
|
+
})
|
|
290
|
+
} catch {
|
|
291
|
+
// Audit is best-effort — never fail provisioning on a log write error.
|
|
292
|
+
}
|
|
293
|
+
}
|