@manhd_d/ai-harness 0.6.2 → 0.8.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 +52 -0
- package/CONTRIBUTING.md +4 -0
- package/README.md +20 -3
- package/adapters/java-spring/adapter.yaml +3 -2
- package/adapters/java-spring/checks/verification.yaml +12 -0
- package/adapters/java-spring/evals/cases.json +10 -0
- package/adapters/java-spring/rules/conventions.md +39 -0
- package/adapters/java-spring/rules.md +2 -1
- package/adapters/kubernetes/adapter.yaml +1 -1
- package/adapters/kubernetes/rules.md +3 -0
- package/adapters/react-native/adapter.yaml +3 -2
- package/adapters/react-native/checks/verification.yaml +12 -0
- package/adapters/react-native/evals/cases.json +10 -0
- package/adapters/react-native/rules/conventions.md +34 -0
- package/adapters/react-native/rules.md +2 -1
- package/adapters/react-nextjs/adapter.yaml +3 -2
- package/adapters/react-nextjs/checks/verification.yaml +12 -0
- package/adapters/react-nextjs/evals/cases.json +10 -0
- package/adapters/react-nextjs/rules/conventions.md +37 -0
- package/adapters/react-nextjs/rules.md +2 -1
- package/capabilities/architecture/README.md +11 -0
- package/capabilities/architecture/agents/architecture-reviewer.md +16 -0
- package/capabilities/architecture/agents/solution-architect.md +16 -0
- package/capabilities/architecture/capability.json +35 -0
- package/capabilities/architecture/checks/verification.json +29 -0
- package/capabilities/architecture/evals/cases.json +35 -0
- package/capabilities/architecture/skills/harness-architecture-change-impact/SKILL.md +43 -0
- package/capabilities/architecture/skills/harness-architecture-decision-records/SKILL.md +43 -0
- package/capabilities/architecture/skills/harness-architecture-project-context/SKILL.md +43 -0
- package/capabilities/architecture/skills/harness-architecture-quality-attributes/SKILL.md +43 -0
- package/capabilities/architecture/skills/harness-architecture-review/SKILL.md +43 -0
- package/capabilities/architecture/skills/harness-architecture-solution-design/SKILL.md +44 -0
- package/capabilities/architecture/templates/adr.md +29 -0
- package/capabilities/architecture/templates/nfr.md +22 -0
- package/capabilities/architecture/templates/overview.md +24 -0
- package/capabilities/architecture/templates/project-map.md +26 -0
- package/capabilities/architecture/templates/review.md +23 -0
- package/capabilities/architecture/templates/solution-design.md +29 -0
- package/capabilities/architecture/workflows/change-review.md +12 -0
- package/capabilities/architecture/workflows/discovery-to-design.md +16 -0
- package/capabilities/delivery/README.md +11 -0
- package/capabilities/delivery/agents/project-manager.md +16 -0
- package/capabilities/delivery/capability.json +29 -0
- package/capabilities/delivery/checks/verification.json +29 -0
- package/capabilities/delivery/evals/cases.json +25 -0
- package/capabilities/delivery/skills/harness-delivery-planning/SKILL.md +43 -0
- package/capabilities/delivery/skills/harness-delivery-risk-management/SKILL.md +42 -0
- package/capabilities/delivery/skills/harness-delivery-scope-change/SKILL.md +43 -0
- package/capabilities/delivery/templates/change-assessment.md +23 -0
- package/capabilities/delivery/templates/delivery-plan.md +25 -0
- package/capabilities/delivery/templates/risk-register.md +19 -0
- package/capabilities/delivery/workflows/execution-change.md +9 -0
- package/capabilities/delivery/workflows/planning.md +12 -0
- package/capabilities/product/README.md +7 -0
- package/capabilities/product/agents/business-analyst.md +3 -0
- package/capabilities/product/agents/product-owner.md +5 -1
- package/capabilities/product/agents/project-manager.md +5 -0
- package/capabilities/product/capability.json +9 -4
- package/capabilities/product/checks/verification.json +12 -0
- package/capabilities/product/evals/cases.json +10 -0
- package/capabilities/product/skills/harness-product-acceptance-criteria/SKILL.md +17 -0
- package/capabilities/product/skills/harness-product-backlog-prioritization/SKILL.md +17 -0
- package/capabilities/product/skills/harness-product-brd-generation/SKILL.md +17 -0
- package/capabilities/product/skills/harness-product-business-analysis/SKILL.md +17 -0
- package/capabilities/product/skills/harness-product-document-review/SKILL.md +17 -0
- package/capabilities/product/skills/harness-product-idea-discovery/SKILL.md +17 -0
- package/capabilities/product/skills/harness-product-knowledge-analysis/SKILL.md +17 -0
- package/capabilities/product/skills/harness-product-mvp-definition/SKILL.md +43 -0
- package/capabilities/product/skills/harness-product-prd-generation/SKILL.md +17 -0
- package/capabilities/product/skills/harness-product-product-outcomes/SKILL.md +42 -0
- package/capabilities/product/skills/harness-product-product-research/SKILL.md +17 -0
- package/capabilities/product/skills/harness-product-srs-generation/SKILL.md +17 -0
- package/capabilities/product/skills/harness-product-traceability/SKILL.md +17 -0
- package/capabilities/product/skills/harness-product-use-case-generation/SKILL.md +17 -0
- package/capabilities/product/skills/harness-product-user-stories/SKILL.md +17 -0
- package/capabilities/product/templates/mvp.md +25 -0
- package/capabilities/product/templates/outcomes.md +23 -0
- package/capabilities/product/workflows/document-driven.md +4 -0
- package/capabilities/product/workflows/idea-driven.md +4 -0
- package/capabilities/product/workflows/product-to-solution.md +15 -0
- package/cli/agents.js +165 -0
- package/cli/environment.js +170 -0
- package/cli/harness.js +40 -10
- package/cli/main.js +47 -11
- package/cli/readiness.js +117 -0
- package/config/agents.default.json +80 -0
- package/core/agents/documentation.md +13 -0
- package/core/agents/implementer.md +7 -0
- package/core/agents/orchestrator.md +13 -0
- package/core/agents/planner.md +7 -0
- package/core/agents/reviewer.md +8 -0
- package/core/checks/verification.json +37 -0
- package/core/evals/cases.json +101 -0
- package/core/evals/composition.json +15 -0
- package/core/rules/foundations.md +1 -0
- package/core/rules/project-conventions.md +61 -0
- package/core/skills/harness-review/SKILL.md +2 -0
- package/core/skills/harness-spec/SKILL.md +6 -2
- package/core/workflows/agent-routing.md +28 -0
- package/core/workflows/bugfix.md +2 -0
- package/core/workflows/clarification.md +28 -0
- package/core/workflows/project-setup.md +37 -0
- package/core/workflows/solution-framing.md +33 -0
- package/core/workflows/spec-to-code.md +18 -3
- package/docs/adapters.md +6 -0
- package/docs/agent-configuration.md +99 -0
- package/docs/agent-handoffs.md +19 -0
- package/docs/architecture.md +33 -1
- package/docs/change-readiness.md +67 -0
- package/docs/cli.md +41 -3
- package/docs/documentation-workflows.md +7 -0
- package/docs/end-to-end-workflow.md +10 -1
- package/docs/getting-started.md +29 -2
- package/docs/harness-maintenance.md +80 -0
- package/docs/monetization.md +1 -1
- package/docs/project-configuration.md +86 -0
- package/docs/project-conventions.md +67 -0
- package/docs/research/solution-workflow-sources.md +30 -0
- package/docs/skill-matrix.md +28 -4
- package/docs/solution-workflow.md +67 -0
- package/docs/templates/harness-change-impact.md +30 -0
- package/docs/tool-setup/README.md +70 -0
- package/docs/tool-setup/claude-code.md +57 -0
- package/docs/tool-setup/codex.md +44 -0
- package/docs/tool-setup/copilot.md +42 -0
- package/docs/tool-setup/cursor.md +44 -0
- package/docs/tool-setup/gemini-cli.md +41 -0
- package/docs/versioning.md +37 -0
- package/evals/README.md +16 -0
- package/evals/real-task-comparison.md +49 -0
- package/harness.yaml +4 -2
- package/package.json +2 -1
- package/templates/AGENTS.md +13 -1
- package/templates/environment.json +6 -0
- package/templates/openspec/decisions.json +5 -0
- package/templates/openspec/design.md +6 -0
- package/templates/openspec/tasks.md +1 -0
- package/templates/openspec/verification.json +4 -0
|
@@ -24,3 +24,20 @@ description: Translate requirement and story IDs into consistent main, alternati
|
|
|
24
24
|
- Never imply a draft flow is accepted without sign-off.
|
|
25
25
|
- Avoid duplicating product scope in a competing document; reference established IDs.
|
|
26
26
|
- No assumptions about system behavior presented as approved facts.
|
|
27
|
+
|
|
28
|
+
## Template
|
|
29
|
+
Read `.harness/capabilities/product/templates/use-cases.md`; `aih new-doc use-cases` creates only a missing
|
|
30
|
+
draft. Reuse canonical existing documents and source pointers; never regenerate approved content by default.
|
|
31
|
+
|
|
32
|
+
## Example
|
|
33
|
+
Synthetic illustration: UC-001 contains main lookup, no-result, permission denial and provider timeout paths linked to FR/AC.
|
|
34
|
+
|
|
35
|
+
## Quality checklist
|
|
36
|
+
- Actor, precondition, result and alternate/error flows are explicit.
|
|
37
|
+
- Actual decisions/status and source revisions distinguish approved facts from proposals.
|
|
38
|
+
- Scope, relevant IDs, unresolved questions and verification limits are recorded.
|
|
39
|
+
|
|
40
|
+
## Handoff
|
|
41
|
+
Use `.harness/capabilities/product/workflows/product-to-solution.md`; supply reviewed source/requirement
|
|
42
|
+
IDs, output paths and actual owner decisions to the next BA/PO/SA/PM role as applicable. Return gaps
|
|
43
|
+
to their owner; a single host may perform roles sequentially without claiming independent approval.
|
|
@@ -23,3 +23,20 @@ description: Turn approved requirements into atomic epics and user stories with
|
|
|
23
23
|
## Constraints
|
|
24
24
|
- Do not fabricate persona needs or change scope via user-story rewriting.
|
|
25
25
|
- A story marked Ready requires actual acceptance criteria and PO review.
|
|
26
|
+
|
|
27
|
+
## Template
|
|
28
|
+
Read `.harness/capabilities/product/templates/backlog.md`; `aih new-doc backlog` creates only a missing
|
|
29
|
+
draft. Reuse canonical existing documents and source pointers; never regenerate approved content by default.
|
|
30
|
+
|
|
31
|
+
## Example
|
|
32
|
+
Synthetic illustration: US-001: authorized employee finds a customer; links FR-001 and AC-US-001-01, with access-denial criteria.
|
|
33
|
+
|
|
34
|
+
## Quality checklist
|
|
35
|
+
- Story has user value, dependency and observable acceptance.
|
|
36
|
+
- Actual decisions/status and source revisions distinguish approved facts from proposals.
|
|
37
|
+
- Scope, relevant IDs, unresolved questions and verification limits are recorded.
|
|
38
|
+
|
|
39
|
+
## Handoff
|
|
40
|
+
Use `.harness/capabilities/product/workflows/product-to-solution.md`; supply reviewed source/requirement
|
|
41
|
+
IDs, output paths and actual owner decisions to the next BA/PO/SA/PM role as applicable. Return gaps
|
|
42
|
+
to their owner; a single host may perform roles sequentially without claiming independent approval.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# MVP scope
|
|
2
|
+
|
|
3
|
+
Status: draft / pending review
|
|
4
|
+
Owner: TODO
|
|
5
|
+
Sources and revisions: TODO
|
|
6
|
+
Scope / related requirement and decision IDs: TODO
|
|
7
|
+
Last verified against code: TODO (or not applicable with reason)
|
|
8
|
+
|
|
9
|
+
## User and outcome
|
|
10
|
+
Target actor, problem, value-bearing end-to-end journey and validation evidence.
|
|
11
|
+
## Scope cuts
|
|
12
|
+
| Capability | In / out | Value / dependency | Requirement / AC | Decision source |
|
|
13
|
+
| --- | --- | --- | --- | --- |
|
|
14
|
+
| TODO | TODO | TODO | TODO | TODO |
|
|
15
|
+
## Minimum viable quality
|
|
16
|
+
Authorization, data integrity, failure recovery and relevant NFRs that cannot be deferred.
|
|
17
|
+
## Acceptance and learning
|
|
18
|
+
Observable journey completion, success signals, measurement owner, known baseline/target or unknown.
|
|
19
|
+
## Open decisions
|
|
20
|
+
Options/trade-offs, recommended cut with evidence and actual owner answer; unanswered choices remain pending.
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
## Review and handoff
|
|
24
|
+
Record actual owner decisions, unresolved consequential questions, verification performed/skipped,
|
|
25
|
+
next role, source/output paths and affected OpenSpec change. Draft text is not an accepted decision.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Product outcomes
|
|
2
|
+
|
|
3
|
+
Status: draft / pending review
|
|
4
|
+
Owner: TODO
|
|
5
|
+
Sources and revisions: TODO
|
|
6
|
+
Scope / related requirement and decision IDs: TODO
|
|
7
|
+
Last verified against code: TODO (or not applicable with reason)
|
|
8
|
+
|
|
9
|
+
## Value hypothesis
|
|
10
|
+
User problem and expected benefit; link MVP/PRD instead of duplicating scope.
|
|
11
|
+
## Measurement plan
|
|
12
|
+
| Outcome / metric | Definition / source | Baseline | Proposed target / authority | Owner / period |
|
|
13
|
+
| --- | --- | --- | --- | --- |
|
|
14
|
+
| TODO | TODO | unknown | pending | TODO |
|
|
15
|
+
## Experiment and guardrails
|
|
16
|
+
Hypothesis, smallest experiment, cost/privacy limits, negative effects and decision checkpoint.
|
|
17
|
+
## Evidence and next action
|
|
18
|
+
Separate observed results from forecasts; propose continue/pivot/stop without self-approving scope changes.
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
## Review and handoff
|
|
22
|
+
Record actual owner decisions, unresolved consequential questions, verification performed/skipped,
|
|
23
|
+
next role, source/output paths and affected OpenSpec change. Draft text is not an accepted decision.
|
|
@@ -11,3 +11,7 @@ Use when a project already supplies authorized BA/PO materials (Confluence, BRD,
|
|
|
11
11
|
7. **Source changes:** compare actual source revisions and propose a reviewed requirements/code impact diff rather than overwriting user-owned artifacts.
|
|
12
12
|
|
|
13
13
|
Canonical enterprise source stays in the enterprise system; consumer repo stores pointers, versioned derived docs and project-local engineering artifacts. RAG is optional permission-scoped retrieval, not a replacement for a system of record. The `aih` CLI does not ingest data or run agents.
|
|
14
|
+
|
|
15
|
+
For new/material journeys, continue with `product-to-solution.md` after resolving the relevant scope.
|
|
16
|
+
Use reviewed MVP/outcomes and optional Architecture/Delivery to assess solution/dependencies before
|
|
17
|
+
OpenSpec implementation. Existing approved sources remain canonical; small clear changes stay compact.
|
|
@@ -11,3 +11,7 @@ Use this when the developer starts with an idea, including a solo project. Produ
|
|
|
11
11
|
7. **Changes:** when the idea changes, perform impact analysis and propose minimal document diffs; never silently overwrite approved requirements.
|
|
12
12
|
|
|
13
13
|
Deliverables are **consumer-owned** `docs/discovery/`, `docs/product/` and `openspec/` files. For Confluence publishing, preserve URLs/page versions and request explicit write permission; the Harness CLI does not connect or publish to Confluence.
|
|
14
|
+
|
|
15
|
+
For new/material journeys, continue with `product-to-solution.md` after resolving the relevant scope.
|
|
16
|
+
Use reviewed MVP/outcomes and optional Architecture/Delivery to assess solution/dependencies before
|
|
17
|
+
OpenSpec implementation. Existing approved sources remain canonical; small clear changes stay compact.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# BA → PO → Solution → Delivery
|
|
2
|
+
|
|
3
|
+
Use for a new product or material feature. For a small clear fix, use the Core compact path.
|
|
4
|
+
1. BA preserves idea/approved document revisions, identifies users, business rules and contradictions;
|
|
5
|
+
load existing business-analysis/use-case/AC skills. Source/output register is authoritative.
|
|
6
|
+
2. PO uses mvp-definition and product-outcomes to propose a complete journey, in/out scope, minimum
|
|
7
|
+
quality and success evidence. Resolve consequential unknowns with actual owner; no self-approval.
|
|
8
|
+
3. Pass reviewed FR/US/AC/NFR and decision status to installed Architecture discovery-to-design.
|
|
9
|
+
If Architecture is not selected, Core spec uses the same constraints and records relevant choices.
|
|
10
|
+
4. Pass accepted solution boundaries, AC, dependencies and actual estimates to optional Delivery planning;
|
|
11
|
+
absent Delivery, record a proportional task order in the OpenSpec change.
|
|
12
|
+
5. UI/UX can iterate with SA/PO; adapters implement accepted technical decisions. Verify and review
|
|
13
|
+
against requirements/ADRs. Evidence and scope changes loop back to the owning role.
|
|
14
|
+
6. Carry source revisions, IDs, output paths, decisions, unresolved questions, actual checks and next
|
|
15
|
+
owner in every handoff. A role label never creates a worker or approves a stakeholder decision.
|
package/cli/agents.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { safe, stat, readJson, isObject, SLUG, SOURCE, HarnessError } from './files.js';
|
|
4
|
+
|
|
5
|
+
export const AGENT_CONFIG = '.harness/agents.json';
|
|
6
|
+
export const DEFAULT_AGENTS = 'config/agents.default.json';
|
|
7
|
+
export const PRESETS = ['recommended', 'economy', 'custom'];
|
|
8
|
+
export const REASONING = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'];
|
|
9
|
+
const ROLE = /^[a-z][a-z0-9-]*:[a-z][a-z0-9-]*$/;
|
|
10
|
+
const text = value => typeof value === 'string' && value.trim() === value && value.length > 0
|
|
11
|
+
&& value.length <= 256 && !/[\x00-\x1f\x7f]/.test(value);
|
|
12
|
+
const requireThat = (condition, label) => { if (!condition) throw new HarnessError('Invalid agent configuration: ' + label); };
|
|
13
|
+
function fields(value, allowed, required, label) {
|
|
14
|
+
requireThat(isObject(value) && Object.keys(value).every(key => allowed.includes(key))
|
|
15
|
+
&& required.every(key => Object.hasOwn(value, key)), label + ' fields');
|
|
16
|
+
}
|
|
17
|
+
function reference(value, label) {
|
|
18
|
+
requireThat(text(value), label + ' must be nonempty single-line text (max 256 characters)');
|
|
19
|
+
if (value.startsWith('@')) requireThat(SLUG.test(value.slice(1)), label + ' alias syntax');
|
|
20
|
+
else requireThat(!/\s/.test(value), label + ' model ID must not contain whitespace');
|
|
21
|
+
}
|
|
22
|
+
function provider(value, label) { requireThat(typeof value === 'string' && SLUG.test(value), label + ' provider ID'); }
|
|
23
|
+
function reasoning(value, label) { requireThat(REASONING.includes(value), label + ' reasoning level'); }
|
|
24
|
+
|
|
25
|
+
export function resolveModel(config, referenceValue, visited = []) {
|
|
26
|
+
if (!referenceValue.startsWith('@')) return { model: referenceValue, provider: config.provider };
|
|
27
|
+
const alias = referenceValue.slice(1);
|
|
28
|
+
requireThat(Object.hasOwn(config.models, alias), 'unknown alias @' + alias);
|
|
29
|
+
requireThat(!visited.includes(alias), 'cyclic alias @' + alias);
|
|
30
|
+
const descriptor = config.models[alias];
|
|
31
|
+
const next = typeof descriptor === 'string' ? descriptor : descriptor.model;
|
|
32
|
+
const resolved = resolveModel(config, next, [...visited, alias]);
|
|
33
|
+
return { ...resolved, provider: typeof descriptor === 'object' ? descriptor.provider ?? resolved.provider : resolved.provider };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function validateAgentConfig(config) {
|
|
37
|
+
fields(config, ['schema_version', 'provider', 'defaultModel', 'defaultReasoning', 'models', 'agents', 'execution', 'hosts'],
|
|
38
|
+
['schema_version', 'provider', 'defaultModel', 'defaultReasoning', 'models', 'agents', 'execution'], 'root');
|
|
39
|
+
requireThat(config.schema_version === 1, 'unsupported schema_version');
|
|
40
|
+
provider(config.provider, 'default'); reference(config.defaultModel, 'defaultModel');
|
|
41
|
+
reasoning(config.defaultReasoning, 'default');
|
|
42
|
+
requireThat(isObject(config.models) && Object.keys(config.models).length <= 64, 'models map (max 64 aliases)');
|
|
43
|
+
for (const [alias, descriptor] of Object.entries(config.models)) {
|
|
44
|
+
requireThat(SLUG.test(alias), 'alias ID');
|
|
45
|
+
if (typeof descriptor === 'string') reference(descriptor, 'models.' + alias);
|
|
46
|
+
else {
|
|
47
|
+
fields(descriptor, ['model', 'provider'], ['model'], 'models.' + alias);
|
|
48
|
+
reference(descriptor.model, 'models.' + alias);
|
|
49
|
+
if (descriptor.provider !== undefined) provider(descriptor.provider, 'models.' + alias);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
resolveModel(config, config.defaultModel);
|
|
53
|
+
for (const alias of Object.keys(config.models)) resolveModel(config, '@' + alias);
|
|
54
|
+
requireThat(isObject(config.agents) && Object.keys(config.agents).length <= 128, 'agents map (max 128 roles)');
|
|
55
|
+
for (const [id, agent] of Object.entries(config.agents)) {
|
|
56
|
+
requireThat(ROLE.test(id), 'role ID must use namespace:role');
|
|
57
|
+
fields(agent, ['model', 'provider', 'reasoning', 'enabled'], [], id);
|
|
58
|
+
if (agent.model !== undefined) { reference(agent.model, id); resolveModel(config, agent.model); }
|
|
59
|
+
if (agent.provider !== undefined) provider(agent.provider, id);
|
|
60
|
+
if (agent.reasoning !== undefined) reasoning(agent.reasoning, id);
|
|
61
|
+
requireThat(agent.enabled === undefined || typeof agent.enabled === 'boolean', id + ' enabled');
|
|
62
|
+
}
|
|
63
|
+
const execution = config.execution;
|
|
64
|
+
fields(execution, ['mode', 'maxConcurrentAgents', 'maxRounds', 'autoEscalateToPremium'],
|
|
65
|
+
['mode', 'maxConcurrentAgents', 'maxRounds', 'autoEscalateToPremium'], 'execution');
|
|
66
|
+
requireThat(['single-agent', 'hybrid', 'multi-agent'].includes(execution.mode), 'execution mode');
|
|
67
|
+
requireThat(Number.isSafeInteger(execution.maxConcurrentAgents) && execution.maxConcurrentAgents >= 1
|
|
68
|
+
&& execution.maxConcurrentAgents <= 32, 'maxConcurrentAgents must be an integer from 1 to 32');
|
|
69
|
+
requireThat(execution.mode !== 'single-agent' || execution.maxConcurrentAgents === 1, 'single-agent requires maxConcurrentAgents=1');
|
|
70
|
+
requireThat(Number.isSafeInteger(execution.maxRounds) && execution.maxRounds >= 1 && execution.maxRounds <= 20,
|
|
71
|
+
'maxRounds must be an integer from 1 to 20');
|
|
72
|
+
requireThat(execution.autoEscalateToPremium === false, 'autoEscalateToPremium must be false; select premium explicitly');
|
|
73
|
+
const hosts = config.hosts ?? {};
|
|
74
|
+
requireThat(isObject(hosts) && Object.keys(hosts).length <= 16, 'hosts map (max 16 profiles)');
|
|
75
|
+
for (const [id, profile] of Object.entries(hosts)) {
|
|
76
|
+
requireThat(SLUG.test(id), 'host ID'); fields(profile, ['models', 'reasoning'], [], 'host ' + id);
|
|
77
|
+
for (const key of ['models', 'reasoning']) {
|
|
78
|
+
const map = profile[key] ?? {};
|
|
79
|
+
requireThat(isObject(map) && Object.keys(map).length <= 128, 'host ' + id + ' ' + key + ' map');
|
|
80
|
+
for (const [from, to] of Object.entries(map)) {
|
|
81
|
+
requireThat(key === 'models' ? text(from) && from.includes('/') : REASONING.includes(from), 'host mapping key');
|
|
82
|
+
requireThat(text(to) && !to.startsWith('@'), 'host mapping target must be explicit native text');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return config;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function readConfiguration(root, relative) {
|
|
90
|
+
const filename = safe(root, relative), info = stat(filename);
|
|
91
|
+
if (!info) return null;
|
|
92
|
+
requireThat(info.isFile() && info.size <= 1024 * 1024, relative + ' must be a file up to 1 MiB');
|
|
93
|
+
let config;
|
|
94
|
+
try { config = JSON.parse(fs.readFileSync(filename, 'utf8')); }
|
|
95
|
+
catch { throw new HarnessError('Invalid JSON in ' + relative); }
|
|
96
|
+
return validateAgentConfig(config);
|
|
97
|
+
}
|
|
98
|
+
export function loadAgentConfig(root) { return readConfiguration(root, AGENT_CONFIG); }
|
|
99
|
+
|
|
100
|
+
export function agentDefaults(source = SOURCE, preset = 'recommended') {
|
|
101
|
+
requireThat(PRESETS.includes(preset), 'unknown preset; use recommended, economy or custom');
|
|
102
|
+
// Older alternate bundles remain usable; the packaged CLI supplies the new canonical seed.
|
|
103
|
+
const config = structuredClone(readConfiguration(source, DEFAULT_AGENTS) ?? readConfiguration(SOURCE, DEFAULT_AGENTS));
|
|
104
|
+
requireThat(config !== null, 'missing canonical default');
|
|
105
|
+
if (preset === 'economy') {
|
|
106
|
+
config.defaultModel = '@economy'; config.defaultReasoning = 'low';
|
|
107
|
+
config.execution.mode = 'single-agent'; config.execution.maxConcurrentAgents = 1;
|
|
108
|
+
for (const agent of Object.values(config.agents)) { agent.model = '@economy'; agent.reasoning = 'low'; }
|
|
109
|
+
}
|
|
110
|
+
return validateAgentConfig(config);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function registeredRoles(source, capabilities = []) {
|
|
114
|
+
const roles = {};
|
|
115
|
+
const folder = path.dirname(safe(source, 'core/agents/.role-index'));
|
|
116
|
+
for (const name of fs.readdirSync(folder).filter(name => name.endsWith('.md')).sort()) {
|
|
117
|
+
roles['core:' + name.slice(0, -3)] = 'core/agents/' + name;
|
|
118
|
+
}
|
|
119
|
+
for (const id of capabilities) {
|
|
120
|
+
const metadata = readJson(safe(source, `capabilities/${id}/capability.json`));
|
|
121
|
+
for (const file of metadata.agents ?? []) {
|
|
122
|
+
const role = id + ':' + path.basename(file, '.md');
|
|
123
|
+
roles[role] = `capabilities/${id}/${file}`;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return roles;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function resolveAgent(config, id) {
|
|
130
|
+
const agent = Object.hasOwn(config.agents, id) ? config.agents[id] : {};
|
|
131
|
+
const resolved = resolveModel(config, agent.model ?? config.defaultModel);
|
|
132
|
+
return { id, ...resolved, provider: agent.provider ?? resolved.provider,
|
|
133
|
+
reasoning: agent.reasoning ?? config.defaultReasoning, enabled: agent.enabled !== false,
|
|
134
|
+
source: Object.hasOwn(config.agents, id) ? 'role' : 'project-default' };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function agentPlan(root, host) {
|
|
138
|
+
if (host !== undefined) requireThat(typeof host === 'string' && SLUG.test(host), 'host ID');
|
|
139
|
+
const config = loadAgentConfig(root);
|
|
140
|
+
const presets = [{ label: 'A', value: 'recommended', description: 'Sol, hybrid, at most two concurrent agents when supported.' },
|
|
141
|
+
{ label: 'B', value: 'economy', description: 'Economy model, single-agent roles.' },
|
|
142
|
+
{ label: 'C', value: 'custom', description: 'Edit provider/model/reasoning and role/host mappings explicitly.' }];
|
|
143
|
+
const note = 'Read-only declaration. No provider/model probing, native config writes, agent spawning, concurrency/token enforcement or usage tracking. Verify model access, effort and delegation in the actual host before execution.';
|
|
144
|
+
if (!config) return { status: 'not-configured', configured: false, presets, roles: [], note };
|
|
145
|
+
const lock = readJson(safe(root, '.harness/lock.json'));
|
|
146
|
+
const registry = registeredRoles(path.join(root, '.harness'), lock.capabilities ?? []);
|
|
147
|
+
const ids = [...new Set([...Object.keys(registry), ...Object.keys(config.agents)])].sort();
|
|
148
|
+
const profile = config.hosts?.[host] ?? {};
|
|
149
|
+
const roles = ids.map(id => {
|
|
150
|
+
const requested = resolveAgent(config, id);
|
|
151
|
+
const active = requested.enabled && Object.hasOwn(registry, id);
|
|
152
|
+
const mapping = profile.models?.[requested.provider + '/' + requested.model];
|
|
153
|
+
const incompatible = !mapping && ((host === 'claude-code' &&
|
|
154
|
+
(!['anthropic', 'bedrock', 'vertex'].includes(requested.provider) || requested.model.startsWith('gpt-'))) ||
|
|
155
|
+
(host === 'gemini-cli' && (!['google', 'vertex'].includes(requested.provider) || requested.model.startsWith('gpt-'))));
|
|
156
|
+
return { ...requested, active, contract: registry[id] ? '.harness/' + registry[id] : null,
|
|
157
|
+
host_model: mapping ?? requested.model, host_reasoning: profile.reasoning?.[requested.reasoning] ?? requested.reasoning,
|
|
158
|
+
mapping: mapping ? 'explicit-user-host-mapping' : 'requested',
|
|
159
|
+
availability: active && incompatible ? 'incompatible-provider' : 'unverified',
|
|
160
|
+
runtime_verified: false };
|
|
161
|
+
});
|
|
162
|
+
return { status: roles.some(role => role.active && role.availability === 'incompatible-provider') ? 'host-incompatible' : 'requires-host-verification',
|
|
163
|
+
configured: true, host: host ?? null, presets, execution: config.execution, roles,
|
|
164
|
+
missing_mappings: Object.keys(registry).filter(id => !Object.hasOwn(config.agents, id)), note };
|
|
165
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { safe, stat, isObject, SLUG, HarnessError } from './files.js';
|
|
4
|
+
import { agentPlan } from './agents.js';
|
|
5
|
+
|
|
6
|
+
export const ENVIRONMENT = '.harness/environment.json';
|
|
7
|
+
export const MACHINE = '.harness/machine.json';
|
|
8
|
+
export const emptyEnvironment = () => ({ schema_version: 1, tools: {}, files: {}, commands: {} });
|
|
9
|
+
const validText = s => typeof s === 'string' && s.trim().length > 0 && !/[\0\r\n]/.test(s);
|
|
10
|
+
const own = (o, k) => Object.hasOwn(o, k);
|
|
11
|
+
function requireThat(ok, message) { if (!ok) throw new HarnessError(message); }
|
|
12
|
+
|
|
13
|
+
export function readConfig(root, name) {
|
|
14
|
+
const p = safe(root, name), info = stat(p);
|
|
15
|
+
if (!info) return null;
|
|
16
|
+
requireThat(info.size <= 1024 * 1024, 'Configuration exceeds 1 MiB: ' + name);
|
|
17
|
+
// Never include parser input (possibly private machine paths) in diagnostics.
|
|
18
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf8')); }
|
|
19
|
+
catch { throw new HarnessError('Invalid JSON: ' + name); }
|
|
20
|
+
}
|
|
21
|
+
function keys(value, allowed, label) {
|
|
22
|
+
requireThat(isObject(value) && Object.keys(value).every(k => allowed.includes(k)), 'Invalid fields: ' + label);
|
|
23
|
+
}
|
|
24
|
+
export function loadEnvironment(root) {
|
|
25
|
+
const shared = readConfig(root, ENVIRONMENT);
|
|
26
|
+
const machine = readConfig(root, MACHINE);
|
|
27
|
+
if (shared === null) {
|
|
28
|
+
requireThat(machine === null, 'Machine configuration requires environment.json');
|
|
29
|
+
return { configured: false, shared: emptyEnvironment(), machine: null };
|
|
30
|
+
}
|
|
31
|
+
keys(shared, ['schema_version', 'tools', 'files', 'commands'], ENVIRONMENT);
|
|
32
|
+
requireThat(shared.schema_version === 1, 'Unsupported environment schema');
|
|
33
|
+
for (const group of ['tools', 'files', 'commands']) {
|
|
34
|
+
requireThat(isObject(shared[group]), 'Missing environment map: ' + group);
|
|
35
|
+
for (const [id, value] of Object.entries(shared[group])) {
|
|
36
|
+
requireThat(SLUG.test(id), 'Invalid environment identifier');
|
|
37
|
+
const label = group + '/' + id;
|
|
38
|
+
if (group === 'commands') {
|
|
39
|
+
keys(value, ['tool', 'args'], label);
|
|
40
|
+
requireThat(validText(value.tool) && own(shared.tools, value.tool), 'Unknown command tool: ' + id);
|
|
41
|
+
requireThat(Array.isArray(value.args) && value.args.every(a => typeof a === 'string' && !/[\0\r\n]/.test(a)), 'Invalid arguments: ' + id);
|
|
42
|
+
} else {
|
|
43
|
+
const field = group === 'tools' ? 'executable' : 'path';
|
|
44
|
+
keys(value, [field, 'required'], label);
|
|
45
|
+
requireThat(validText(value[field]), 'Missing path: ' + label);
|
|
46
|
+
requireThat(value.required === undefined || typeof value.required === 'boolean', 'Invalid required flag: ' + label);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (machine !== null) {
|
|
51
|
+
keys(machine, ['schema_version', 'tools', 'files'], MACHINE);
|
|
52
|
+
requireThat(machine.schema_version === 1, 'Unsupported machine schema');
|
|
53
|
+
for (const group of ['tools', 'files']) {
|
|
54
|
+
if (machine[group] === undefined) continue;
|
|
55
|
+
requireThat(isObject(machine[group]), 'Invalid machine map: ' + group);
|
|
56
|
+
const field = group === 'tools' ? 'executable' : 'path';
|
|
57
|
+
for (const [id, value] of Object.entries(machine[group])) {
|
|
58
|
+
requireThat(SLUG.test(id) && own(shared[group], id), 'Machine override must reference a declared ' + group + ' entry');
|
|
59
|
+
keys(value, [field], 'machine ' + group + '/' + id);
|
|
60
|
+
requireThat(validText(value[field]), 'Invalid machine override: ' + group + '/' + id);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { configured: true, shared, machine };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function expand(value, env) {
|
|
68
|
+
const missing = [];
|
|
69
|
+
const resolved = value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, key) => {
|
|
70
|
+
if (!own(env, key) || !validText(env[key])) { missing.push(key); return ''; }
|
|
71
|
+
return env[key];
|
|
72
|
+
});
|
|
73
|
+
return { resolved, missing: [...new Set(missing)], invalid: /\$\{/.test(resolved) };
|
|
74
|
+
}
|
|
75
|
+
function pathStatus(value, root, executable, env) {
|
|
76
|
+
if ((process.platform !== 'win32' && /^[A-Za-z]:[\\/]|^\\\\/.test(value)) ||
|
|
77
|
+
(process.platform === 'win32' && value.startsWith('/'))) return 'platform-mismatch';
|
|
78
|
+
if (value.startsWith('~')) return 'unsupported-path'; // No implicit shell expansion.
|
|
79
|
+
const bare = !path.isAbsolute(value) && !/[\\/]/.test(value);
|
|
80
|
+
const candidates = executable && bare
|
|
81
|
+
? (env.PATH || '').split(path.delimiter).filter(Boolean).filter(path.isAbsolute).map(dir => path.join(dir, value))
|
|
82
|
+
: [path.resolve(root, value)];
|
|
83
|
+
const extensions = executable && process.platform === 'win32' && !path.extname(value)
|
|
84
|
+
? (env.PATHEXT || '.EXE;.CMD;.BAT').split(';').filter(e => /^\.[A-Za-z0-9]+$/.test(e)) : [''];
|
|
85
|
+
let notExecutable = false;
|
|
86
|
+
for (const candidate of candidates) for (const ext of extensions) {
|
|
87
|
+
try {
|
|
88
|
+
const p = candidate + ext;
|
|
89
|
+
if (!fs.statSync(p).isFile()) continue;
|
|
90
|
+
fs.accessSync(p, executable && process.platform !== 'win32' ? fs.constants.X_OK : fs.constants.F_OK);
|
|
91
|
+
return 'present';
|
|
92
|
+
} catch (e) { if (e.code === 'EACCES') notExecutable = true; }
|
|
93
|
+
}
|
|
94
|
+
return notExecutable ? 'not-accessible' : 'missing';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function environmentReport(root, env = process.env) {
|
|
98
|
+
const { configured, shared, machine } = loadEnvironment(root);
|
|
99
|
+
const entries = [];
|
|
100
|
+
for (const group of ['tools', 'files']) for (const [id, value] of Object.entries(shared[group])) {
|
|
101
|
+
const field = group === 'tools' ? 'executable' : 'path';
|
|
102
|
+
const override = machine?.[group]?.[id];
|
|
103
|
+
const expanded = expand(override ? override[field] : value[field], env);
|
|
104
|
+
entries.push({ group, id, source: override ? 'machine' : 'project', required: value.required !== false,
|
|
105
|
+
status: expanded.missing.length ? 'missing-env' : expanded.invalid ? 'invalid-reference'
|
|
106
|
+
: pathStatus(expanded.resolved, root, group === 'tools', env),
|
|
107
|
+
missing_variables: expanded.missing });
|
|
108
|
+
}
|
|
109
|
+
// Arguments are references only; do not execute or print resolved values.
|
|
110
|
+
for (const [id, command] of Object.entries(shared.commands)) {
|
|
111
|
+
const args = command.args.map(a => expand(a, env));
|
|
112
|
+
const missing = [...new Set(args.flatMap(a => a.missing))];
|
|
113
|
+
entries.push({ group: 'commands', id, source: 'project', required: shared.tools[command.tool].required !== false,
|
|
114
|
+
status: missing.length ? 'missing-env' : args.some(a => a.invalid) ? 'invalid-reference' : 'declared',
|
|
115
|
+
missing_variables: missing });
|
|
116
|
+
}
|
|
117
|
+
const problems = entries.filter(e => e.required && !['present', 'declared'].includes(e.status))
|
|
118
|
+
.map(e => e.group + '/' + e.id + ': ' + e.status);
|
|
119
|
+
return { configured, machine_present: machine !== null, entries, problems,
|
|
120
|
+
note: 'Presence/reference checks only. No commands executed; values and arguments are omitted. Never store credentials here.' };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function setupPlan(root, adapters, env = process.env, host) {
|
|
124
|
+
const environment = environmentReport(root, env);
|
|
125
|
+
const configuredTools = environment.entries.some(e => e.group === 'tools');
|
|
126
|
+
const questions = [];
|
|
127
|
+
if (adapters.includes('java-spring') && !configuredTools) {
|
|
128
|
+
const wrapper = process.platform === 'win32' ? 'mvnw.cmd' : 'mvnw';
|
|
129
|
+
const hasWrapper = Boolean(stat(safe(root, wrapper)));
|
|
130
|
+
const gradle = ['gradlew', 'gradlew.bat', 'build.gradle', 'build.gradle.kts'].some(p => Boolean(stat(safe(root, p))));
|
|
131
|
+
if (gradle) {
|
|
132
|
+
questions.push({ id: 'build-tool', question: 'Which build tool does this project use?',
|
|
133
|
+
options: [{ label: 'A', value: 'gradle', description: 'Use the existing Gradle setup.' },
|
|
134
|
+
{ label: 'B', value: 'maven', description: 'Use Maven only if project conventions require it.' }],
|
|
135
|
+
recommendation: 'A', reason: 'Gradle files exist; confirm the project convention.', allow_free_text: true });
|
|
136
|
+
} else questions.push({ id: 'maven-mode', question: 'How should Maven be invoked?',
|
|
137
|
+
options: [{ label: 'A', value: 'wrapper', description: 'Use the repository Maven Wrapper.' },
|
|
138
|
+
{ label: 'B', value: 'path', description: 'Use Maven available on PATH.' },
|
|
139
|
+
{ label: 'C', value: 'custom', description: 'Use an internal Maven executable and optional settings file.' }],
|
|
140
|
+
recommendation: hasWrapper ? 'A' : null,
|
|
141
|
+
reason: hasWrapper ? 'A wrapper exists for this OS; review it before execution.' : 'No wrapper found for this OS; ask which Maven the team uses.',
|
|
142
|
+
allow_free_text: true });
|
|
143
|
+
}
|
|
144
|
+
if (!environment.configured || !environment.entries.length) questions.push({
|
|
145
|
+
id: 'project-context', question: 'Where are the authoritative project requirements and conventions?',
|
|
146
|
+
options: [{ label: 'A', value: 'repository', description: 'Use existing repository documentation and build files.' },
|
|
147
|
+
{ label: 'B', value: 'external', description: 'Use authorized BA/Confluence sources with revisions.' },
|
|
148
|
+
{ label: 'C', value: 'idea', description: 'Start from the user idea and clarify the scope.' }],
|
|
149
|
+
recommendation: null, reason: 'The host must inspect existing local.md and sources before recommending.',
|
|
150
|
+
allow_free_text: true
|
|
151
|
+
}, {
|
|
152
|
+
id: 'project-mode', question: 'What is the scope of the affected code?',
|
|
153
|
+
options: [{ label: 'A', value: 'maintain', description: 'Extend existing modules using their conventions and shared APIs.' },
|
|
154
|
+
{ label: 'B', value: 'new', description: 'Start a new application with a minimal agreed adapter baseline.' },
|
|
155
|
+
{ label: 'C', value: 'mixed', description: 'Add a new module while preserving repository-wide contracts.' }],
|
|
156
|
+
recommendation: null,
|
|
157
|
+
reason: 'Inspect the task, local.md and neighboring code; stack detection or environment configuration does not establish project mode. Skip if already known.',
|
|
158
|
+
allow_free_text: true
|
|
159
|
+
});
|
|
160
|
+
return { status: environment.problems.length ? 'blocked' : 'planned', environment, questions,
|
|
161
|
+
agent_configuration: agentPlan(root, host),
|
|
162
|
+
next_steps: ['Read AGENTS.md, .harness/local.md and the installed project-setup workflow.',
|
|
163
|
+
'Apply installed Core project-conventions.md and relevant adapter conventions; establish mode per affected module even when no setup questions are suggested.',
|
|
164
|
+
'Inspect comparable code and search public shared APIs before creating files, dependencies or abstractions; record source paths and reuse/new-code rationale.',
|
|
165
|
+
'Skip answered questions; ask at most 1–3 consequential questions with evidence-based recommendations.',
|
|
166
|
+
'Review agents.json provider/model/reasoning and preset choices; verify access/effort/delegation in the actual host before execution. No automatic premium fallback.',
|
|
167
|
+
'Record shared decisions in local.md/environment.json; put only machine paths in ignored machine.json.',
|
|
168
|
+
'Run aih doctor, then use the reviewed project commands through your AI host.'],
|
|
169
|
+
note: 'Read-only host-facing suggestions, not an interactive terminal wizard or approval.' };
|
|
170
|
+
}
|
package/cli/harness.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import { emptyEnvironment, environmentReport } from './environment.js';
|
|
3
|
+
import { AGENT_CONFIG, agentDefaults, loadAgentConfig } from './agents.js';
|
|
2
4
|
import path from 'node:path';
|
|
3
5
|
import { randomUUID } from 'node:crypto';
|
|
4
6
|
import { loadAdapter } from './adapter-schema.js';
|
|
@@ -65,6 +67,7 @@ export function bundle(source, config) {
|
|
|
65
67
|
files[dest] = fs.readFileSync(safe(source, `adapters/${a}/skills/${name}/SKILL.md`));
|
|
66
68
|
}
|
|
67
69
|
}
|
|
70
|
+
files['.harness/templates/agents.default.json'] = encode(agentDefaults(source));
|
|
68
71
|
for (const c of capabilities) {
|
|
69
72
|
const info = loadCapability(source, c);
|
|
70
73
|
for (const name of info.skills) {
|
|
@@ -156,12 +159,15 @@ export function transaction(root, changes, writer = atomicWrite) {
|
|
|
156
159
|
}
|
|
157
160
|
}
|
|
158
161
|
|
|
159
|
-
export function install(root, source, { command = 'init', adapters, capabilities, dryRun = false, allowMajor = false, writer } = {}) {
|
|
162
|
+
export function install(root, source, { command = 'init', adapters, capabilities, agentPreset, dryRun = false, allowMajor = false, writer } = {}) {
|
|
160
163
|
assertSeparate(root, source);
|
|
161
164
|
return guard(root, () => {
|
|
162
165
|
const hasLock = Boolean(stat(safe(root, LOCK)));
|
|
163
166
|
if (command === 'update' && !hasLock) throw new HarnessError('Not installed; run init first');
|
|
164
167
|
const old = hasLock ? loadLock(root) : null;
|
|
168
|
+
const existingAgents = loadAgentConfig(root);
|
|
169
|
+
if (agentPreset !== undefined && command !== 'init') throw new HarnessError('Agent preset is supported only by init');
|
|
170
|
+
if (agentPreset !== undefined && existingAgents) throw new HarnessError('agents.json is project-owned; edit it explicitly instead of applying an init preset');
|
|
165
171
|
const config = projectConfig(root, adapters, capabilities, hasLock);
|
|
166
172
|
const { files, lock, warnings } = bundle(source, config);
|
|
167
173
|
if (old) {
|
|
@@ -185,11 +191,20 @@ export function install(root, source, { command = 'init', adapters, capabilities
|
|
|
185
191
|
}
|
|
186
192
|
if (old) for (const name of Object.keys(old.files)) if (!Object.hasOwn(files, name)) changes[name] = null;
|
|
187
193
|
const seeds = {
|
|
194
|
+
[AGENT_CONFIG]: encode(agentDefaults(source, agentPreset ?? 'recommended')),
|
|
188
195
|
[CONFIG]: encode(config),
|
|
196
|
+
'.harness/environment.json': encode(emptyEnvironment()),
|
|
189
197
|
'openspec/config.yaml': fs.readFileSync(safe(source, 'templates/openspec/config.yaml')),
|
|
190
|
-
'.harness/local.md': Buffer.from('# Project overrides\n\nRecord project facts and reviewed exceptions here. Never weaken security boundaries.\n'),
|
|
198
|
+
'.harness/local.md': Buffer.from('# Project overrides\n\nRecord project facts and reviewed exceptions here. Never weaken security boundaries.\n\n## Development conventions\nRecord scope/mode (new, maintain or mixed), authoritative source paths, module boundaries, reusable public APIs, naming/error/test conventions and configured checks. Link existing guides instead of duplicating them. Unknowns are not approved defaults; the AI host inspects evidence and asks only consequential questions.\n'),
|
|
191
199
|
};
|
|
192
200
|
for (const [name, data] of Object.entries(seeds)) if (!stat(safe(root, name))) changes[name] = data;
|
|
201
|
+
// Project-owned ignore file: preserve all prior rules, place our anchored rule last.
|
|
202
|
+
// This does not remove an already tracked machine file from the Git index.
|
|
203
|
+
const ignoreName = '.harness/.gitignore', ignorePath = safe(root, ignoreName);
|
|
204
|
+
const ignore = stat(ignorePath) ? fs.readFileSync(ignorePath, 'utf8') : '';
|
|
205
|
+
if (ignore.trimEnd().split(/\r?\n/).at(-1) !== '/machine.json') {
|
|
206
|
+
changes[ignoreName] = Buffer.from(ignore + (ignore && !ignore.endsWith('\n') ? '\n' : '') + '/machine.json\n');
|
|
207
|
+
}
|
|
193
208
|
const newLock = encode(lock);
|
|
194
209
|
if (!hasLock || !fs.readFileSync(safe(root, LOCK)).equals(newLock)) changes[LOCK] = newLock;
|
|
195
210
|
const plan = Object.entries(changes).map(([name, data]) => ({ path: name, action: data === null ? 'delete' : 'write' }));
|
|
@@ -201,6 +216,7 @@ export function install(root, source, { command = 'init', adapters, capabilities
|
|
|
201
216
|
export function check(root) {
|
|
202
217
|
const lock = loadLock(root), config = projectConfig(root, undefined, undefined, true);
|
|
203
218
|
const problems = drift(root, lock);
|
|
219
|
+
loadAgentConfig(root); // Optional on legacy installs; validate user config without managing its bytes.
|
|
204
220
|
if (JSON.stringify([...config.adapters].sort()) !== JSON.stringify([...lock.adapters].sort())) problems.push(`${CONFIG}: adapter selection changed; run update`);
|
|
205
221
|
if (JSON.stringify([...(config.capabilities ?? [])].sort()) !== JSON.stringify([...(lock.capabilities ?? [])].sort())) problems.push(`${CONFIG}: capability selection changed; run update`);
|
|
206
222
|
return { status: problems.length ? 'error' : 'ok', version: lock.version, problems };
|
|
@@ -214,6 +230,10 @@ export function newChange(root, name) {
|
|
|
214
230
|
safe(root, `openspec/changes/${name}/proposal.md`);
|
|
215
231
|
if (stat(folder)) throw new HarnessError(`Change already exists: ${name}`);
|
|
216
232
|
const mapping = { 'proposal.md': 'proposal.md', 'design.md': 'design.md', 'tasks.md': 'tasks.md', 'spec.md': `specs/${name}/spec.md`, '.openspec.yaml': '.openspec.yaml' };
|
|
233
|
+
const locked = loadLock(root);
|
|
234
|
+
for (const name of ['decisions.json', 'verification.json']) {
|
|
235
|
+
if (Object.hasOwn(locked.files, '.harness/templates/openspec/' + name)) mapping[name] = name;
|
|
236
|
+
}
|
|
217
237
|
const changes = {};
|
|
218
238
|
for (const [src, dest] of Object.entries(mapping)) {
|
|
219
239
|
const text = fs.readFileSync(safe(root, `.harness/templates/openspec/${src}`), 'utf8');
|
|
@@ -224,20 +244,28 @@ export function newChange(root, name) {
|
|
|
224
244
|
});
|
|
225
245
|
}
|
|
226
246
|
|
|
227
|
-
//
|
|
228
|
-
export function newDoc(root, name) {
|
|
247
|
+
// Capability drafts are project-owned and never added to the managed bundle.
|
|
248
|
+
export function newDoc(root, name, capability = 'product') {
|
|
229
249
|
if (!SLUG.test(name)) throw new HarnessError('Document type must be a lowercase hyphenated slug');
|
|
250
|
+
if (!SLUG.test(capability)) throw new HarnessError('Capability must be a lowercase hyphenated slug');
|
|
230
251
|
return guard(root, () => {
|
|
231
252
|
if (check(root).status !== 'ok') throw new HarnessError('Fix installation drift before creating a document');
|
|
232
253
|
const lock = loadLock(root);
|
|
233
|
-
if (!(lock.capabilities ?? []).includes(
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const
|
|
254
|
+
if (!(lock.capabilities ?? []).includes(capability)) {
|
|
255
|
+
throw new HarnessError(capability === 'product' ? 'Product capability is not installed' : `Capability is not installed: ${capability}`);
|
|
256
|
+
}
|
|
257
|
+
const sourceName = `.harness/capabilities/${capability}/templates/${name}.md`;
|
|
258
|
+
if (!Object.hasOwn(lock.files, `.harness/capabilities/${capability}/capability.json`)) {
|
|
259
|
+
throw new HarnessError(`Capability metadata is not locked: ${capability}`);
|
|
260
|
+
}
|
|
261
|
+
const declared = loadCapability(path.join(root, '.harness'), capability).templates;
|
|
262
|
+
if (!declared.includes(`templates/${name}.md`)) throw new HarnessError(`Unknown document type for ${capability}: ${name}`);
|
|
263
|
+
if (!Object.hasOwn(lock.files, sourceName)) throw new HarnessError(`Document template is not locked: ${capability}/${name}`);
|
|
264
|
+
const folder = capability === 'product' && ['idea', 'research'].includes(name) ? 'discovery' : capability;
|
|
237
265
|
const outputName = `docs/${folder}/${name}.md`;
|
|
238
266
|
if (stat(safe(root, outputName))) throw new HarnessError(`Document already exists: ${outputName}`);
|
|
239
267
|
transaction(root, { [outputName]: fs.readFileSync(safe(root, sourceName)) });
|
|
240
|
-
return { status: 'ok', type: name, path: outputName,
|
|
268
|
+
return { status: 'ok', type: name, capability, path: outputName,
|
|
241
269
|
note: 'Template only; populate from authorized evidence and obtain human approval.' };
|
|
242
270
|
});
|
|
243
271
|
}
|
|
@@ -251,5 +279,7 @@ export function doctor(root) {
|
|
|
251
279
|
catch { return false; }
|
|
252
280
|
}));
|
|
253
281
|
}
|
|
254
|
-
|
|
282
|
+
const state = check(root), environment = environmentReport(root);
|
|
283
|
+
return { ...state, status: state.status !== 'ok' || environment.problems.length ? 'error' : 'ok',
|
|
284
|
+
tools, environment, note: 'Tool presence only; no commands executed or compatibility claim made. Configured project tools take precedence over generic PATH hints.' };
|
|
255
285
|
}
|