@aperant/framework 0.11.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 +213 -30
- 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-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 +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-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,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* recipe-diff.mjs — read-only Markdown diff generator for a recipe.
|
|
3
|
+
*
|
|
4
|
+
* Loads the recipe, walks `changes[]`, and emits a Markdown report
|
|
5
|
+
* enumerating every proposed write WITH `--- before` / `+++ after`
|
|
6
|
+
* blocks. The generator is pure read — it never writes to the adopter's
|
|
7
|
+
* tree (ID-02). The companion `apply-recipe` command is the only
|
|
8
|
+
* writer.
|
|
9
|
+
*
|
|
10
|
+
* Public contract:
|
|
11
|
+
* generateRecipeDiff({projectDir, recipeId, recipes}) → {
|
|
12
|
+
* format: 'markdown',
|
|
13
|
+
* body: string,
|
|
14
|
+
* changes: [{path, kind, summary}],
|
|
15
|
+
* envelope: {status, recipe_version, compatible}
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* The Markdown is user-facing and load-bearing — drift = golden-snapshot
|
|
19
|
+
* test failure by design (subtask 2.1 / TD-03).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
23
|
+
import { dirname, isAbsolute, join, relative, resolve } from 'node:path'
|
|
24
|
+
import { fileURLToPath } from 'node:url'
|
|
25
|
+
|
|
26
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Defense-in-depth path-under-root check. Returns the validated absolute
|
|
30
|
+
* path or null when the candidate escapes the root. The diff generator
|
|
31
|
+
* surfaces a "template not found" envelope rather than throwing (read-
|
|
32
|
+
* only generator stays non-fatal).
|
|
33
|
+
*
|
|
34
|
+
* @param {string} rootAbs
|
|
35
|
+
* @param {string} relPath
|
|
36
|
+
* @returns {string|null}
|
|
37
|
+
*/
|
|
38
|
+
function safeJoinUnderRoot(rootAbs, relPath) {
|
|
39
|
+
if (typeof relPath !== 'string' || relPath.length === 0) return null
|
|
40
|
+
if (isAbsolute(relPath)) return null
|
|
41
|
+
const candidate = resolve(rootAbs, relPath)
|
|
42
|
+
const rel = relative(rootAbs, candidate)
|
|
43
|
+
if (rel.startsWith('..') || isAbsolute(rel)) return null
|
|
44
|
+
return candidate
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {{projectDir: string, recipeId: string, recipes: object[]}} args
|
|
49
|
+
* @returns {{
|
|
50
|
+
* format: 'markdown',
|
|
51
|
+
* body: string,
|
|
52
|
+
* changes: Array<{path: string, kind: string, summary: string}>,
|
|
53
|
+
* envelope: {status: 'ok'|'error', recipe_version: string|null, compatible: boolean}
|
|
54
|
+
* }}
|
|
55
|
+
*/
|
|
56
|
+
export function generateRecipeDiff({ projectDir, recipeId, recipes }) {
|
|
57
|
+
const recipe = (recipes ?? []).find((r) => r && r.recipeId === recipeId)
|
|
58
|
+
if (!recipe) {
|
|
59
|
+
return {
|
|
60
|
+
format: 'markdown',
|
|
61
|
+
body: `# No recipe available\n\nrecipeId \`${recipeId}\` is not in the registered recipes list.\n`,
|
|
62
|
+
changes: [],
|
|
63
|
+
envelope: { status: 'error', recipe_version: null, compatible: false },
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const adopterRoot = resolve(projectDir)
|
|
68
|
+
const changeRows = []
|
|
69
|
+
const sections = []
|
|
70
|
+
|
|
71
|
+
for (const change of recipe.changes ?? []) {
|
|
72
|
+
// Defense-in-depth: a malicious recipe could otherwise read
|
|
73
|
+
// arbitrary host files via `change.path: '../../etc/passwd'`. The
|
|
74
|
+
// diff generator is read-only but the read CONTENT lands in the
|
|
75
|
+
// emitted Markdown — refuse to enumerate paths outside root.
|
|
76
|
+
const targetAbs = safeJoinUnderRoot(adopterRoot, change.path)
|
|
77
|
+
if (targetAbs === null) {
|
|
78
|
+
sections.push(
|
|
79
|
+
`## ${change.path}\n\n_(rejected: change.path escapes project root — recipe defect)_\n`,
|
|
80
|
+
)
|
|
81
|
+
changeRows.push({
|
|
82
|
+
path: change.path,
|
|
83
|
+
kind: change.kind,
|
|
84
|
+
summary: 'rejected (path escapes root)',
|
|
85
|
+
})
|
|
86
|
+
continue
|
|
87
|
+
}
|
|
88
|
+
const exists = existsSync(targetAbs)
|
|
89
|
+
const before = exists ? safeReadFile(targetAbs) : ''
|
|
90
|
+
const after = renderAfter(change, recipe, adopterRoot)
|
|
91
|
+
|
|
92
|
+
const summary = renderChangeSummary(change, exists)
|
|
93
|
+
changeRows.push({ path: change.path, kind: change.kind, summary })
|
|
94
|
+
|
|
95
|
+
sections.push(renderChangeSection(change, before, after, exists))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const body = renderBody(recipe, sections)
|
|
99
|
+
return {
|
|
100
|
+
format: 'markdown',
|
|
101
|
+
body,
|
|
102
|
+
changes: changeRows,
|
|
103
|
+
envelope: {
|
|
104
|
+
status: 'ok',
|
|
105
|
+
recipe_version: recipe.recipeVersion ?? null,
|
|
106
|
+
compatible: true,
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function renderBody(recipe, sections) {
|
|
112
|
+
const lines = []
|
|
113
|
+
lines.push(`# Recipe diff: ${recipe.name ?? recipe.recipeId}`)
|
|
114
|
+
lines.push('')
|
|
115
|
+
lines.push(
|
|
116
|
+
`**Recipe:** \`${recipe.recipeId}\` v${recipe.recipeVersion} ` +
|
|
117
|
+
`(compatibleWithApplied: ${JSON.stringify(recipe.compatibleWithApplied ?? [])})`,
|
|
118
|
+
)
|
|
119
|
+
lines.push('')
|
|
120
|
+
lines.push(recipe.description ?? '')
|
|
121
|
+
lines.push('')
|
|
122
|
+
lines.push(`This recipe proposes **${(recipe.changes ?? []).length}** changes.`)
|
|
123
|
+
lines.push(
|
|
124
|
+
'No file is written until you run `apt-tools apply-recipe . --confirm` ' +
|
|
125
|
+
'and accept each change individually.',
|
|
126
|
+
)
|
|
127
|
+
lines.push('')
|
|
128
|
+
for (const section of sections) {
|
|
129
|
+
lines.push(section)
|
|
130
|
+
lines.push('')
|
|
131
|
+
}
|
|
132
|
+
return lines.join('\n').replace(/\n{3,}/g, '\n\n')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function renderChangeSummary(change, exists) {
|
|
136
|
+
const verb =
|
|
137
|
+
change.kind === 'file-create' ? 'create' : change.kind === 'json-merge' ? 'merge into' : 'edit'
|
|
138
|
+
const target = exists ? change.path : `${change.path} (new file)`
|
|
139
|
+
return `${verb} ${target}`
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function renderChangeSection(change, before, after, exists) {
|
|
143
|
+
const lines = []
|
|
144
|
+
lines.push(`## ${change.path}`)
|
|
145
|
+
lines.push('')
|
|
146
|
+
lines.push(
|
|
147
|
+
`- kind: \`${change.kind}\``,
|
|
148
|
+
`- marker: \`${change.marker ?? '(none)'}\``,
|
|
149
|
+
`- target ${exists ? 'exists' : 'does not exist'} on disk`,
|
|
150
|
+
)
|
|
151
|
+
lines.push('')
|
|
152
|
+
lines.push('```diff')
|
|
153
|
+
const beforeBlock = before === '' ? '(empty / file does not exist)' : before
|
|
154
|
+
for (const line of beforeBlock.split('\n')) lines.push(`--- ${line}`)
|
|
155
|
+
lines.push('')
|
|
156
|
+
for (const line of after.split('\n')) lines.push(`+++ ${line}`)
|
|
157
|
+
lines.push('```')
|
|
158
|
+
return lines.join('\n')
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Render the "after" content per change kind. For json-merge we stringify
|
|
163
|
+
* the patch payload; for text-insert / file-create we load the template
|
|
164
|
+
* from the recipe directory (when present).
|
|
165
|
+
*/
|
|
166
|
+
function renderAfter(change, recipe, adopterRoot) {
|
|
167
|
+
if (change.kind === 'json-merge') {
|
|
168
|
+
return JSON.stringify(change.after ?? {}, null, 2)
|
|
169
|
+
}
|
|
170
|
+
if (change.template) {
|
|
171
|
+
const tplPath = locateRecipeTemplate(recipe, change.template, adopterRoot)
|
|
172
|
+
if (tplPath && existsSync(tplPath)) {
|
|
173
|
+
return safeReadFile(tplPath)
|
|
174
|
+
}
|
|
175
|
+
return `(template not found on disk: ${change.template})`
|
|
176
|
+
}
|
|
177
|
+
if (typeof change.after === 'string') return change.after
|
|
178
|
+
return ''
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Resolve a recipe template path relative to the framework's recipes/
|
|
183
|
+
* directory. The framework ships recipes at packages/framework/recipes/
|
|
184
|
+
* <id>/templates/. We compute the directory from this module's location
|
|
185
|
+
* so the resolver works regardless of cwd.
|
|
186
|
+
*/
|
|
187
|
+
function locateRecipeTemplate(recipe, templateName, _adopterRoot) {
|
|
188
|
+
// __dirname → packages/framework/src/cli/verify-proof/sandbox/
|
|
189
|
+
// → up four levels → packages/framework/
|
|
190
|
+
// Defense-in-depth: reject `../` escapes in templateName AND validate
|
|
191
|
+
// recipeId so a crafted id (e.g. '../../evil') cannot shift the anchor
|
|
192
|
+
// outside the framework root. discoverBundledRecipes() skips schema
|
|
193
|
+
// validation, so the ^[a-z][a-z0-9_-]*$ pattern may not have been enforced.
|
|
194
|
+
if (typeof recipe.recipeId !== 'string' || !/^[a-z][a-z0-9_-]*$/.test(recipe.recipeId)) {
|
|
195
|
+
return null
|
|
196
|
+
}
|
|
197
|
+
const frameworkRoot = resolve(__dirname, '..', '..', '..', '..')
|
|
198
|
+
const templatesRoot = join(frameworkRoot, 'recipes', recipe.recipeId, 'templates')
|
|
199
|
+
return safeJoinUnderRoot(templatesRoot, templateName)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function safeReadFile(path) {
|
|
203
|
+
try {
|
|
204
|
+
return readFileSync(path, 'utf-8')
|
|
205
|
+
} catch {
|
|
206
|
+
return ''
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sandbox-config.mjs — central loader for sandbox recipes + the
|
|
3
|
+
* adopter's verification.sandbox config block.
|
|
4
|
+
*
|
|
5
|
+
* Two responsibilities:
|
|
6
|
+
*
|
|
7
|
+
* 1. discoverBundledRecipes() — walk packages/framework/recipes/<id>/
|
|
8
|
+
* and return every parseable recipe.json. Mirrors the bundled-
|
|
9
|
+
* drivers discovery pattern in commands/detect-runtime.mjs.
|
|
10
|
+
*
|
|
11
|
+
* 2. readSandboxConfig(projectDir) — read .aperant/config.json's
|
|
12
|
+
* verification.sandbox block (or null if absent). Pure read; no
|
|
13
|
+
* merge with config.local.json — sandbox state is shared, not
|
|
14
|
+
* per-device.
|
|
15
|
+
*
|
|
16
|
+
* Stays in src/cli/verify-proof/sandbox/ so consumers under that tree
|
|
17
|
+
* share one loader. The CLI commands (recipe-diff, apply-recipe,
|
|
18
|
+
* verify-sandbox) all import from here.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
22
|
+
import { dirname, join, resolve } from 'node:path'
|
|
23
|
+
import { fileURLToPath } from 'node:url'
|
|
24
|
+
|
|
25
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Resolve the framework's recipes directory at runtime. Works whether
|
|
29
|
+
* the module is loaded from source (packages/framework/src/...) or from
|
|
30
|
+
* the installed dist (.aperant/deps/node_modules/@aperant/framework/...).
|
|
31
|
+
*
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
export function resolveRecipesDir() {
|
|
35
|
+
// __dirname → packages/framework/src/cli/verify-proof/sandbox/
|
|
36
|
+
// → up four levels → packages/framework/
|
|
37
|
+
return resolve(__dirname, '..', '..', '..', '..', 'recipes')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Walk the recipes/ directory and return every recipe.json that parses.
|
|
42
|
+
* Each entry is the raw recipe object (NOT validated — callers can
|
|
43
|
+
* call validateManifest if they need schema guarantees).
|
|
44
|
+
*
|
|
45
|
+
* @returns {Array<object>}
|
|
46
|
+
*/
|
|
47
|
+
export function discoverBundledRecipes() {
|
|
48
|
+
const dir = resolveRecipesDir()
|
|
49
|
+
if (!existsSync(dir)) return []
|
|
50
|
+
const out = []
|
|
51
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
52
|
+
if (!entry.isDirectory()) continue
|
|
53
|
+
const recipePath = join(dir, entry.name, 'recipe.json')
|
|
54
|
+
if (!existsSync(recipePath)) continue
|
|
55
|
+
try {
|
|
56
|
+
out.push(JSON.parse(readFileSync(recipePath, 'utf-8')))
|
|
57
|
+
} catch {
|
|
58
|
+
/* skip malformed recipes */
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return out
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Read .aperant/config.json's verification.sandbox block, or null when
|
|
66
|
+
* absent / unparseable. Pure read — no defaults injected.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} projectDir
|
|
69
|
+
* @returns {object|null}
|
|
70
|
+
*/
|
|
71
|
+
export function readSandboxConfig(projectDir) {
|
|
72
|
+
const path = join(resolve(projectDir), '.aperant', 'config.json')
|
|
73
|
+
if (!existsSync(path)) return null
|
|
74
|
+
try {
|
|
75
|
+
const config = JSON.parse(readFileSync(path, 'utf-8'))
|
|
76
|
+
const v = config?.verification
|
|
77
|
+
if (v === null || typeof v !== 'object') return null
|
|
78
|
+
return v.sandbox ?? null
|
|
79
|
+
} catch {
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://aperant.dev/schemas/verification-sandbox-v1.json",
|
|
4
|
+
"title": "Aperant verification.sandbox config block v1",
|
|
5
|
+
"description": "Structural contract for the optional verification.sandbox config block at .aperant/config.json. When this block is ABSENT every existing code path is byte-identical to today (Fast Path Guarantee, ID-07). When present, it records which recipe the adopter opted into and the audit metadata of the apply-recipe run that wrote it.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["enabled", "recipeId", "recipeVersion"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"enabled": {
|
|
11
|
+
"type": "boolean",
|
|
12
|
+
"description": "Master switch. False suppresses the parallel gate even when a recipe is applied — used to wind back without removing the recipe artifacts."
|
|
13
|
+
},
|
|
14
|
+
"recipeId": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"pattern": "^[a-z][a-z0-9_-]*$",
|
|
17
|
+
"description": "Identifier of the recipe applied to this project (e.g. 'electron-libsql')."
|
|
18
|
+
},
|
|
19
|
+
"recipeVersion": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"description": "Semver of the recipe at apply time (e.g. '0.1.0'). Used by `apt-tools recipe-diff` to detect already-applied / incremental / incompatible cases via compatibleWithApplied[]."
|
|
22
|
+
},
|
|
23
|
+
"appliedAt": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"description": "ISO-8601 timestamp recorded at apply time. Optional — only written by apply-recipe."
|
|
26
|
+
},
|
|
27
|
+
"appliedHash": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"pattern": "^[a-f0-9]{64}$",
|
|
30
|
+
"description": "SHA-256 of the canonical recipe manifest contents at apply time. Optional — used to detect drift between the applied recipe and the on-disk recipe library."
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* self-test.mjs — two-instance concurrent self-test for parallel
|
|
3
|
+
* /apt:verify-proof.
|
|
4
|
+
*
|
|
5
|
+
* Spawns TWO instances of the recipe's launch command concurrently
|
|
6
|
+
* (Promise.all with B jittered 50-150 ms) against per-instance sandbox
|
|
7
|
+
* dirs. Asserts BOTH instances reach the recipe's `readiness.logPattern`
|
|
8
|
+
* within `readiness.timeoutMs` AND neither emits `SQLITE_BUSY` or
|
|
9
|
+
* `EADDRINUSE`/`bind` errors in stderr.
|
|
10
|
+
*
|
|
11
|
+
* The verdict envelope shape mirrors verify-proof v2's
|
|
12
|
+
* `verification.json :: meta.verdict`:
|
|
13
|
+
* {verified: bool, verdict: 'approved'|'needs_human',
|
|
14
|
+
* reason: null|'self_test_failed_sqlite_busy'|
|
|
15
|
+
* 'self_test_failed_port_conflict'|
|
|
16
|
+
* 'self_test_failed_app_init_timeout',
|
|
17
|
+
* durationMs: number}
|
|
18
|
+
*
|
|
19
|
+
* Cross-platform via `node:child_process.spawn` (NO shell). win32 short-
|
|
20
|
+
* circuits at the verify-sandbox command layer (subtask 4.2) — this
|
|
21
|
+
* module assumes a POSIX-ish host.
|
|
22
|
+
*
|
|
23
|
+
* No state-file writing here (4.2's job). Pure functional contract so
|
|
24
|
+
* the integration tests can drive it via `vi.spyOn(child_process,
|
|
25
|
+
* 'spawn')`.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import child_process from 'node:child_process'
|
|
29
|
+
import { mkdirSync } from 'node:fs'
|
|
30
|
+
import { tmpdir } from 'node:os'
|
|
31
|
+
import { join, resolve } from 'node:path'
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {Object} SelfTestVerdict
|
|
35
|
+
* @property {boolean} verified
|
|
36
|
+
* @property {'approved'|'needs_human'} verdict
|
|
37
|
+
* @property {null|'self_test_failed_sqlite_busy'|'self_test_failed_port_conflict'|'self_test_failed_app_init_timeout'} reason
|
|
38
|
+
* @property {number} durationMs
|
|
39
|
+
* @property {{A: ChildOutcome, B: ChildOutcome}} children
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @typedef {Object} ChildOutcome
|
|
44
|
+
* @property {boolean} ready
|
|
45
|
+
* @property {string|null} failureReason
|
|
46
|
+
* @property {string} stderrTail
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {{projectDir: string, recipeId: string, recipes: object[]}} args
|
|
51
|
+
* @returns {Promise<SelfTestVerdict>}
|
|
52
|
+
*/
|
|
53
|
+
export async function runSelfTest({ projectDir, recipeId, recipes }) {
|
|
54
|
+
const recipe = (recipes ?? []).find((r) => r && r.recipeId === recipeId)
|
|
55
|
+
if (!recipe) {
|
|
56
|
+
throw new Error(`runSelfTest: recipe not found: ${recipeId}`)
|
|
57
|
+
}
|
|
58
|
+
const launch = recipe.launch
|
|
59
|
+
if (!launch?.binary) {
|
|
60
|
+
throw new Error(`runSelfTest: recipe ${recipeId} missing launch.{binary, args}`)
|
|
61
|
+
}
|
|
62
|
+
const readiness = recipe.readiness
|
|
63
|
+
if (!readiness || typeof readiness.logPattern !== 'string') {
|
|
64
|
+
throw new Error(`runSelfTest: recipe ${recipeId} missing readiness.logPattern`)
|
|
65
|
+
}
|
|
66
|
+
const timeoutMs = readiness.timeoutMs ?? 30000
|
|
67
|
+
const pid = process.pid
|
|
68
|
+
const sandboxRoot = join(tmpdir(), `aperant-sandbox-${pid}-${Date.now()}`)
|
|
69
|
+
const dirs = {
|
|
70
|
+
A: join(sandboxRoot, 'A'),
|
|
71
|
+
B: join(sandboxRoot, 'B'),
|
|
72
|
+
}
|
|
73
|
+
mkdirSync(dirs.A, { recursive: true })
|
|
74
|
+
mkdirSync(dirs.B, { recursive: true })
|
|
75
|
+
|
|
76
|
+
const startedAt = Date.now()
|
|
77
|
+
const jitter = 50 + Math.random() * 100
|
|
78
|
+
|
|
79
|
+
// Concurrency by construction: A starts immediately, B's start is
|
|
80
|
+
// awaited via a microtask + jitter delay. Promise.all races them.
|
|
81
|
+
const [outcomeA, outcomeB] = await Promise.all([
|
|
82
|
+
spawnInstance({ index: 'A', launch, readiness, timeoutMs, sandboxDir: dirs.A, projectDir }),
|
|
83
|
+
delay(jitter).then(() =>
|
|
84
|
+
spawnInstance({ index: 'B', launch, readiness, timeoutMs, sandboxDir: dirs.B, projectDir }),
|
|
85
|
+
),
|
|
86
|
+
])
|
|
87
|
+
|
|
88
|
+
const durationMs = Date.now() - startedAt
|
|
89
|
+
const verdict = decideVerdict(outcomeA, outcomeB)
|
|
90
|
+
return {
|
|
91
|
+
...verdict,
|
|
92
|
+
durationMs,
|
|
93
|
+
children: { A: outcomeA, B: outcomeB },
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Race a readiness signal against the timeout, scanning stderr for the
|
|
99
|
+
* three known-bad patterns. Resolves to a ChildOutcome regardless of
|
|
100
|
+
* which arm wins. Always kills the child before resolving.
|
|
101
|
+
*/
|
|
102
|
+
function spawnInstance({ index: _index, launch, readiness, timeoutMs, sandboxDir, projectDir }) {
|
|
103
|
+
return new Promise((resolveOutcome) => {
|
|
104
|
+
const env = renderLaunchEnv(launch.envTemplate ?? {}, sandboxDir)
|
|
105
|
+
const child = child_process.spawn(launch.binary, launch.args ?? [], {
|
|
106
|
+
cwd: resolve(projectDir),
|
|
107
|
+
env: { ...process.env, ...env, SANDBOX_DIR: sandboxDir },
|
|
108
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
let resolved = false
|
|
112
|
+
let stderrBuf = ''
|
|
113
|
+
const stdoutBuf = []
|
|
114
|
+
const MAX_STDERR_BYTES = 4096
|
|
115
|
+
|
|
116
|
+
const finish = (outcome) => {
|
|
117
|
+
if (resolved) return
|
|
118
|
+
resolved = true
|
|
119
|
+
clearTimeout(timer)
|
|
120
|
+
try {
|
|
121
|
+
child.kill('SIGTERM')
|
|
122
|
+
} catch {
|
|
123
|
+
/* already gone */
|
|
124
|
+
}
|
|
125
|
+
// SIGKILL grace
|
|
126
|
+
setTimeout(() => {
|
|
127
|
+
try {
|
|
128
|
+
child.kill('SIGKILL')
|
|
129
|
+
} catch {
|
|
130
|
+
/* gone */
|
|
131
|
+
}
|
|
132
|
+
}, 5000).unref?.()
|
|
133
|
+
resolveOutcome(outcome)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// onStderrText: appends to stderrBuf and checks failure-reason patterns.
|
|
137
|
+
// Must NOT be called with stdout data — informational app logs that mention
|
|
138
|
+
// "SQLITE_BUSY" or "EADDRINUSE" must not trigger failure verdicts (LOG-001).
|
|
139
|
+
const onStderrText = (text) => {
|
|
140
|
+
stderrBuf = (stderrBuf + text).slice(-MAX_STDERR_BYTES)
|
|
141
|
+
if (/SQLITE_BUSY/i.test(text)) {
|
|
142
|
+
finish({
|
|
143
|
+
ready: false,
|
|
144
|
+
failureReason: 'self_test_failed_sqlite_busy',
|
|
145
|
+
stderrTail: stderrBuf.slice(-500),
|
|
146
|
+
})
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
if (/EADDRINUSE|bind\s+failed|address already in use|EACCES|EPERM/i.test(text)) {
|
|
150
|
+
finish({
|
|
151
|
+
ready: false,
|
|
152
|
+
failureReason: 'self_test_failed_port_conflict',
|
|
153
|
+
stderrTail: stderrBuf.slice(-500),
|
|
154
|
+
})
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// onStdoutText: checks readiness signal only. Never touches stderrBuf and
|
|
160
|
+
// never triggers failure-reason heuristics — stdout is not an error stream.
|
|
161
|
+
const onStdoutText = (text) => {
|
|
162
|
+
if (text.includes(readiness.logPattern)) {
|
|
163
|
+
finish({ ready: true, failureReason: null, stderrTail: stderrBuf.slice(-500) })
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
child.stdout?.on?.('data', (chunk) => {
|
|
168
|
+
const text = String(chunk)
|
|
169
|
+
stdoutBuf.push(text)
|
|
170
|
+
onStdoutText(text)
|
|
171
|
+
})
|
|
172
|
+
child.stderr?.on?.('data', (chunk) => {
|
|
173
|
+
const text = String(chunk)
|
|
174
|
+
onStderrText(text)
|
|
175
|
+
// Readiness may also appear on stderr (some apps log to stderr only).
|
|
176
|
+
if (text.includes(readiness.logPattern)) {
|
|
177
|
+
finish({ ready: true, failureReason: null, stderrTail: stderrBuf.slice(-500) })
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
child.on?.('error', (err) => {
|
|
181
|
+
finish({
|
|
182
|
+
ready: false,
|
|
183
|
+
failureReason: 'self_test_failed_app_init_timeout',
|
|
184
|
+
stderrTail: String(err.message ?? err),
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
child.on?.('exit', () => {
|
|
188
|
+
if (!resolved) {
|
|
189
|
+
// Process exited before signalling readiness. Treat as init timeout
|
|
190
|
+
// unless the stderr already revealed a known bad pattern (handled
|
|
191
|
+
// above before exit).
|
|
192
|
+
finish({
|
|
193
|
+
ready: false,
|
|
194
|
+
failureReason: 'self_test_failed_app_init_timeout',
|
|
195
|
+
stderrTail: stderrBuf.slice(-500),
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
const timer = setTimeout(() => {
|
|
201
|
+
finish({
|
|
202
|
+
ready: false,
|
|
203
|
+
failureReason: 'self_test_failed_app_init_timeout',
|
|
204
|
+
stderrTail: stderrBuf.slice(-500),
|
|
205
|
+
})
|
|
206
|
+
}, timeoutMs)
|
|
207
|
+
// Don't keep the event loop alive on the timer.
|
|
208
|
+
timer.unref?.()
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Combine two outcomes into the v2-shaped verdict envelope.
|
|
214
|
+
* Failure precedence: sqlite_busy > port_conflict > app_init_timeout.
|
|
215
|
+
*
|
|
216
|
+
* Every spawnInstance() resolution sets failureReason explicitly on the
|
|
217
|
+
* !ready path (the four finish() call sites cover SQLITE_BUSY, port
|
|
218
|
+
* conflict, app_init_timeout, and the child-error path which also maps
|
|
219
|
+
* to app_init_timeout). The priority loop therefore always finds a
|
|
220
|
+
* match; the unreachable fallthrough that used to live here was
|
|
221
|
+
* deleted per CLAUDE.md guideline 2 (no handling for impossible
|
|
222
|
+
* scenarios).
|
|
223
|
+
*/
|
|
224
|
+
function decideVerdict(a, b) {
|
|
225
|
+
if (a.ready && b.ready) {
|
|
226
|
+
return { verified: true, verdict: /** @type {const} */ ('approved'), reason: null }
|
|
227
|
+
}
|
|
228
|
+
const reasons = [a.failureReason, b.failureReason].filter(Boolean)
|
|
229
|
+
const priority = [
|
|
230
|
+
'self_test_failed_sqlite_busy',
|
|
231
|
+
'self_test_failed_port_conflict',
|
|
232
|
+
'self_test_failed_app_init_timeout',
|
|
233
|
+
]
|
|
234
|
+
for (const cand of priority) {
|
|
235
|
+
if (reasons.includes(cand)) {
|
|
236
|
+
return {
|
|
237
|
+
verified: false,
|
|
238
|
+
verdict: /** @type {const} */ ('needs_human'),
|
|
239
|
+
reason: /** @type {any} */ (cand),
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
throw new Error(
|
|
244
|
+
`self-test decideVerdict: unreachable — both outcomes failed without a known reason: ${JSON.stringify({ a, b })}`,
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Substitute ${SANDBOX_DIR} placeholders in the recipe's envTemplate.
|
|
250
|
+
*/
|
|
251
|
+
function renderLaunchEnv(envTemplate, sandboxDir) {
|
|
252
|
+
const out = {}
|
|
253
|
+
for (const [k, v] of Object.entries(envTemplate)) {
|
|
254
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: config template marker, not a typo
|
|
255
|
+
out[k] = typeof v === 'string' ? v.replaceAll('${SANDBOX_DIR}', sandboxDir) : v
|
|
256
|
+
}
|
|
257
|
+
return out
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function delay(ms) {
|
|
261
|
+
return new Promise((res) => {
|
|
262
|
+
const t = setTimeout(res, ms)
|
|
263
|
+
t.unref?.()
|
|
264
|
+
})
|
|
265
|
+
}
|
package/templates/config.json
CHANGED
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
},
|
|
19
19
|
"qa": {
|
|
20
20
|
"approved_threshold": 7,
|
|
21
|
-
"approved_with_notes_threshold": 5,
|
|
22
21
|
"require_proof_for_ui": true,
|
|
23
22
|
"max_fix_iterations": 3,
|
|
24
23
|
"grace": {
|
|
@@ -116,6 +115,16 @@
|
|
|
116
115
|
"diagram": {
|
|
117
116
|
"mode": "file"
|
|
118
117
|
},
|
|
118
|
+
"roadmap": {
|
|
119
|
+
"$comment": "Roadmap backend toggle (task 26-05-26 D-09 + D-13). 'markdown' (default) keeps the existing OSS file-write path under .aperant/roadmap/{scope}/. 'cloud-mcp' routes write-side subcommands (add-milestone, add-phase, set-phase-status, delete-phase, delete-milestone) through the Aperant Cloud MCP server. Cloud-mcp requires roadmap.cloudMcp.teamSlug + a bearer token (env MCP_ROADMAP_TOKEN OR .aperant/team.json::roadmapMcpToken). Fast Path Guarantee: a missing 'roadmap' block is byte-identical to backend='markdown'.",
|
|
120
|
+
"backend": "markdown",
|
|
121
|
+
"cloudMcp": {
|
|
122
|
+
"teamSlug": null,
|
|
123
|
+
"mcpServerUrl": null,
|
|
124
|
+
"tokenSource": null,
|
|
125
|
+
"scopeMap": {}
|
|
126
|
+
}
|
|
127
|
+
},
|
|
119
128
|
"docs_policy": {
|
|
120
129
|
"$comment": "C33 docs-coherence policy. Override via /apt:setup Batch 7. Keys are globs/paths; values are 'auto-update' | 'flag-drift' | 'skip'. Missing block → defaults from packages/framework/templates/docs-policy-defaults.json apply."
|
|
121
130
|
},
|
|
@@ -17,7 +17,7 @@ JSON schema reference for `.aperant/proof/{app}/{date}/{run}/verification.json`
|
|
|
17
17
|
"started": "{ISO timestamp}",
|
|
18
18
|
"updated": "{ISO timestamp}",
|
|
19
19
|
"spec": "{spec.md path or task description}",
|
|
20
|
-
"verdict": "pending | approved |
|
|
20
|
+
"verdict": "pending | approved | rejected | needs_human",
|
|
21
21
|
"tools": ["electron", "puppeteer", "cdp_video"],
|
|
22
22
|
"selected_driver": {
|
|
23
23
|
"driverId": "electron",
|
|
@@ -47,6 +47,11 @@ JSON schema reference for `.aperant/proof/{app}/{date}/{run}/verification.json`
|
|
|
47
47
|
}
|
|
48
48
|
},
|
|
49
49
|
|
|
50
|
+
"gates": {
|
|
51
|
+
"automation_gate": "pass | fail",
|
|
52
|
+
"proof_gate": "pass | fail"
|
|
53
|
+
},
|
|
54
|
+
|
|
50
55
|
"coverage": {
|
|
51
56
|
"registry_file": ".aperant/features/{area}.json",
|
|
52
57
|
"registered_count": 0,
|
|
@@ -76,7 +81,7 @@ JSON schema reference for `.aperant/proof/{app}/{date}/{run}/verification.json`
|
|
|
76
81
|
"actual": "Grid correctly tiled to 2+1",
|
|
77
82
|
"evidence": "electron/screenshots/03-grid.png",
|
|
78
83
|
"analysis": "Screenshot shows three terminal panes. Top row has 2 equal-width panes, bottom row has 1 centered pane. Grid lines are clean, no overlap or gap artifacts. Each pane shows an active shell prompt with correct path.",
|
|
79
|
-
"result": "pass | fail | blocked | skip",
|
|
84
|
+
"result": "pass | fail | blocked | skip | needs_human_with_transport",
|
|
80
85
|
"severity": null
|
|
81
86
|
},
|
|
82
87
|
{
|
|
@@ -189,7 +194,19 @@ JSON schema reference for `.aperant/proof/{app}/{date}/{run}/verification.json`
|
|
|
189
194
|
| `fallback_attempts` | array | IMMUTABLE | Drivers the resolver REJECTED before selecting. Each entry has `driver_id` + `reason` (missing capabilities, stability filter, hash drift, policy denial). Audit trail for coverage-loss decisions. |
|
|
190
195
|
|
|
191
196
|
### automated_checks
|
|
192
|
-
Each check has `status` (pass
|
|
197
|
+
Each check has `status` (closed set `{pass, fail}`) and `output` (captured terminal output). These are populated in Step 3 before any outcome driving begins.
|
|
198
|
+
|
|
199
|
+
**Hard STOP gate (v0.9.0).** `status` MUST be one of `{pass, fail}`. The legacy `"enforced_at_commit"` dodge string is REJECTED — the workflow runner aborts at Step 3 with `automation_gate: fail` if any check carries a non-conforming status. Each check must actually execute in the run (no deferring to commit-time enforcement).
|
|
200
|
+
|
|
201
|
+
### gates (v0.9.0)
|
|
202
|
+
|
|
203
|
+
Top-level `{automation_gate, proof_gate}` derived from `automated_checks` (gate 1) and per-outcome evidence (gate 2). Closed set `{pass, fail}` for each. The two-gate envelope drives `meta.verdict`:
|
|
204
|
+
|
|
205
|
+
| automation_gate | proof_gate | verdict |
|
|
206
|
+
|---|---|---|
|
|
207
|
+
| `pass` | `pass` | `approved` |
|
|
208
|
+
| `pass` | `fail` | `needs_human` (UI-surface transport gap; install driver and re-run) |
|
|
209
|
+
| `fail` | * | `rejected` (any of lint/typecheck/tests red) |
|
|
193
210
|
|
|
194
211
|
### coverage
|
|
195
212
|
| Field | Type | Description |
|
|
@@ -219,7 +236,7 @@ Each test entry tracks one sub-feature verification. Fields:
|
|
|
219
236
|
| `actual` | string | OVERWRITE | What actually happened (filled after testing) |
|
|
220
237
|
| `evidence` | string | OVERWRITE | Screenshot filename or video timestamp. v0.8.6 — paths are PER-SURFACE: `{surface}/screenshots/{outcome-id}.png`. |
|
|
221
238
|
| `analysis` | string | OVERWRITE | Adversarial analysis of evidence — describe what you SEE, not what you expect |
|
|
222
|
-
| `result` | string | OVERWRITE | `pass`, `fail`, `blocked`, or `
|
|
239
|
+
| `result` | string | OVERWRITE | `pass`, `fail`, `blocked`, `skip`, or `needs_human_with_transport` (v0.9.0 — UI-surface outcome lacks driver transport; host must install MCP server / driver and re-run). `blocked` keeps its legacy meaning: driver advertised the capability but couldn't perform it in this run. |
|
|
223
240
|
| `severity` | string/null | OVERWRITE | `blocker`, `major`, `minor`, `cosmetic`, or `null` if pass |
|
|
224
241
|
|
|
225
242
|
### skipped (MANDATORY)
|
|
@@ -242,10 +259,12 @@ Four QA dimensions, each with `score` (1-10) and `rationale` (string). Scores ar
|
|
|
242
259
|
- Any major in the area → max score 5
|
|
243
260
|
- A dimension cannot score above 5 if any blocker exists
|
|
244
261
|
|
|
245
|
-
**Verdict derivation:**
|
|
246
|
-
-
|
|
247
|
-
-
|
|
248
|
-
-
|
|
262
|
+
**Verdict derivation (v0.9.0 two-gate envelope — see workflow Step 7):**
|
|
263
|
+
- `automation_gate: pass` AND `proof_gate: pass` → `approved`
|
|
264
|
+
- `automation_gate: pass` AND `proof_gate: fail` → `needs_human`
|
|
265
|
+
- `automation_gate: fail` OR critical issues unresolved → `rejected`
|
|
266
|
+
|
|
267
|
+
Dimensions are informational rationale only — they do NOT drive the verdict in v0.9.0+.
|
|
249
268
|
|
|
250
269
|
### gaps
|
|
251
270
|
Issues discovered during testing. Feeds into the fix planning pipeline.
|