@manhd_d/ai-harness 0.14.0 → 0.15.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 +43 -0
- package/README.md +21 -9
- package/adapters/react-native/adapter.yaml +1 -1
- package/adapters/react-native/skills/harness-react-native-behavior-testing/SKILL.md +9 -0
- package/adapters/react-nextjs/adapter.yaml +1 -1
- package/adapters/react-nextjs/skills/harness-react-nextjs-behavior-testing/SKILL.md +9 -0
- package/capabilities/product/capability.json +1 -1
- package/capabilities/product/workflows/idea-to-prd.md +15 -0
- package/capabilities/product/workflows/idea-to-prd.workflow.json +82 -0
- package/cli/adapters.js +1 -1
- package/cli/agents.js +14 -5
- package/cli/capabilities.js +2 -1
- package/cli/end-user.js +30 -0
- package/cli/environment.js +2 -0
- package/cli/evaluation-contract.js +11 -1
- package/cli/files.js +7 -2
- package/cli/harness.js +53 -11
- package/cli/journey-evidence.js +34 -0
- package/cli/kiro.js +15 -0
- package/cli/knowledge.js +104 -0
- package/cli/main.js +60 -22
- package/cli/portable-config.js +6 -3
- package/cli/project-decisions.js +47 -0
- package/cli/readiness.js +82 -41
- package/cli/setup.js +66 -0
- package/cli/skill-requirements.js +47 -0
- package/cli/start.js +15 -2
- package/cli/structured.js +65 -0
- package/cli/sync-integrations.js +49 -0
- package/cli/verification-report.js +9 -5
- package/cli/verification.js +7 -2
- package/cli/workflow.js +76 -0
- package/core/agents/orchestrator.md +1 -1
- package/core/agents/test-engineer.md +4 -2
- package/core/checks/verification.json +12 -0
- package/core/evals/cases.json +75 -1
- package/core/rules/context.md +9 -0
- package/core/skills/harness-end-user-testing/SKILL.md +55 -0
- package/core/skills/harness-review/SKILL.md +4 -0
- package/core/skills/harness-spec/SKILL.md +3 -1
- package/core/skills/harness-start/SKILL.md +66 -0
- package/core/skills/harness-verification/SKILL.md +2 -0
- package/core/workflows/adaptive-verification.md +5 -0
- package/core/workflows/agent-routing.md +6 -0
- package/core/workflows/approvals.schema.json +62 -0
- package/core/workflows/clarification.md +6 -1
- package/core/workflows/continuous-development.md +5 -0
- package/core/workflows/end-user-acceptance.md +36 -0
- package/core/workflows/greenfield-setup.md +27 -0
- package/core/workflows/project-setup.md +13 -2
- package/core/workflows/seed.md +25 -0
- package/core/workflows/spec-to-code.md +19 -8
- package/core/workflows/spec-to-code.workflow.json +99 -0
- package/core/workflows/workflow.schema.json +113 -0
- package/docs/README.md +8 -0
- package/docs/adaptive-verification.md +10 -1
- package/docs/agent-configuration.md +7 -0
- package/docs/agent-handoffs.md +6 -1
- package/docs/architecture.md +43 -3
- package/docs/cli.md +39 -4
- package/docs/configuration-backup.md +10 -2
- package/docs/conformance-runner.md +64 -0
- package/docs/end-to-end-workflow.md +4 -0
- package/docs/end-user-testing.md +128 -0
- package/docs/frontend-mobile.md +7 -0
- package/docs/getting-started.md +17 -5
- package/docs/harness-maintenance.md +1 -0
- package/docs/i18n/README.es.md +17 -7
- package/docs/i18n/README.vi.md +18 -8
- package/docs/i18n/README.zh-CN.md +17 -7
- package/docs/project-configuration.md +6 -0
- package/docs/python-installation.md +85 -0
- package/docs/session-onboarding.md +215 -0
- package/docs/skill-matrix.md +5 -3
- package/docs/start.md +7 -2
- package/docs/tool-setup/README.md +13 -14
- package/docs/tool-setup/claude-code.md +10 -0
- package/docs/tool-setup/codex.md +10 -0
- package/docs/tool-setup/copilot.md +10 -0
- package/docs/tool-setup/cursor.md +10 -0
- package/docs/tool-setup/gemini-cli.md +10 -0
- package/docs/tool-setup/kiro.md +112 -38
- package/docs/versioning.md +56 -0
- package/docs/workflow-contract.md +66 -0
- package/evals/README.md +7 -0
- package/harness.yaml +13 -1
- package/package.json +7 -3
- package/scripts/conformance-runner.js +119 -0
- package/scripts/score-eval.js +3 -1
- package/templates/AGENTS.md +6 -1
- package/templates/decisions.json +4 -0
- package/templates/local.md +21 -0
- package/templates/openspec/approvals.json +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased — Workflow contracts and npm-independent pip distribution
|
|
4
|
+
|
|
5
|
+
- Add schema-1 spec-to-code/Product idea-to-prd JSON contracts, offline graph/role/gate validation and Markdown drift checks; clarify contract/driver/evidence ownership.
|
|
6
|
+
- Add workflow validate, named readiness stages and opt-in recorded approvals; preserve legacy implement/review behavior and describe identity/semantic limitations.
|
|
7
|
+
- Add a separate Claude headless conformance driver with per-case progress, optional deterministic assertions, independent judge and 3–5 trial pass rates; live hosts/human audit remain NOT_RUN until actually recorded.
|
|
8
|
+
- Add zero-dependency Python launcher and PEP 517 wheel/sdist packaging from the npm shared allowlist; pip/offline install needs Node 22+ but no npm/npx.
|
|
9
|
+
- Add offline wheel parity/install/update smoke, dynamic shared version checks and dual-channel release from the same reviewed tag; PyPI Trusted Publisher setup required before publication.
|
|
10
|
+
- Unify six-host harness-start guidance and make README host-neutral.
|
|
11
|
+
|
|
12
|
+
## Unreleased — Missing-only setup and scoped project decisions
|
|
13
|
+
|
|
14
|
+
- Add setup --apply/--dry-run using the installed snapshot; preserve user answers, managed payload/lock and machine data, with validation before writes.
|
|
15
|
+
- Add optional schema-1 project/module decisions, confirmed/proposed/superseded states and validated replacement history; expose decisions and reuse guidance without permanent approval claims.
|
|
16
|
+
- Include shared decisions in same-project backup/freshness, omit them from transfer, and reject malformed environment config before installation writes.
|
|
17
|
+
- Add compact start orientation, existing/new-project workflows, interruption/source-change scenarios and packaged clone checks. Keep one-command install → /harness-start; no new runtime dependency or distribution bump.
|
|
18
|
+
- Publish a live-host validation protocol; actual Kiro activation/model behavior and token savings remain NOT_RUN until measured.
|
|
19
|
+
|
|
20
|
+
## Unreleased — Session onboarding and knowledge freshness
|
|
21
|
+
|
|
22
|
+
- Add Kiro init/update --host kiro, canonical native wrappers and locked SessionStart v1; first chat: /harness-start.
|
|
23
|
+
- Keep Kiro questions in the current session, reuse saved facts/tasks and preserve partial setup.
|
|
24
|
+
- Add structured local.md with legacy preservation, seed plan/record/check and separate source/context hashes.
|
|
25
|
+
- Add skill requires diagnostics, role extensions/load order, missing-only external propose sidecars and MCP alias-only discovery.
|
|
26
|
+
- Document first/later sessions, offline/upgrade and backup/transfer boundaries; live loading/token savings remain unmeasured. No distribution bump or provider calls.
|
|
27
|
+
|
|
28
|
+
## Unreleased — End-user acceptance
|
|
29
|
+
|
|
30
|
+
- Add Core end-user-testing skill/workflow and test-engineer/review routing to exercise real browser/device/CLI/API journeys.
|
|
31
|
+
- Adaptive plans disclose surface/tool hints and missing context; preserve existing runners and actual host permissions.
|
|
32
|
+
- Add optional schema-1 end-user targets/surface/journey and bounded binary attachments; mocks, unknown dependencies and wrong-interface evidence cannot verify full end-user AC.
|
|
33
|
+
- Reject browser/mobile/CLI transport-only PASS; connect React/Next.js and React Native 1.4.0, evidence docs and unscored behavior cases.
|
|
34
|
+
- No browser/LLM runner, installed dependency, model calls or distribution bump; actual execution remains in the authorized host.
|
|
35
|
+
|
|
3
36
|
## Unreleased — Guided start and portable configuration
|
|
4
37
|
|
|
5
38
|
- Add start: short terminal intake, saved task/source/output choices, read-only non-TTY plan and host handoff.
|
|
@@ -146,6 +179,16 @@
|
|
|
146
179
|
- Check release metadata consistency and establish single-release-PR version discipline; retain v0.6.0 until a dedicated release PR.
|
|
147
180
|
|
|
148
181
|
|
|
182
|
+
## [0.15.0](https://github.com/manh0303/ai-harness/compare/v0.14.0...v0.15.0) (2026-10-14)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
### Features
|
|
186
|
+
|
|
187
|
+
* add actual end-user acceptance and journey evidence ([ee99fd6](https://github.com/manh0303/ai-harness/commit/ee99fd6825d46efb20e80ccb36cdec9e7b214b05))
|
|
188
|
+
* add in-session Kiro onboarding and knowledge freshness ([8f2e68b](https://github.com/manh0303/ai-harness/commit/8f2e68b0b2200437096cc69ce5e8acae5b85002b))
|
|
189
|
+
* add workflow contracts and offline pip distribution ([6cde81a](https://github.com/manh0303/ai-harness/commit/6cde81a0cc1c41922cce82458fbe88cca7913edd))
|
|
190
|
+
* scaffold missing setup and persist scoped project decisions ([38877c8](https://github.com/manh0303/ai-harness/commit/38877c896e94a35a6d5407d2ab662538088cfd7b))
|
|
191
|
+
|
|
149
192
|
## [0.14.0](https://github.com/manh0303/ai-harness/compare/v0.13.0...v0.14.0) (2026-10-14)
|
|
150
193
|
|
|
151
194
|
|
package/README.md
CHANGED
|
@@ -10,23 +10,35 @@ Works with any project through Core workflows. Optional stack support: Spring Bo
|
|
|
10
10
|
|
|
11
11
|
## Quick start
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Requires **Node.js 22+** and your AI coding tool. Run in your **project root**:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
npx --yes @manhd_d/ai-harness init
|
|
17
|
-
npx --yes @manhd_d/ai-harness start
|
|
16
|
+
npx --yes @manhd_d/ai-harness init --host codex
|
|
18
17
|
```
|
|
19
18
|
|
|
20
|
-
|
|
19
|
+
Set `--host` to `codex`, `claude-code`, `cursor`, `copilot`, `gemini-cli` or `kiro`.
|
|
20
|
+
In your first AI session, invoke **harness-start** if the host exposes it, or send:
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
`npx --yes @manhd_d/ai-harness check`; then open the same project in your AI tool.
|
|
22
|
+
> Read .agents/skills/harness-start/SKILL.md and follow it for this project. My task: [task].
|
|
24
23
|
|
|
25
|
-
|
|
24
|
+
The agent inspects the project, asks only missing questions and saves context. Kiro exposes
|
|
25
|
+
`/harness-start`; native discovery varies by client. [Host guides](docs/tool-setup/README.md).
|
|
26
|
+
`npx` does not create a global command; use `npx --yes @manhd_d/ai-harness check` to check integrity.
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
**pip / restricted networks** (included in the next reviewed release; PyPI publication requires maintainer setup):
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
```bash
|
|
31
|
+
python -m pip install manhd-ai-harness
|
|
32
|
+
aih init --host codex
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Requires Python 3.9+ **and Node.js 22+**. The wheel bundles the same JavaScript CLI and assets;
|
|
36
|
+
it never calls npm or `npx`. Offline: `python -m pip install --no-index --no-deps /path/to/package.whl`.
|
|
37
|
+
[Offline install & update](docs/python-installation.md) · [Settings & backup](docs/configuration-backup.md).
|
|
38
|
+
|
|
39
|
+
Use `init --detect` for stack detection. Your host runs tests and controls tools, delegation and permissions.
|
|
40
|
+
Harness owns workflow contracts and evidence formats; hosts or separate drivers execute them.
|
|
41
|
+
[Workflow contracts](docs/workflow-contract.md) · [Conformance runner](docs/conformance-runner.md).
|
|
30
42
|
|
|
31
43
|
## Where it runs
|
|
32
44
|
|
|
@@ -47,3 +47,12 @@ from Node.js component tests. Keep existing tooling and Core permission/review g
|
|
|
47
47
|
## Handoff
|
|
48
48
|
Pass AC/test/source paths, actual failures/results, device/toolchain needs and affected platforms to
|
|
49
49
|
Implementer/Reviewer. The Harness emits instructions/checklists; the authorized host runs actual tests.
|
|
50
|
+
|
|
51
|
+
## End-user acceptance
|
|
52
|
+
|
|
53
|
+
When affected AC requires the public UI/device journey, load Core harness-end-user-testing and
|
|
54
|
+
`.harness/core/workflows/end-user-acceptance.md`. Run actual controls in the existing browser/device
|
|
55
|
+
runner, assert visible and persisted outcomes, and capture redacted evidence. Use schema-1 reports
|
|
56
|
+
with verification_target=end-user and the expected surface; mocks/unknown dependencies do not verify
|
|
57
|
+
full acceptance. Missing browser/device/account/environment is BLOCKED. Reuse the existing framework;
|
|
58
|
+
Playwright is a web option, not a forced dependency or native-device proof.
|
|
@@ -47,3 +47,12 @@ call production services or claim browser/SSR/security verification from unit te
|
|
|
47
47
|
Pass observed failures/results, AC IDs, relevant test/source paths and blocked environment needs to
|
|
48
48
|
Implementer/Reviewer. Use `.agents/skills/harness-react-nextjs-server-boundaries/SKILL.md` for direct
|
|
49
49
|
authorization/cache verification when relevant; the CLI does not run these tests.
|
|
50
|
+
|
|
51
|
+
## End-user acceptance
|
|
52
|
+
|
|
53
|
+
When affected AC requires the public UI/device journey, load Core harness-end-user-testing and
|
|
54
|
+
`.harness/core/workflows/end-user-acceptance.md`. Run actual controls in the existing browser/device
|
|
55
|
+
runner, assert visible and persisted outcomes, and capture redacted evidence. Use schema-1 reports
|
|
56
|
+
with verification_target=end-user and the expected surface; mocks/unknown dependencies do not verify
|
|
57
|
+
full acceptance. Missing browser/device/account/environment is BLOCKED. Reuse the existing framework;
|
|
58
|
+
Playwright is a web option, not a forced dependency or native-device proof.
|
|
@@ -18,3 +18,18 @@ Store project-owned generated drafts under `docs/product/`: product-brief, sourc
|
|
|
18
18
|
|
|
19
19
|
## Integration boundary
|
|
20
20
|
After approval, UI/UX consumes PRD + requirement IDs + stories/AC; OpenSpec consumes approved stories and rules. This workflow does not invoke LLM, Jira, Confluence, Figma or git actions by itself.
|
|
21
|
+
|
|
22
|
+
## Contract stages
|
|
23
|
+
|
|
24
|
+
| Stage ID | Owner |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| `intake` | product:business-analyst |
|
|
27
|
+
| `business-discovery` | product:business-analyst |
|
|
28
|
+
| `brd-gate` | product:business-analyst |
|
|
29
|
+
| `prd` | product:product-manager |
|
|
30
|
+
| `backlog` | product:product-owner |
|
|
31
|
+
| `traceability` | product:business-analyst |
|
|
32
|
+
| `independent-qa` | product:requirements-reviewer |
|
|
33
|
+
| `human-approval` | human:product-owner |
|
|
34
|
+
|
|
35
|
+
Machine contract: idea-to-prd.workflow.json. `ready --workflow idea-to-prd` uses the installed Product contract and per-change approvals; output checks require the declared docs/product files. Configure a different driver contract for externally stored artifacts. Sign-off remains external evidence, not identity verification.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"id": "idea-to-prd",
|
|
4
|
+
"stages": [
|
|
5
|
+
{
|
|
6
|
+
"id": "intake",
|
|
7
|
+
"owner": "product:business-analyst",
|
|
8
|
+
"next": [
|
|
9
|
+
"business-discovery"
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"id": "business-discovery",
|
|
14
|
+
"owner": "product:business-analyst",
|
|
15
|
+
"next": [
|
|
16
|
+
"brd-gate"
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"id": "brd-gate",
|
|
21
|
+
"owner": "product:business-analyst",
|
|
22
|
+
"next": [
|
|
23
|
+
"prd"
|
|
24
|
+
],
|
|
25
|
+
"approval": {
|
|
26
|
+
"role": "product-owner"
|
|
27
|
+
},
|
|
28
|
+
"on_reject": "business-discovery"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "prd",
|
|
32
|
+
"owner": "product:product-manager",
|
|
33
|
+
"next": [
|
|
34
|
+
"backlog"
|
|
35
|
+
],
|
|
36
|
+
"entry_gate": {
|
|
37
|
+
"approval_from": "brd-gate"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "backlog",
|
|
42
|
+
"owner": "product:product-owner",
|
|
43
|
+
"next": [
|
|
44
|
+
"traceability"
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"id": "traceability",
|
|
49
|
+
"owner": "product:business-analyst",
|
|
50
|
+
"next": [
|
|
51
|
+
"independent-qa"
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"id": "independent-qa",
|
|
56
|
+
"owner": "product:requirements-reviewer",
|
|
57
|
+
"next": [
|
|
58
|
+
"human-approval"
|
|
59
|
+
],
|
|
60
|
+
"outputs": [
|
|
61
|
+
"docs/product/brd.md",
|
|
62
|
+
"docs/product/prd.md",
|
|
63
|
+
"docs/product/acceptance-criteria.md",
|
|
64
|
+
"docs/product/traceability.md"
|
|
65
|
+
]
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"id": "human-approval",
|
|
69
|
+
"owner": "human:product-owner",
|
|
70
|
+
"next": [],
|
|
71
|
+
"entry_gate": {
|
|
72
|
+
"deterministic": [
|
|
73
|
+
"outputs-present"
|
|
74
|
+
]
|
|
75
|
+
},
|
|
76
|
+
"approval": {
|
|
77
|
+
"role": "product-owner"
|
|
78
|
+
},
|
|
79
|
+
"on_reject": "prd"
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
}
|
package/cli/adapters.js
CHANGED
|
@@ -145,7 +145,7 @@ export function evaluate(root, reportPath, scope = 'adapters') {
|
|
|
145
145
|
const bytes = encode(dataset), hash = digest(bytes);
|
|
146
146
|
if (!reportPath) return { status: 'planned', scope, dataset, report: { schema_version: 1,
|
|
147
147
|
metadata: { model: '', harness_commit: '', dataset_sha256: hash, bundle_sha256: lock.bundle_sha256, reviewer: '', run_id: '', parameters: '' },
|
|
148
|
-
results: dataset.cases.map(c => ({ id: c.id, scores: c.criteria.map(() => null), evidence: '' })) },
|
|
148
|
+
results: dataset.cases.map(c => ({ id: c.id, scores: c.criteria.map(() => null), ...(c.assertions ? { assertions: c.assertions.map(() => null) } : {}), evidence: '' })) },
|
|
149
149
|
note: 'Unscored template; run a real model separately and record reviewer evidence. No model is invoked.' };
|
|
150
150
|
const report = readJson(path.resolve(reportPath));
|
|
151
151
|
requireThat(report?.metadata?.bundle_sha256 === lock.bundle_sha256, 'Evaluation report belongs to a different installed bundle');
|
package/cli/agents.js
CHANGED
|
@@ -54,7 +54,11 @@ export function validateAgentConfig(config) {
|
|
|
54
54
|
requireThat(isObject(config.agents) && Object.keys(config.agents).length <= 128, 'agents map (max 128 roles)');
|
|
55
55
|
for (const [id, agent] of Object.entries(config.agents)) {
|
|
56
56
|
requireThat(ROLE.test(id), 'role ID must use namespace:role');
|
|
57
|
-
fields(agent, ['model', 'provider', 'reasoning', 'enabled'], [], id);
|
|
57
|
+
fields(agent, ['model', 'provider', 'reasoning', 'enabled', 'extensions'], [], id);
|
|
58
|
+
if (agent.extensions !== undefined) {
|
|
59
|
+
requireThat(Array.isArray(agent.extensions) && agent.extensions.length <= 16 && new Set(agent.extensions).size === agent.extensions.length, id + ' extensions');
|
|
60
|
+
for (const file of agent.extensions) requireThat(typeof file === 'string' && /^[A-Za-z0-9._/-]+[.]md$/.test(file) && !file.startsWith('/') && !file.split('/').some(p => !p || p === '.' || p === '..') && !/^\.harness\/(?:core|adapters|capabilities|templates)\//.test(file), id + ' extension path must be project-owned Markdown');
|
|
61
|
+
}
|
|
58
62
|
if (agent.model !== undefined) { reference(agent.model, id); resolveModel(config, agent.model); }
|
|
59
63
|
if (agent.provider !== undefined) provider(agent.provider, id);
|
|
60
64
|
if (agent.reasoning !== undefined) reasoning(agent.reasoning, id);
|
|
@@ -110,7 +114,11 @@ function readConfiguration(root, relative) {
|
|
|
110
114
|
catch { throw new HarnessError('Invalid JSON in ' + relative); }
|
|
111
115
|
return validateAgentConfig(config);
|
|
112
116
|
}
|
|
113
|
-
export function loadAgentConfig(root) {
|
|
117
|
+
export function loadAgentConfig(root) {
|
|
118
|
+
const config = readConfiguration(root, AGENT_CONFIG);
|
|
119
|
+
for (const agent of Object.values(config?.agents ?? {})) for (const file of agent.extensions ?? []) safe(root, file);
|
|
120
|
+
return config;
|
|
121
|
+
}
|
|
114
122
|
|
|
115
123
|
export function agentDefaults(source = SOURCE, preset = 'recommended', host) {
|
|
116
124
|
requireThat(PRESETS.includes(preset), 'unknown preset; use recommended, economy or custom');
|
|
@@ -146,7 +154,7 @@ export function resolveAgent(config, id) {
|
|
|
146
154
|
const agent = Object.hasOwn(config.agents, id) ? config.agents[id] : {};
|
|
147
155
|
const resolved = resolveModel(config, agent.model ?? config.defaultModel);
|
|
148
156
|
return { id, ...resolved, provider: agent.provider ?? resolved.provider,
|
|
149
|
-
reasoning: agent.reasoning ?? config.defaultReasoning, enabled: agent.enabled !== false,
|
|
157
|
+
extensions: agent.extensions ?? [], reasoning: agent.reasoning ?? config.defaultReasoning, enabled: agent.enabled !== false,
|
|
150
158
|
source: Object.hasOwn(config.agents, id) ? 'role' : 'project-default' };
|
|
151
159
|
}
|
|
152
160
|
|
|
@@ -172,13 +180,14 @@ export function agentPlan(root, host) {
|
|
|
172
180
|
const parts = registry[id]?.split('/');
|
|
173
181
|
const canonical = parts ? (parts[0] === 'core' ? 'core:' : parts[1] + ':') + path.basename(registry[id], '.md') : id;
|
|
174
182
|
const skills = registry[id] ? preferredSkills(fs.readFileSync(safe(path.join(root, '.harness'), registry[id]), 'utf8')) : { required: [], optional: [] };
|
|
175
|
-
|
|
183
|
+
const extensionFiles = requested.extensions.map(file => { const info = stat(safe(root,file)); requireThat(!info || info.size <= 65536, 'extension exceeds 64 KiB'); return { path: file, status: info ? 'present' : 'missing' }; });
|
|
184
|
+
return { ...requested, extension_files: extensionFiles, contract_load_order: [registry[id] ? '.harness/' + registry[id] : null, ...requested.extensions].filter(Boolean), active, canonical_id: canonical, skills, contract: registry[id] ? '.harness/' + registry[id] : null,
|
|
176
185
|
host_model: mapping ?? requested.model, host_reasoning: profile.reasoning?.[requested.reasoning] ?? requested.reasoning,
|
|
177
186
|
mapping: mapping ? 'explicit-user-host-mapping' : 'requested',
|
|
178
187
|
availability: active && incompatible ? 'incompatible-provider' : 'unverified',
|
|
179
188
|
runtime_verified: false };
|
|
180
189
|
});
|
|
181
|
-
return { status: roles.some(role => role.active && role.availability === 'incompatible-provider') ? 'host-incompatible' : 'requires-host-verification',
|
|
190
|
+
return { status: roles.some(role => role.active && role.extension_files.some(f => f.status === 'missing')) ? 'blocked' : roles.some(role => role.active && role.availability === 'incompatible-provider') ? 'host-incompatible' : 'requires-host-verification',
|
|
182
191
|
configured: true, host: host ?? null, presets, execution: config.execution, roles,
|
|
183
192
|
missing_mappings: Object.keys(registry).filter(id => !Object.hasOwn(config.agents, id)), note };
|
|
184
193
|
}
|
package/cli/capabilities.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { skillRequirements } from './skill-requirements.js';
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import { manifest, readJson, safe, SLUG, HarnessError, payload } from './files.js';
|
|
3
4
|
import { validateContract } from './module-contract.js';
|
|
@@ -17,7 +18,7 @@ function skillMetadata(body, name) {
|
|
|
17
18
|
for (const heading of ['## Input', '## Procedure', '## Output', '## Constraints']) {
|
|
18
19
|
if (!body.includes(heading)) fail(`Missing ${heading} in skill: ${name}`);
|
|
19
20
|
}
|
|
20
|
-
return { name, description };
|
|
21
|
+
return { name, description, requires: skillRequirements(body) };
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
export function loadCapability(source, id) {
|
package/cli/end-user.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Tool selection is a host decision. This plan never runs or installs browser/device tooling.
|
|
2
|
+
export function endUserPlan({ adapters, frameworks, hasStaticPage, hasApi, hasCli }) {
|
|
3
|
+
const surfaces = [];
|
|
4
|
+
if (hasStaticPage || adapters.some(id => ['react-nextjs', 'astro'].includes(id))) surfaces.push('web');
|
|
5
|
+
if (adapters.includes('react-native')) surfaces.push('mobile');
|
|
6
|
+
if (hasApi || adapters.includes('java-spring')) surfaces.push('api');
|
|
7
|
+
if (hasCli) surfaces.push('cli');
|
|
8
|
+
const tools = surfaces.map(surface => ({ surface, availability: 'unverified',
|
|
9
|
+
candidates: surface === 'web' ? frameworks.filter(x => ['@playwright/test', 'playwright', 'cypress'].includes(x)).length
|
|
10
|
+
? frameworks.filter(x => ['@playwright/test', 'playwright', 'cypress'].includes(x)) : ['authorized host browser tool', 'Playwright if a new runner is agreed']
|
|
11
|
+
: surface === 'mobile' ? frameworks.filter(x => ['detox', 'maestro'].includes(x)).length
|
|
12
|
+
? frameworks.filter(x => ['detox', 'maestro'].includes(x)) : ['existing device/simulator tooling; clarify missing runner']
|
|
13
|
+
: surface === 'cli' ? ['authorized host terminal and existing CLI tests'] : ['existing HTTP/API test client'],
|
|
14
|
+
}));
|
|
15
|
+
return { executed: false, status: 'planned', surface_hints: surfaces, tools,
|
|
16
|
+
workflow: '.harness/core/workflows/end-user-acceptance.md', skill: 'harness-end-user-testing', role: 'core:test-engineer',
|
|
17
|
+
required_context: ['Reviewed AC and affected user journeys/roles; inspect input sources before asking.',
|
|
18
|
+
'Actual app URL/start command or public CLI/API entry; reachable test environment and test-account references.',
|
|
19
|
+
'Existing runner or available host tools, isolated fixtures and permitted mutations/cleanup.',
|
|
20
|
+
'Real vs mocked dependencies, tested revision and configured evidence/output destination.'],
|
|
21
|
+
completion_gate: ['Exercise the actual user-facing interface for applicable AC, including relevant failure/role/state paths.',
|
|
22
|
+
'Record observed user outcomes with requirement-linked JSON assertions and redacted screenshot/trace/video receipts where applicable.',
|
|
23
|
+
'For full acceptance use verification_target=end-user and a matching real/not-applicable dependency journey; mocks/unknown dependencies cannot verify that target.',
|
|
24
|
+
'Unavailable browser/device/account/service is BLOCKED; a deliberate skip is NOT_RUN; never replace UI proof with API/build/unit proof.',
|
|
25
|
+
'Browser/device/CLI/API automation does not establish business-owner UAT approval or actual model compliance.'],
|
|
26
|
+
host_prompt: 'Read AGENTS.md, the end-user-acceptance workflow and harness-end-user-testing. Inspect approved requirements, app entry points, existing test configuration and actual host tools. '
|
|
27
|
+
+ 'Reuse established answers; ask only missing consequential environment/account/fixture decisions. Run affected user journeys through the real interface, assert visible and persisted outcomes, '
|
|
28
|
+
+ 'classify real/mocked dependencies and save redacted evidence using the existing verification report. Mark required missing access BLOCKED; report gaps before completion.',
|
|
29
|
+
note: 'Surface hints are metadata only. Requirements choose actual journeys; no generic login/CRUD/browser suite is forced on every project.' };
|
|
30
|
+
}
|
package/cli/environment.js
CHANGED
|
@@ -5,6 +5,7 @@ import { agentPlan } from './agents.js';
|
|
|
5
5
|
import { readConfig } from './project-config.js';
|
|
6
6
|
export { readConfig } from './project-config.js';
|
|
7
7
|
import { artifactReport } from './artifacts.js';
|
|
8
|
+
import { projectDecisionReport } from './project-decisions.js';
|
|
8
9
|
|
|
9
10
|
export const ENVIRONMENT = '.harness/environment.json';
|
|
10
11
|
export const MACHINE = '.harness/machine.json';
|
|
@@ -155,6 +156,7 @@ export function setupPlan(root, adapters, env = process.env, host) {
|
|
|
155
156
|
});
|
|
156
157
|
questions.push(...artifacts.questions);
|
|
157
158
|
return { status: environment.problems.length ? 'blocked' : 'planned', environment, artifacts, questions,
|
|
159
|
+
project_decisions: projectDecisionReport(root),
|
|
158
160
|
agent_configuration: agentPlan(root, host),
|
|
159
161
|
next_steps: ['Read AGENTS.md, .harness/local.md and the installed project-setup workflow.',
|
|
160
162
|
'Apply installed Core project-conventions.md and relevant adapter conventions; establish mode per affected module even when no setup questions are suggested.',
|
|
@@ -30,10 +30,20 @@ export function casesFrom(source, relative) {
|
|
|
30
30
|
&& doc.cases.length > 0, `Invalid evaluation: ${relative}`);
|
|
31
31
|
const ids = new Set();
|
|
32
32
|
for (const c of doc.cases) {
|
|
33
|
-
requireThat(
|
|
33
|
+
requireThat(c && Object.keys(c).every(k => ['id', 'prompt', 'criteria', 'critical', 'assertions'].includes(k)) && ['id', 'prompt', 'criteria', 'critical'].every(k => Object.hasOwn(c, k))
|
|
34
34
|
&& typeof c.id === 'string' && SLUG.test(c.id) && !ids.has(c.id)
|
|
35
35
|
&& text(c.prompt) && Array.isArray(c.criteria) && c.criteria.length > 0 && c.criteria.every(text)
|
|
36
36
|
&& typeof c.critical === 'boolean', `Invalid eval case: ${relative}`);
|
|
37
|
+
if (c.assertions !== undefined) {
|
|
38
|
+
requireThat(Array.isArray(c.assertions) && c.assertions.length <= 32, `Invalid assertions: ${relative}`);
|
|
39
|
+
const assertionIds = new Set();
|
|
40
|
+
for (const a of c.assertions) {
|
|
41
|
+
requireThat(exact(a, ['id', 'type', 'target', 'value']) && typeof a.id === 'string' && SLUG.test(a.id)
|
|
42
|
+
&& !assertionIds.has(a.id) && ['contains', 'not-contains'].includes(a.type)
|
|
43
|
+
&& ['transcript', 'tool-inputs'].includes(a.target) && text(a.value) && a.value.length <= 4096, `Invalid assertion: ${relative}`);
|
|
44
|
+
assertionIds.add(a.id);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
37
47
|
ids.add(c.id);
|
|
38
48
|
}
|
|
39
49
|
return doc.cases;
|
package/cli/files.js
CHANGED
|
@@ -67,8 +67,13 @@ export function assertSeparate(root, source) {
|
|
|
67
67
|
};
|
|
68
68
|
// npm local installs intentionally place the package in the consumer's node_modules.
|
|
69
69
|
const localPackage = path.relative(root, source).split(path.sep)[0] === 'node_modules';
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
const relativeParts = path.relative(root, source).split(path.sep);
|
|
71
|
+
const pythonPackage = relativeParts[0] === '.venv' && relativeParts.includes('site-packages')
|
|
72
|
+
&& relativeParts.slice(-2).join('/') === 'ai_harness/_bundle'
|
|
73
|
+
&& stat(path.join(source, '_python_distribution.json'))?.isFile()
|
|
74
|
+
&& readJson(path.join(source, '_python_distribution.json')).name === 'manhd-ai-harness';
|
|
75
|
+
if (inside(source, root) || (inside(root, source) && !localPackage && !pythonPackage)) {
|
|
76
|
+
throw new HarnessError('Source and target must not overlap, except a package installed in node_modules or .venv site-packages');
|
|
72
77
|
}
|
|
73
78
|
}
|
|
74
79
|
|
package/cli/harness.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import { localContext } from './knowledge.js';
|
|
3
|
+
import { kiroPayload, KIRO_HOOK } from './kiro.js';
|
|
4
|
+
import { requirementReport } from './skill-requirements.js';
|
|
2
5
|
import { loadStartConfig } from './start-config.js';
|
|
6
|
+
import { PROJECT_DECISIONS, emptyProjectDecisions, loadProjectDecisions, projectDecisionReport } from './project-decisions.js';
|
|
3
7
|
import { checkpointPath, validateCheckpoint } from './checkpoint.js';
|
|
4
8
|
import { readConfig } from './project-config.js';
|
|
5
9
|
import { claudeReport, claudeBlock, linkClaude } from './claude.js';
|
|
6
|
-
import { emptyEnvironment, environmentReport } from './environment.js';
|
|
10
|
+
import { emptyEnvironment, environmentReport, loadEnvironment } from './environment.js';
|
|
7
11
|
import { AGENT_CONFIG, agentDefaults, loadAgentConfig } from './agents.js';
|
|
8
12
|
import { reviewVerification, techspecEvidence } from './verification-report.js';
|
|
9
13
|
import { ARTIFACTS, INTEGRATIONS, emptyArtifacts, emptyIntegrations, loadArtifactContract, artifactReport, docRoute } from './artifacts.js';
|
|
@@ -95,7 +99,7 @@ export function bundle(source, config) {
|
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
function managedName(name) {
|
|
98
|
-
return name === 'AGENTS.md' || name === 'CLAUDE.md' || ['.harness/core/', '.harness/adapters/', '.harness/capabilities/', '.harness/integrations/', '.harness/templates/', '.agents/skills/harness-', '.claude/skills/harness-'].some(prefix => name.startsWith(prefix));
|
|
102
|
+
return name === 'AGENTS.md' || name === 'CLAUDE.md' || name === KIRO_HOOK || ['.harness/core/', '.harness/adapters/', '.harness/capabilities/', '.harness/integrations/', '.harness/templates/', '.agents/skills/harness-', '.claude/skills/harness-', '.kiro/skills/harness-'].some(prefix => name.startsWith(prefix));
|
|
99
103
|
}
|
|
100
104
|
|
|
101
105
|
export function loadLock(root) {
|
|
@@ -133,6 +137,18 @@ export function guard(root, operation) {
|
|
|
133
137
|
finally { fs.closeSync(fd); fs.unlinkSync(filename); }
|
|
134
138
|
}
|
|
135
139
|
|
|
140
|
+
export function snapshotCheck(root) {
|
|
141
|
+
const lock = loadLock(root), problems = drift(root, lock);
|
|
142
|
+
return { status: problems.length ? 'error' : 'ok', version: lock.version, problems };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function userIgnoreChanges(root) {
|
|
146
|
+
const name = '.harness/.gitignore', filename = safe(root, name);
|
|
147
|
+
const data = stat(filename) ? fs.readFileSync(filename, 'utf8') : '';
|
|
148
|
+
return data.trimEnd().split(/\r?\n/).slice(-2).join('\n') === '/backups/\n/machine.json' ? {}
|
|
149
|
+
: { [name]: Buffer.from(data + (data && !data.endsWith('\n') ? '\n' : '') + '/backups/\n/machine.json\n') };
|
|
150
|
+
}
|
|
151
|
+
|
|
136
152
|
export function atomicWrite(filename, data, mode = 0o644) {
|
|
137
153
|
fs.mkdirSync(path.dirname(filename), { recursive: true });
|
|
138
154
|
const tmp = path.join(path.dirname(filename), `.harness-tmp-${randomUUID()}`);
|
|
@@ -174,6 +190,9 @@ export function install(root, source, { command = 'init', adapters, capabilities
|
|
|
174
190
|
const old = hasLock ? loadLock(root) : null;
|
|
175
191
|
const existingAgents = loadAgentConfig(root);
|
|
176
192
|
loadStartConfig(root);
|
|
193
|
+
loadProjectDecisions(root);
|
|
194
|
+
loadEnvironment(root);
|
|
195
|
+
localContext(root);
|
|
177
196
|
loadArtifactContract(root); // Validate project-owned declarations before planning any writes.
|
|
178
197
|
if (agentPreset !== undefined && command !== 'init') throw new HarnessError('Agent preset is supported only by init');
|
|
179
198
|
if (agentPreset !== undefined && existingAgents) throw new HarnessError('agents.json is project-owned; edit it explicitly instead of applying an init preset');
|
|
@@ -196,6 +215,9 @@ export function install(root, source, { command = 'init', adapters, capabilities
|
|
|
196
215
|
lock.files[destination] = digest(files[destination]);
|
|
197
216
|
}
|
|
198
217
|
}
|
|
218
|
+
if (host === 'kiro' || Object.keys(old?.files ?? {}).some(p => p.startsWith('.kiro/skills/harness-'))) {
|
|
219
|
+
for (const [name, data] of Object.entries(kiroPayload(files))) { files[name] = data; lock.files[name] = digest(data); }
|
|
220
|
+
}
|
|
199
221
|
lock.files = Object.fromEntries(Object.entries(lock.files).sort(([a],[b]) => a.localeCompare(b)));
|
|
200
222
|
lock.bundle_sha256 = digest(encode(lock.files));
|
|
201
223
|
if (old) {
|
|
@@ -223,18 +245,15 @@ export function install(root, source, { command = 'init', adapters, capabilities
|
|
|
223
245
|
[CONFIG]: encode(config),
|
|
224
246
|
[ARTIFACTS]: encode(emptyArtifacts()),
|
|
225
247
|
[INTEGRATIONS]: encode(emptyIntegrations()),
|
|
248
|
+
[PROJECT_DECISIONS]: encode(emptyProjectDecisions()),
|
|
226
249
|
'.harness/environment.json': encode(emptyEnvironment()),
|
|
227
250
|
'openspec/config.yaml': fs.readFileSync(safe(source, 'templates/openspec/config.yaml')),
|
|
228
|
-
'.harness/local.md': Buffer.from('# Project overrides\n\nRecord project facts and reviewed exceptions here
|
|
251
|
+
'.harness/local.md': stat(safe(source, 'templates/local.md')) ? fs.readFileSync(safe(source, 'templates/local.md')) : Buffer.from('# Project overrides\n\nRecord project facts and reviewed exceptions here.\n'),
|
|
229
252
|
};
|
|
230
253
|
for (const [name, data] of Object.entries(seeds)) if (!stat(safe(root, name))) changes[name] = data;
|
|
231
254
|
// Project-owned ignore file: preserve all prior rules, place our anchored rule last.
|
|
232
255
|
// This does not remove an already tracked machine file from the Git index.
|
|
233
|
-
|
|
234
|
-
const ignore = stat(ignorePath) ? fs.readFileSync(ignorePath, 'utf8') : '';
|
|
235
|
-
if (ignore.trimEnd().split(/\r?\n/).slice(-2).join('\n') !== '/backups/\n/machine.json') {
|
|
236
|
-
changes[ignoreName] = Buffer.from(ignore + (ignore && !ignore.endsWith('\n') ? '\n' : '') + '/backups/\n/machine.json\n');
|
|
237
|
-
}
|
|
256
|
+
Object.assign(changes, userIgnoreChanges(root));
|
|
238
257
|
const newLock = encode(lock);
|
|
239
258
|
if (!hasLock || !fs.readFileSync(safe(root, LOCK)).equals(newLock)) changes[LOCK] = newLock;
|
|
240
259
|
const plan = Object.entries(changes).map(([name, data]) => ({ path: name, action: data === null ? 'delete' : 'write' }));
|
|
@@ -248,12 +267,32 @@ export function check(root) {
|
|
|
248
267
|
const problems = drift(root, lock);
|
|
249
268
|
loadArtifactContract(root);
|
|
250
269
|
loadStartConfig(root);
|
|
270
|
+
loadProjectDecisions(root);
|
|
271
|
+
localContext(root);
|
|
251
272
|
loadAgentConfig(root); // Optional on legacy installs; validate user config without managing its bytes.
|
|
252
273
|
if (JSON.stringify([...config.adapters].sort()) !== JSON.stringify([...lock.adapters].sort())) problems.push(`${CONFIG}: adapter selection changed; run update`);
|
|
253
274
|
if (JSON.stringify([...(config.capabilities ?? [])].sort()) !== JSON.stringify([...(lock.capabilities ?? [])].sort())) problems.push(`${CONFIG}: capability selection changed; run update`);
|
|
254
275
|
return { status: problems.length ? 'error' : 'ok', version: lock.version, problems };
|
|
255
276
|
}
|
|
256
277
|
|
|
278
|
+
export function changeSidecars(root, name) {
|
|
279
|
+
if (!SLUG.test(name) || name === 'archive') throw new HarnessError('Invalid change ID');
|
|
280
|
+
return guard(root, () => {
|
|
281
|
+
if (check(root).status !== 'ok') throw new HarnessError('Fix installation drift before creating sidecars');
|
|
282
|
+
const base = 'openspec/changes/' + name;
|
|
283
|
+
if (!stat(safe(root, base + '/proposal.md'))) throw new HarnessError('Sidecars require an existing proposal.md');
|
|
284
|
+
const lock = loadLock(root), changes = {};
|
|
285
|
+
for (const file of ['.openspec.yaml', 'decisions.json', 'verification.json', 'artifact-impact.json']) {
|
|
286
|
+
const template = '.harness/templates/openspec/' + file;
|
|
287
|
+
if (!Object.hasOwn(lock.files, template)) throw new HarnessError('Update Harness to install sidecar templates');
|
|
288
|
+
const destination = base + '/' + file;
|
|
289
|
+
if (!stat(safe(root,destination))) changes[destination] = Buffer.from(fs.readFileSync(safe(root,template),'utf8').replaceAll('{{change_id}}',name));
|
|
290
|
+
}
|
|
291
|
+
transaction(root, changes);
|
|
292
|
+
return { status: 'ok', change: name, created: Object.keys(changes), note: 'Draft sidecars only; existing files preserved.' };
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
257
296
|
export function newChange(root, name) {
|
|
258
297
|
if (!SLUG.test(name) || name === 'archive') throw new HarnessError('Change ID must be a lowercase hyphenated slug, excluding archive');
|
|
259
298
|
return guard(root, () => {
|
|
@@ -320,7 +359,7 @@ export function newDoc(root, name, capability = 'product', options = {}) {
|
|
|
320
359
|
});
|
|
321
360
|
}
|
|
322
361
|
|
|
323
|
-
export function doctor(root) {
|
|
362
|
+
export function doctor(root, skill) {
|
|
324
363
|
const extensions = process.platform === 'win32' ? (process.env.PATHEXT || '.EXE;.CMD;.BAT').split(';') : [''];
|
|
325
364
|
const tools = {};
|
|
326
365
|
for (const name of ['git', 'node', 'npm', 'java', 'mvn', 'openspec']) {
|
|
@@ -330,8 +369,11 @@ export function doctor(root) {
|
|
|
330
369
|
}));
|
|
331
370
|
}
|
|
332
371
|
const state = check(root), environment = environmentReport(root);
|
|
333
|
-
|
|
334
|
-
|
|
372
|
+
const prerequisites = skill ? requirementReport(root, skill) : null;
|
|
373
|
+
return { ...state, status: state.status !== 'ok' || environment.problems.length || prerequisites?.status === 'blocked' ? 'error' : 'ok',
|
|
374
|
+
prerequisites, tools, environment, decisions: projectDecisionReport(root),
|
|
375
|
+
context: { structured: localContext(root).structured, warnings: localContext(root).structured ? [] : ['Legacy prose is valid; use harness-start to add source-linked structured context when needed.'] },
|
|
376
|
+
claude: claudeReport(root), artifacts: artifactReport(root), note: 'Tool presence only; no commands executed or compatibility claim made. Configured project tools take precedence over generic PATH hints.' };
|
|
335
377
|
}
|
|
336
378
|
|
|
337
379
|
export function saveCheckpoint(root, id, report, expected) {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { safe, stat, digest, isObject, HarnessError } from './files.js';
|
|
3
|
+
const text = value => typeof value === 'string' && value.trim().length > 0 && !/[\0\r\n]/.test(value);
|
|
4
|
+
const assert = (ok, message) => { if (!ok) throw new HarnessError('Invalid user journey: ' + message); };
|
|
5
|
+
const interactions = { web: 'browser', mobile: 'device', cli: 'terminal', api: 'http' };
|
|
6
|
+
export function reviewJourney(root, row) {
|
|
7
|
+
if (row.journey === undefined) return { acceptance_scope: 'not-declared' };
|
|
8
|
+
const journey = row.journey;
|
|
9
|
+
assert(isObject(journey) && Object.keys(journey).every(k => ['surface', 'actor', 'entrypoint', 'interaction', 'dependencies', 'attachments'].includes(k)), 'fields');
|
|
10
|
+
assert(Object.hasOwn(interactions, journey.surface) && text(journey.actor) && text(journey.entrypoint), 'surface/actor/entrypoint');
|
|
11
|
+
assert(journey.interaction === interactions[journey.surface], 'surface must be exercised by its actual interface');
|
|
12
|
+
assert(['real', 'mocked', 'not-applicable', 'unknown'].includes(journey.dependencies), 'dependency classification');
|
|
13
|
+
if (/^https?:\/\//.test(journey.entrypoint)) {
|
|
14
|
+
let url; try { url = new URL(journey.entrypoint); } catch { throw new HarnessError('Invalid user journey: entrypoint URL'); }
|
|
15
|
+
assert(!url.username && !url.password && ![...url.searchParams.keys()].some(k => /^(?:token|access_token|api[_-]?key|password|secret|authorization|sig|signature)$/i.test(k)), 'entrypoint must not contain credentials');
|
|
16
|
+
}
|
|
17
|
+
const method = { 'browser-e2e': 'web', 'mobile-e2e': 'mobile', 'cli-e2e': 'cli', api: 'api', 'backend-e2e': 'api', authorization: 'api', security: 'api' }[row.method];
|
|
18
|
+
assert(method ? method === journey.surface : ['end-user', 'business-workflow'].includes(row.method), 'scenario method/surface mismatch');
|
|
19
|
+
assert(Array.isArray(journey.attachments) && journey.attachments.length <= 20, 'attachment list (max 20)');
|
|
20
|
+
const kinds = { screenshot: /\.(?:png|jpe?g|webp)$/i, trace: /\.zip$/i, video: /\.(?:webm|mp4)$/i, report: /\.(?:html|json)$/i };
|
|
21
|
+
const paths = new Set(); let totalSize = 0;
|
|
22
|
+
for (const ref of journey.attachments) {
|
|
23
|
+
assert(isObject(ref) && Object.keys(ref).sort().join(',') === 'kind,path,sha256' && Object.hasOwn(kinds, ref.kind)
|
|
24
|
+
&& text(ref.path) && kinds[ref.kind].test(ref.path) && /^[a-f0-9]{64}$/.test(ref.sha256), 'attachment type/path/digest');
|
|
25
|
+
assert(!paths.has(ref.path), 'duplicate attachment'); paths.add(ref.path);
|
|
26
|
+
const filename = safe(root, ref.path), info = stat(filename);
|
|
27
|
+
assert(info?.isFile() && info.size > 0 && info.size <= 50 * 1024 * 1024, 'attachment missing/empty/over 50 MiB');
|
|
28
|
+
totalSize += info.size; assert(totalSize <= 100 * 1024 * 1024, 'attachments exceed 100 MiB total');
|
|
29
|
+
assert(digest(fs.readFileSync(filename)) === ref.sha256, 'attachment digest mismatch');
|
|
30
|
+
}
|
|
31
|
+
const scope = !row.executed ? 'not-executed' : journey.dependencies === 'mocked' ? 'interface-only'
|
|
32
|
+
: journey.dependencies === 'unknown' ? 'unverified-dependencies' : 'end-user';
|
|
33
|
+
return { journey, acceptance_scope: scope };
|
|
34
|
+
}
|