@manhd_d/ai-harness 0.12.0 → 0.14.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 +35 -0
- package/README.md +28 -37
- package/adapters/ai-python/adapter.yaml +1 -1
- package/adapters/ai-python/rules.md +3 -0
- package/adapters/ai-python/skills/harness-ai-python-rag-access/SKILL.md +1 -0
- package/adapters/ai-python/skills/harness-ai-python-tool-safety/SKILL.md +1 -0
- package/adapters/astro/adapter.yaml +3 -5
- package/adapters/astro/checks/verification.yaml +6 -6
- package/adapters/astro/evals/cases.json +5 -5
- package/adapters/astro/rules/architecture.md +2 -8
- package/adapters/astro/rules/deployment.md +2 -8
- package/adapters/astro/rules.md +5 -14
- package/adapters/astro/skills/harness-astro-islands/SKILL.md +3 -2
- package/adapters/astro/skills/harness-astro-performance/SKILL.md +1 -0
- package/adapters/astro/skills/harness-astro-project-architecture/SKILL.md +9 -19
- package/adapters/astro/skills/harness-astro-seo/SKILL.md +1 -0
- package/adapters/java-spring/adapter.yaml +2 -4
- package/adapters/java-spring/rules/database.md +2 -2
- package/adapters/java-spring/rules/performance.md +2 -2
- package/adapters/java-spring/rules.md +4 -1
- package/adapters/java-spring/skills/harness-java-spring-spring-api/SKILL.md +1 -0
- package/adapters/kubernetes/adapter.yaml +1 -1
- package/adapters/kubernetes/rules.md +3 -0
- package/adapters/kubernetes/skills/harness-kubernetes-helm-release/SKILL.md +1 -0
- package/adapters/kubernetes/skills/harness-kubernetes-rollout-safety/SKILL.md +1 -0
- package/adapters/kubernetes/skills/harness-kubernetes-workload-review/SKILL.md +1 -0
- package/adapters/react-native/adapter.yaml +1 -1
- package/adapters/react-native/rules.md +3 -0
- package/adapters/react-native/skills/harness-react-native-behavior-testing/SKILL.md +1 -0
- package/adapters/react-native/skills/harness-react-native-device-permissions/SKILL.md +1 -0
- package/adapters/react-native/skills/harness-react-native-offline-sync/SKILL.md +1 -0
- package/adapters/react-native/skills/harness-react-native-screen-delivery/SKILL.md +1 -0
- package/adapters/react-nextjs/adapter.yaml +1 -1
- package/adapters/react-nextjs/rules.md +3 -0
- package/adapters/react-nextjs/skills/harness-react-nextjs-accessible-ui/SKILL.md +1 -0
- package/adapters/react-nextjs/skills/harness-react-nextjs-behavior-testing/SKILL.md +1 -0
- package/adapters/react-nextjs/skills/harness-react-nextjs-feature-delivery/SKILL.md +1 -0
- package/adapters/react-nextjs/skills/harness-react-nextjs-server-boundaries/SKILL.md +1 -0
- package/bin/aih.js +1 -1
- package/capabilities/analysis/capability.json +48 -0
- package/capabilities/analysis/checks/verification.json +16 -0
- package/capabilities/analysis/evals/cases.json +15 -0
- package/capabilities/architecture/agents/architecture-reviewer.md +5 -0
- package/capabilities/architecture/agents/solution-architect.md +5 -0
- package/capabilities/architecture/capability.json +1 -1
- package/capabilities/delivery/agents/project-manager.md +10 -11
- package/capabilities/delivery/capability.json +8 -2
- package/capabilities/delivery/templates/delivery-plan.md +2 -23
- package/capabilities/discovery/capability.json +37 -0
- package/capabilities/discovery/checks/verification.json +16 -0
- package/capabilities/discovery/evals/cases.json +15 -0
- package/capabilities/product/agents/business-analyst.md +5 -0
- package/capabilities/product/agents/product-manager.md +5 -0
- package/capabilities/product/agents/product-owner.md +5 -0
- package/capabilities/product/agents/project-manager.md +9 -14
- package/capabilities/product/agents/requirements-reviewer.md +5 -0
- package/capabilities/product/capability.json +8 -2
- package/capabilities/product/common/document-contract.md +10 -0
- package/capabilities/product/evals/cases.json +11 -0
- package/capabilities/product/skills/harness-product-acceptance-criteria/SKILL.md +4 -7
- package/capabilities/product/skills/harness-product-backlog-prioritization/SKILL.md +2 -6
- package/capabilities/product/skills/harness-product-brd-generation/SKILL.md +2 -6
- package/capabilities/product/skills/harness-product-business-analysis/SKILL.md +7 -10
- package/capabilities/product/skills/harness-product-document-review/SKILL.md +2 -6
- package/capabilities/product/skills/harness-product-idea-discovery/SKILL.md +2 -6
- package/capabilities/product/skills/harness-product-knowledge-analysis/SKILL.md +2 -6
- package/capabilities/product/skills/harness-product-mvp-definition/SKILL.md +1 -2
- package/capabilities/product/skills/harness-product-prd-generation/SKILL.md +2 -6
- package/capabilities/product/skills/harness-product-product-outcomes/SKILL.md +1 -2
- package/capabilities/product/skills/harness-product-product-research/SKILL.md +2 -6
- package/capabilities/product/skills/harness-product-srs-generation/SKILL.md +2 -6
- package/capabilities/product/skills/harness-product-techspec/SKILL.md +1 -1
- package/capabilities/product/skills/harness-product-traceability/SKILL.md +2 -6
- package/capabilities/product/skills/harness-product-use-case-generation/SKILL.md +2 -6
- package/capabilities/product/skills/harness-product-user-stories/SKILL.md +2 -6
- package/capabilities/product/templates/delivery-plan.md +3 -17
- package/cli/adapters.js +9 -5
- package/cli/agents.js +45 -9
- package/cli/capabilities.js +39 -8
- package/cli/checkpoint.js +57 -0
- package/cli/claude.js +40 -0
- package/cli/comparison.js +46 -0
- package/cli/composition.js +1 -0
- package/cli/files.js +2 -1
- package/cli/harness.js +78 -20
- package/cli/main.js +51 -15
- package/cli/maven.js +28 -0
- package/cli/portable-config.js +121 -0
- package/cli/start-config.js +15 -0
- package/cli/start.js +138 -0
- package/cli/verification-report.js +1 -1
- package/config/agents.claude-code.json +100 -0
- package/config/agents.default.json +25 -5
- package/core/agents/documentation.md +5 -0
- package/core/agents/implementer.md +5 -0
- package/core/agents/orchestrator.md +5 -0
- package/core/agents/planner.md +5 -0
- package/core/agents/project-manager.md +20 -0
- package/core/agents/reviewer.md +5 -0
- package/core/agents/security-reviewer.md +5 -0
- package/core/agents/test-engineer.md +5 -0
- package/core/checks/verification.json +12 -0
- package/core/evals/cases.json +44 -0
- package/core/policies/permissions.md +3 -1
- package/core/workflows/agent-routing.md +27 -26
- package/core/workflows/continuous-development.md +3 -0
- package/core/workflows/project-setup.md +18 -0
- package/core/workflows/spec-to-code.md +1 -1
- package/docs/README.md +6 -1
- package/docs/agent-configuration.md +9 -5
- package/docs/agent-first.md +66 -0
- package/docs/agent-handoffs.md +4 -0
- package/docs/architecture.md +14 -0
- package/docs/cli.md +21 -0
- package/docs/configuration-backup.md +122 -0
- package/docs/continuous-development.md +4 -0
- package/docs/examples/astro-micro-tools.md +1 -1
- package/docs/examples/checkpoint.json +20 -0
- package/docs/examples/spring-boot-api.md +2 -4
- package/docs/getting-started.md +9 -6
- package/docs/i18n/README.es.md +35 -0
- package/docs/i18n/README.vi.md +21 -43
- package/docs/i18n/README.zh-CN.md +35 -0
- package/docs/project-configuration.md +9 -1
- package/docs/skill-matrix.md +7 -7
- package/docs/start.md +38 -0
- package/docs/tool-setup/README.md +69 -67
- package/docs/tool-setup/claude-code.md +41 -49
- package/docs/tool-setup/codex.md +31 -43
- package/docs/tool-setup/copilot.md +35 -38
- package/docs/tool-setup/cursor.md +25 -40
- package/docs/tool-setup/gemini-cli.md +31 -35
- package/docs/tool-setup/kiro.md +55 -0
- package/docs/versioning.md +24 -0
- package/evals/real-task-comparison.md +4 -0
- package/harness.yaml +4 -2
- package/package.json +3 -2
- package/scripts/compare-tasks.js +15 -0
- package/templates/AGENTS.md +20 -28
- package/templates/delivery-plan.md +25 -0
- package/adapters/astro/skills/harness-astro-cloudflare/SKILL.md +0 -27
- package/adapters/astro/skills/harness-astro-microfrontend/SKILL.md +0 -27
- package/adapters/java-spring/skills/harness-java-spring-database-migration/SKILL.md +0 -23
- package/adapters/java-spring/skills/harness-java-spring-kafka-outbox/SKILL.md +0 -23
|
@@ -32,8 +32,7 @@ For customer lookup, propose task completion and observed latency as measures. W
|
|
|
32
32
|
- Experiment does not silently expand collection or release scope
|
|
33
33
|
|
|
34
34
|
## Handoff
|
|
35
|
-
|
|
36
|
-
actual checks and next role. Use the module workflow; role names are not human authorization.
|
|
35
|
+
Read the shared installed contract `.harness/capabilities/product/common/document-contract.md`; pass only affected scope and relevant perspectives.
|
|
37
36
|
|
|
38
37
|
## Constraints
|
|
39
38
|
Follow Core permissions, clarification and project-conventions. Ask only consequential unknowns with
|
|
@@ -28,17 +28,13 @@ description: Research comparable products, user needs and implementation feasibi
|
|
|
28
28
|
|
|
29
29
|
## Template
|
|
30
30
|
Read `.harness/capabilities/product/templates/research.md`; `aih new-doc research` creates only a missing
|
|
31
|
-
draft.
|
|
31
|
+
draft. Apply the shared document contract for authority, preservation and review.
|
|
32
32
|
|
|
33
33
|
## Example
|
|
34
34
|
Synthetic illustration: With no browsing access, competitor claims are UNKNOWN and research questions are proposed rather than citations invented.
|
|
35
35
|
|
|
36
36
|
## Quality checklist
|
|
37
37
|
- Retrieved facts, inferences and forecasts have provenance.
|
|
38
|
-
- Actual decisions/status and source revisions distinguish approved facts from proposals.
|
|
39
|
-
- Scope, relevant IDs, unresolved questions and verification limits are recorded.
|
|
40
38
|
|
|
41
39
|
## Handoff
|
|
42
|
-
|
|
43
|
-
IDs, output paths and actual owner decisions to the next BA/PO/SA/PM role as applicable. Return gaps
|
|
44
|
-
to their owner; a single host may perform roles sequentially without claiming independent approval.
|
|
40
|
+
Read the shared installed contract `.harness/capabilities/product/common/document-contract.md`; pass only affected scope and relevant perspectives.
|
|
@@ -29,17 +29,13 @@ description: Produce a testable Software Requirements Specification linked to ap
|
|
|
29
29
|
|
|
30
30
|
## Template
|
|
31
31
|
Read `.harness/capabilities/product/templates/srs.md`; `aih new-doc srs` creates only a missing
|
|
32
|
-
draft.
|
|
32
|
+
draft. Apply the shared document contract for authority, preservation and review.
|
|
33
33
|
|
|
34
34
|
## Example
|
|
35
35
|
Synthetic illustration: FR-001 defines search behavior; NFR-001 target is pending until agreed and is passed to Architecture for a scenario/test plan.
|
|
36
36
|
|
|
37
37
|
## Quality checklist
|
|
38
38
|
- FR/NFR contracts, exception paths and trace IDs remain consistent.
|
|
39
|
-
- Actual decisions/status and source revisions distinguish approved facts from proposals.
|
|
40
|
-
- Scope, relevant IDs, unresolved questions and verification limits are recorded.
|
|
41
39
|
|
|
42
40
|
## Handoff
|
|
43
|
-
|
|
44
|
-
IDs, output paths and actual owner decisions to the next BA/PO/SA/PM role as applicable. Return gaps
|
|
45
|
-
to their owner; a single host may perform roles sequentially without claiming independent approval.
|
|
41
|
+
Read the shared installed contract `.harness/capabilities/product/common/document-contract.md`; pass only affected scope and relevant perspectives.
|
|
@@ -32,4 +32,4 @@ Idea: a static calculator needs UX/formula/build/accessibility evidence, no API
|
|
|
32
32
|
Source revisions, requirement IDs, contracts, actual execution evidence and limits are distinguishable. Partial updates preserve approved text.
|
|
33
33
|
|
|
34
34
|
## Handoff
|
|
35
|
-
|
|
35
|
+
Read the shared installed contract `.harness/capabilities/product/common/document-contract.md`; pass only affected scope and relevant perspectives.
|
|
@@ -25,20 +25,16 @@ description: Maintain bidirectional links from document sources through BRD/PRD/
|
|
|
25
25
|
|
|
26
26
|
## Template
|
|
27
27
|
Read `.harness/capabilities/product/templates/traceability.md`; `aih new-doc traceability` creates only a missing
|
|
28
|
-
draft.
|
|
28
|
+
draft. Apply the shared document contract for authority, preservation and review.
|
|
29
29
|
|
|
30
30
|
## Example
|
|
31
31
|
Synthetic illustration: SRC-001 → FR-001 → US-001 → AC-US-001-01 → ADR-001/OpenSpec task/test links form a checkable chain.
|
|
32
32
|
|
|
33
33
|
## Quality checklist
|
|
34
34
|
- Stable IDs, source versions and downstream gaps are maintained.
|
|
35
|
-
- Actual decisions/status and source revisions distinguish approved facts from proposals.
|
|
36
|
-
- Scope, relevant IDs, unresolved questions and verification limits are recorded.
|
|
37
35
|
|
|
38
36
|
## Handoff
|
|
39
|
-
|
|
40
|
-
IDs, output paths and actual owner decisions to the next BA/PO/SA/PM role as applicable. Return gaps
|
|
41
|
-
to their owner; a single host may perform roles sequentially without claiming independent approval.
|
|
37
|
+
Read the shared installed contract `.harness/capabilities/product/common/document-contract.md`; pass only affected scope and relevant perspectives.
|
|
42
38
|
|
|
43
39
|
## Continuous changes
|
|
44
40
|
Extend the same matrix through scoped code/contracts and tests/raw evidence with their revisions.
|
|
@@ -27,17 +27,13 @@ description: Translate requirement and story IDs into consistent main, alternati
|
|
|
27
27
|
|
|
28
28
|
## Template
|
|
29
29
|
Read `.harness/capabilities/product/templates/use-cases.md`; `aih new-doc use-cases` creates only a missing
|
|
30
|
-
draft.
|
|
30
|
+
draft. Apply the shared document contract for authority, preservation and review.
|
|
31
31
|
|
|
32
32
|
## Example
|
|
33
33
|
Synthetic illustration: UC-001 contains main lookup, no-result, permission denial and provider timeout paths linked to FR/AC.
|
|
34
34
|
|
|
35
35
|
## Quality checklist
|
|
36
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
37
|
|
|
40
38
|
## Handoff
|
|
41
|
-
|
|
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.
|
|
39
|
+
Read the shared installed contract `.harness/capabilities/product/common/document-contract.md`; pass only affected scope and relevant perspectives.
|
|
@@ -26,17 +26,13 @@ description: Turn approved requirements into atomic epics and user stories with
|
|
|
26
26
|
|
|
27
27
|
## Template
|
|
28
28
|
Read `.harness/capabilities/product/templates/backlog.md`; `aih new-doc backlog` creates only a missing
|
|
29
|
-
draft.
|
|
29
|
+
draft. Apply the shared document contract for authority, preservation and review.
|
|
30
30
|
|
|
31
31
|
## Example
|
|
32
32
|
Synthetic illustration: US-001: authorized employee finds a customer; links FR-001 and AC-US-001-01, with access-denial criteria.
|
|
33
33
|
|
|
34
34
|
## Quality checklist
|
|
35
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
36
|
|
|
39
37
|
## Handoff
|
|
40
|
-
|
|
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.
|
|
38
|
+
Read the shared installed contract `.harness/capabilities/product/common/document-contract.md`; pass only affected scope and relevant perspectives.
|
|
@@ -1,18 +1,4 @@
|
|
|
1
|
-
# Delivery plan
|
|
1
|
+
# Delivery plan — compatibility template
|
|
2
2
|
|
|
3
|
-
Status: draft
|
|
4
|
-
|
|
5
|
-
## Milestones
|
|
6
|
-
| Milestone | Scope / deliverables | Dependency | Estimate evidence | Proposed date | Accountable owner |
|
|
7
|
-
| --- | --- | --- | --- | --- | --- |
|
|
8
|
-
|
|
9
|
-
## RAID log
|
|
10
|
-
| ID | Risk / Assumption / Issue / Dependency | Probability / impact (if evidenced) | Mitigation | Owner | Status |
|
|
11
|
-
| --- | --- | --- | --- | --- | --- |
|
|
12
|
-
|
|
13
|
-
## Communications / change control
|
|
14
|
-
- Stakeholders and review cadence:
|
|
15
|
-
- Scope change requests and PRD/BRD impact:
|
|
16
|
-
- Escalation route:
|
|
17
|
-
|
|
18
|
-
Never substitute invented effort, capacity or promised dates for team estimates.
|
|
3
|
+
Status: draft / pending review
|
|
4
|
+
Canonical template: .harness/templates/delivery-plan.md. Existing document paths remain project-owned; new-doc resolves this alias without migrating old documents.
|
package/cli/adapters.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import { mavenModules } from './maven.js';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { safe, readJson, manifest, HarnessError, check, loadLock, encode, digest } from './harness.js';
|
|
4
5
|
import { scoreDataset } from '../scripts/score-eval.js';
|
|
@@ -23,11 +24,13 @@ function probe(root, file) {
|
|
|
23
24
|
return fs.readFileSync(filename, 'utf8');
|
|
24
25
|
}
|
|
25
26
|
export function detect(root, source) {
|
|
26
|
-
const matches = [];
|
|
27
|
+
const matches = [], maven = mavenModules(root);
|
|
27
28
|
for (const id of manifest(source).adapters) {
|
|
28
29
|
const a = loadAdapter(source, id), evidence = [];
|
|
29
30
|
for (const rule of a.detection) {
|
|
30
|
-
const
|
|
31
|
+
const probes = rule.file === 'pom.xml' ? maven.files : [{ file: rule.file, body: probe(root, rule.file) }];
|
|
32
|
+
for (const entry of probes) {
|
|
33
|
+
const body = entry.body;
|
|
31
34
|
if (body === null) continue;
|
|
32
35
|
let found;
|
|
33
36
|
if (rule.kind === 'dependencies') {
|
|
@@ -41,7 +44,8 @@ export function detect(root, source) {
|
|
|
41
44
|
}
|
|
42
45
|
found = rule.values.filter(v => deps.has(v));
|
|
43
46
|
} else found = rule.values.filter(v => body.includes(v));
|
|
44
|
-
if (found.length) evidence.push({ file:
|
|
47
|
+
if (found.length) evidence.push({ file: entry.file, kind: rule.kind, matched: found });
|
|
48
|
+
}
|
|
45
49
|
}
|
|
46
50
|
if (evidence.length) matches.push({ id, evidence });
|
|
47
51
|
}
|
|
@@ -51,8 +55,8 @@ export function detect(root, source) {
|
|
|
51
55
|
const astroDetected = matches.some(match => match.id === 'astro');
|
|
52
56
|
const filtered = matches.filter(match => !(astroDetected && match.id === 'react-nextjs'
|
|
53
57
|
&& !match.evidence.some(row => row.matched.includes('next'))));
|
|
54
|
-
return { status: 'ok', adapters: filtered.map(m => m.id).sort(), matches: filtered,
|
|
55
|
-
note: 'Heuristic
|
|
58
|
+
return { status: 'ok', adapters: filtered.map(m => m.id).sort(), matches: filtered, warnings: maven.warnings,
|
|
59
|
+
note: 'Heuristic metadata only; bounded declared Maven modules (depth 2, 64 POMs, 4 MiB total). No application execution; other monorepos require scoped host discovery.' };
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
function installedAdapters(root) {
|
package/cli/agents.js
CHANGED
|
@@ -61,7 +61,7 @@ export function validateAgentConfig(config) {
|
|
|
61
61
|
requireThat(agent.enabled === undefined || typeof agent.enabled === 'boolean', id + ' enabled');
|
|
62
62
|
}
|
|
63
63
|
const execution = config.execution;
|
|
64
|
-
fields(execution, ['mode', 'maxConcurrentAgents', 'maxRounds', 'autoEscalateToPremium'],
|
|
64
|
+
fields(execution, ['mode', 'maxConcurrentAgents', 'maxRounds', 'autoEscalateToPremium', 'autonomy', 'budget'],
|
|
65
65
|
['mode', 'maxConcurrentAgents', 'maxRounds', 'autoEscalateToPremium'], 'execution');
|
|
66
66
|
requireThat(['single-agent', 'hybrid', 'multi-agent'].includes(execution.mode), 'execution mode');
|
|
67
67
|
requireThat(Number.isSafeInteger(execution.maxConcurrentAgents) && execution.maxConcurrentAgents >= 1
|
|
@@ -70,6 +70,21 @@ export function validateAgentConfig(config) {
|
|
|
70
70
|
requireThat(Number.isSafeInteger(execution.maxRounds) && execution.maxRounds >= 1 && execution.maxRounds <= 20,
|
|
71
71
|
'maxRounds must be an integer from 1 to 20');
|
|
72
72
|
requireThat(execution.autoEscalateToPremium === false, 'autoEscalateToPremium must be false; select premium explicitly');
|
|
73
|
+
if (execution.autonomy !== undefined) {
|
|
74
|
+
const a=execution.autonomy;
|
|
75
|
+
fields(a,['mode','actions','repositories'],['mode','actions','repositories'],'autonomy');
|
|
76
|
+
requireThat(['assisted','autopilot'].includes(a.mode),'autonomy mode');
|
|
77
|
+
const allowed=['read','edit','test','commit','push','create-pr','source-write','merge','release','deploy'];
|
|
78
|
+
requireThat(Array.isArray(a.actions)&&new Set(a.actions).size===a.actions.length&&a.actions.every(v=>allowed.includes(v)),'autonomy actions');
|
|
79
|
+
requireThat(Array.isArray(a.repositories)&&new Set(a.repositories).size===a.repositories.length&&a.repositories.every(v=>text(v)&&/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(v)),'autonomy repositories');
|
|
80
|
+
requireThat(a.mode!=='autopilot'||(a.actions.length>0&&a.repositories.length>0),'autopilot needs explicit actions and repository scope');
|
|
81
|
+
}
|
|
82
|
+
if (execution.budget !== undefined) {
|
|
83
|
+
const b=execution.budget;fields(b,['maxTokens','maxCost','currency'],[],'budget');
|
|
84
|
+
requireThat(b.maxTokens===undefined||(Number.isSafeInteger(b.maxTokens)&&b.maxTokens>0),'maxTokens');
|
|
85
|
+
requireThat(b.maxCost===undefined||(Number.isFinite(b.maxCost)&&b.maxCost>=0&&text(b.currency)),'maxCost/currency');
|
|
86
|
+
requireThat(b.maxTokens!==undefined||b.maxCost!==undefined,'budget needs a declared limit');
|
|
87
|
+
}
|
|
73
88
|
const hosts = config.hosts ?? {};
|
|
74
89
|
requireThat(isObject(hosts) && Object.keys(hosts).length <= 16, 'hosts map (max 16 profiles)');
|
|
75
90
|
for (const [id, profile] of Object.entries(hosts)) {
|
|
@@ -97,10 +112,11 @@ function readConfiguration(root, relative) {
|
|
|
97
112
|
}
|
|
98
113
|
export function loadAgentConfig(root) { return readConfiguration(root, AGENT_CONFIG); }
|
|
99
114
|
|
|
100
|
-
export function agentDefaults(source = SOURCE, preset = 'recommended') {
|
|
115
|
+
export function agentDefaults(source = SOURCE, preset = 'recommended', host) {
|
|
101
116
|
requireThat(PRESETS.includes(preset), 'unknown preset; use recommended, economy or custom');
|
|
102
117
|
// Older alternate bundles remain usable; the packaged CLI supplies the new canonical seed.
|
|
103
|
-
const
|
|
118
|
+
const seed = host === 'claude-code' ? 'config/agents.claude-code.json' : DEFAULT_AGENTS;
|
|
119
|
+
const config = structuredClone(readConfiguration(source, seed) ?? readConfiguration(SOURCE, seed));
|
|
104
120
|
requireThat(config !== null, 'missing canonical default');
|
|
105
121
|
if (preset === 'economy') {
|
|
106
122
|
config.defaultModel = '@economy'; config.defaultReasoning = 'low';
|
|
@@ -120,7 +136,7 @@ export function registeredRoles(source, capabilities = []) {
|
|
|
120
136
|
const metadata = readJson(safe(source, `capabilities/${id}/capability.json`));
|
|
121
137
|
for (const file of metadata.agents ?? []) {
|
|
122
138
|
const role = id + ':' + path.basename(file, '.md');
|
|
123
|
-
roles[role] = `capabilities/${id}/${file}`;
|
|
139
|
+
roles[role] = metadata.agent_aliases?.[file] ?? `capabilities/${metadata.asset_owner ?? id}/${file}`;
|
|
124
140
|
}
|
|
125
141
|
}
|
|
126
142
|
return roles;
|
|
@@ -137,10 +153,10 @@ export function resolveAgent(config, id) {
|
|
|
137
153
|
export function agentPlan(root, host) {
|
|
138
154
|
if (host !== undefined) requireThat(typeof host === 'string' && SLUG.test(host), 'host ID');
|
|
139
155
|
const config = loadAgentConfig(root);
|
|
140
|
-
const presets = [{ label: 'A', value: 'recommended', description: '
|
|
156
|
+
const presets = [{ label: 'A', value: 'recommended', description: 'Keep the host-selected model; hybrid, at most two concurrent agents when supported.' },
|
|
141
157
|
{ label: 'B', value: 'economy', description: 'Economy model, single-agent roles.' },
|
|
142
158
|
{ 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.';
|
|
159
|
+
const note = 'Read-only declaration. No provider/model probing, native config writes, agent spawning, permission/concurrency/token enforcement or usage tracking. Autonomy and budget are declarations; verify actual user authorization and host limits. Verify model access, effort and delegation in the actual host before execution.';
|
|
144
160
|
if (!config) return { status: 'not-configured', configured: false, presets, roles: [], note };
|
|
145
161
|
const lock = readJson(safe(root, '.harness/lock.json'));
|
|
146
162
|
const registry = registeredRoles(path.join(root, '.harness'), lock.capabilities ?? []);
|
|
@@ -151,9 +167,12 @@ export function agentPlan(root, host) {
|
|
|
151
167
|
const active = requested.enabled && Object.hasOwn(registry, id);
|
|
152
168
|
const mapping = profile.models?.[requested.provider + '/' + requested.model];
|
|
153
169
|
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
|
-
|
|
170
|
+
(!['host', 'anthropic', 'bedrock', 'vertex'].includes(requested.provider) || requested.model.startsWith('gpt-'))) ||
|
|
171
|
+
(host === 'gemini-cli' && (!['host', 'google', 'vertex'].includes(requested.provider) || requested.model.startsWith('gpt-'))));
|
|
172
|
+
const parts = registry[id]?.split('/');
|
|
173
|
+
const canonical = parts ? (parts[0] === 'core' ? 'core:' : parts[1] + ':') + path.basename(registry[id], '.md') : id;
|
|
174
|
+
const skills = registry[id] ? preferredSkills(fs.readFileSync(safe(path.join(root, '.harness'), registry[id]), 'utf8')) : { required: [], optional: [] };
|
|
175
|
+
return { ...requested, active, canonical_id: canonical, skills, contract: registry[id] ? '.harness/' + registry[id] : null,
|
|
157
176
|
host_model: mapping ?? requested.model, host_reasoning: profile.reasoning?.[requested.reasoning] ?? requested.reasoning,
|
|
158
177
|
mapping: mapping ? 'explicit-user-host-mapping' : 'requested',
|
|
159
178
|
availability: active && incompatible ? 'incompatible-provider' : 'unverified',
|
|
@@ -163,3 +182,20 @@ export function agentPlan(root, host) {
|
|
|
163
182
|
configured: true, host: host ?? null, presets, execution: config.execution, roles,
|
|
164
183
|
missing_mappings: Object.keys(registry).filter(id => !Object.hasOwn(config.agents, id)), note };
|
|
165
184
|
}
|
|
185
|
+
|
|
186
|
+
export function preferredSkills(body) {
|
|
187
|
+
const result={required:[],optional:[]};
|
|
188
|
+
const section=body.split('## Preferred skills')[1]?.split(/\n## /)[0] ?? '';
|
|
189
|
+
for(const line of section.split(/\r?\n/)){
|
|
190
|
+
const kind=line.match(/^- (Required|Optional):/)?.[1]?.toLowerCase();if(!kind)continue;
|
|
191
|
+
result[kind].push(...[...line.matchAll(/harness-[a-z0-9-]+/g)].map(m=>m[0]));
|
|
192
|
+
}
|
|
193
|
+
for(const list of Object.values(result))if(new Set(list).size!==list.length)throw new HarnessError('Duplicate preferred skill');
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
export function validatePreferredSkills(body, known) {
|
|
197
|
+
if(!body.includes('## Preferred skills'))throw new HarnessError('Agent missing preferred skill declaration');
|
|
198
|
+
const declared=preferredSkills(body);
|
|
199
|
+
for(const name of [...declared.required,...declared.optional])if(!known.has(name))throw new HarnessError('Unknown preferred skill: '+name);
|
|
200
|
+
return declared;
|
|
201
|
+
}
|
package/cli/capabilities.js
CHANGED
|
@@ -26,22 +26,44 @@ export function loadCapability(source, id) {
|
|
|
26
26
|
const meta = readJson(safe(source, prefix + 'capability.json'));
|
|
27
27
|
// Schema 1 supports optional role and template lists; legacy UI/UX stays valid.
|
|
28
28
|
const required = ['schema_version', 'id', 'description', 'skills', 'workflows'];
|
|
29
|
-
const allowed = new Set([...required, 'agents', 'templates', 'module_version', 'requires', 'recommends', 'conflicts', 'compatible_adapters', 'verification', 'evaluation']);
|
|
29
|
+
const allowed = new Set([...required, 'agents', 'templates', 'module_version', 'requires', 'recommends', 'conflicts', 'compatible_adapters', 'verification', 'evaluation', 'asset_owner', 'template_aliases', 'agent_aliases']);
|
|
30
30
|
if (!meta || typeof meta !== 'object' || Array.isArray(meta)
|
|
31
31
|
|| required.some(k => !Object.hasOwn(meta, k))
|
|
32
32
|
|| Object.keys(meta).some(k => !allowed.has(k))
|
|
33
33
|
|| meta.schema_version !== 1 || meta.id !== id
|
|
34
34
|
|| typeof meta.description !== 'string' || !meta.description.trim()) fail(`Invalid capability: ${id}`);
|
|
35
35
|
validateContract(meta, 'capability', id);
|
|
36
|
+
const owner = meta.asset_owner ?? id;
|
|
37
|
+
if (typeof owner !== 'string' || !/^[a-z][a-z0-9-]*$/.test(owner)) fail('Invalid capability asset owner');
|
|
38
|
+
const assetPrefix = 'capabilities/' + owner + '/';
|
|
39
|
+
if (owner !== id) {
|
|
40
|
+
const ownerMeta = readJson(safe(source, assetPrefix + 'capability.json'));
|
|
41
|
+
if (ownerMeta?.asset_owner) fail('Capability profiles require a canonical owner');
|
|
42
|
+
const canonical = loadCapability(source, owner);
|
|
43
|
+
for (const key of ['skills','workflows','agents','templates']) {
|
|
44
|
+
if (!Array.isArray(meta[key] ?? []) || !(meta[key] ?? []).every(v => (canonical[key] ?? []).includes(v))) fail('Profile contains undeclared canonical ' + key);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const [field, list, pattern] of [
|
|
48
|
+
['template_aliases', meta.templates ?? [], /^templates\/[a-z][a-z0-9-]*[.]md$/],
|
|
49
|
+
['agent_aliases', meta.agents ?? [], /^core\/agents\/[a-z][a-z0-9-]*[.]md$/],
|
|
50
|
+
]) {
|
|
51
|
+
const aliases = meta[field] ?? {};
|
|
52
|
+
if (!aliases || typeof aliases !== 'object' || Array.isArray(aliases)) fail('Invalid capability aliases');
|
|
53
|
+
for (const [from, to] of Object.entries(aliases)) {
|
|
54
|
+
if (!list.includes(from) || typeof to !== 'string' || !pattern.test(to)) fail('Invalid capability alias');
|
|
55
|
+
if (!fs.readFileSync(safe(source, to), 'utf8').trim()) fail('Empty canonical alias');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
36
58
|
if (!Array.isArray(meta.skills) || !meta.skills.length || !unique(meta.skills)
|
|
37
|
-
|| !meta.skills.every(name => validName(name) && name.startsWith(`harness-${
|
|
59
|
+
|| !meta.skills.every(name => validName(name) && name.startsWith(`harness-${owner}-`))) fail(`Invalid capability skills: ${id}`);
|
|
38
60
|
if (!Array.isArray(meta.workflows) || !meta.workflows.length || !unique(meta.workflows)
|
|
39
61
|
|| !meta.workflows.every(p => /^workflows\/[a-z][a-z0-9-]*\.md$/.test(p))) fail(`Invalid capability workflows: ${id}`);
|
|
40
62
|
for (const name of meta.skills) {
|
|
41
|
-
skillMetadata(fs.readFileSync(safe(source,
|
|
63
|
+
skillMetadata(fs.readFileSync(safe(source, assetPrefix + `skills/${name}/SKILL.md`), 'utf8'), name);
|
|
42
64
|
}
|
|
43
65
|
for (const workflow of meta.workflows) {
|
|
44
|
-
if (!fs.readFileSync(safe(source,
|
|
66
|
+
if (!fs.readFileSync(safe(source, assetPrefix + workflow), 'utf8').trim()) fail(`Empty capability workflow: ${id}/${workflow}`);
|
|
45
67
|
}
|
|
46
68
|
for (const [kind, pattern] of [
|
|
47
69
|
['agents', new RegExp('^agents/[a-z][a-z0-9-]*[.]md$')],
|
|
@@ -50,7 +72,7 @@ export function loadCapability(source, id) {
|
|
|
50
72
|
const entries = meta[kind] ?? [];
|
|
51
73
|
if (!Array.isArray(entries) || !unique(entries) || !entries.every(p => typeof p === 'string' && pattern.test(p))) fail(`Invalid capability ${kind}: ${id}`);
|
|
52
74
|
for (const file of entries) {
|
|
53
|
-
const body = fs.readFileSync(safe(source,
|
|
75
|
+
const body = fs.readFileSync(safe(source, assetPrefix + file), 'utf8');
|
|
54
76
|
if (!body.trim()) fail(`Empty capability ${kind}: ${id}/${file}`);
|
|
55
77
|
if (kind === 'agents' && !['## Inputs', '## Outputs', '## Permissions and limits', '## Handoff'].every(h => body.includes(h))) {
|
|
56
78
|
fail(`Invalid capability agent contract: ${id}/${file}`);
|
|
@@ -65,10 +87,16 @@ export function loadCapability(source, id) {
|
|
|
65
87
|
return { ...meta, agents: meta.agents ?? [], templates: meta.templates ?? [], checks, cases };
|
|
66
88
|
}
|
|
67
89
|
|
|
90
|
+
export const RETIRED_SKILLS = {
|
|
91
|
+
'harness-java-spring-kafka-outbox': 'Ask SA to assess requirements and existing contracts; no messaging pattern is selected by Harness.',
|
|
92
|
+
'harness-java-spring-database-migration': 'Use the approved SA data/deployment decision and project tooling; no universal migration recipe is installed.',
|
|
93
|
+
'harness-astro-cloudflare': 'Use approved hosting and provider documentation; Astro no longer selects a cloud provider.',
|
|
94
|
+
'harness-astro-microfrontend': 'Use approved SA module/deployment boundaries; no microfrontend recipe is installed.',
|
|
95
|
+
};
|
|
68
96
|
export function listCapabilities(source) {
|
|
69
97
|
return (manifest(source).capabilities ?? []).map(id => {
|
|
70
|
-
const { description, skills, workflows, agents, templates, module_version, verification, evaluation } = loadCapability(source, id);
|
|
71
|
-
return { id, description, skills, workflows, agents, templates, module_version: module_version ?? null, verification: verification ?? null, evaluation: evaluation ?? null };
|
|
98
|
+
const { description, skills, workflows, agents, templates, module_version, verification, evaluation, asset_owner } = loadCapability(source, id);
|
|
99
|
+
return { id, description, skills, workflows, agents, templates, asset_owner: asset_owner ?? id, module_version: module_version ?? null, verification: verification ?? null, evaluation: evaluation ?? null };
|
|
72
100
|
});
|
|
73
101
|
}
|
|
74
102
|
|
|
@@ -86,7 +114,10 @@ export function skillCatalog(root, lock) {
|
|
|
86
114
|
export function readSkill(root, lock, name) {
|
|
87
115
|
if (!validName(name) || !name.startsWith('harness-')) fail('Invalid skill name');
|
|
88
116
|
const relative = `.agents/skills/${name}/SKILL.md`;
|
|
89
|
-
if (!Object.hasOwn(lock.files, relative))
|
|
117
|
+
if (!Object.hasOwn(lock.files, relative)) {
|
|
118
|
+
if (RETIRED_SKILLS[name]) return { status: 'planned', name, retired: true, executed: false, message: RETIRED_SKILLS[name] };
|
|
119
|
+
fail(`Skill is not installed: ${name}`);
|
|
120
|
+
}
|
|
90
121
|
const content = payload(root, relative).toString('utf8');
|
|
91
122
|
skillMetadata(content, name);
|
|
92
123
|
return { status: 'ok', name, path: relative, content };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { safe, stat, digest, isObject, HarnessError } from './files.js';
|
|
3
|
+
import { readConfig } from './project-config.js';
|
|
4
|
+
import { redacted } from './verification-report.js';
|
|
5
|
+
const requireThat=(v,m)=>{if(!v)throw new HarnessError('Invalid checkpoint: '+m);};
|
|
6
|
+
const text=v=>typeof v==='string'&&v.trim().length>0&&v.length<=1024&&!/[\0\r\n]/.test(v);
|
|
7
|
+
export function checkpointPath(id) {
|
|
8
|
+
requireThat(typeof id==='string'&&/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(id)&&id!=='archive','change ID');
|
|
9
|
+
return 'openspec/changes/'+id+'/checkpoint.json';
|
|
10
|
+
}
|
|
11
|
+
function references(root,rows,verify=false) {
|
|
12
|
+
requireThat(Array.isArray(rows),'reference list');
|
|
13
|
+
return rows.map(r=>{
|
|
14
|
+
requireThat(isObject(r)&&text(r.path)&&/^[a-f0-9]{64}$/.test(r.sha256),'local reference/digest');
|
|
15
|
+
const p=safe(root,r.path),info=stat(p);
|
|
16
|
+
requireThat(info?.isFile()&&info.size<=1024*1024,'reference missing/oversized');
|
|
17
|
+
const actual=digest(fs.readFileSync(p));if(verify)requireThat(actual===r.sha256,'evidence digest mismatch');
|
|
18
|
+
return {...r,current_sha256:actual,current:actual===r.sha256};
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
export function validateCheckpoint(root,id,row) {
|
|
22
|
+
requireThat(isObject(row)&&row.schema_version===1&&row.change_id===id,'schema/change mismatch');redacted(row);
|
|
23
|
+
requireThat(text(row.phase)&&text(row.summary)&&text(row.next_action),'phase/summary/next action');
|
|
24
|
+
requireThat(typeof row.timestamp==='string'&&/T.*(?:Z|[+-]\d\d:\d\d)$/.test(row.timestamp)&&Number.isFinite(Date.parse(row.timestamp)),'timezone timestamp');
|
|
25
|
+
references(root,row.baseline);
|
|
26
|
+
requireThat(Array.isArray(row.completed)&&Array.isArray(row.pending)&&Array.isArray(row.blockers),'task lists');
|
|
27
|
+
const ids=new Set();
|
|
28
|
+
for(const task of [...row.completed,...row.pending,...(row.in_progress?[row.in_progress]:[])]){
|
|
29
|
+
requireThat(isObject(task)&&text(task.id)&&!ids.has(task.id)&&text(task.summary),'unique task ID/summary');ids.add(task.id);
|
|
30
|
+
}
|
|
31
|
+
for(const task of row.completed)requireThat(references(root,task.evidence,true).length>0,'completed task needs evidence');
|
|
32
|
+
requireThat(row.in_progress===null||isObject(row.in_progress),'in_progress');
|
|
33
|
+
requireThat(row.blockers.every(text),'blockers');
|
|
34
|
+
requireThat(Array.isArray(row.external_sources)&&row.external_sources.every(s=>isObject(s)&&text(s.location)&&text(s.revision)),'external source declarations');
|
|
35
|
+
requireThat(Array.isArray(row.operations),'operation list');const operations=new Set();
|
|
36
|
+
for(const op of row.operations){
|
|
37
|
+
requireThat(isObject(op)&&text(op.id)&&!operations.has(op.id)&&text(op.action)&&text(op.target)&&['prepared','completed','unknown'].includes(op.status),'operation receipt');operations.add(op.id);
|
|
38
|
+
const refs=references(root,op.evidence,true);if(op.status==='completed')requireThat(refs.length>0,'completed operation needs evidence');
|
|
39
|
+
}
|
|
40
|
+
if(row.usage!==undefined){
|
|
41
|
+
requireThat(isObject(row.usage)&&text(row.usage.source),'measured usage source');
|
|
42
|
+
for(const k of ['tokens','cost','remaining_tokens'])if(row.usage[k]!==null&&row.usage[k]!==undefined)requireThat(Number.isFinite(row.usage[k])&&row.usage[k]>=0,'usage');
|
|
43
|
+
if(row.usage.cost!==null&&row.usage.cost!==undefined)requireThat(text(row.usage.currency),'cost currency');
|
|
44
|
+
}
|
|
45
|
+
return row;
|
|
46
|
+
}
|
|
47
|
+
export function resumePlan(root,id) {
|
|
48
|
+
const name=checkpointPath(id),row=readConfig(root,name);requireThat(row!==null,'checkpoint missing');validateCheckpoint(root,id,row);
|
|
49
|
+
const baseline=references(root,row.baseline),pendingOperations=row.operations.filter(o=>o.status!=='completed');
|
|
50
|
+
const revalidate=baseline.some(r=>!r.current);
|
|
51
|
+
return {status:row.blockers.length?'blocked':'planned',executed:false,change_id:id,checkpoint:name,
|
|
52
|
+
checkpoint_sha256:digest(fs.readFileSync(safe(root,name))),baseline,revalidate_required:revalidate,
|
|
53
|
+
phase:row.phase,summary:row.summary,completed:row.completed,pending:row.pending,in_progress:row.in_progress,blockers:row.blockers,
|
|
54
|
+
next_action:row.next_action,operations_to_reconcile:pendingOperations,
|
|
55
|
+
external_sources:row.external_sources.map(s=>({...s,availability:'unverified'})),usage:row.usage??null,
|
|
56
|
+
note:'Read-only recovery plan. Reconcile changed sources, completed evidence and unknown/prepared side effects against actual host/remote state before retrying. Receipts and usage are producer-declared; no remote operation, model or quota probe.'};
|
|
57
|
+
}
|
package/cli/claude.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { safe, stat, HarnessError } from './files.js';
|
|
3
|
+
export const CLAUDE_START = '<!-- ai-harness:claude:start -->';
|
|
4
|
+
export const CLAUDE_END = '<!-- ai-harness:claude:end -->';
|
|
5
|
+
export const CLAUDE_IMPORT = Buffer.from(CLAUDE_START + '\n@AGENTS.md\n' + CLAUDE_END);
|
|
6
|
+
export function claudeBlock(data) {
|
|
7
|
+
const a=data.indexOf(CLAUDE_START), b=data.indexOf(CLAUDE_END);
|
|
8
|
+
if(a===-1 && b===-1)return null;
|
|
9
|
+
if(a===-1 || b<a || data.indexOf(CLAUDE_START,a+CLAUDE_START.length)!==-1 || data.indexOf(CLAUDE_END,b+CLAUDE_END.length)!==-1)
|
|
10
|
+
throw new HarnessError('CLAUDE.md has invalid Harness markers');
|
|
11
|
+
return data.subarray(a,b+CLAUDE_END.length);
|
|
12
|
+
}
|
|
13
|
+
function activeText(text) {
|
|
14
|
+
return text.replace(/^[ \t]*(\x60{3,}|~{3,})[^\r\n]*\r?\n[\s\S]*?^[ \t]*\1[ \t]*(?:\r?\n|$)/gm,'')
|
|
15
|
+
.replace(/\x60[^\x60\r\n]*\x60/g,'').replace(/<!--[\s\S]*?-->/g,'');
|
|
16
|
+
}
|
|
17
|
+
export function hasAgentsImport(text, nested=false) {
|
|
18
|
+
return new RegExp('(?:^|\\s)@' + (nested ? '\\.\\./' : '(?:\\./)?') + 'AGENTS\\.md(?=\\s|$)').test(activeText(text));
|
|
19
|
+
}
|
|
20
|
+
export function linkClaude(existing) {
|
|
21
|
+
const block=claudeBlock(existing);
|
|
22
|
+
if(block){if(block.toString().replaceAll('\r\n','\n')!==CLAUDE_IMPORT.toString())throw new HarnessError('CLAUDE managed import differs; reconcile before linking');return existing;}
|
|
23
|
+
if(hasAgentsImport(existing.toString('utf8')))return existing;
|
|
24
|
+
const newline=existing.includes(Buffer.from('\r\n'))?'\r\n':'\n';
|
|
25
|
+
const separator=existing.length?(existing.at(-1)===10?newline:newline+newline):'';
|
|
26
|
+
return Buffer.concat([existing,Buffer.from(separator),Buffer.from(CLAUDE_IMPORT.toString().replaceAll('\n',newline)+newline)]);
|
|
27
|
+
}
|
|
28
|
+
export function claudeReport(root) {
|
|
29
|
+
const files=[];
|
|
30
|
+
for(const name of ['CLAUDE.md','CLAUDE.local.md','.claude/CLAUDE.md']){
|
|
31
|
+
const p=safe(root,name),info=stat(p);if(!info)continue;
|
|
32
|
+
if(info.size>1024*1024)throw new HarnessError('Claude instruction file exceeds 1 MiB: '+name);
|
|
33
|
+
const body=fs.readFileSync(p,'utf8');
|
|
34
|
+
files.push({path:name,imports_agents:hasAgentsImport(body,name.startsWith('.claude/'))});
|
|
35
|
+
}
|
|
36
|
+
const linked=files.some(f=>f.imports_agents);
|
|
37
|
+
return {status:files.length&&!linked?'warning':'unverified',files,
|
|
38
|
+
warnings:files.length&&!linked?['Claude instructions may shadow AGENTS.md. Review actual version/settings; opt in to init/update --link-claude or configure the host to load both.']:[],
|
|
39
|
+
note:'Local import inspection only. Ancestor/global files, host version/plugin/settings and actual instruction loading are unverified; confirm with Claude /context. No native settings are changed.'};
|
|
40
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { isObject, HarnessError } from './files.js';
|
|
2
|
+
import { redacted } from './verification-report.js';
|
|
3
|
+
const tasks=['spring-api','transaction-defect','maven-onboarding'];
|
|
4
|
+
const metrics=['human_correction_rounds','unnecessary_questions','accepted_criteria','total_criteria','elapsed_seconds','tokens','cost'];
|
|
5
|
+
const requireThat=(v,m)=>{if(!v)throw new HarnessError('Invalid real-task comparison: '+m);};
|
|
6
|
+
export function comparisonTemplate(){
|
|
7
|
+
return {schema_version:1,status:'unperformed',metadata:{model:null,runtime:null,harness_commit:null,starting_commit:null,budget:null},tasks,
|
|
8
|
+
runs:tasks.flatMap(task=>['without-harness','with-harness'].flatMap(condition=>[1,2,3].map(repeat=>({task_id:task,condition,repeat,status:'UNPERFORMED',evidence:[],usage_source:null,currency:null,...Object.fromEntries(metrics.map(m=>[m,null]))}))))};
|
|
9
|
+
}
|
|
10
|
+
function distribution(values) {
|
|
11
|
+
if(!values.length)return {observed_runs:0,median:null,min:null,max:null};
|
|
12
|
+
const sorted=[...values].sort((a,b)=>a-b),i=Math.floor(sorted.length/2);
|
|
13
|
+
return {observed_runs:values.length,median:sorted.length%2?sorted[i]:(sorted[i-1]+sorted[i])/2,min:sorted[0],max:sorted.at(-1)};
|
|
14
|
+
}
|
|
15
|
+
export function summarizeComparison(report){
|
|
16
|
+
requireThat(isObject(report)&&report.schema_version===1&&isObject(report.metadata),'schema');redacted(report);
|
|
17
|
+
requireThat(Array.isArray(report.tasks)&&report.tasks.length===3&&new Set(report.tasks).size===3&&report.tasks.every(t=>typeof t==='string'&&/^[a-z][a-z0-9-]*$/.test(t)),'three unique task IDs');
|
|
18
|
+
requireThat(Array.isArray(report.runs)&&report.runs.length===18,'18 independent run slots');
|
|
19
|
+
const ids=new Set();
|
|
20
|
+
for(const row of report.runs){
|
|
21
|
+
requireThat(isObject(row)&&report.tasks.includes(row.task_id)&&['with-harness','without-harness'].includes(row.condition)&&[1,2,3].includes(row.repeat),'task/condition/repeat');
|
|
22
|
+
const id=row.task_id+'/'+row.condition+'/'+row.repeat;requireThat(!ids.has(id),'duplicate run');ids.add(id);
|
|
23
|
+
requireThat(['UNPERFORMED','COMPLETED','FAILED','INTERRUPTED','BLOCKED'].includes(row.status),'status');
|
|
24
|
+
requireThat(Array.isArray(row.evidence)&&row.evidence.every(v=>typeof v==='string'&&v.trim()&&!/[\0\r\n]/.test(v)),'evidence');
|
|
25
|
+
if(row.status!=='UNPERFORMED')requireThat(row.evidence.length>0,'observed run needs evidence');
|
|
26
|
+
for(const m of metrics){requireThat(row[m]===null||(typeof row[m]==='number'&&Number.isFinite(row[m])&&row[m]>=0),'metric '+m);
|
|
27
|
+
if(['human_correction_rounds','unnecessary_questions','accepted_criteria','total_criteria','tokens'].includes(m)&&row[m]!==null)requireThat(Number.isSafeInteger(row[m]),'integer metric '+m);
|
|
28
|
+
}
|
|
29
|
+
if(row.status==='UNPERFORMED')requireThat(metrics.every(m=>row[m]===null),'unperformed metrics must stay null');
|
|
30
|
+
if(row.accepted_criteria!==null)requireThat(row.total_criteria!==null&&row.accepted_criteria<=row.total_criteria,'accepted/total criteria');
|
|
31
|
+
if(row.tokens!==null||row.cost!==null)requireThat(typeof row.usage_source==='string'&&row.usage_source.trim(),'actual usage source');
|
|
32
|
+
if(row.cost!==null)requireThat(typeof row.currency==='string'&&row.currency.trim(),'cost currency');
|
|
33
|
+
}
|
|
34
|
+
const currencies=new Set(report.runs.filter(r=>r.cost!==null).map(r=>r.currency));
|
|
35
|
+
requireThat(currencies.size<=1,'mixed cost currencies');
|
|
36
|
+
for(const task of report.tasks)requireThat(new Set(report.runs.filter(r=>r.task_id===task&&r.total_criteria!==null).map(r=>r.total_criteria)).size<=1,'inconsistent acceptance totals');
|
|
37
|
+
const observed=report.runs.some(r=>r.status!=='UNPERFORMED');
|
|
38
|
+
if(observed)for(const key of ['model','runtime','harness_commit','starting_commit'])requireThat(typeof report.metadata[key]==='string'&&report.metadata[key].trim(),'metadata '+key);
|
|
39
|
+
const summaries=report.tasks.flatMap(task=>['without-harness','with-harness'].map(condition=>{
|
|
40
|
+
const rows=report.runs.filter(r=>r.task_id===task&&r.condition===condition);
|
|
41
|
+
return {task_id:task,condition,statuses:Object.fromEntries(['UNPERFORMED','COMPLETED','FAILED','INTERRUPTED','BLOCKED'].map(s=>[s,rows.filter(r=>r.status===s).length])),
|
|
42
|
+
metrics:Object.fromEntries(metrics.map(m=>[m,distribution(rows.map(r=>r[m]).filter(v=>v!==null))]))};
|
|
43
|
+
}));
|
|
44
|
+
return {status:'ok',executed:false,observed_runs:report.runs.filter(r=>r.status!=='UNPERFORMED').length,summaries,
|
|
45
|
+
note:'Summarizes manually supplied observations only. Null is unknown, not zero. Retain all failures/interruptions; independently review evidence, equal acceptance criteria and matching model/task/budget. No productivity claim or model execution is inferred.'};
|
|
46
|
+
}
|
package/cli/composition.js
CHANGED
|
@@ -73,6 +73,7 @@ export function validateRegistry(source) {
|
|
|
73
73
|
const graph = new Map();
|
|
74
74
|
for (const {kind,id,data} of entries) {
|
|
75
75
|
const owner=moduleRef(kind,id);
|
|
76
|
+
if (data.asset_owner && !declared.has(moduleRef('capability',data.asset_owner))) throw new HarnessError('Unknown capability asset owner');
|
|
76
77
|
for (const field of ['requires','recommends','conflicts']) {
|
|
77
78
|
for (const ref of data[field] ?? []) {
|
|
78
79
|
if (!declared.has(ref)) throw new HarnessError(`Unknown ${field} reference ${ref} in ${owner}`);
|
package/cli/files.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
|
+
import { claudeBlock } from './claude.js';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
5
6
|
|
|
6
7
|
// Resolve from the installed package, never the consumer's cwd or npm's bin symlink.
|
|
@@ -100,7 +101,7 @@ export function payload(root, name) {
|
|
|
100
101
|
const filename = safe(root, name);
|
|
101
102
|
if (!stat(filename)) return null;
|
|
102
103
|
const data = fs.readFileSync(filename);
|
|
103
|
-
return name === 'AGENTS.md' ? block(data) : data;
|
|
104
|
+
return name === 'AGENTS.md' ? block(data) : name === 'CLAUDE.md' ? claudeBlock(data) : data;
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
export function manifest(source) {
|