@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.
- package/CHANGELOG.md +57 -0
- package/README.md +22 -7
- 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/bin/aih.js +1 -1
- 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 +60 -15
- package/cli/journey-evidence.js +34 -0
- package/cli/kiro.js +15 -0
- package/cli/knowledge.js +104 -0
- package/cli/main.js +80 -19
- package/cli/portable-config.js +124 -0
- 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-config.js +15 -0
- package/cli/start.js +151 -0
- 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 +97 -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 +30 -1
- 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 +11 -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 +53 -3
- package/docs/cli.md +55 -3
- package/docs/configuration-backup.md +130 -0
- 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 +21 -7
- package/docs/harness-maintenance.md +1 -0
- package/docs/i18n/README.es.md +18 -5
- package/docs/i18n/README.vi.md +19 -6
- package/docs/i18n/README.zh-CN.md +18 -5
- package/docs/project-configuration.md +15 -1
- 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 +43 -0
- 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 +67 -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 -0
- 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,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
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
22
|
+
> Read .agents/skills/harness-start/SKILL.md and follow it for this project. My task: [task].
|
|
23
23
|
|
|
24
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -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.
|
package/bin/aih.js
CHANGED
|
@@ -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,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
|
|
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 :
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
331
|
-
|
|
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) {
|