@manhd_d/ai-harness 0.0.0-stage → 0.6.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 +96 -0
- package/CONTRIBUTING.md +13 -0
- package/LICENSE +21 -0
- package/README.md +328 -2
- package/SECURITY.md +10 -0
- package/adapters/ai-python/adapter.yaml +46 -0
- package/adapters/ai-python/checks/verification.yaml +32 -0
- package/adapters/ai-python/evals/cases.json +25 -0
- package/adapters/ai-python/rules/architecture.md +3 -0
- package/adapters/ai-python/rules/database.md +3 -0
- package/adapters/ai-python/rules/performance.md +3 -0
- package/adapters/ai-python/rules/security.md +3 -0
- package/adapters/ai-python/rules.md +29 -0
- package/adapters/ai-python/skills/harness-ai-python-rag-access/SKILL.md +23 -0
- package/adapters/ai-python/skills/harness-ai-python-tool-safety/SKILL.md +23 -0
- package/adapters/astro/README.md +23 -0
- package/adapters/astro/adapter.yaml +31 -0
- package/adapters/astro/checks/verification.yaml +59 -0
- package/adapters/astro/evals/cases.json +45 -0
- package/adapters/astro/rules/architecture.md +9 -0
- package/adapters/astro/rules/deployment.md +9 -0
- package/adapters/astro/rules/performance.md +8 -0
- package/adapters/astro/rules/security.md +9 -0
- package/adapters/astro/rules.md +17 -0
- package/adapters/astro/skills/harness-astro-cloudflare/SKILL.md +27 -0
- package/adapters/astro/skills/harness-astro-islands/SKILL.md +28 -0
- package/adapters/astro/skills/harness-astro-microfrontend/SKILL.md +27 -0
- package/adapters/astro/skills/harness-astro-performance/SKILL.md +27 -0
- package/adapters/astro/skills/harness-astro-project-architecture/SKILL.md +28 -0
- package/adapters/astro/skills/harness-astro-seo/SKILL.md +28 -0
- package/adapters/java-spring/adapter.yaml +42 -0
- package/adapters/java-spring/checks/verification.yaml +33 -0
- package/adapters/java-spring/evals/cases.json +25 -0
- package/adapters/java-spring/rules/architecture.md +3 -0
- package/adapters/java-spring/rules/database.md +3 -0
- package/adapters/java-spring/rules/performance.md +3 -0
- package/adapters/java-spring/rules/security.md +3 -0
- package/adapters/java-spring/rules.md +34 -0
- package/adapters/java-spring/skills/harness-java-spring-database-migration/SKILL.md +23 -0
- package/adapters/java-spring/skills/harness-java-spring-kafka-outbox/SKILL.md +23 -0
- package/adapters/java-spring/skills/harness-java-spring-spring-api/SKILL.md +23 -0
- package/adapters/kubernetes/adapter.yaml +58 -0
- package/adapters/kubernetes/checks/verification.yaml +63 -0
- package/adapters/kubernetes/evals/cases.json +25 -0
- package/adapters/kubernetes/rules/architecture.md +5 -0
- package/adapters/kubernetes/rules/reliability.md +5 -0
- package/adapters/kubernetes/rules/security.md +5 -0
- package/adapters/kubernetes/rules.md +9 -0
- package/adapters/kubernetes/skills/harness-kubernetes-helm-release/SKILL.md +26 -0
- package/adapters/kubernetes/skills/harness-kubernetes-rollout-safety/SKILL.md +27 -0
- package/adapters/kubernetes/skills/harness-kubernetes-workload-review/SKILL.md +27 -0
- package/adapters/react-native/adapter.yaml +28 -0
- package/adapters/react-native/checks/verification.yaml +30 -0
- package/adapters/react-native/evals/cases.json +25 -0
- package/adapters/react-native/rules/architecture.md +3 -0
- package/adapters/react-native/rules/database.md +3 -0
- package/adapters/react-native/rules/performance.md +3 -0
- package/adapters/react-native/rules/security.md +3 -0
- package/adapters/react-native/rules.md +23 -0
- package/adapters/react-native/skills/harness-react-native-device-permissions/SKILL.md +23 -0
- package/adapters/react-native/skills/harness-react-native-offline-sync/SKILL.md +23 -0
- package/adapters/react-nextjs/adapter.yaml +28 -0
- package/adapters/react-nextjs/checks/verification.yaml +49 -0
- package/adapters/react-nextjs/evals/cases.json +25 -0
- package/adapters/react-nextjs/rules/architecture.md +3 -0
- package/adapters/react-nextjs/rules/database.md +3 -0
- package/adapters/react-nextjs/rules/performance.md +3 -0
- package/adapters/react-nextjs/rules/security.md +3 -0
- package/adapters/react-nextjs/rules.md +27 -0
- package/adapters/react-nextjs/skills/harness-react-nextjs-accessible-ui/SKILL.md +23 -0
- package/adapters/react-nextjs/skills/harness-react-nextjs-server-boundaries/SKILL.md +23 -0
- package/bin/aih.js +4 -0
- package/capabilities/devops/README.md +24 -0
- package/capabilities/devops/capability.json +20 -0
- package/capabilities/devops/checks/verification.json +17 -0
- package/capabilities/devops/evals/cases.json +25 -0
- package/capabilities/devops/skills/harness-devops-ci-cd/SKILL.md +27 -0
- package/capabilities/devops/skills/harness-devops-deployment/SKILL.md +27 -0
- package/capabilities/devops/skills/harness-devops-docker/SKILL.md +25 -0
- package/capabilities/devops/skills/harness-devops-incident-response/SKILL.md +27 -0
- package/capabilities/devops/skills/harness-devops-infrastructure/SKILL.md +28 -0
- package/capabilities/devops/skills/harness-devops-observability/SKILL.md +25 -0
- package/capabilities/devops/workflows/incident-recovery.md +12 -0
- package/capabilities/devops/workflows/pr-to-production.md +16 -0
- package/capabilities/monetization/README.md +40 -0
- package/capabilities/monetization/capability.json +25 -0
- package/capabilities/monetization/checks/verification.json +53 -0
- package/capabilities/monetization/evals/cases.json +45 -0
- package/capabilities/monetization/skills/harness-monetization-ad-placement-ux/SKILL.md +28 -0
- package/capabilities/monetization/skills/harness-monetization-adsense-integration/SKILL.md +29 -0
- package/capabilities/monetization/skills/harness-monetization-analytics-revenue/SKILL.md +28 -0
- package/capabilities/monetization/skills/harness-monetization-publisher-compliance/SKILL.md +28 -0
- package/capabilities/monetization/skills/harness-monetization-seo-growth/SKILL.md +28 -0
- package/capabilities/monetization/templates/ad-placement-review.md +31 -0
- package/capabilities/monetization/templates/analytics-report.md +38 -0
- package/capabilities/monetization/templates/privacy-consent-checklist.md +39 -0
- package/capabilities/monetization/templates/site-monetization-plan.md +46 -0
- package/capabilities/monetization/workflows/site-to-adsense.md +19 -0
- package/capabilities/monetization/workflows/traffic-to-revenue-review.md +14 -0
- package/capabilities/product/README.md +78 -0
- package/capabilities/product/agents/business-analyst.md +15 -0
- package/capabilities/product/agents/product-manager.md +15 -0
- package/capabilities/product/agents/product-owner.md +15 -0
- package/capabilities/product/agents/project-manager.md +15 -0
- package/capabilities/product/agents/requirements-reviewer.md +15 -0
- package/capabilities/product/capability.json +55 -0
- package/capabilities/product/checks/verification.json +41 -0
- package/capabilities/product/evals/cases.json +35 -0
- package/capabilities/product/skills/harness-product-acceptance-criteria/SKILL.md +25 -0
- package/capabilities/product/skills/harness-product-backlog-prioritization/SKILL.md +25 -0
- package/capabilities/product/skills/harness-product-brd-generation/SKILL.md +25 -0
- package/capabilities/product/skills/harness-product-business-analysis/SKILL.md +26 -0
- package/capabilities/product/skills/harness-product-document-review/SKILL.md +26 -0
- package/capabilities/product/skills/harness-product-idea-discovery/SKILL.md +27 -0
- package/capabilities/product/skills/harness-product-knowledge-analysis/SKILL.md +28 -0
- package/capabilities/product/skills/harness-product-prd-generation/SKILL.md +26 -0
- package/capabilities/product/skills/harness-product-product-research/SKILL.md +27 -0
- package/capabilities/product/skills/harness-product-srs-generation/SKILL.md +28 -0
- package/capabilities/product/skills/harness-product-traceability/SKILL.md +24 -0
- package/capabilities/product/skills/harness-product-use-case-generation/SKILL.md +26 -0
- package/capabilities/product/skills/harness-product-user-stories/SKILL.md +25 -0
- package/capabilities/product/templates/acceptance-criteria.md +27 -0
- package/capabilities/product/templates/backlog.md +23 -0
- package/capabilities/product/templates/brd.md +43 -0
- package/capabilities/product/templates/business-rules.md +27 -0
- package/capabilities/product/templates/decision-log.md +7 -0
- package/capabilities/product/templates/delivery-plan.md +18 -0
- package/capabilities/product/templates/idea.md +38 -0
- package/capabilities/product/templates/prd-lite.md +39 -0
- package/capabilities/product/templates/prd.md +36 -0
- package/capabilities/product/templates/product-brief.md +27 -0
- package/capabilities/product/templates/research.md +31 -0
- package/capabilities/product/templates/review-report.md +26 -0
- package/capabilities/product/templates/source-register.md +19 -0
- package/capabilities/product/templates/srs.md +46 -0
- package/capabilities/product/templates/traceability.md +23 -0
- package/capabilities/product/templates/use-cases.md +34 -0
- package/capabilities/product/workflows/change-impact-analysis.md +15 -0
- package/capabilities/product/workflows/confluence-to-spec.md +21 -0
- package/capabilities/product/workflows/document-driven.md +13 -0
- package/capabilities/product/workflows/idea-driven.md +13 -0
- package/capabilities/product/workflows/idea-to-prd.md +20 -0
- package/capabilities/uiux/README.md +21 -0
- package/capabilities/uiux/capability.json +23 -0
- package/capabilities/uiux/checks/verification.json +17 -0
- package/capabilities/uiux/design-system/tokens.example.json +21 -0
- package/capabilities/uiux/evals/cases.json +15 -0
- package/capabilities/uiux/integrations/confluence.md +10 -0
- package/capabilities/uiux/integrations/figma.md +10 -0
- package/capabilities/uiux/skills/harness-uiux-design-review/SKILL.md +24 -0
- package/capabilities/uiux/skills/harness-uiux-design-system/SKILL.md +24 -0
- package/capabilities/uiux/skills/harness-uiux-figma-design/SKILL.md +26 -0
- package/capabilities/uiux/skills/harness-uiux-requirements-analysis/SKILL.md +26 -0
- package/capabilities/uiux/skills/harness-uiux-screen-planning/SKILL.md +24 -0
- package/capabilities/uiux/skills/harness-uiux-user-flow/SKILL.md +25 -0
- package/capabilities/uiux/templates/requirements.md +7 -0
- package/capabilities/uiux/templates/screen-spec.md +15 -0
- package/capabilities/uiux/workflows/docs-to-figma.md +22 -0
- package/capabilities/uiux/workflows/idea-to-figma.md +11 -0
- package/cli/adapter-schema.js +42 -0
- package/cli/adapters.js +149 -0
- package/cli/capabilities.js +93 -0
- package/cli/composition.js +96 -0
- package/cli/evaluation-contract.js +40 -0
- package/cli/files.js +125 -0
- package/cli/harness.js +255 -0
- package/cli/main.js +114 -0
- package/cli/module-contract.js +29 -0
- package/core/agents/implementer.md +16 -0
- package/core/agents/planner.md +16 -0
- package/core/agents/reviewer.md +16 -0
- package/core/agents/security-reviewer.md +16 -0
- package/core/agents/test-engineer.md +16 -0
- package/core/checks/verification.json +28 -0
- package/core/evals/cases.json +25 -0
- package/core/evals/composition.json +33 -0
- package/core/policies/permissions.md +17 -0
- package/core/policies/security.md +15 -0
- package/core/rules/context.md +15 -0
- package/core/rules/foundations.md +24 -0
- package/core/skills/harness-ai-evaluation/SKILL.md +21 -0
- package/core/skills/harness-debugging/SKILL.md +20 -0
- package/core/skills/harness-review/SKILL.md +20 -0
- package/core/skills/harness-spec/SKILL.md +21 -0
- package/core/skills/harness-tdd/SKILL.md +20 -0
- package/core/workflows/bugfix.md +9 -0
- package/core/workflows/spec-to-code.md +19 -0
- package/core/workflows/upgrade.md +10 -0
- package/docs/adapters.md +83 -0
- package/docs/agent-handoffs.md +79 -0
- package/docs/architecture.md +107 -0
- package/docs/cli.md +104 -0
- package/docs/documentation-workflows.md +71 -0
- package/docs/end-to-end-workflow.md +116 -0
- package/docs/examples/ai-rag-service.md +60 -0
- package/docs/examples/astro-adsense-microtools.md +80 -0
- package/docs/examples/astro-micro-tools.md +125 -0
- package/docs/examples/document-driven-project.md +62 -0
- package/docs/examples/fullstack-web.md +80 -0
- package/docs/examples/idea-driven-project.md +71 -0
- package/docs/examples/react-native-app.md +57 -0
- package/docs/examples/spring-boot-api.md +100 -0
- package/docs/getting-started.md +148 -0
- package/docs/monetization.md +53 -0
- package/docs/npm-release.md +70 -0
- package/docs/openspec.md +23 -0
- package/docs/pull-request.md +37 -0
- package/docs/reference-analysis.md +22 -0
- package/docs/release-policy.md +17 -0
- package/docs/skill-matrix.md +134 -0
- package/docs/verification.md +20 -0
- package/docs/versioning.md +89 -0
- package/evals/README.md +45 -0
- package/evals/cases.json +41 -0
- package/harness.yaml +24 -0
- package/integrations/confluence/README.md +31 -0
- package/integrations/github/README.md +17 -0
- package/integrations/github/pull_request_template.md +22 -0
- package/integrations/gitlab/README.md +9 -0
- package/integrations/gitlab/harness-ci.yml +10 -0
- package/integrations/mcp/README.md +16 -0
- package/integrations/mcp/server-inventory.example.json +16 -0
- package/package.json +42 -4
- package/scripts/score-eval.js +40 -0
- package/templates/AGENTS.md +18 -0
- package/templates/openspec/.openspec.yaml +1 -0
- package/templates/openspec/config.yaml +14 -0
- package/templates/openspec/design.md +14 -0
- package/templates/openspec/proposal.md +19 -0
- package/templates/openspec/spec.md +8 -0
- package/templates/openspec/tasks.md +11 -0
package/cli/adapters.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { safe, readJson, manifest, HarnessError, check, loadLock, encode, digest } from './harness.js';
|
|
4
|
+
import { scoreDataset } from '../scripts/score-eval.js';
|
|
5
|
+
|
|
6
|
+
import { loadAdapter, requireThat, object } from './adapter-schema.js';
|
|
7
|
+
import { loadCapability } from './capabilities.js';
|
|
8
|
+
import { casesFrom, checksFrom } from './evaluation-contract.js';
|
|
9
|
+
export { loadAdapter } from './adapter-schema.js';
|
|
10
|
+
|
|
11
|
+
export function listAdapters(source) {
|
|
12
|
+
return { status: 'ok', adapters: manifest(source).adapters.map(id => {
|
|
13
|
+
const a = loadAdapter(source, id);
|
|
14
|
+
return { id, description: a.description, module_version: a.module_version ?? null, skills: a.skills, detection: a.detection };
|
|
15
|
+
}) };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function exists(root, file) { return fs.existsSync(safe(root, file)); }
|
|
19
|
+
function probe(root, file) {
|
|
20
|
+
const filename = safe(root, file);
|
|
21
|
+
if (!fs.existsSync(filename)) return null;
|
|
22
|
+
requireThat(fs.statSync(filename).size <= 1024 * 1024, `Detection file exceeds 1 MiB: ${file}`);
|
|
23
|
+
return fs.readFileSync(filename, 'utf8');
|
|
24
|
+
}
|
|
25
|
+
export function detect(root, source) {
|
|
26
|
+
const matches = [];
|
|
27
|
+
for (const id of manifest(source).adapters) {
|
|
28
|
+
const a = loadAdapter(source, id), evidence = [];
|
|
29
|
+
for (const rule of a.detection) {
|
|
30
|
+
const body = probe(root, rule.file);
|
|
31
|
+
if (body === null) continue;
|
|
32
|
+
let found;
|
|
33
|
+
if (rule.kind === 'dependencies') {
|
|
34
|
+
let pkg; try { pkg = JSON.parse(body); } catch { throw new HarnessError(`Invalid JSON during detection: ${rule.file}`); }
|
|
35
|
+
requireThat(object(pkg), `Invalid package metadata: ${rule.file}`);
|
|
36
|
+
const deps = new Set();
|
|
37
|
+
for (const field of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
|
|
38
|
+
if (pkg[field] === undefined) continue;
|
|
39
|
+
requireThat(object(pkg[field]), `Invalid ${field}: ${rule.file}`);
|
|
40
|
+
for (const key of Object.keys(pkg[field])) deps.add(key);
|
|
41
|
+
}
|
|
42
|
+
found = rule.values.filter(v => deps.has(v));
|
|
43
|
+
} else found = rule.values.filter(v => body.includes(v));
|
|
44
|
+
if (found.length) evidence.push({ file: rule.file, kind: rule.kind, matched: found });
|
|
45
|
+
}
|
|
46
|
+
if (evidence.length) matches.push({ id, evidence });
|
|
47
|
+
}
|
|
48
|
+
// Astro can use @astrojs/react islands (react-dom dependency) without being a
|
|
49
|
+
// React/Next.js application. Suppress that heuristic-only overlap unless Next
|
|
50
|
+
// itself is present. Explicit adapter selection remains unrestricted.
|
|
51
|
+
const astroDetected = matches.some(match => match.id === 'astro');
|
|
52
|
+
const filtered = matches.filter(match => !(astroDetected && match.id === 'react-nextjs'
|
|
53
|
+
&& !match.evidence.some(row => row.matched.includes('next'))));
|
|
54
|
+
return { status: 'ok', adapters: filtered.map(m => m.id).sort(), matches: filtered,
|
|
55
|
+
note: 'Heuristic, current target only; review evidence. No recursive monorepo scan or application execution.' };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function installedAdapters(root) {
|
|
59
|
+
const result = check(root);
|
|
60
|
+
requireThat(result.status === 'ok', 'Fix installation drift before reading capabilities');
|
|
61
|
+
const lock = loadLock(root), source = path.join(root, '.harness');
|
|
62
|
+
// Require capability files to be covered by the installed lock, not merely present nearby.
|
|
63
|
+
const adapters = lock.adapters.map(id => {
|
|
64
|
+
requireThat(Object.hasOwn(lock.files, `.harness/adapters/${id}/adapter.yaml`), `Adapter ${id} needs aih update to enable capabilities`);
|
|
65
|
+
const a = loadAdapter(source, id);
|
|
66
|
+
for (const file of [a.verification, a.evaluation, ...a.rules, ...a.skills.map(s => `skills/${s}/SKILL.md`)]) {
|
|
67
|
+
requireThat(Object.hasOwn(lock.files, `.harness/adapters/${id}/${file}`), `Capability is not locked: ${id}/${file}`);
|
|
68
|
+
}
|
|
69
|
+
return a;
|
|
70
|
+
});
|
|
71
|
+
return { adapters, lock };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const SCOPES = new Set(['adapters', 'capabilities', 'core', 'all']);
|
|
75
|
+
function installedEvaluations(root, scope) {
|
|
76
|
+
requireThat(SCOPES.has(scope), `Invalid evaluation scope: ${scope}`);
|
|
77
|
+
const { adapters, lock } = installedAdapters(root);
|
|
78
|
+
const source = path.join(root, '.harness');
|
|
79
|
+
const include = part => part === scope || scope === 'all';
|
|
80
|
+
const capabilities = [];
|
|
81
|
+
if (include('capabilities')) {
|
|
82
|
+
for (const id of lock.capabilities ?? []) {
|
|
83
|
+
const name = `.harness/capabilities/${id}/capability.json`;
|
|
84
|
+
requireThat(Object.hasOwn(lock.files, name), `Capability is not locked: ${id}`);
|
|
85
|
+
const c = loadCapability(source, id);
|
|
86
|
+
for (const file of [c.verification, c.evaluation].filter(Boolean)) {
|
|
87
|
+
requireThat(Object.hasOwn(lock.files, `.harness/capabilities/${id}/${file}`),
|
|
88
|
+
`Capability is not locked: ${id}/${file}`);
|
|
89
|
+
}
|
|
90
|
+
capabilities.push(c);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
let core = { checks: [], cases: [] }, composition = [];
|
|
94
|
+
if (include('core')) {
|
|
95
|
+
for (const relative of ['core/checks/verification.json', 'core/evals/cases.json', 'core/evals/composition.json']) {
|
|
96
|
+
requireThat(Object.hasOwn(lock.files, `.harness/${relative}`), 'Core evaluation assets missing; run aih update');
|
|
97
|
+
}
|
|
98
|
+
core = { checks: checksFrom(source, 'core/checks/verification.json'), cases: casesFrom(source, 'core/evals/cases.json') };
|
|
99
|
+
const manifest = readJson(safe(source, 'core/evals/composition.json'));
|
|
100
|
+
requireThat(manifest?.schema_version === 1 && Array.isArray(manifest.cases), 'Invalid composition dataset');
|
|
101
|
+
const selected = new Set([...lock.adapters.map(x => `adapter:${x}`), ...(lock.capabilities ?? []).map(x => `capability:${x}`)]);
|
|
102
|
+
const ids = new Set();
|
|
103
|
+
for (const row of manifest.cases) {
|
|
104
|
+
requireThat(typeof row.id === 'string' && /^[a-z][a-z0-9-]*$/.test(row.id) && !ids.has(row.id)
|
|
105
|
+
&& Array.isArray(row.requires) && row.requires.length > 0 && new Set(row.requires).size === row.requires.length
|
|
106
|
+
&& row.requires.every(ref => typeof ref === 'string' && /^(adapter|capability):[a-z][a-z0-9-]*$/.test(ref))
|
|
107
|
+
&& typeof row.prompt === 'string' && row.prompt.trim()
|
|
108
|
+
&& Array.isArray(row.criteria) && row.criteria.length > 0 && row.criteria.every(x => typeof x === 'string' && x.trim())
|
|
109
|
+
&& typeof row.critical === 'boolean', `Invalid composition scenario: ${row.id}`);
|
|
110
|
+
ids.add(row.id);
|
|
111
|
+
if (row.requires.every(x => selected.has(x))) {
|
|
112
|
+
const { requires, ...scenario } = row;
|
|
113
|
+
composition.push(scenario);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { adapters: include('adapters') ? adapters : [], capabilities, core, composition, lock };
|
|
118
|
+
}
|
|
119
|
+
export function verify(root, scope = 'adapters') {
|
|
120
|
+
const { adapters, capabilities, core, lock } = installedEvaluations(root, scope);
|
|
121
|
+
const checks = [
|
|
122
|
+
...adapters.flatMap(a => a.checks.map(c => ({ adapter: a.id, ...c }))),
|
|
123
|
+
...capabilities.flatMap(c => c.checks.map(check => ({ capability: c.id, ...check }))),
|
|
124
|
+
...core.checks.map(c => ({ core: true, ...c })),
|
|
125
|
+
];
|
|
126
|
+
return { status: 'planned', version: lock.version, scope, executed: false,
|
|
127
|
+
checks: checks.map(c => ({ ...c,
|
|
128
|
+
applicability: c.when_file === null ? 'review-required' : exists(root, c.when_file) ? 'candidate' : 'not-detected' })),
|
|
129
|
+
note: 'Advisory plan only; commands are NOT executed and manual gates require human evidence.' };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function evaluate(root, reportPath, scope = 'adapters') {
|
|
133
|
+
const { adapters, capabilities, core, composition, lock } = installedEvaluations(root, scope);
|
|
134
|
+
const dataset = { schema_version: 1, cases: [
|
|
135
|
+
...adapters.flatMap(a => a.cases.map(c => ({ ...c, id: `${a.id}/${c.id}` }))),
|
|
136
|
+
...capabilities.flatMap(c => c.cases.map(row => ({ ...row, id: `capability/${c.id}/${row.id}` }))),
|
|
137
|
+
...core.cases.map(c => ({ ...c, id: `core/${c.id}` })),
|
|
138
|
+
...composition.map(c => ({ ...c, id: `composition/${c.id}` })),
|
|
139
|
+
] };
|
|
140
|
+
requireThat(dataset.cases.length > 0, 'Select at least one adapter or capability to evaluate, or use --scope core/all');
|
|
141
|
+
const bytes = encode(dataset), hash = digest(bytes);
|
|
142
|
+
if (!reportPath) return { status: 'planned', scope, dataset, report: { schema_version: 1,
|
|
143
|
+
metadata: { model: '', harness_commit: '', dataset_sha256: hash, bundle_sha256: lock.bundle_sha256, reviewer: '', run_id: '', parameters: '' },
|
|
144
|
+
results: dataset.cases.map(c => ({ id: c.id, scores: c.criteria.map(() => null), evidence: '' })) },
|
|
145
|
+
note: 'Unscored template; run a real model separately and record reviewer evidence. No model is invoked.' };
|
|
146
|
+
const report = readJson(path.resolve(reportPath));
|
|
147
|
+
requireThat(report?.metadata?.bundle_sha256 === lock.bundle_sha256, 'Evaluation report belongs to a different installed bundle');
|
|
148
|
+
return { ...scoreDataset(report, bytes), version: lock.version, scope, dataset_sha256: hash };
|
|
149
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { manifest, readJson, safe, SLUG, HarnessError, payload } from './files.js';
|
|
3
|
+
import { validateContract } from './module-contract.js';
|
|
4
|
+
import { checksFrom, casesFrom } from './evaluation-contract.js';
|
|
5
|
+
|
|
6
|
+
const validName = name => typeof name === 'string' && SLUG.test(name);
|
|
7
|
+
const fail = message => { throw new HarnessError(message); };
|
|
8
|
+
const exact = (value, fields) => value && typeof value === 'object' && !Array.isArray(value)
|
|
9
|
+
&& Object.keys(value).sort().join(',') === [...fields].sort().join(',');
|
|
10
|
+
const unique = values => new Set(values).size === values.length;
|
|
11
|
+
|
|
12
|
+
function skillMetadata(body, name) {
|
|
13
|
+
const front = /^---\r?\n([\s\S]*?)\r?\n---\r?\n/.exec(body);
|
|
14
|
+
if (!front || !front[1].split(/\r?\n/).includes(`name: ${name}`)) fail(`Invalid skill metadata: ${name}`);
|
|
15
|
+
const description = front[1].split(/\r?\n/).find(line => line.startsWith('description: '))?.slice(13)?.trim();
|
|
16
|
+
if (!description) fail(`Missing skill description: ${name}`);
|
|
17
|
+
for (const heading of ['## Input', '## Procedure', '## Output', '## Constraints']) {
|
|
18
|
+
if (!body.includes(heading)) fail(`Missing ${heading} in skill: ${name}`);
|
|
19
|
+
}
|
|
20
|
+
return { name, description };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function loadCapability(source, id) {
|
|
24
|
+
if (!validName(id)) fail('Invalid capability ID');
|
|
25
|
+
const prefix = `capabilities/${id}/`;
|
|
26
|
+
const meta = readJson(safe(source, prefix + 'capability.json'));
|
|
27
|
+
// Schema 1 supports optional role and template lists; legacy UI/UX stays valid.
|
|
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']);
|
|
30
|
+
if (!meta || typeof meta !== 'object' || Array.isArray(meta)
|
|
31
|
+
|| required.some(k => !Object.hasOwn(meta, k))
|
|
32
|
+
|| Object.keys(meta).some(k => !allowed.has(k))
|
|
33
|
+
|| meta.schema_version !== 1 || meta.id !== id
|
|
34
|
+
|| typeof meta.description !== 'string' || !meta.description.trim()) fail(`Invalid capability: ${id}`);
|
|
35
|
+
validateContract(meta, 'capability', id);
|
|
36
|
+
if (!Array.isArray(meta.skills) || !meta.skills.length || !unique(meta.skills)
|
|
37
|
+
|| !meta.skills.every(name => validName(name) && name.startsWith(`harness-${id}-`))) fail(`Invalid capability skills: ${id}`);
|
|
38
|
+
if (!Array.isArray(meta.workflows) || !meta.workflows.length || !unique(meta.workflows)
|
|
39
|
+
|| !meta.workflows.every(p => /^workflows\/[a-z][a-z0-9-]*\.md$/.test(p))) fail(`Invalid capability workflows: ${id}`);
|
|
40
|
+
for (const name of meta.skills) {
|
|
41
|
+
skillMetadata(fs.readFileSync(safe(source, prefix + `skills/${name}/SKILL.md`), 'utf8'), name);
|
|
42
|
+
}
|
|
43
|
+
for (const workflow of meta.workflows) {
|
|
44
|
+
if (!fs.readFileSync(safe(source, prefix + workflow), 'utf8').trim()) fail(`Empty capability workflow: ${id}/${workflow}`);
|
|
45
|
+
}
|
|
46
|
+
for (const [kind, pattern] of [
|
|
47
|
+
['agents', new RegExp('^agents/[a-z][a-z0-9-]*[.]md$')],
|
|
48
|
+
['templates', new RegExp('^templates/[a-z][a-z0-9-]*[.]md$')],
|
|
49
|
+
]) {
|
|
50
|
+
const entries = meta[kind] ?? [];
|
|
51
|
+
if (!Array.isArray(entries) || !unique(entries) || !entries.every(p => typeof p === 'string' && pattern.test(p))) fail(`Invalid capability ${kind}: ${id}`);
|
|
52
|
+
for (const file of entries) {
|
|
53
|
+
const body = fs.readFileSync(safe(source, prefix + file), 'utf8');
|
|
54
|
+
if (!body.trim()) fail(`Empty capability ${kind}: ${id}/${file}`);
|
|
55
|
+
if (kind === 'agents' && !['## Inputs', '## Outputs', '## Permissions and limits', '## Handoff'].every(h => body.includes(h))) {
|
|
56
|
+
fail(`Invalid capability agent contract: ${id}/${file}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Optional capability verification/evaluation, installed as locked data.
|
|
61
|
+
if (meta.verification !== undefined && meta.verification !== 'checks/verification.json') fail(`Invalid capability verification: ${id}`);
|
|
62
|
+
if (meta.evaluation !== undefined && meta.evaluation !== 'evals/cases.json') fail(`Invalid capability evaluation: ${id}`);
|
|
63
|
+
const checks = meta.verification ? checksFrom(source, prefix + meta.verification) : [];
|
|
64
|
+
const cases = meta.evaluation ? casesFrom(source, prefix + meta.evaluation) : [];
|
|
65
|
+
return { ...meta, agents: meta.agents ?? [], templates: meta.templates ?? [], checks, cases };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function listCapabilities(source) {
|
|
69
|
+
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 };
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Progressive disclosure: return metadata only; never include skill bodies in a catalog.
|
|
76
|
+
export function skillCatalog(root, lock) {
|
|
77
|
+
const skills = Object.keys(lock.files).filter(p => /^\.agents\/skills\/harness-[a-z0-9-]+\/SKILL\.md$/.test(p)).sort()
|
|
78
|
+
.map(p => {
|
|
79
|
+
const name = p.split('/')[2];
|
|
80
|
+
const contents = payload(root, p).toString('utf8');
|
|
81
|
+
return { ...skillMetadata(contents, name), path: p };
|
|
82
|
+
});
|
|
83
|
+
return { status: 'ok', version: lock.version, skills, note: 'Descriptions only. Use aih skill <name> to fetch full instructions when relevant.' };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function readSkill(root, lock, name) {
|
|
87
|
+
if (!validName(name) || !name.startsWith('harness-')) fail('Invalid skill name');
|
|
88
|
+
const relative = `.agents/skills/${name}/SKILL.md`;
|
|
89
|
+
if (!Object.hasOwn(lock.files, relative)) fail(`Skill is not installed: ${name}`);
|
|
90
|
+
const content = payload(root, relative).toString('utf8');
|
|
91
|
+
skillMetadata(content, name);
|
|
92
|
+
return { status: 'ok', name, path: relative, content };
|
|
93
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { manifest, validateAdapters, validateCapabilities, HarnessError } from './files.js';
|
|
2
|
+
import { loadAdapter } from './adapter-schema.js';
|
|
3
|
+
import { loadCapability } from './capabilities.js';
|
|
4
|
+
import { moduleRef } from './module-contract.js';
|
|
5
|
+
|
|
6
|
+
export function resolveModules(source, { adapters = [], capabilities = [] } = {}) {
|
|
7
|
+
validateAdapters(adapters); validateCapabilities(capabilities);
|
|
8
|
+
const registry = manifest(source);
|
|
9
|
+
const unknownA = adapters.filter(id => !registry.adapters.includes(id));
|
|
10
|
+
const unknownC = capabilities.filter(id => !(registry.capabilities ?? []).includes(id));
|
|
11
|
+
if (unknownA.length || unknownC.length) {
|
|
12
|
+
throw new HarnessError(`Unknown modules: ${[
|
|
13
|
+
...unknownA.map(id => moduleRef('adapter', id)),
|
|
14
|
+
...unknownC.map(id => moduleRef('capability', id)),
|
|
15
|
+
].join(', ')}`);
|
|
16
|
+
}
|
|
17
|
+
const sortedA = [...adapters].sort(), sortedC = [...capabilities].sort();
|
|
18
|
+
const modules = [
|
|
19
|
+
...sortedA.map(id => ({ kind: 'adapter', id, data: loadAdapter(source, id) })),
|
|
20
|
+
...sortedC.map(id => ({ kind: 'capability', id, data: loadCapability(source, id) })),
|
|
21
|
+
];
|
|
22
|
+
const selected = new Map(modules.map(m => [moduleRef(m.kind, m.id), m]));
|
|
23
|
+
const declared = new Set([...registry.adapters.map(x => moduleRef('adapter', x)),
|
|
24
|
+
...(registry.capabilities ?? []).map(x => moduleRef('capability', x))]);
|
|
25
|
+
const warnings = [], graph = new Map();
|
|
26
|
+
for (const { kind, id, data } of modules) {
|
|
27
|
+
const owner = moduleRef(kind, id);
|
|
28
|
+
for (const field of ['requires', 'recommends', 'conflicts']) {
|
|
29
|
+
for (const ref of data[field] ?? []) {
|
|
30
|
+
if (!declared.has(ref)) throw new HarnessError(`Unknown ${field} reference ${ref} in ${owner}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
for (const dep of data.requires ?? []) {
|
|
34
|
+
if (!selected.has(dep)) throw new HarnessError(`Missing required module ${dep} for ${owner}`);
|
|
35
|
+
}
|
|
36
|
+
for (const denied of data.conflicts ?? []) {
|
|
37
|
+
if (selected.has(denied)) throw new HarnessError(`Incompatible modules: ${owner} conflicts with ${denied}`);
|
|
38
|
+
}
|
|
39
|
+
if (data.compatible_adapters?.length) {
|
|
40
|
+
for (const adapter of sortedA) {
|
|
41
|
+
if (!data.compatible_adapters.includes(adapter)) {
|
|
42
|
+
throw new HarnessError(`Incompatible adapter ${adapter} for ${owner}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
for (const hint of data.recommends ?? []) {
|
|
47
|
+
if (!selected.has(hint)) warnings.push(`${owner} recommends ${hint} (optional)`);
|
|
48
|
+
}
|
|
49
|
+
graph.set(owner, data.requires ?? []);
|
|
50
|
+
}
|
|
51
|
+
const states = new Map();
|
|
52
|
+
function visit(ref) {
|
|
53
|
+
if (states.get(ref) === 'visiting') throw new HarnessError(`Module dependency cycle at ${ref}`);
|
|
54
|
+
if (states.get(ref) === 'done') return;
|
|
55
|
+
states.set(ref, 'visiting');
|
|
56
|
+
for (const dep of graph.get(ref) ?? []) visit(dep);
|
|
57
|
+
states.set(ref, 'done');
|
|
58
|
+
}
|
|
59
|
+
for (const ref of graph.keys()) visit(ref);
|
|
60
|
+
return { status: 'ok', adapters: sortedA, capabilities: sortedC,
|
|
61
|
+
modules: modules.map(m => ({ id: m.id, kind: m.kind, module_version: m.data.module_version ?? null })),
|
|
62
|
+
warnings };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Validate the complete catalog without pretending every registered module is selected. */
|
|
66
|
+
export function validateRegistry(source) {
|
|
67
|
+
const registry = manifest(source);
|
|
68
|
+
const entries = [
|
|
69
|
+
...registry.adapters.map(id => ({kind:'adapter',id,data:loadAdapter(source,id)})),
|
|
70
|
+
...(registry.capabilities ?? []).map(id => ({kind:'capability',id,data:loadCapability(source,id)})),
|
|
71
|
+
];
|
|
72
|
+
const declared = new Set(entries.map(m=>moduleRef(m.kind,m.id)));
|
|
73
|
+
const graph = new Map();
|
|
74
|
+
for (const {kind,id,data} of entries) {
|
|
75
|
+
const owner=moduleRef(kind,id);
|
|
76
|
+
for (const field of ['requires','recommends','conflicts']) {
|
|
77
|
+
for (const ref of data[field] ?? []) {
|
|
78
|
+
if (!declared.has(ref)) throw new HarnessError(`Unknown ${field} reference ${ref} in ${owner}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
for (const id of data.compatible_adapters ?? []) {
|
|
82
|
+
if (!registry.adapters.includes(id)) throw new HarnessError(`Unknown compatible adapter ${id} in ${owner}`);
|
|
83
|
+
}
|
|
84
|
+
graph.set(owner,data.requires ?? []);
|
|
85
|
+
}
|
|
86
|
+
const states=new Map();
|
|
87
|
+
function visit(ref) {
|
|
88
|
+
if (states.get(ref)==='visiting') throw new HarnessError(`Module dependency cycle at ${ref}`);
|
|
89
|
+
if (states.get(ref)==='done') return;
|
|
90
|
+
states.set(ref,'visiting');
|
|
91
|
+
for (const dep of graph.get(ref) ?? []) visit(dep);
|
|
92
|
+
states.set(ref,'done');
|
|
93
|
+
}
|
|
94
|
+
for (const ref of graph.keys()) visit(ref);
|
|
95
|
+
return {status:'ok',modules:entries.length};
|
|
96
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { readJson, safe, HarnessError, SLUG } from './files.js';
|
|
2
|
+
const requireThat = (condition, message) => { if (!condition) throw new HarnessError(message); };
|
|
3
|
+
const text = v => typeof v === 'string' && v.trim().length > 0;
|
|
4
|
+
const exact = (v, keys) => v && typeof v === 'object' && !Array.isArray(v)
|
|
5
|
+
&& Object.keys(v).sort().join(',') === [...keys].sort().join(',');
|
|
6
|
+
export function checksFrom(source, relative) {
|
|
7
|
+
const doc = readJson(safe(source, relative));
|
|
8
|
+
requireThat(exact(doc, ['schema_version', 'checks']) && doc.schema_version === 1 && Array.isArray(doc.checks)
|
|
9
|
+
&& doc.checks.length > 0, `Invalid verification: ${relative}`);
|
|
10
|
+
const ids = new Set();
|
|
11
|
+
for (const check of doc.checks) {
|
|
12
|
+
requireThat(!ids.has(check?.id), `Duplicate checks: ${relative}`);
|
|
13
|
+
requireThat(exact(check, ['id', 'title', 'command', 'when_file', 'evidence', 'execution'])
|
|
14
|
+
&& typeof check.id === 'string' && SLUG.test(check.id) && !ids.has(check.id)
|
|
15
|
+
&& text(check.title) && Array.isArray(check.command) && check.command.every(text)
|
|
16
|
+
&& Array.isArray(check.evidence) && check.evidence.length > 0 && check.evidence.every(text)
|
|
17
|
+
&& ((check.execution === 'advisory' && check.command.length > 0)
|
|
18
|
+
|| (check.execution === 'manual' && check.command.length === 0)), `Invalid check: ${relative}`);
|
|
19
|
+
if (check.when_file !== null) {
|
|
20
|
+
requireThat(typeof check.when_file === 'string', `Invalid when_file: ${relative}`);
|
|
21
|
+
safe(source, check.when_file);
|
|
22
|
+
}
|
|
23
|
+
ids.add(check.id);
|
|
24
|
+
}
|
|
25
|
+
return doc.checks;
|
|
26
|
+
}
|
|
27
|
+
export function casesFrom(source, relative) {
|
|
28
|
+
const doc = readJson(safe(source, relative));
|
|
29
|
+
requireThat(exact(doc, ['schema_version', 'cases']) && doc.schema_version === 1 && Array.isArray(doc.cases)
|
|
30
|
+
&& doc.cases.length > 0, `Invalid evaluation: ${relative}`);
|
|
31
|
+
const ids = new Set();
|
|
32
|
+
for (const c of doc.cases) {
|
|
33
|
+
requireThat(exact(c, ['id', 'prompt', 'criteria', 'critical'])
|
|
34
|
+
&& typeof c.id === 'string' && SLUG.test(c.id) && !ids.has(c.id)
|
|
35
|
+
&& text(c.prompt) && Array.isArray(c.criteria) && c.criteria.length > 0 && c.criteria.every(text)
|
|
36
|
+
&& typeof c.critical === 'boolean', `Invalid eval case: ${relative}`);
|
|
37
|
+
ids.add(c.id);
|
|
38
|
+
}
|
|
39
|
+
return doc.cases;
|
|
40
|
+
}
|
package/cli/files.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
// Resolve from the installed package, never the consumer's cwd or npm's bin symlink.
|
|
7
|
+
export const SOURCE = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
8
|
+
export const START = '<!-- ai-harness:start -->';
|
|
9
|
+
export const END = '<!-- ai-harness:end -->';
|
|
10
|
+
export const LOCK = '.harness/lock.json';
|
|
11
|
+
export const CONFIG = '.harness/project.json';
|
|
12
|
+
export const VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
13
|
+
export const SLUG = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
14
|
+
export const isObject = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
15
|
+
|
|
16
|
+
export class HarnessError extends Error {}
|
|
17
|
+
|
|
18
|
+
export function encode(value) {
|
|
19
|
+
// Matches v0.1 Python json.dumps(indent=2, sort_keys=True, ensure_ascii=False).
|
|
20
|
+
const sorted = item => Array.isArray(item) ? item.map(sorted) : isObject(item)
|
|
21
|
+
? Object.fromEntries(Object.keys(item).sort().map(k => [k, sorted(item[k])])) : item;
|
|
22
|
+
return Buffer.from(JSON.stringify(sorted(value), null, 2) + '\n');
|
|
23
|
+
}
|
|
24
|
+
export const digest = data => createHash('sha256').update(data).digest('hex');
|
|
25
|
+
|
|
26
|
+
export function stat(filename) {
|
|
27
|
+
try { return fs.lstatSync(filename); }
|
|
28
|
+
catch (error) { if (error.code === 'ENOENT') return undefined; throw error; }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function safe(root, name) {
|
|
32
|
+
if (typeof name !== 'string' || !name || !/^[a-zA-Z0-9._/-]+$/.test(name)
|
|
33
|
+
|| path.posix.isAbsolute(name) || path.posix.normalize(name) !== name
|
|
34
|
+
|| name.split('/').some(p => !p || p === '.' || p === '..' || p.endsWith('.')
|
|
35
|
+
|| /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(p))) {
|
|
36
|
+
throw new HarnessError(`Unsafe relative path: ${name}`);
|
|
37
|
+
}
|
|
38
|
+
let current = root;
|
|
39
|
+
const parts = name.split('/');
|
|
40
|
+
for (const [index, part] of parts.entries()) {
|
|
41
|
+
current = path.join(current, part);
|
|
42
|
+
const info = stat(current);
|
|
43
|
+
if (!info) continue;
|
|
44
|
+
if (info.isSymbolicLink()) throw new HarnessError(`Symlink is not supported: ${current}`);
|
|
45
|
+
if (index < parts.length - 1 ? !info.isDirectory() : !info.isFile()) {
|
|
46
|
+
throw new HarnessError(`Unexpected filesystem entry: ${current}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return current;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function targetRoot(value) {
|
|
53
|
+
const root = path.resolve(value);
|
|
54
|
+
for (let current = root; ; current = path.dirname(current)) {
|
|
55
|
+
if (stat(current)?.isSymbolicLink()) throw new HarnessError(`Target contains a symlink: ${current}`);
|
|
56
|
+
if (path.dirname(current) === current) break;
|
|
57
|
+
}
|
|
58
|
+
if (!stat(root)?.isDirectory()) throw new HarnessError('Target must be an existing directory');
|
|
59
|
+
return root;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function assertSeparate(root, source) {
|
|
63
|
+
const inside = (parent, child) => {
|
|
64
|
+
const rel = path.relative(parent, child);
|
|
65
|
+
return !rel || (!rel.startsWith('..' + path.sep) && rel !== '..' && !path.isAbsolute(rel));
|
|
66
|
+
};
|
|
67
|
+
// npm local installs intentionally place the package in the consumer's node_modules.
|
|
68
|
+
const localPackage = path.relative(root, source).split(path.sep)[0] === 'node_modules';
|
|
69
|
+
if (inside(source, root) || (inside(root, source) && !localPackage)) {
|
|
70
|
+
throw new HarnessError('Source and target must not overlap, except a source installed in node_modules');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function readJson(filename) {
|
|
75
|
+
try { return JSON.parse(fs.readFileSync(filename, 'utf8')); }
|
|
76
|
+
catch (error) { throw new HarnessError(`Cannot read JSON document: ${filename}: ${error.message}`); }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function block(data) {
|
|
80
|
+
const a = data.indexOf(START), b = data.indexOf(END);
|
|
81
|
+
if (a === -1 && b === -1) return null;
|
|
82
|
+
if (a === -1 || b < a || data.indexOf(START, a + START.length) !== -1
|
|
83
|
+
|| data.indexOf(END, b + END.length) !== -1) {
|
|
84
|
+
throw new HarnessError('AGENTS.md has duplicate, reversed or incomplete harness markers');
|
|
85
|
+
}
|
|
86
|
+
return data.subarray(a, b + END.length);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function mergeAgents(existing, generated) {
|
|
90
|
+
const old = block(existing);
|
|
91
|
+
if (old === null) {
|
|
92
|
+
const separator = !existing.length ? '' : existing.at(-1) === 10 ? '\n' : '\n\n';
|
|
93
|
+
return Buffer.concat([existing, Buffer.from(separator), generated, Buffer.from('\n')]);
|
|
94
|
+
}
|
|
95
|
+
const offset = existing.indexOf(START);
|
|
96
|
+
return Buffer.concat([existing.subarray(0, offset), generated, existing.subarray(offset + old.length)]);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function payload(root, name) {
|
|
100
|
+
const filename = safe(root, name);
|
|
101
|
+
if (!stat(filename)) return null;
|
|
102
|
+
const data = fs.readFileSync(filename);
|
|
103
|
+
return name === 'AGENTS.md' ? block(data) : data;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function manifest(source) {
|
|
107
|
+
const m = readJson(safe(source, 'harness.yaml'));
|
|
108
|
+
if (!isObject(m) || m.schema_version !== 1 || !VERSION.test(m.version)) {
|
|
109
|
+
throw new HarnessError('Invalid harness manifest schema/version');
|
|
110
|
+
}
|
|
111
|
+
validateAdapters(m.adapters);
|
|
112
|
+
validateCapabilities(m.capabilities ?? []);
|
|
113
|
+
if (!m.adapters.length) throw new HarnessError('Empty adapter registry');
|
|
114
|
+
return m;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function validateAdapters(names) {
|
|
118
|
+
if (!Array.isArray(names) || names.some(a => typeof a !== 'string' || !SLUG.test(a))
|
|
119
|
+
|| new Set(names).size !== names.length) throw new HarnessError('Invalid or duplicate adapters');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function validateCapabilities(names) {
|
|
123
|
+
if (!Array.isArray(names) || names.some(a => typeof a !== 'string' || !SLUG.test(a))
|
|
124
|
+
|| new Set(names).size !== names.length) throw new HarnessError('Invalid or duplicate capabilities');
|
|
125
|
+
}
|