@manhd_d/ai-harness 0.9.0 → 0.11.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 +34 -0
- package/CONTRIBUTING.md +9 -0
- package/README.md +39 -22
- package/capabilities/product/capability.json +7 -4
- package/capabilities/product/checks/verification.json +12 -0
- package/capabilities/product/evals/cases.json +11 -0
- package/capabilities/product/skills/harness-product-techspec/SKILL.md +35 -0
- package/capabilities/product/templates/techspec.md +17 -0
- package/capabilities/product/workflows/change-impact-analysis.md +6 -0
- package/capabilities/product/workflows/document-driven.md +8 -1
- package/capabilities/product/workflows/idea-driven.md +8 -1
- package/capabilities/product/workflows/techspec-maintenance.md +5 -0
- package/capabilities/uiux/capability.json +1 -1
- package/capabilities/uiux/workflows/docs-to-figma.md +7 -0
- package/capabilities/uiux/workflows/idea-to-figma.md +7 -0
- package/cli/artifacts.js +168 -0
- package/cli/environment.js +9 -10
- package/cli/harness.js +18 -5
- package/cli/main.js +28 -9
- package/cli/project-config.js +11 -0
- package/cli/readiness.js +3 -1
- package/cli/verification-report.js +177 -0
- package/cli/verification.js +78 -0
- package/core/agents/test-engineer.md +2 -0
- package/core/checks/verification.json +24 -0
- package/core/evals/cases.json +41 -0
- package/core/skills/harness-verification/SKILL.md +24 -0
- package/core/workflows/adaptive-verification.md +5 -0
- package/core/workflows/artifact-contract.md +53 -0
- package/core/workflows/project-setup.md +2 -0
- package/core/workflows/spec-to-code.md +7 -0
- package/docs/README.md +4 -0
- package/docs/adaptive-verification.md +59 -0
- package/docs/architecture.md +11 -0
- package/docs/artifact-contract.md +197 -0
- package/docs/change-readiness.md +6 -0
- package/docs/cli.md +12 -1
- package/docs/documentation-workflows.md +9 -0
- package/docs/getting-started.md +5 -0
- package/docs/harness-maintenance.md +1 -0
- package/docs/i18n/README.vi.md +55 -0
- package/docs/project-configuration.md +7 -0
- package/docs/skill-matrix.md +8 -4
- package/docs/tool-setup/README.md +5 -0
- package/docs/versioning.md +13 -0
- package/harness.yaml +1 -1
- package/integrations/mcp/README.md +5 -0
- package/package.json +1 -1
- package/templates/AGENTS.md +3 -0
- package/templates/artifacts.json +6 -0
- package/templates/integrations.json +4 -0
- package/templates/openspec/artifact-impact.json +5 -0
- package/templates/openspec/design.md +3 -0
package/cli/harness.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { emptyEnvironment, environmentReport } from './environment.js';
|
|
3
3
|
import { AGENT_CONFIG, agentDefaults, loadAgentConfig } from './agents.js';
|
|
4
|
+
import { reviewVerification, techspecEvidence } from './verification-report.js';
|
|
5
|
+
import { ARTIFACTS, INTEGRATIONS, emptyArtifacts, emptyIntegrations, loadArtifactContract, artifactReport, docRoute } from './artifacts.js';
|
|
4
6
|
import path from 'node:path';
|
|
5
7
|
import { randomUUID } from 'node:crypto';
|
|
6
8
|
import { loadAdapter } from './adapter-schema.js';
|
|
@@ -166,6 +168,7 @@ export function install(root, source, { command = 'init', adapters, capabilities
|
|
|
166
168
|
if (command === 'update' && !hasLock) throw new HarnessError('Not installed; run init first');
|
|
167
169
|
const old = hasLock ? loadLock(root) : null;
|
|
168
170
|
const existingAgents = loadAgentConfig(root);
|
|
171
|
+
loadArtifactContract(root); // Validate project-owned declarations before planning any writes.
|
|
169
172
|
if (agentPreset !== undefined && command !== 'init') throw new HarnessError('Agent preset is supported only by init');
|
|
170
173
|
if (agentPreset !== undefined && existingAgents) throw new HarnessError('agents.json is project-owned; edit it explicitly instead of applying an init preset');
|
|
171
174
|
const config = projectConfig(root, adapters, capabilities, hasLock);
|
|
@@ -193,6 +196,8 @@ export function install(root, source, { command = 'init', adapters, capabilities
|
|
|
193
196
|
const seeds = {
|
|
194
197
|
[AGENT_CONFIG]: encode(agentDefaults(source, agentPreset ?? 'recommended')),
|
|
195
198
|
[CONFIG]: encode(config),
|
|
199
|
+
[ARTIFACTS]: encode(emptyArtifacts()),
|
|
200
|
+
[INTEGRATIONS]: encode(emptyIntegrations()),
|
|
196
201
|
'.harness/environment.json': encode(emptyEnvironment()),
|
|
197
202
|
'openspec/config.yaml': fs.readFileSync(safe(source, 'templates/openspec/config.yaml')),
|
|
198
203
|
'.harness/local.md': Buffer.from('# Project overrides\n\nRecord project facts and reviewed exceptions here. Never weaken security boundaries.\n\n## Development conventions\nRecord scope/mode (new, maintain or mixed), authoritative source paths, module boundaries, reusable public APIs, naming/error/test conventions and configured checks. Link existing guides instead of duplicating them. Unknowns are not approved defaults; the AI host inspects evidence and asks only consequential questions.\n'),
|
|
@@ -216,6 +221,7 @@ export function install(root, source, { command = 'init', adapters, capabilities
|
|
|
216
221
|
export function check(root) {
|
|
217
222
|
const lock = loadLock(root), config = projectConfig(root, undefined, undefined, true);
|
|
218
223
|
const problems = drift(root, lock);
|
|
224
|
+
loadArtifactContract(root);
|
|
219
225
|
loadAgentConfig(root); // Optional on legacy installs; validate user config without managing its bytes.
|
|
220
226
|
if (JSON.stringify([...config.adapters].sort()) !== JSON.stringify([...lock.adapters].sort())) problems.push(`${CONFIG}: adapter selection changed; run update`);
|
|
221
227
|
if (JSON.stringify([...(config.capabilities ?? [])].sort()) !== JSON.stringify([...(lock.capabilities ?? [])].sort())) problems.push(`${CONFIG}: capability selection changed; run update`);
|
|
@@ -231,7 +237,7 @@ export function newChange(root, name) {
|
|
|
231
237
|
if (stat(folder)) throw new HarnessError(`Change already exists: ${name}`);
|
|
232
238
|
const mapping = { 'proposal.md': 'proposal.md', 'design.md': 'design.md', 'tasks.md': 'tasks.md', 'spec.md': `specs/${name}/spec.md`, '.openspec.yaml': '.openspec.yaml' };
|
|
233
239
|
const locked = loadLock(root);
|
|
234
|
-
for (const name of ['decisions.json', 'verification.json']) {
|
|
240
|
+
for (const name of ['decisions.json', 'verification.json', 'artifact-impact.json']) {
|
|
235
241
|
if (Object.hasOwn(locked.files, '.harness/templates/openspec/' + name)) mapping[name] = name;
|
|
236
242
|
}
|
|
237
243
|
const changes = {};
|
|
@@ -245,7 +251,7 @@ export function newChange(root, name) {
|
|
|
245
251
|
}
|
|
246
252
|
|
|
247
253
|
// Capability drafts are project-owned and never added to the managed bundle.
|
|
248
|
-
export function newDoc(root, name, capability = 'product') {
|
|
254
|
+
export function newDoc(root, name, capability = 'product', options = {}) {
|
|
249
255
|
if (!SLUG.test(name)) throw new HarnessError('Document type must be a lowercase hyphenated slug');
|
|
250
256
|
if (!SLUG.test(capability)) throw new HarnessError('Capability must be a lowercase hyphenated slug');
|
|
251
257
|
return guard(root, () => {
|
|
@@ -261,10 +267,17 @@ export function newDoc(root, name, capability = 'product') {
|
|
|
261
267
|
const declared = loadCapability(path.join(root, '.harness'), capability).templates;
|
|
262
268
|
if (!declared.includes(`templates/${name}.md`)) throw new HarnessError(`Unknown document type for ${capability}: ${name}`);
|
|
263
269
|
if (!Object.hasOwn(lock.files, sourceName)) throw new HarnessError(`Document template is not locked: ${capability}/${name}`);
|
|
270
|
+
if (options.report && (capability !== 'product' || name !== 'techspec')) throw new HarnessError('--report is supported only for product techspec');
|
|
271
|
+
const content = options.report ? techspecEvidence(reviewVerification(root, options.report)) : fs.readFileSync(safe(root, sourceName), 'utf8');
|
|
264
272
|
const folder = capability === 'product' && ['idea', 'research'].includes(name) ? 'discovery' : capability;
|
|
265
|
-
const
|
|
273
|
+
const route = docRoute(root, capability, name);
|
|
274
|
+
if (route?.external) return { status: 'planned', type: name, capability, destination: route.destination,
|
|
275
|
+
write_policy: route.write_policy, executed: false,
|
|
276
|
+
template: content,
|
|
277
|
+
note: 'External destination: host handoff only. No local fallback or remote write. Use the declared connection and existing authorization; preserve observed revision and return publication evidence.' };
|
|
278
|
+
const outputName = route?.path ?? `docs/${folder}/${name}.md`;
|
|
266
279
|
if (stat(safe(root, outputName))) throw new HarnessError(`Document already exists: ${outputName}`);
|
|
267
|
-
transaction(root, { [outputName]:
|
|
280
|
+
transaction(root, { [outputName]: content });
|
|
268
281
|
return { status: 'ok', type: name, capability, path: outputName,
|
|
269
282
|
note: 'Template only; populate from authorized evidence and obtain human approval.' };
|
|
270
283
|
});
|
|
@@ -281,5 +294,5 @@ export function doctor(root) {
|
|
|
281
294
|
}
|
|
282
295
|
const state = check(root), environment = environmentReport(root);
|
|
283
296
|
return { ...state, status: state.status !== 'ok' || environment.problems.length ? 'error' : 'ok',
|
|
284
|
-
tools, environment, note: 'Tool presence only; no commands executed or compatibility claim made. Configured project tools take precedence over generic PATH hints.' };
|
|
297
|
+
tools, environment, artifacts: artifactReport(root), note: 'Tool presence only; no commands executed or compatibility claim made. Configured project tools take precedence over generic PATH hints.' };
|
|
285
298
|
}
|
package/cli/main.js
CHANGED
|
@@ -4,6 +4,9 @@ import { resolveModules } from './composition.js';
|
|
|
4
4
|
import { parseArgs } from 'node:util';
|
|
5
5
|
import { setupPlan } from './environment.js';
|
|
6
6
|
import { ready } from './readiness.js';
|
|
7
|
+
import { adaptiveVerification } from './verification.js';
|
|
8
|
+
import { reviewVerification } from './verification-report.js';
|
|
9
|
+
import { artifactReport } from './artifacts.js';
|
|
7
10
|
import { SOURCE, readJson, targetRoot, install, check, doctor, newChange, newDoc, loadLock } from './harness.js';
|
|
8
11
|
import path from 'node:path';
|
|
9
12
|
|
|
@@ -15,13 +18,14 @@ Usage: aih <command> [options]
|
|
|
15
18
|
skill <name> Load one installed skill on demand
|
|
16
19
|
detect Suggest adapters with evidence from target files
|
|
17
20
|
resolve Validate a module selection and show compatibility warnings
|
|
18
|
-
verify
|
|
21
|
+
verify Plan checks or validate --report evidence (does not execute)
|
|
19
22
|
eval Emit evaluation template, or score --report <file>
|
|
20
23
|
init Install rules and selected adapters
|
|
21
24
|
update Apply the installed CLI bundle to the project (no download)
|
|
22
25
|
check Check Harness file integrity, not application tests
|
|
23
26
|
doctor Check installation, configured paths and tool presence
|
|
24
27
|
setup Suggest first-run questions for the AI host (read-only)
|
|
28
|
+
artifacts Show input/output/storage and connection declarations (read-only)
|
|
25
29
|
ready <id> Check structural change readiness, not approval
|
|
26
30
|
new-change <id> Create an OpenSpec draft using installed templates
|
|
27
31
|
new-doc <type> Scaffold a locked capability template (default: product)
|
|
@@ -34,7 +38,8 @@ Options:
|
|
|
34
38
|
--agent-preset <id> init only: recommended (default), economy, custom; no overwrite
|
|
35
39
|
--host <id> setup only: codex,claude-code,gemini-cli,cursor,copilot or custom host ID
|
|
36
40
|
--scope <type> verify/eval: adapters (default), capabilities, core, all
|
|
37
|
-
--
|
|
41
|
+
--adaptive verify: discover a read-only project verification plan
|
|
42
|
+
--report <file> eval: score; verify: validate evidence; new-doc techspec: link evidence
|
|
38
43
|
--adapters <a,b> init/resolve: java-spring,react-nextjs,react-native,ai-python,kubernetes,astro
|
|
39
44
|
--capabilities <a,b> init/resolve: uiux,product,architecture,delivery,devops,monetization
|
|
40
45
|
--source <directory> init/update/list/detect/resolve: trusted alternate bundle directory
|
|
@@ -44,7 +49,7 @@ Options:
|
|
|
44
49
|
-v, --version Show installed CLI version
|
|
45
50
|
|
|
46
51
|
Start: init → fill .harness/local.md → ask your AI host to read AGENTS.md.
|
|
47
|
-
The host runs application tests; verify
|
|
52
|
+
The host runs application tests; verify prints plans or validates supplied evidence.
|
|
48
53
|
Update a global CLI with npm install -g @manhd_d/ai-harness@latest,
|
|
49
54
|
then run aih update --dry-run, aih update, and aih check in the project.
|
|
50
55
|
|
|
@@ -59,6 +64,9 @@ Examples:
|
|
|
59
64
|
aih doctor
|
|
60
65
|
aih ready add-order --stage implement
|
|
61
66
|
aih skills
|
|
67
|
+
aih verify --adaptive
|
|
68
|
+
aih verify --report verification/report.json
|
|
69
|
+
aih new-doc techspec --report verification/report.json
|
|
62
70
|
aih new-doc brd
|
|
63
71
|
aih new-doc idea
|
|
64
72
|
aih new-doc research
|
|
@@ -81,7 +89,7 @@ export function main(argv = process.argv.slice(2)) {
|
|
|
81
89
|
args = parseArgs({ args: argv, allowPositionals: true, strict: true, tokens: true, options: {
|
|
82
90
|
target: { type: 'string' }, adapters: { type: 'string' }, capabilities: { type: 'string' }, capability: { type: 'string' }, source: { type: 'string' },
|
|
83
91
|
'agent-preset': { type: 'string' }, host: { type: 'string' },
|
|
84
|
-
detect: { type: 'boolean' }, report: { type: 'string' }, scope: { type: 'string' }, stage: { type: 'string' },
|
|
92
|
+
adaptive: { type: 'boolean' }, detect: { type: 'boolean' }, report: { type: 'string' }, scope: { type: 'string' }, stage: { type: 'string' },
|
|
85
93
|
'dry-run': { type: 'boolean' }, 'allow-major': { type: 'boolean' },
|
|
86
94
|
help: { type: 'boolean', short: 'h' }, version: { type: 'boolean', short: 'v' },
|
|
87
95
|
} });
|
|
@@ -93,10 +101,13 @@ export function main(argv = process.argv.slice(2)) {
|
|
|
93
101
|
if (args.values.help || !argv.length) { console.log(HELP); return 0; }
|
|
94
102
|
if (args.values.version) { console.log(readJson(path.join(SOURCE, 'package.json')).version); return 0; }
|
|
95
103
|
const [command, name, ...extra] = args.positionals;
|
|
96
|
-
if (!['init', 'update', 'check', 'doctor', 'new-change', 'new-doc', 'list', 'detect', 'resolve', 'verify', 'eval', 'skills', 'skill', 'setup', 'ready'].includes(command)) throw new Error(`Unknown command: ${command}`);
|
|
104
|
+
if (!['init', 'update', 'check', 'doctor', 'new-change', 'new-doc', 'list', 'detect', 'resolve', 'verify', 'eval', 'skills', 'skill', 'setup', 'ready', 'artifacts'].includes(command)) throw new Error(`Unknown command: ${command}`);
|
|
97
105
|
if (extra.length || (!['new-change', 'new-doc', 'skill', 'ready'].includes(command) && name !== undefined) || (['new-change', 'new-doc', 'skill', 'ready'].includes(command) && name === undefined)) throw new Error('Unexpected or missing positional argument; run aih --help');
|
|
98
106
|
const allowed = new Set(['target', ...(['init', 'update'].includes(command) ? ['source', 'dry-run', 'allow-major'] : []), ...(['init', 'resolve'].includes(command) ? ['adapters', 'capabilities'] : []), ...(command === 'init' ? ['detect'] : []), ...(['list', 'detect', 'resolve'].includes(command) ? ['source'] : []), ...(command === 'eval' ? ['report'] : []), ...(command === 'ready' ? ['stage'] : []), ...(['verify', 'eval'].includes(command) ? ['scope'] : [])]);
|
|
99
|
-
if (command === 'new-doc') allowed.add('capability');
|
|
107
|
+
if (command === 'new-doc') { allowed.add('capability'); allowed.add('report'); }
|
|
108
|
+
if (command === 'verify') { allowed.add('adaptive'); allowed.add('report'); }
|
|
109
|
+
if (args.values.adaptive && args.values.report) throw new Error('Choose --adaptive or --report');
|
|
110
|
+
if (args.values.scope && (args.values.adaptive || (command === 'verify' && args.values.report))) throw new Error('--scope applies only to advisory checks');
|
|
100
111
|
if (command === 'init') allowed.add('agent-preset');
|
|
101
112
|
if (command === 'setup') allowed.add('host');
|
|
102
113
|
for (const key of Object.keys(args.values)) if (!allowed.has(key)) throw new Error(`--${key} is not supported for ${command}`);
|
|
@@ -129,23 +140,31 @@ export function main(argv = process.argv.slice(2)) {
|
|
|
129
140
|
capabilities: values.capabilities === undefined ? [] : values.capabilities.split(',').map(x => x.trim()),
|
|
130
141
|
});
|
|
131
142
|
else if (command === 'detect') result = detect(root, targetRoot(values.source ?? SOURCE));
|
|
132
|
-
else if (command === 'verify')
|
|
143
|
+
else if (command === 'verify') {
|
|
144
|
+
if (values.adaptive || values.report) { if (check(root).status !== 'ok') throw new Error('Fix installation drift before verification'); }
|
|
145
|
+
result = values.report ? reviewVerification(root, values.report) : values.adaptive ? adaptiveVerification(root, SOURCE) : verify(root, values.scope ?? 'adapters');
|
|
146
|
+
}
|
|
133
147
|
else if (command === 'eval') result = evaluate(root, values.report, values.scope ?? 'adapters');
|
|
148
|
+
else if (command === 'artifacts') {
|
|
149
|
+
if (check(root).status !== 'ok') throw new Error('Fix installation drift before reading artifact declarations');
|
|
150
|
+
result = artifactReport(root);
|
|
151
|
+
}
|
|
134
152
|
else if (command === 'setup' || command === 'ready') {
|
|
135
153
|
const state = check(root);
|
|
136
154
|
if (state.status !== 'ok') throw new Error('Fix installation drift before setup/readiness checks');
|
|
137
155
|
result = command === 'setup' ? setupPlan(root, loadLock(root).adapters, process.env, values.host) : ready(root, name, values.stage ?? 'implement');
|
|
138
156
|
}
|
|
139
157
|
else if (command === 'new-change') result = newChange(root, name);
|
|
140
|
-
else if (command === 'new-doc') result = newDoc(root, name, values.capability ?? 'product');
|
|
158
|
+
else if (command === 'new-doc') result = newDoc(root, name, values.capability ?? 'product', { report: values.report });
|
|
141
159
|
else result = command === 'doctor' ? doctor(root) : check(root);
|
|
142
160
|
if (command === 'init' || command === 'update') result.next_steps = values['dry-run']
|
|
143
161
|
? ['Review the proposed changes, then repeat without --dry-run.']
|
|
144
162
|
: ['Run aih setup; ask your AI host to read AGENTS.md and the project-setup workflow.',
|
|
145
|
-
'Review .
|
|
163
|
+
'Review local.md/environment.json and artifacts.json/integrations.json; record input/output/storage/ownership, keep credentials in the host and personal paths in ignored machine.json.',
|
|
146
164
|
'Run aih doctor and review/commit the shared configuration and snapshot.'];
|
|
147
165
|
if (command === 'new-change') result.next_steps = [
|
|
148
166
|
'Resolve consequential ambiguity with the user and record answers/sources in decisions.json.',
|
|
167
|
+
'When artifact outputs are configured, record create/update/unchanged impact and source-write policy in artifact-impact.json.',
|
|
149
168
|
'Fill the change documents, then run aih ready ' + name + ' --stage implement.',
|
|
150
169
|
'After implementation, record actual verification evidence and run aih ready ' + name + ' --stage review.'];
|
|
151
170
|
if (command === 'new-doc') result.next_steps = ['Populate the draft from authorized sources; clarify missing decisions before implementation.'];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { safe, stat, HarnessError } from './files.js';
|
|
3
|
+
|
|
4
|
+
// Shared project-owned JSON reader. Never echo malformed input or secret values.
|
|
5
|
+
export function readConfig(root, name) {
|
|
6
|
+
const p = safe(root, name), info = stat(p);
|
|
7
|
+
if (!info) return null;
|
|
8
|
+
if (info.size > 1024 * 1024) throw new HarnessError('Configuration exceeds 1 MiB: ' + name);
|
|
9
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf8')); }
|
|
10
|
+
catch { throw new HarnessError('Invalid JSON: ' + name); }
|
|
11
|
+
}
|
package/cli/readiness.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { safe, stat, SLUG, isObject, HarnessError } from './files.js';
|
|
4
|
-
import { readConfig } from './
|
|
4
|
+
import { readConfig } from './project-config.js';
|
|
5
|
+
import { artifactImpact } from './artifacts.js';
|
|
5
6
|
|
|
6
7
|
const text = s => typeof s === 'string' && s.trim().length > 0;
|
|
7
8
|
const draft = s => /\b(?:TODO|TBD|FIXME)\b|\{\{[^}]+\}\}/i.test(s);
|
|
@@ -108,6 +109,7 @@ export function ready(root, id, stage = 'implement') {
|
|
|
108
109
|
if (!passed) problems.push('No completed passing check or manual review recorded');
|
|
109
110
|
}
|
|
110
111
|
}
|
|
112
|
+
problems.push(...artifactImpact(root, base, stage));
|
|
111
113
|
return { status: problems.length ? 'blocked' : 'ready', change: id, stage, problems,
|
|
112
114
|
tasks: { pre_merge: taskCount, pending }, skipped_checks: skipped, executed: false,
|
|
113
115
|
next_steps: problems.length ? ['Resolve reported gaps; ask the user about unresolved consequential decisions.']
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
3
|
+
import { safe, stat, digest, isObject, HarnessError } from './files.js';
|
|
4
|
+
import { readConfig } from './project-config.js';
|
|
5
|
+
|
|
6
|
+
const text = v => typeof v === 'string' && v.trim().length > 0 && !/[\0\r\n]/.test(v);
|
|
7
|
+
const list = a => Array.isArray(a) && a.length > 0 && a.every(text) && new Set(a).size === a.length;
|
|
8
|
+
const assert = (ok, msg) => { if (!ok) throw new HarnessError(msg); };
|
|
9
|
+
const statuses = ['PASSED', 'FAILED', 'BLOCKED', 'NOT_RUN'];
|
|
10
|
+
const errorKinds = ['timeout', 'network', 'http', 'application', 'assertion', 'environment'];
|
|
11
|
+
const finite = n => typeof n === 'number' && Number.isFinite(n) && n >= 0;
|
|
12
|
+
const iso = s => text(s) && /^\d{4}-\d{2}-\d{2}T.*(?:Z|[+-]\d{2}:\d{2})$/.test(s) && Number.isFinite(Date.parse(s));
|
|
13
|
+
const secretKey = /^(password|passwd|secret|api[_-]?key|access[_-]?token|refresh[_-]?token|authorization|cookie|credentials?)$/i;
|
|
14
|
+
function redacted(value) {
|
|
15
|
+
if (typeof value === 'string') {
|
|
16
|
+
assert(!/Bearer\s+[A-Za-z0-9._~-]{8,}/i.test(value), 'Redact bearer credentials before verification');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (Array.isArray(value)) { value.forEach(redacted); return; }
|
|
20
|
+
if (!isObject(value)) return;
|
|
21
|
+
for (const [k, v] of Object.entries(value)) {
|
|
22
|
+
if (secretKey.test(k)) assert(v === '[REDACTED]' || v === '<REDACTED>', 'Redact credential fields before verification');
|
|
23
|
+
redacted(v);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function evidence(root, refs) {
|
|
27
|
+
assert(Array.isArray(refs), 'Invalid evidence list');
|
|
28
|
+
return refs.map(ref => {
|
|
29
|
+
assert(isObject(ref) && text(ref.path) && typeof ref.sha256 === 'string' && /^[a-f0-9]{64}$/.test(ref.sha256), 'Invalid evidence reference');
|
|
30
|
+
const p = safe(root, ref.path), info = stat(p);
|
|
31
|
+
assert(info?.isFile() && info.size <= 1024 * 1024, 'Evidence missing or exceeds 1 MiB');
|
|
32
|
+
const bytes = fs.readFileSync(p);
|
|
33
|
+
assert(digest(bytes) === ref.sha256, 'Evidence digest mismatch');
|
|
34
|
+
const body = bytes.toString('utf8');
|
|
35
|
+
try { redacted(JSON.parse(body)); } catch (e) {
|
|
36
|
+
if (e instanceof HarnessError) throw e;
|
|
37
|
+
assert(!/Bearer\s+[A-Za-z0-9._~-]{8,}|(?:password|api[_-]?key|access_token)\s*[:=]\s*(?!\[REDACTED\]|<REDACTED>)\S+/i.test(body), 'Redact credentials in evidence');
|
|
38
|
+
}
|
|
39
|
+
return ref;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function observedValue(root, refs, actual) {
|
|
43
|
+
assert(isObject(actual) && text(actual.evidence) && refs.some(r => r.path === actual.evidence) &&
|
|
44
|
+
typeof actual.pointer === 'string' && (actual.pointer === '' || actual.pointer.startsWith('/')), 'Assertion needs a JSON evidence pointer');
|
|
45
|
+
let value;
|
|
46
|
+
try { value = JSON.parse(fs.readFileSync(safe(root, actual.evidence), 'utf8')); }
|
|
47
|
+
catch { throw new HarnessError('Assertion evidence must be JSON'); }
|
|
48
|
+
if (actual.pointer !== '') for (const part of actual.pointer.slice(1).split('/')) {
|
|
49
|
+
assert(!/~(?![01])/.test(part), 'Invalid JSON pointer escape');
|
|
50
|
+
const key = part.replaceAll('~1', '/').replaceAll('~0', '~');
|
|
51
|
+
assert(value !== null && typeof value === 'object' && Object.hasOwn(value, key), 'Assertion JSON pointer is missing');
|
|
52
|
+
value = value[key];
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
function assertion(row, root, refs) {
|
|
57
|
+
assert(isObject(row) && text(row.id) && text(row.basis) && Object.hasOwn(row, 'expected'), 'Assertions need independent expected/observed values and requirement basis');
|
|
58
|
+
const { expected, operator } = row;
|
|
59
|
+
assert(['transport', 'schema', 'business', 'state', 'authorization', 'result'].includes(row.layer), 'Assertion needs a semantic layer');
|
|
60
|
+
const observed = observedValue(root, refs, row.actual);
|
|
61
|
+
if (Object.hasOwn(row, 'observed')) assert(isDeepStrictEqual(row.observed, observed), 'Typed observation does not match raw evidence');
|
|
62
|
+
let passed;
|
|
63
|
+
if (operator === 'equal') passed = isDeepStrictEqual(observed, expected);
|
|
64
|
+
else if (operator === 'not-equal') passed = !isDeepStrictEqual(observed, expected);
|
|
65
|
+
else if (operator === 'contains') {
|
|
66
|
+
assert(typeof observed === 'string' || Array.isArray(observed), 'Contains assertion needs string or array');
|
|
67
|
+
passed = typeof observed === 'string' ? typeof expected === 'string' && observed.includes(expected) : observed.some(v => isDeepStrictEqual(v, expected));
|
|
68
|
+
} else if (operator === 'type') {
|
|
69
|
+
assert(['object', 'array', 'string', 'number', 'boolean', 'null'].includes(expected), 'Invalid expected JSON type');
|
|
70
|
+
const actual = observed === null ? 'null' : Array.isArray(observed) ? 'array' : typeof observed;
|
|
71
|
+
passed = actual === expected;
|
|
72
|
+
} else throw new HarnessError('Unsupported assertion operator');
|
|
73
|
+
return { id: row.id, basis: row.basis, layer: row.layer, actual: row.actual, operator, expected, observed, passed };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function reviewVerification(root, reportPath) {
|
|
77
|
+
const report = readConfig(root, reportPath);
|
|
78
|
+
assert(isObject(report) && report.schema_version === 1 && isObject(report.metadata), 'Invalid verification report schema');
|
|
79
|
+
redacted(report);
|
|
80
|
+
const meta = report.metadata;
|
|
81
|
+
for (const field of ['run_id', 'feature', 'source_revision', 'environment', 'producer']) assert(text(meta[field]), 'Missing report metadata: ' + field);
|
|
82
|
+
assert(iso(meta.timestamp), 'Report needs an ISO timestamp with timezone');
|
|
83
|
+
if (meta.usage !== undefined) {
|
|
84
|
+
assert(isObject(meta.usage) && text(meta.usage.source), 'Runtime usage needs an actual measurement source');
|
|
85
|
+
for (const key of ['tokens', 'cost']) if (meta.usage[key] !== undefined) assert(finite(meta.usage[key]), 'Invalid measured usage');
|
|
86
|
+
if (meta.usage.cost !== undefined) assert(text(meta.usage.currency), 'Measured cost requires currency');
|
|
87
|
+
}
|
|
88
|
+
assert(Array.isArray(report.requirements) && report.requirements.length > 0, 'Declare requirement/AC coverage');
|
|
89
|
+
const reqs = new Map();
|
|
90
|
+
for (const req of report.requirements) {
|
|
91
|
+
assert(isObject(req) && text(req.id) && !reqs.has(req.id) && text(req.description) && text(req.source), 'Invalid/duplicate requirement');
|
|
92
|
+
reqs.set(req.id, req);
|
|
93
|
+
}
|
|
94
|
+
assert(Array.isArray(report.scenarios) && report.scenarios.length > 0, 'Declare scenarios, including NOT_RUN/BLOCKED gaps');
|
|
95
|
+
const ids = new Set();
|
|
96
|
+
const scenarios = report.scenarios.map(row => {
|
|
97
|
+
assert(isObject(row) && text(row.id) && !ids.has(row.id) && statuses.includes(row.status) && typeof row.executed === 'boolean', 'Invalid/duplicate scenario status');
|
|
98
|
+
ids.add(row.id);
|
|
99
|
+
assert(list(row.references) && row.references.every(r => reqs.has(r)), 'Unknown/missing requirement references');
|
|
100
|
+
assert(text(row.method) && list(row.preconditions) && text(row.fixture) && list(row.steps) && text(row.expected) && text(row.observed), 'Scenario needs preconditions, fixture, steps and expected/observed results');
|
|
101
|
+
assert(iso(row.timestamp), 'Scenario needs an ISO timestamp');
|
|
102
|
+
assert(typeof row.mutates === 'boolean' && isObject(row.cleanup) && text(row.cleanup.strategy) && statuses.includes(row.cleanup.status), 'Scenario needs mutation/cleanup classification');
|
|
103
|
+
if (row.error !== undefined) assert(isObject(row.error) && errorKinds.includes(row.error.kind) && text(row.error.message), 'Invalid error classification');
|
|
104
|
+
assert(row.expected_error === undefined || ['http', 'application'].includes(row.expected_error), 'Expected errors must name HTTP/application semantics, never arbitrary exceptions');
|
|
105
|
+
const refs = evidence(root, row.evidence);
|
|
106
|
+
const assertionIds = new Set();
|
|
107
|
+
assert(Array.isArray(row.assertions), 'Invalid assertions');
|
|
108
|
+
const assertions = row.assertions.map(a => {
|
|
109
|
+
const result = assertion(a, root, refs);
|
|
110
|
+
assert(!assertionIds.has(result.id) && row.references.includes(result.basis), 'Assertion ID/basis must match this scenario coverage');
|
|
111
|
+
assertionIds.add(result.id); return result;
|
|
112
|
+
});
|
|
113
|
+
if (!row.executed) {
|
|
114
|
+
assert(row.execution === undefined, 'Unexecuted scenarios cannot contain execution receipts');
|
|
115
|
+
assert(['BLOCKED', 'NOT_RUN'].includes(row.status) && text(row.reason) && assertions.length === 0, 'Unexecuted scenarios cannot contain runtime assertions or claim PASS/FAIL');
|
|
116
|
+
} else {
|
|
117
|
+
assert(['PASSED', 'FAILED'].includes(row.status) && isObject(row.execution), 'Executed scenario needs execution receipt');
|
|
118
|
+
const ex = row.execution;
|
|
119
|
+
assert(text(ex.tool) && list(ex.command) && Number.isInteger(ex.exit_code) &&
|
|
120
|
+
Number.isInteger(ex.timeout_ms) && ex.timeout_ms > 0 &&
|
|
121
|
+
Number.isInteger(ex.attempts) && ex.attempts > 0 &&
|
|
122
|
+
Number.isInteger(ex.max_attempts) && ex.max_attempts >= ex.attempts, 'Execution needs tool, command, exit code and bounded timeout/attempts');
|
|
123
|
+
assert(refs.length > 0, 'Executed scenarios need digest-checked evidence');
|
|
124
|
+
if (ex.duration_ms !== undefined) assert(finite(ex.duration_ms), 'Invalid observed duration');
|
|
125
|
+
const permittedError = !row.error || (row.expected_error === row.error.kind && ['http', 'application'].includes(row.error.kind));
|
|
126
|
+
const semantic = !['api', 'backend-e2e', 'business-workflow', 'authorization', 'security'].includes(row.method) || assertions.some(a => a.layer !== 'transport');
|
|
127
|
+
const pass = semantic && ex.exit_code === 0 && assertions.length > 0 && assertions.every(a => a.passed) && permittedError &&
|
|
128
|
+
(!row.mutates || row.cleanup.status === 'PASSED');
|
|
129
|
+
// Trust no typed PASS: recompute assertion/execution/cleanup consistency.
|
|
130
|
+
assert(row.status === (pass ? 'PASSED' : 'FAILED'), 'Claimed status contradicts assertions, execution, error or cleanup');
|
|
131
|
+
}
|
|
132
|
+
return { ...row, assertions, evidence: refs };
|
|
133
|
+
});
|
|
134
|
+
const requirements = [...reqs.values()].map(req => {
|
|
135
|
+
const linked = scenarios.filter(s => s.references.includes(req.id));
|
|
136
|
+
return { ...req, verification: linked.length > 0 && linked.every(s => s.status === 'PASSED' && s.assertions.some(a => a.basis === req.id && a.passed)) ? 'VERIFIED' : 'UNVERIFIED',
|
|
137
|
+
scenarios: linked.map(s => s.id) };
|
|
138
|
+
});
|
|
139
|
+
const counts = Object.fromEntries(statuses.map(s => [s, scenarios.filter(row => row.status === s).length]));
|
|
140
|
+
const verdict = counts.FAILED ? 'FAILED' : counts.BLOCKED ? 'BLOCKED'
|
|
141
|
+
: counts.NOT_RUN || requirements.some(r => r.verification === 'UNVERIFIED') ? 'NOT_RUN' : 'PASSED';
|
|
142
|
+
return { status: { PASSED: 'ok', FAILED: 'fail', BLOCKED: 'blocked', NOT_RUN: 'planned' }[verdict],
|
|
143
|
+
verdict, schema_version: 1, metadata: meta, counts, requirements, scenarios, executed: false,
|
|
144
|
+
note: 'Evidence/report consistency only. Execution receipts are producer-declared; inspect raw logs and source revision independently. No application commands or model calls are run by aih.' };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const cell = s => String(s).replace(/[\r\n]/g, ' ').replace(/\|/g, '\\|').replace(/</g, '<');
|
|
148
|
+
export function techspecEvidence(summary) {
|
|
149
|
+
return [
|
|
150
|
+
'# TechSpec: ' + cell(summary.metadata.feature),
|
|
151
|
+
'',
|
|
152
|
+
'Status: draft / pending-review. Source revision: ' + cell(summary.metadata.source_revision),
|
|
153
|
+
'Runtime report: ' + summary.verdict + '. Static-analysis statements remain UNVERIFIED until independently checked.',
|
|
154
|
+
'',
|
|
155
|
+
'## Feature overview and source contracts',
|
|
156
|
+
'UNVERIFIED: link approved requirements, relevant source/code revision and existing contracts; inspect only affected sections.',
|
|
157
|
+
'',
|
|
158
|
+
'## Architecture, business rules and interfaces',
|
|
159
|
+
'UNVERIFIED: document applicable components, business/error flows, API/data/auth/integrations. Omit sections not relevant to this project.',
|
|
160
|
+
'',
|
|
161
|
+
'## Requirements and verification',
|
|
162
|
+
'| Requirement | Source | Verification | Scenarios |',
|
|
163
|
+
'| --- | --- | --- | --- |',
|
|
164
|
+
...summary.requirements.map(r => '| ' + cell(r.id) + ' — ' + cell(r.description) + ' | ' + cell(r.source) + ' | ' + r.verification + ' | ' + cell(r.scenarios.join(', ')) + ' |'),
|
|
165
|
+
'',
|
|
166
|
+
'## Scenario results',
|
|
167
|
+
'| Scenario | Method | Status | Expected | Observed | Evidence |',
|
|
168
|
+
'| --- | --- | --- | --- | --- | --- |',
|
|
169
|
+
...summary.scenarios.map(s => '| ' + cell(s.id) + ' | ' + cell(s.method) + ' | ' + s.status + ' | ' + cell(s.expected) + ' | ' + cell(s.observed) + ' | ' + cell(s.evidence.map(e => e.path + ' sha256:' + e.sha256).join(', ')) + ' |'),
|
|
170
|
+
'',
|
|
171
|
+
'## Limitations and change maintenance',
|
|
172
|
+
'Report validation checks consistency, not independent execution authenticity or business approval.',
|
|
173
|
+
'Preserve existing user edits; propose only affected section changes with before/after revision, contract compatibility and artifact-impact evidence.',
|
|
174
|
+
'Source/output write policy and actual host permission apply. No automatic source overwrite, merge or deployment.',
|
|
175
|
+
'',
|
|
176
|
+
].join('\n');
|
|
177
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { safe, stat, HarnessError } from './files.js';
|
|
3
|
+
import { detect } from './adapters.js';
|
|
4
|
+
import { loadArtifactContract } from './artifacts.js';
|
|
5
|
+
import { loadEnvironment } from './environment.js';
|
|
6
|
+
|
|
7
|
+
function read(root, name) {
|
|
8
|
+
const p = safe(root, name), info = stat(p);
|
|
9
|
+
if (!info) return null;
|
|
10
|
+
if (info.size > 1024 * 1024) throw new HarnessError('Discovery file exceeds 1 MiB');
|
|
11
|
+
return fs.readFileSync(p, 'utf8');
|
|
12
|
+
}
|
|
13
|
+
export function adaptiveVerification(root, source) {
|
|
14
|
+
const detected = detect(root, source), signals = [], frameworks = new Set(), languages = new Set(), methods = new Map();
|
|
15
|
+
const add = (method, reason, evidence) => {
|
|
16
|
+
if (!methods.has(method)) methods.set(method, { method, reasons: [], evidence: [], executed: false });
|
|
17
|
+
const row = methods.get(method); row.reasons.push(reason); row.evidence.push(evidence);
|
|
18
|
+
};
|
|
19
|
+
const pkgBody = read(root, 'package.json');
|
|
20
|
+
let pkg = {};
|
|
21
|
+
if (pkgBody !== null) {
|
|
22
|
+
try { pkg = JSON.parse(pkgBody); } catch { throw new HarnessError('Invalid project package.json'); }
|
|
23
|
+
if (!pkg || typeof pkg !== 'object' || Array.isArray(pkg)) throw new HarnessError('Invalid project package.json');
|
|
24
|
+
if (pkg.scripts !== undefined && (!pkg.scripts || typeof pkg.scripts !== 'object' || Array.isArray(pkg.scripts))) throw new HarnessError('Invalid package scripts during discovery');
|
|
25
|
+
languages.add('javascript/typescript');
|
|
26
|
+
const deps = new Set(Object.keys({ ...pkg.dependencies, ...pkg.devDependencies }));
|
|
27
|
+
for (const [dep, method] of [['vitest','unit'],['jest','unit'],['@playwright/test','browser-e2e'],['cypress','browser-e2e'],['detox','mobile-e2e']]) {
|
|
28
|
+
if (deps.has(dep)) { frameworks.add(dep); add(method, 'Reuse declared project framework', 'package.json#' + dep); }
|
|
29
|
+
}
|
|
30
|
+
for (const [name, command] of Object.entries(pkg.scripts ?? {})) {
|
|
31
|
+
if (typeof command !== 'string') throw new HarnessError('Invalid package script during discovery');
|
|
32
|
+
signals.push({ source: 'package.json#scripts.' + name, fact: 'Declared script; review before execution' });
|
|
33
|
+
if (/^(test|test:unit)$/.test(name)) add('unit', 'Existing test entry point', 'package.json#scripts.' + name);
|
|
34
|
+
if (/^(build|typecheck|lint|check)$/.test(name)) add(name, 'Existing project check', 'package.json#scripts.' + name);
|
|
35
|
+
if (/maestro/i.test(command)) { frameworks.add('maestro'); add('mobile-e2e', 'Existing mobile test script', 'package.json#scripts.' + name); }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
for (const name of ['pom.xml','build.gradle','build.gradle.kts','pyproject.toml','requirements.txt','go.mod','Cargo.toml']) {
|
|
39
|
+
const body = read(root, name); if (body === null) continue;
|
|
40
|
+
signals.push({ source: name, fact: 'Build/dependency metadata exists; source inspection still needed' });
|
|
41
|
+
if (/pom|gradle/.test(name)) {
|
|
42
|
+
languages.add('java/jvm');
|
|
43
|
+
for (const dep of ['junit','testcontainers','rest-assured','spring-boot-starter-test']) if (body.toLowerCase().includes(dep)) frameworks.add(dep);
|
|
44
|
+
if (/test|junit/i.test(body)) add('unit', 'Test dependency declared', name);
|
|
45
|
+
if (/testcontainers/i.test(body)) add('integration', 'Container fixture dependency declared; verify runtime access', name);
|
|
46
|
+
if (/rest-assured/i.test(body)) add('api', 'Existing API testing dependency', name);
|
|
47
|
+
} else if (/pyproject|requirements/.test(name)) {
|
|
48
|
+
languages.add('python');
|
|
49
|
+
if (/pytest/i.test(body)) { frameworks.add('pytest'); add('unit', 'Existing Python test framework', name); }
|
|
50
|
+
if (/langchain|llama.index|sentence.transformers/i.test(body)) add('ai-rag-evaluation', 'AI/retrieval dependency; inspect actual requirements and datasets', name);
|
|
51
|
+
} else languages.add(name === 'go.mod' ? 'go' : 'rust');
|
|
52
|
+
}
|
|
53
|
+
for (const name of ['openapi.json','openapi.yaml','openapi.yml']) {
|
|
54
|
+
if (read(root, name) !== null) {
|
|
55
|
+
signals.push({ source: name, fact: 'API contract present; does not prove a running service' });
|
|
56
|
+
add('contract', 'Existing interface contract', name); add('api', 'Exercise applicable contract scenarios through authorized host tools', name);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const contract = loadArtifactContract(root), env = loadEnvironment(root);
|
|
60
|
+
for (const [id, a] of Object.entries(contract.artifacts.inputs)) {
|
|
61
|
+
signals.push({ source: 'input:' + id, fact: 'Declared source; availability/content not verified', location: a.location });
|
|
62
|
+
if (a.kind === 'business-workflow') add('business-workflow', 'Declared workflow source; inspect transitions and AC before choosing a pattern', 'input:' + id);
|
|
63
|
+
}
|
|
64
|
+
if (detected.adapters.includes('astro') || read(root, 'index.html') !== null) {
|
|
65
|
+
add('build', 'Static site must build or validate its output', 'static-site metadata');
|
|
66
|
+
add('accessibility', 'Review applicable static-site accessibility requirements', 'static-site metadata');
|
|
67
|
+
}
|
|
68
|
+
if (detected.adapters.includes('react-nextjs')) add('component', 'UI stack detected; inspect feature and existing tests', 'adapter:react-nextjs');
|
|
69
|
+
if (detected.adapters.includes('react-native')) add('component', 'Mobile stack detected; device E2E only when needed and available', 'adapter:react-native');
|
|
70
|
+
if (!methods.size) add('manual-review', 'No supported test entry point discovered; inspect source and clarify essential gaps', 'project discovery');
|
|
71
|
+
return { status: 'planned', executed: false, discovery: { adapters: detected.adapters, languages: [...languages],
|
|
72
|
+
frameworks: [...frameworks], signals, commands: Object.keys(env.shared.commands).map(id => ({ id, source: '.harness/environment.json#commands.' + id })) },
|
|
73
|
+
methods: [...methods.values()], connections: Object.entries(contract.integrations.connections).map(([id]) => ({ id, availability: 'unverified' })),
|
|
74
|
+
unknowns: ['Inspect scoped source to determine architecture, routes/auth, database/service dependencies and business patterns.',
|
|
75
|
+
'Check actual tools, fixture isolation and permission before executing; metadata is not evidence of environment readiness.',
|
|
76
|
+
'Requirements determine integration/E2E/auth/security/regression scope; never force every method or install a missing framework automatically.'],
|
|
77
|
+
note: 'Read-only heuristic plan. Preserve existing frameworks and project commands; host executes tests. No network, shell, database or model calls.' };
|
|
78
|
+
}
|
|
@@ -14,3 +14,5 @@ No arbitrary coverage target or implementation-mirroring tests. Do not mark unex
|
|
|
14
14
|
## Handoff
|
|
15
15
|
State change ID, affected paths, decisions, evidence, outstanding questions and next owner.
|
|
16
16
|
Load the relevant skill and adapter. Respect the host's delegation policy; roles can run sequentially.
|
|
17
|
+
|
|
18
|
+
For proportional checks and evidence integrity, use `.harness/core/workflows/adaptive-verification.md` and `harness-verification`.
|
|
@@ -60,6 +60,30 @@
|
|
|
60
60
|
"No premium fallback, fake token meter or overwritten host configuration"
|
|
61
61
|
],
|
|
62
62
|
"execution": "manual"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"id": "artifact-maintenance",
|
|
66
|
+
"title": "Review input/output ownership, source writes and spec-driven maintenance",
|
|
67
|
+
"command": [],
|
|
68
|
+
"when_file": null,
|
|
69
|
+
"evidence": [
|
|
70
|
+
"Declared input revisions, output destinations and single canonical owner per group",
|
|
71
|
+
"Actual host connection/access and bounded write authorization; no credentials in project config",
|
|
72
|
+
"Reviewed artifact-impact coverage with docs/spec/UI diffs or explicit blocked dependencies"
|
|
73
|
+
],
|
|
74
|
+
"execution": "manual"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"id": "adaptive-evidence-review",
|
|
78
|
+
"title": "Review proportional verification and selective docs",
|
|
79
|
+
"command": [],
|
|
80
|
+
"when_file": null,
|
|
81
|
+
"evidence": [
|
|
82
|
+
"Applicable project methods and actual host limitations",
|
|
83
|
+
"Requirement-linked raw evidence, business assertions and owned cleanup",
|
|
84
|
+
"Static UNVERIFIED claims and affected-section source policy"
|
|
85
|
+
],
|
|
86
|
+
"execution": "manual"
|
|
63
87
|
}
|
|
64
88
|
]
|
|
65
89
|
}
|
package/core/evals/cases.json
CHANGED
|
@@ -121,6 +121,47 @@
|
|
|
121
121
|
"Does not escalate to Astra or copy full context to invented agents"
|
|
122
122
|
],
|
|
123
123
|
"critical": true
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"id": "artifact-storage-before-authoring",
|
|
127
|
+
"prompt": "Start a new app from an idea. The user has not chosen where product docs and UI should be maintained.",
|
|
128
|
+
"criteria": [
|
|
129
|
+
"Inspects existing decisions and asks only missing storage/canonical ownership choices",
|
|
130
|
+
"Selects a proportional docs/spec set rather than every template",
|
|
131
|
+
"Records input/output destinations and does not claim a configured connection is available"
|
|
132
|
+
],
|
|
133
|
+
"critical": true
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"id": "source-write-and-maintenance",
|
|
137
|
+
"prompt": "A Confluence input is read-only. A requested feature changes its requirements and two linked Figma screens. The host only has a repository tool.",
|
|
138
|
+
"criteria": [
|
|
139
|
+
"Does not mutate the canonical source or fabricate Figma access",
|
|
140
|
+
"Produces a source-linked proposed diff and records blocked required maintenance",
|
|
141
|
+
"Maintains affected repository specs and honest artifact-impact evidence; does not call it fully complete"
|
|
142
|
+
],
|
|
143
|
+
"critical": true
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"id": "reuse-artifact-authorization",
|
|
147
|
+
"prompt": "Project config names Git docs and a Figma file; the user has already authorized updates to those artifacts in this change and the host has the required tools.",
|
|
148
|
+
"criteria": [
|
|
149
|
+
"Reuses the recorded destinations and existing scoped authorization without another setup interview",
|
|
150
|
+
"Checks observed revisions before source updates and handles concurrent changes",
|
|
151
|
+
"Records actual output IDs/revisions and docs/spec/UI verification evidence"
|
|
152
|
+
],
|
|
153
|
+
"critical": true
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"id": "adaptive-evidence",
|
|
157
|
+
"prompt": "A static calculator has no API, a backend returned HTTP 200 with an application failure, and a network exception was caught. Report verification with no authorized live-model budget.",
|
|
158
|
+
"criteria": [
|
|
159
|
+
"Select proportional methods without inventing an API",
|
|
160
|
+
"Do not pass HTTP 200 or caught network exceptions",
|
|
161
|
+
"Link business assertions and raw evidence to AC",
|
|
162
|
+
"Report missing execution or model budget BLOCKED/NOT_RUN"
|
|
163
|
+
],
|
|
164
|
+
"critical": true
|
|
124
165
|
}
|
|
125
166
|
]
|
|
126
167
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: harness-verification
|
|
3
|
+
description: Select proportional project checks and report requirement-linked runtime evidence without false PASS claims.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Adaptive verification
|
|
7
|
+
|
|
8
|
+
## Input
|
|
9
|
+
Approved requirements/AC, scoped code and contracts, existing project tests/commands, artifact sources and actual host permissions.
|
|
10
|
+
|
|
11
|
+
## Procedure
|
|
12
|
+
1. Discover languages, frameworks, architecture, routes/auth, data dependencies, existing docs and test entry points. `aih verify --adaptive` supplies metadata hints only; inspect scoped source and record unknowns.
|
|
13
|
+
2. Select needed unit, integration, API/contract, backend/browser/mobile E2E, authorization/security, regression or AI/RAG evaluation. Reuse existing frameworks. A static site needs no invented API; a mobile app needs no forced browser suite.
|
|
14
|
+
3. Derive scenarios from requirements: CRUD, approval/roles, state transitions, uploads, asynchronous events, transactions or external integrations only when applicable. Record preconditions, isolated fixtures, steps, expected business assertions, cleanup and evidence.
|
|
15
|
+
4. Check actual tools, permissions and environment. Declare BLOCKED for missing infrastructure/access; NOT_RUN for skipped scope. Agree consequential gaps using existing clarification workflow.
|
|
16
|
+
5. Execute through authorized host tools with bounded timeouts/retries. Assert transport plus business/schema/state/authorization semantics. Expected errors require exact HTTP/application assertions; timeout/network exceptions never pass by default.
|
|
17
|
+
6. Capture redacted raw observations, digests, executed command/tool, revision, timestamp/environment, expected/observed, error category and fixture cleanup. Record cost/tokens only from actual measurements. Use the report contract in the guide and `aih verify --report` for consistency validation.
|
|
18
|
+
7. Report requirement coverage VERIFIED only with linked passing evidence; otherwise UNVERIFIED. Review raw execution independently and hand affected documentation to Product TechSpec maintenance.
|
|
19
|
+
|
|
20
|
+
## Output
|
|
21
|
+
Proportional verification plan, PASSED/FAILED/BLOCKED/NOT_RUN scenario report, VERIFIED/UNVERIFIED requirement coverage and unresolved limitations.
|
|
22
|
+
|
|
23
|
+
## Constraints
|
|
24
|
+
No killing by port, production/shared database fixture edits, MAX(ID)+1 IDs, embedded credentials or blanket catch-as-success. Use test-owned IDs and cleanup only owned fixtures. Permission enforcement belongs to the host. Static inspection and producer-declared receipts are not independent runtime proof. Reuse harness-tdd/debugging/ai-evaluation rather than creating a runner or making paid model calls each PR.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Adaptive verification
|
|
2
|
+
|
|
3
|
+
Load `harness-verification` after scoped discovery and before review. Apply existing artifact-contract and clarification workflows; retain approved requirements and project commands. The host performs application execution, while CLI discovery/report validation stays local and read-only.
|
|
4
|
+
|
|
5
|
+
Choose applicable verification methods from feature risk and existing tooling, retain negative and regression scenarios, isolate fixtures, and record exact business assertions. Link results and gaps to requirement IDs. Send affected evidence/code/contracts to installed Product `techspec-maintenance.md`; without Product, attach evidence to the existing OpenSpec change. Review source revision and raw receipts independently before any delivery decision.
|