@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.
Files changed (93) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +21 -9
  3. package/adapters/react-native/adapter.yaml +1 -1
  4. package/adapters/react-native/skills/harness-react-native-behavior-testing/SKILL.md +9 -0
  5. package/adapters/react-nextjs/adapter.yaml +1 -1
  6. package/adapters/react-nextjs/skills/harness-react-nextjs-behavior-testing/SKILL.md +9 -0
  7. package/capabilities/product/capability.json +1 -1
  8. package/capabilities/product/workflows/idea-to-prd.md +15 -0
  9. package/capabilities/product/workflows/idea-to-prd.workflow.json +82 -0
  10. package/cli/adapters.js +1 -1
  11. package/cli/agents.js +14 -5
  12. package/cli/capabilities.js +2 -1
  13. package/cli/end-user.js +30 -0
  14. package/cli/environment.js +2 -0
  15. package/cli/evaluation-contract.js +11 -1
  16. package/cli/files.js +7 -2
  17. package/cli/harness.js +53 -11
  18. package/cli/journey-evidence.js +34 -0
  19. package/cli/kiro.js +15 -0
  20. package/cli/knowledge.js +104 -0
  21. package/cli/main.js +60 -22
  22. package/cli/portable-config.js +6 -3
  23. package/cli/project-decisions.js +47 -0
  24. package/cli/readiness.js +82 -41
  25. package/cli/setup.js +66 -0
  26. package/cli/skill-requirements.js +47 -0
  27. package/cli/start.js +15 -2
  28. package/cli/structured.js +65 -0
  29. package/cli/sync-integrations.js +49 -0
  30. package/cli/verification-report.js +9 -5
  31. package/cli/verification.js +7 -2
  32. package/cli/workflow.js +76 -0
  33. package/core/agents/orchestrator.md +1 -1
  34. package/core/agents/test-engineer.md +4 -2
  35. package/core/checks/verification.json +12 -0
  36. package/core/evals/cases.json +75 -1
  37. package/core/rules/context.md +9 -0
  38. package/core/skills/harness-end-user-testing/SKILL.md +55 -0
  39. package/core/skills/harness-review/SKILL.md +4 -0
  40. package/core/skills/harness-spec/SKILL.md +3 -1
  41. package/core/skills/harness-start/SKILL.md +66 -0
  42. package/core/skills/harness-verification/SKILL.md +2 -0
  43. package/core/workflows/adaptive-verification.md +5 -0
  44. package/core/workflows/agent-routing.md +6 -0
  45. package/core/workflows/approvals.schema.json +62 -0
  46. package/core/workflows/clarification.md +6 -1
  47. package/core/workflows/continuous-development.md +5 -0
  48. package/core/workflows/end-user-acceptance.md +36 -0
  49. package/core/workflows/greenfield-setup.md +27 -0
  50. package/core/workflows/project-setup.md +13 -2
  51. package/core/workflows/seed.md +25 -0
  52. package/core/workflows/spec-to-code.md +19 -8
  53. package/core/workflows/spec-to-code.workflow.json +99 -0
  54. package/core/workflows/workflow.schema.json +113 -0
  55. package/docs/README.md +8 -0
  56. package/docs/adaptive-verification.md +10 -1
  57. package/docs/agent-configuration.md +7 -0
  58. package/docs/agent-handoffs.md +6 -1
  59. package/docs/architecture.md +43 -3
  60. package/docs/cli.md +39 -4
  61. package/docs/configuration-backup.md +10 -2
  62. package/docs/conformance-runner.md +64 -0
  63. package/docs/end-to-end-workflow.md +4 -0
  64. package/docs/end-user-testing.md +128 -0
  65. package/docs/frontend-mobile.md +7 -0
  66. package/docs/getting-started.md +17 -5
  67. package/docs/harness-maintenance.md +1 -0
  68. package/docs/i18n/README.es.md +17 -7
  69. package/docs/i18n/README.vi.md +18 -8
  70. package/docs/i18n/README.zh-CN.md +17 -7
  71. package/docs/project-configuration.md +6 -0
  72. package/docs/python-installation.md +85 -0
  73. package/docs/session-onboarding.md +215 -0
  74. package/docs/skill-matrix.md +5 -3
  75. package/docs/start.md +7 -2
  76. package/docs/tool-setup/README.md +13 -14
  77. package/docs/tool-setup/claude-code.md +10 -0
  78. package/docs/tool-setup/codex.md +10 -0
  79. package/docs/tool-setup/copilot.md +10 -0
  80. package/docs/tool-setup/cursor.md +10 -0
  81. package/docs/tool-setup/gemini-cli.md +10 -0
  82. package/docs/tool-setup/kiro.md +112 -38
  83. package/docs/versioning.md +56 -0
  84. package/docs/workflow-contract.md +66 -0
  85. package/evals/README.md +7 -0
  86. package/harness.yaml +13 -1
  87. package/package.json +7 -3
  88. package/scripts/conformance-runner.js +119 -0
  89. package/scripts/score-eval.js +3 -1
  90. package/templates/AGENTS.md +6 -1
  91. package/templates/decisions.json +4 -0
  92. package/templates/local.md +21 -0
  93. 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
- Install/sign in to an AI tool below. Requires Node.js 22+. Run in your **project root** terminal:
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
- Run `start` for guided intake, then send its `host_prompt` to your AI tool. [Settings, backup & transfer](docs/configuration-backup.md).
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
- `npx` does not install a global `aih` command. Check the installation with
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
- Then ask your AI tool:
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
- > Read AGENTS.md. Inspect this project and its approved decisions. Implement [task], run appropriate checks and maintain the affected docs.
28
+ **pip / restricted networks** (included in the next reviewed release; PyPI publication requires maintainer setup):
28
29
 
29
- Use `init --detect` for supported stack detection. Existing project instructions and custom skills are preserved; `check` checks Harness integrity. Your host executes application tests and controls tools, delegation and permissions.
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
 
@@ -27,5 +27,5 @@
27
27
  ],
28
28
  "verification": "checks/verification.yaml",
29
29
  "evaluation": "evals/cases.json",
30
- "module_version": "1.3.0"
30
+ "module_version": "1.4.0"
31
31
  }
@@ -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.
@@ -27,5 +27,5 @@
27
27
  ],
28
28
  "verification": "checks/verification.yaml",
29
29
  "evaluation": "evals/cases.json",
30
- "module_version": "1.3.0"
30
+ "module_version": "1.4.0"
31
31
  }
@@ -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.
@@ -57,7 +57,7 @@
57
57
  "templates/outcomes.md",
58
58
  "templates/techspec.md"
59
59
  ],
60
- "module_version": "1.6.0",
60
+ "module_version": "1.7.0",
61
61
  "verification": "checks/verification.json",
62
62
  "evaluation": "evals/cases.json",
63
63
  "template_aliases": {
@@ -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) { return readConfiguration(root, AGENT_CONFIG); }
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
- return { ...requested, active, canonical_id: canonical, skills, contract: registry[id] ? '.harness/' + registry[id] : null,
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
  }
@@ -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) {
@@ -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
+ }
@@ -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(exact(c, ['id', 'prompt', 'criteria', 'critical'])
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
- if (inside(source, root) || (inside(root, source) && !localPackage)) {
71
- throw new HarnessError('Source and target must not overlap, except a source installed in node_modules');
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. Never weaken security boundaries.\n\n## Stack\nstack: unknown\nRecord actual dependency versions and approved design sources. This field does not select technology.\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'),
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
- const ignoreName = '.harness/.gitignore', ignorePath = safe(root, ignoreName);
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
- return { ...state, status: state.status !== 'ok' || environment.problems.length ? 'error' : 'ok',
334
- tools, environment, 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.' };
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
+ }