@manhd_d/ai-harness 0.13.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 (95) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/README.md +22 -7
  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/bin/aih.js +1 -1
  8. package/capabilities/product/capability.json +1 -1
  9. package/capabilities/product/workflows/idea-to-prd.md +15 -0
  10. package/capabilities/product/workflows/idea-to-prd.workflow.json +82 -0
  11. package/cli/adapters.js +1 -1
  12. package/cli/agents.js +14 -5
  13. package/cli/capabilities.js +2 -1
  14. package/cli/end-user.js +30 -0
  15. package/cli/environment.js +2 -0
  16. package/cli/evaluation-contract.js +11 -1
  17. package/cli/files.js +7 -2
  18. package/cli/harness.js +60 -15
  19. package/cli/journey-evidence.js +34 -0
  20. package/cli/kiro.js +15 -0
  21. package/cli/knowledge.js +104 -0
  22. package/cli/main.js +80 -19
  23. package/cli/portable-config.js +124 -0
  24. package/cli/project-decisions.js +47 -0
  25. package/cli/readiness.js +82 -41
  26. package/cli/setup.js +66 -0
  27. package/cli/skill-requirements.js +47 -0
  28. package/cli/start-config.js +15 -0
  29. package/cli/start.js +151 -0
  30. package/cli/structured.js +65 -0
  31. package/cli/sync-integrations.js +49 -0
  32. package/cli/verification-report.js +9 -5
  33. package/cli/verification.js +7 -2
  34. package/cli/workflow.js +76 -0
  35. package/core/agents/orchestrator.md +1 -1
  36. package/core/agents/test-engineer.md +4 -2
  37. package/core/checks/verification.json +12 -0
  38. package/core/evals/cases.json +97 -1
  39. package/core/rules/context.md +9 -0
  40. package/core/skills/harness-end-user-testing/SKILL.md +55 -0
  41. package/core/skills/harness-review/SKILL.md +4 -0
  42. package/core/skills/harness-spec/SKILL.md +3 -1
  43. package/core/skills/harness-start/SKILL.md +66 -0
  44. package/core/skills/harness-verification/SKILL.md +2 -0
  45. package/core/workflows/adaptive-verification.md +5 -0
  46. package/core/workflows/agent-routing.md +6 -0
  47. package/core/workflows/approvals.schema.json +62 -0
  48. package/core/workflows/clarification.md +6 -1
  49. package/core/workflows/continuous-development.md +5 -0
  50. package/core/workflows/end-user-acceptance.md +36 -0
  51. package/core/workflows/greenfield-setup.md +27 -0
  52. package/core/workflows/project-setup.md +30 -1
  53. package/core/workflows/seed.md +25 -0
  54. package/core/workflows/spec-to-code.md +19 -8
  55. package/core/workflows/spec-to-code.workflow.json +99 -0
  56. package/core/workflows/workflow.schema.json +113 -0
  57. package/docs/README.md +11 -0
  58. package/docs/adaptive-verification.md +10 -1
  59. package/docs/agent-configuration.md +7 -0
  60. package/docs/agent-handoffs.md +6 -1
  61. package/docs/architecture.md +53 -3
  62. package/docs/cli.md +55 -3
  63. package/docs/configuration-backup.md +130 -0
  64. package/docs/conformance-runner.md +64 -0
  65. package/docs/end-to-end-workflow.md +4 -0
  66. package/docs/end-user-testing.md +128 -0
  67. package/docs/frontend-mobile.md +7 -0
  68. package/docs/getting-started.md +21 -7
  69. package/docs/harness-maintenance.md +1 -0
  70. package/docs/i18n/README.es.md +18 -5
  71. package/docs/i18n/README.vi.md +19 -6
  72. package/docs/i18n/README.zh-CN.md +18 -5
  73. package/docs/project-configuration.md +15 -1
  74. package/docs/python-installation.md +85 -0
  75. package/docs/session-onboarding.md +215 -0
  76. package/docs/skill-matrix.md +5 -3
  77. package/docs/start.md +43 -0
  78. package/docs/tool-setup/README.md +13 -14
  79. package/docs/tool-setup/claude-code.md +10 -0
  80. package/docs/tool-setup/codex.md +10 -0
  81. package/docs/tool-setup/copilot.md +10 -0
  82. package/docs/tool-setup/cursor.md +10 -0
  83. package/docs/tool-setup/gemini-cli.md +10 -0
  84. package/docs/tool-setup/kiro.md +112 -38
  85. package/docs/versioning.md +67 -0
  86. package/docs/workflow-contract.md +66 -0
  87. package/evals/README.md +7 -0
  88. package/harness.yaml +13 -1
  89. package/package.json +7 -3
  90. package/scripts/conformance-runner.js +119 -0
  91. package/scripts/score-eval.js +3 -1
  92. package/templates/AGENTS.md +6 -0
  93. package/templates/decisions.json +4 -0
  94. package/templates/local.md +21 -0
  95. package/templates/openspec/approvals.json +5 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,45 @@
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
+
36
+ ## Unreleased — Guided start and portable configuration
37
+
38
+ - Add start: short terminal intake, saved task/source/output choices, read-only non-TTY plan and host handoff.
39
+ - Add bounded configuration backup/restore, dry-run conflicts, explicit replacement and before-backup; transfer only module/model preferences with old repo autonomy removed.
40
+ - Keep canonical user config paths, ignore local backups, preserve settings through update, and document config vs full-project recovery in all README entry points.
41
+ - Validate archive/config/path safety before writes; add rollback/transfer/intake tests, package coverage and unscored host scenarios. No model/network calls or distribution bump.
42
+
3
43
  ## Unreleased — Kiro and clearer tool setup
4
44
 
5
45
  - Add Kiro to all README languages and a documented IDE/CLI/Web setup guide with optional native skill pointers.
@@ -139,6 +179,23 @@
139
179
  - Check release metadata consistency and establish single-release-PR version discipline; retain v0.6.0 until a dedicated release PR.
140
180
 
141
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
+
192
+ ## [0.14.0](https://github.com/manh0303/ai-harness/compare/v0.13.0...v0.14.0) (2026-10-14)
193
+
194
+
195
+ ### Features
196
+
197
+ * add guided start and portable user configuration ([2f18d23](https://github.com/manh0303/ai-harness/commit/2f18d23d92811858a3b722b349f423694bfcf3e2))
198
+
142
199
  ## [0.13.0](https://github.com/manh0303/ai-harness/compare/v0.12.0...v0.13.0) (2026-10-13)
143
200
 
144
201
 
package/README.md CHANGED
@@ -10,20 +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
16
+ npx --yes @manhd_d/ai-harness init --host codex
17
17
  ```
18
18
 
19
- `npx` does not install a global `aih` command. Check the installation with
20
- `npx --yes @manhd_d/ai-harness check`; then open the same project in your AI tool.
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
- Then ask your AI tool:
22
+ > Read .agents/skills/harness-start/SKILL.md and follow it for this project. My task: [task].
23
23
 
24
- > Read AGENTS.md. Inspect this project and its approved decisions. Implement [task], run appropriate checks and maintain the affected docs.
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.
25
27
 
26
- 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.
28
+ **pip / restricted networks** (included in the next reviewed release; PyPI publication requires maintainer setup):
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).
27
42
 
28
43
  ## Where it runs
29
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.
package/bin/aih.js CHANGED
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
2
  import { main } from '../cli/main.js';
3
3
 
4
- process.exitCode = main();
4
+ process.exitCode = await main();
@@ -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,8 +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';
5
+ import { loadStartConfig } from './start-config.js';
6
+ import { PROJECT_DECISIONS, emptyProjectDecisions, loadProjectDecisions, projectDecisionReport } from './project-decisions.js';
2
7
  import { checkpointPath, validateCheckpoint } from './checkpoint.js';
3
8
  import { readConfig } from './project-config.js';
4
9
  import { claudeReport, claudeBlock, linkClaude } from './claude.js';
5
- import { emptyEnvironment, environmentReport } from './environment.js';
10
+ import { emptyEnvironment, environmentReport, loadEnvironment } from './environment.js';
6
11
  import { AGENT_CONFIG, agentDefaults, loadAgentConfig } from './agents.js';
7
12
  import { reviewVerification, techspecEvidence } from './verification-report.js';
8
13
  import { ARTIFACTS, INTEGRATIONS, emptyArtifacts, emptyIntegrations, loadArtifactContract, artifactReport, docRoute } from './artifacts.js';
@@ -14,7 +19,7 @@ import { resolveModules } from './composition.js';
14
19
  import { safe, readJson, manifest, encode, digest, stat, block, mergeAgents, payload, validateAdapters, validateCapabilities, assertSeparate, HarnessError, LOCK, CONFIG, VERSION, SLUG, isObject } from './files.js';
15
20
  export * from './files.js';
16
21
 
17
- function projectConfig(root, adapters, capabilities, required = false) {
22
+ export function projectConfig(root, adapters, capabilities, required = false) {
18
23
  const filename = safe(root, CONFIG);
19
24
  if (required && !stat(filename)) throw new HarnessError(`Missing ${CONFIG}; restore it from Git`);
20
25
  const config = stat(filename) ? readJson(filename) : { schema_version: 1, adapters: adapters ?? [], capabilities: capabilities ?? [] };
@@ -94,7 +99,7 @@ export function bundle(source, config) {
94
99
  }
95
100
 
96
101
  function managedName(name) {
97
- 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));
98
103
  }
99
104
 
100
105
  export function loadLock(root) {
@@ -120,7 +125,7 @@ function drift(root, lock) {
120
125
  }).map(([name]) => name);
121
126
  }
122
127
 
123
- function guard(root, operation) {
128
+ export function guard(root, operation) {
124
129
  const filename = safe(root, '.harness-install.lock');
125
130
  let fd;
126
131
  try { fd = fs.openSync(filename, 'wx', 0o600); }
@@ -132,12 +137,24 @@ function guard(root, operation) {
132
137
  finally { fs.closeSync(fd); fs.unlinkSync(filename); }
133
138
  }
134
139
 
135
- export function atomicWrite(filename, data) {
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
+
152
+ export function atomicWrite(filename, data, mode = 0o644) {
136
153
  fs.mkdirSync(path.dirname(filename), { recursive: true });
137
154
  const tmp = path.join(path.dirname(filename), `.harness-tmp-${randomUUID()}`);
138
155
  try {
139
156
  fs.writeFileSync(tmp, data, { flag: 'wx', mode: 0o600 });
140
- fs.chmodSync(tmp, stat(filename) ? fs.statSync(filename).mode & 0o777 : 0o644);
157
+ fs.chmodSync(tmp, stat(filename) ? fs.statSync(filename).mode & 0o777 : mode);
141
158
  fs.renameSync(tmp, filename);
142
159
  } finally { fs.rmSync(tmp, { force: true }); }
143
160
  }
@@ -172,6 +189,10 @@ export function install(root, source, { command = 'init', adapters, capabilities
172
189
  if (command === 'update' && !hasLock) throw new HarnessError('Not installed; run init first');
173
190
  const old = hasLock ? loadLock(root) : null;
174
191
  const existingAgents = loadAgentConfig(root);
192
+ loadStartConfig(root);
193
+ loadProjectDecisions(root);
194
+ loadEnvironment(root);
195
+ localContext(root);
175
196
  loadArtifactContract(root); // Validate project-owned declarations before planning any writes.
176
197
  if (agentPreset !== undefined && command !== 'init') throw new HarnessError('Agent preset is supported only by init');
177
198
  if (agentPreset !== undefined && existingAgents) throw new HarnessError('agents.json is project-owned; edit it explicitly instead of applying an init preset');
@@ -194,6 +215,9 @@ export function install(root, source, { command = 'init', adapters, capabilities
194
215
  lock.files[destination] = digest(files[destination]);
195
216
  }
196
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
+ }
197
221
  lock.files = Object.fromEntries(Object.entries(lock.files).sort(([a],[b]) => a.localeCompare(b)));
198
222
  lock.bundle_sha256 = digest(encode(lock.files));
199
223
  if (old) {
@@ -221,18 +245,15 @@ export function install(root, source, { command = 'init', adapters, capabilities
221
245
  [CONFIG]: encode(config),
222
246
  [ARTIFACTS]: encode(emptyArtifacts()),
223
247
  [INTEGRATIONS]: encode(emptyIntegrations()),
248
+ [PROJECT_DECISIONS]: encode(emptyProjectDecisions()),
224
249
  '.harness/environment.json': encode(emptyEnvironment()),
225
250
  'openspec/config.yaml': fs.readFileSync(safe(source, 'templates/openspec/config.yaml')),
226
- '.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'),
227
252
  };
228
253
  for (const [name, data] of Object.entries(seeds)) if (!stat(safe(root, name))) changes[name] = data;
229
254
  // Project-owned ignore file: preserve all prior rules, place our anchored rule last.
230
255
  // This does not remove an already tracked machine file from the Git index.
231
- const ignoreName = '.harness/.gitignore', ignorePath = safe(root, ignoreName);
232
- const ignore = stat(ignorePath) ? fs.readFileSync(ignorePath, 'utf8') : '';
233
- if (ignore.trimEnd().split(/\r?\n/).at(-1) !== '/machine.json') {
234
- changes[ignoreName] = Buffer.from(ignore + (ignore && !ignore.endsWith('\n') ? '\n' : '') + '/machine.json\n');
235
- }
256
+ Object.assign(changes, userIgnoreChanges(root));
236
257
  const newLock = encode(lock);
237
258
  if (!hasLock || !fs.readFileSync(safe(root, LOCK)).equals(newLock)) changes[LOCK] = newLock;
238
259
  const plan = Object.entries(changes).map(([name, data]) => ({ path: name, action: data === null ? 'delete' : 'write' }));
@@ -245,12 +266,33 @@ export function check(root) {
245
266
  const lock = loadLock(root), config = projectConfig(root, undefined, undefined, true);
246
267
  const problems = drift(root, lock);
247
268
  loadArtifactContract(root);
269
+ loadStartConfig(root);
270
+ loadProjectDecisions(root);
271
+ localContext(root);
248
272
  loadAgentConfig(root); // Optional on legacy installs; validate user config without managing its bytes.
249
273
  if (JSON.stringify([...config.adapters].sort()) !== JSON.stringify([...lock.adapters].sort())) problems.push(`${CONFIG}: adapter selection changed; run update`);
250
274
  if (JSON.stringify([...(config.capabilities ?? [])].sort()) !== JSON.stringify([...(lock.capabilities ?? [])].sort())) problems.push(`${CONFIG}: capability selection changed; run update`);
251
275
  return { status: problems.length ? 'error' : 'ok', version: lock.version, problems };
252
276
  }
253
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
+
254
296
  export function newChange(root, name) {
255
297
  if (!SLUG.test(name) || name === 'archive') throw new HarnessError('Change ID must be a lowercase hyphenated slug, excluding archive');
256
298
  return guard(root, () => {
@@ -317,7 +359,7 @@ export function newDoc(root, name, capability = 'product', options = {}) {
317
359
  });
318
360
  }
319
361
 
320
- export function doctor(root) {
362
+ export function doctor(root, skill) {
321
363
  const extensions = process.platform === 'win32' ? (process.env.PATHEXT || '.EXE;.CMD;.BAT').split(';') : [''];
322
364
  const tools = {};
323
365
  for (const name of ['git', 'node', 'npm', 'java', 'mvn', 'openspec']) {
@@ -327,8 +369,11 @@ export function doctor(root) {
327
369
  }));
328
370
  }
329
371
  const state = check(root), environment = environmentReport(root);
330
- return { ...state, status: state.status !== 'ok' || environment.problems.length ? 'error' : 'ok',
331
- 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.' };
332
377
  }
333
378
 
334
379
  export function saveCheckpoint(root, id, report, expected) {