@manhd_d/ai-harness 0.11.0 → 0.13.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 +38 -0
- package/README.md +26 -38
- 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/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/architecture/checks/verification.json +11 -0
- package/capabilities/architecture/evals/cases.json +10 -0
- package/capabilities/architecture/skills/harness-architecture-change-impact/SKILL.md +5 -0
- package/capabilities/architecture/templates/project-map.md +5 -0
- package/capabilities/architecture/workflows/change-review.md +2 -0
- package/capabilities/delivery/agents/project-manager.md +10 -11
- package/capabilities/delivery/capability.json +8 -2
- package/capabilities/delivery/checks/verification.json +11 -0
- package/capabilities/delivery/evals/cases.json +10 -0
- package/capabilities/delivery/skills/harness-delivery-scope-change/SKILL.md +6 -0
- package/capabilities/delivery/templates/delivery-plan.md +2 -23
- package/capabilities/delivery/workflows/execution-change.md +2 -0
- 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/checks/verification.json +11 -0
- package/capabilities/product/common/document-contract.md +10 -0
- package/capabilities/product/evals/cases.json +21 -0
- package/capabilities/product/skills/harness-product-acceptance-criteria/SKILL.md +4 -7
- package/capabilities/product/skills/harness-product-backlog-prioritization/SKILL.md +9 -6
- package/capabilities/product/skills/harness-product-brd-generation/SKILL.md +2 -6
- package/capabilities/product/skills/harness-product-business-analysis/SKILL.md +12 -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 +9 -7
- 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/backlog.md +7 -0
- package/capabilities/product/templates/delivery-plan.md +3 -17
- package/capabilities/product/templates/traceability.md +7 -0
- package/capabilities/product/workflows/change-impact-analysis.md +4 -2
- package/capabilities/product/workflows/techspec-maintenance.md +3 -1
- 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 +69 -14
- package/cli/main.js +22 -9
- package/cli/maven.js +28 -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 +25 -0
- package/core/evals/cases.json +92 -0
- package/core/evals/composition.json +15 -0
- package/core/policies/permissions.md +3 -1
- package/core/rules/context.md +4 -0
- package/core/skills/harness-debugging/SKILL.md +6 -0
- package/core/skills/harness-review/SKILL.md +6 -0
- package/core/skills/harness-spec/SKILL.md +6 -0
- package/core/workflows/adaptive-verification.md +3 -0
- package/core/workflows/agent-routing.md +27 -26
- package/core/workflows/artifact-contract.md +5 -0
- package/core/workflows/continuous-development.md +101 -0
- package/core/workflows/spec-to-code.md +4 -1
- package/docs/README.md +5 -1
- package/docs/agent-configuration.md +9 -5
- package/docs/agent-first.md +66 -0
- package/docs/agent-handoffs.md +8 -0
- package/docs/architecture.md +8 -0
- package/docs/cli.md +4 -0
- package/docs/continuous-development.md +92 -0
- package/docs/documentation-workflows.md +2 -0
- package/docs/end-to-end-workflow.md +4 -0
- package/docs/examples/astro-micro-tools.md +1 -1
- package/docs/examples/checkpoint.json +20 -0
- package/docs/examples/continuous-development.md +40 -0
- package/docs/examples/spring-boot-api.md +2 -4
- package/docs/getting-started.md +5 -2
- package/docs/i18n/README.es.md +32 -0
- package/docs/i18n/README.vi.md +19 -42
- package/docs/i18n/README.zh-CN.md +32 -0
- package/docs/skill-matrix.md +11 -7
- 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 +17 -0
- package/evals/README.md +4 -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 +19 -26
- package/templates/delivery-plan.md +25 -0
- package/templates/openspec/design.md +10 -0
- package/templates/openspec/tasks.md +8 -1
- 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
|
@@ -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) {
|
package/cli/harness.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import { checkpointPath, validateCheckpoint } from './checkpoint.js';
|
|
3
|
+
import { readConfig } from './project-config.js';
|
|
4
|
+
import { claudeReport, claudeBlock, linkClaude } from './claude.js';
|
|
2
5
|
import { emptyEnvironment, environmentReport } from './environment.js';
|
|
3
6
|
import { AGENT_CONFIG, agentDefaults, loadAgentConfig } from './agents.js';
|
|
4
7
|
import { reviewVerification, techspecEvidence } from './verification-report.js';
|
|
@@ -55,7 +58,7 @@ export function bundle(source, config) {
|
|
|
55
58
|
const unknownCapabilities = capabilities.filter(c => !(m.capabilities ?? []).includes(c));
|
|
56
59
|
if (unknownCapabilities.length) throw new HarnessError(`Unknown capabilities: ${unknownCapabilities.join(', ')}`);
|
|
57
60
|
const files = {};
|
|
58
|
-
for (const folder of ['core', 'integrations', 'templates', ...adapters.map(a => `adapters/${a}`), ...capabilities.map(c => `capabilities/${c}`)]) {
|
|
61
|
+
for (const folder of ['core', 'integrations', 'templates', ...adapters.map(a => `adapters/${a}`), ...capabilities.map(c => `capabilities/${c}`), ...[...new Set(capabilities.map(c => loadCapability(source, c).asset_owner).filter(Boolean))].map(c => `capabilities/${c}`)]) {
|
|
59
62
|
for (const [name, data] of Object.entries(collect(source, folder))) {
|
|
60
63
|
files[`.harness/${name}`] = data;
|
|
61
64
|
if (name.startsWith('core/skills/')) files[`.agents/skills/${name.slice('core/skills/'.length)}`] = data;
|
|
@@ -74,11 +77,12 @@ export function bundle(source, config) {
|
|
|
74
77
|
const info = loadCapability(source, c);
|
|
75
78
|
for (const name of info.skills) {
|
|
76
79
|
const dest = `.agents/skills/${name}/SKILL.md`;
|
|
77
|
-
|
|
78
|
-
files[dest]
|
|
80
|
+
const data = fs.readFileSync(safe(source, `capabilities/${info.asset_owner ?? c}/skills/${name}/SKILL.md`));
|
|
81
|
+
if (Object.hasOwn(files, dest) && !files[dest].equals(data)) throw new HarnessError(`Skill collision: ${name}`);
|
|
82
|
+
files[dest] = data;
|
|
79
83
|
}
|
|
80
84
|
}
|
|
81
|
-
const capabilityLinks = capabilities.map(c => `- For ${c} tasks, read \`.harness/capabilities/${c}/workflows/\` and discover relevant \`.agents/skills/harness-${c}-*/SKILL.md\` by description.`).join('\n')
|
|
85
|
+
const capabilityLinks = capabilities.map(c => `- For ${c} tasks, read \`.harness/capabilities/${loadCapability(source,c).asset_owner ?? c}/workflows/\` and discover relevant \`.agents/skills/harness-${loadCapability(source,c).asset_owner ?? c}-*/SKILL.md\` by description.`).join('\n')
|
|
82
86
|
|| '- No optional capability selected.';
|
|
83
87
|
const links = adapters.map(a => `- Read \`.harness/adapters/${a}/rules.md\` and \`.harness/adapters/${a}/adapter.yaml\`; load relevant skills from \`.agents/skills/harness-${a}-*\`.`).join('\n') || '- No technology adapter selected.';
|
|
84
88
|
const template = fs.readFileSync(safe(source, 'templates/AGENTS.md'), 'utf8');
|
|
@@ -90,7 +94,7 @@ export function bundle(source, config) {
|
|
|
90
94
|
}
|
|
91
95
|
|
|
92
96
|
function managedName(name) {
|
|
93
|
-
return name === 'AGENTS.md' || ['.harness/core/', '.harness/adapters/', '.harness/capabilities/', '.harness/integrations/', '.harness/templates/', '.agents/skills/harness-'].some(prefix => name.startsWith(prefix));
|
|
97
|
+
return name === 'AGENTS.md' || name === 'CLAUDE.md' || ['.harness/core/', '.harness/adapters/', '.harness/capabilities/', '.harness/integrations/', '.harness/templates/', '.agents/skills/harness-', '.claude/skills/harness-'].some(prefix => name.startsWith(prefix));
|
|
94
98
|
}
|
|
95
99
|
|
|
96
100
|
export function loadLock(root) {
|
|
@@ -161,7 +165,7 @@ export function transaction(root, changes, writer = atomicWrite) {
|
|
|
161
165
|
}
|
|
162
166
|
}
|
|
163
167
|
|
|
164
|
-
export function install(root, source, { command = 'init', adapters, capabilities, agentPreset, dryRun = false, allowMajor = false, writer } = {}) {
|
|
168
|
+
export function install(root, source, { command = 'init', adapters, capabilities, agentPreset, dryRun = false, allowMajor = false, host, linkClaude: linkClaudeOpt = false, claudeSkills = false, writer } = {}) {
|
|
165
169
|
assertSeparate(root, source);
|
|
166
170
|
return guard(root, () => {
|
|
167
171
|
const hasLock = Boolean(stat(safe(root, LOCK)));
|
|
@@ -173,6 +177,25 @@ export function install(root, source, { command = 'init', adapters, capabilities
|
|
|
173
177
|
if (agentPreset !== undefined && existingAgents) throw new HarnessError('agents.json is project-owned; edit it explicitly instead of applying an init preset');
|
|
174
178
|
const config = projectConfig(root, adapters, capabilities, hasLock);
|
|
175
179
|
const { files, lock, warnings } = bundle(source, config);
|
|
180
|
+
const claudeName = 'CLAUDE.md', claudePath = safe(root, claudeName);
|
|
181
|
+
const existingClaude = stat(claudePath) ? fs.readFileSync(claudePath) : Buffer.alloc(0);
|
|
182
|
+
if (existingClaude.length > 1024 * 1024) throw new HarnessError('CLAUDE.md exceeds 1 MiB');
|
|
183
|
+
let linkedClaude;
|
|
184
|
+
if (linkClaudeOpt || old?.files[claudeName]) {
|
|
185
|
+
linkedClaude = linkClaude(existingClaude);
|
|
186
|
+
const owned = claudeBlock(linkedClaude);
|
|
187
|
+
if (owned) { files[claudeName] = owned; lock.files[claudeName] = digest(owned); }
|
|
188
|
+
}
|
|
189
|
+
if (claudeSkills || Object.keys(old?.files ?? {}).some(p => p.startsWith('.claude/skills/harness-'))) {
|
|
190
|
+
for (const [name, data] of Object.entries(files).filter(([p]) => /^\.agents\/skills\/harness-[a-z0-9-]+\/SKILL\.md$/.test(p))) {
|
|
191
|
+
const id = name.split('/')[2], description = data.toString().match(/^description: (.+)$/m)?.[1] ?? 'Load canonical Harness instructions.';
|
|
192
|
+
const destination = '.claude/skills/' + id + '/SKILL.md';
|
|
193
|
+
files[destination] = Buffer.from('---\nname: ' + id + '\ndescription: ' + description + '\n---\n\nRead the complete canonical project file ' + String.fromCharCode(96) + name + String.fromCharCode(96) + ' before applying this skill. Follow its inputs, procedure, constraints and handoff; do not substitute this wrapper for the full instructions.\n');
|
|
194
|
+
lock.files[destination] = digest(files[destination]);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
lock.files = Object.fromEntries(Object.entries(lock.files).sort(([a],[b]) => a.localeCompare(b)));
|
|
198
|
+
lock.bundle_sha256 = digest(encode(lock.files));
|
|
176
199
|
if (old) {
|
|
177
200
|
const conflicts = drift(root, old);
|
|
178
201
|
if (conflicts.length) throw new HarnessError(`Managed files changed; reconcile in Git before updating: ${conflicts.join(', ')}`);
|
|
@@ -189,18 +212,18 @@ export function install(root, source, { command = 'init', adapters, capabilities
|
|
|
189
212
|
if (present === null || !present.equals(data)) {
|
|
190
213
|
const filename = safe(root, name);
|
|
191
214
|
const existing = stat(filename) ? fs.readFileSync(filename) : Buffer.alloc(0);
|
|
192
|
-
changes[name] = name === 'AGENTS.md' ? mergeAgents(existing, data) : data;
|
|
215
|
+
changes[name] = name === 'AGENTS.md' ? mergeAgents(existing, data) : name === 'CLAUDE.md' ? linkedClaude : data;
|
|
193
216
|
}
|
|
194
217
|
}
|
|
195
218
|
if (old) for (const name of Object.keys(old.files)) if (!Object.hasOwn(files, name)) changes[name] = null;
|
|
196
219
|
const seeds = {
|
|
197
|
-
[AGENT_CONFIG]: encode(agentDefaults(source, agentPreset ?? 'recommended')),
|
|
220
|
+
[AGENT_CONFIG]: encode(agentDefaults(source, agentPreset ?? 'recommended', host)),
|
|
198
221
|
[CONFIG]: encode(config),
|
|
199
222
|
[ARTIFACTS]: encode(emptyArtifacts()),
|
|
200
223
|
[INTEGRATIONS]: encode(emptyIntegrations()),
|
|
201
224
|
'.harness/environment.json': encode(emptyEnvironment()),
|
|
202
225
|
'openspec/config.yaml': fs.readFileSync(safe(source, 'templates/openspec/config.yaml')),
|
|
203
|
-
'.harness/local.md': Buffer.from('# Project overrides\n\nRecord project facts and reviewed exceptions here. Never weaken security boundaries.\n\n## Development conventions\nRecord scope/mode (new, maintain or mixed), authoritative source paths, module boundaries, reusable public APIs, naming/error/test conventions and configured checks. Link existing guides instead of duplicating them. Unknowns are not approved defaults; the AI host inspects evidence and asks only consequential questions.\n'),
|
|
226
|
+
'.harness/local.md': Buffer.from('# Project overrides\n\nRecord project facts and reviewed exceptions here. Never weaken security boundaries.\n\n## Stack\nstack: unknown\nRecord actual dependency versions and approved design sources. This field does not select technology.\n\n## Development conventions\nRecord scope/mode (new, maintain or mixed), authoritative source paths, module boundaries, reusable public APIs, naming/error/test conventions and configured checks. Link existing guides instead of duplicating them. Unknowns are not approved defaults; the AI host inspects evidence and asks only consequential questions.\n'),
|
|
204
227
|
};
|
|
205
228
|
for (const [name, data] of Object.entries(seeds)) if (!stat(safe(root, name))) changes[name] = data;
|
|
206
229
|
// Project-owned ignore file: preserve all prior rules, place our anchored rule last.
|
|
@@ -257,20 +280,31 @@ export function newDoc(root, name, capability = 'product', options = {}) {
|
|
|
257
280
|
return guard(root, () => {
|
|
258
281
|
if (check(root).status !== 'ok') throw new HarnessError('Fix installation drift before creating a document');
|
|
259
282
|
const lock = loadLock(root);
|
|
260
|
-
|
|
283
|
+
const selected = lock.capabilities ?? [];
|
|
284
|
+
const profile = selected.find(id => {
|
|
285
|
+
const info = loadCapability(path.join(root, '.harness'), id);
|
|
286
|
+
return info.asset_owner === capability && info.templates.includes('templates/' + name + '.md');
|
|
287
|
+
});
|
|
288
|
+
if (!selected.includes(capability) && !profile) {
|
|
261
289
|
throw new HarnessError(capability === 'product' ? 'Product capability is not installed' : `Capability is not installed: ${capability}`);
|
|
262
290
|
}
|
|
263
|
-
const
|
|
291
|
+
const info = loadCapability(path.join(root, '.harness'), capability);
|
|
292
|
+
const owner = info.asset_owner ?? capability;
|
|
293
|
+
const sourceName = info.template_aliases?.['templates/' + name + '.md']
|
|
294
|
+
? '.harness/' + info.template_aliases['templates/' + name + '.md']
|
|
295
|
+
: `.harness/capabilities/${owner}/templates/${name}.md`;
|
|
264
296
|
if (!Object.hasOwn(lock.files, `.harness/capabilities/${capability}/capability.json`)) {
|
|
265
297
|
throw new HarnessError(`Capability metadata is not locked: ${capability}`);
|
|
266
298
|
}
|
|
267
299
|
const declared = loadCapability(path.join(root, '.harness'), capability).templates;
|
|
268
300
|
if (!declared.includes(`templates/${name}.md`)) throw new HarnessError(`Unknown document type for ${capability}: ${name}`);
|
|
269
301
|
if (!Object.hasOwn(lock.files, sourceName)) throw new HarnessError(`Document template is not locked: ${capability}/${name}`);
|
|
270
|
-
if (options.report && (
|
|
302
|
+
if (options.report && (owner !== 'product' || name !== 'techspec')) throw new HarnessError('--report is supported only for product techspec');
|
|
271
303
|
const content = options.report ? techspecEvidence(reviewVerification(root, options.report)) : fs.readFileSync(safe(root, sourceName), 'utf8');
|
|
272
304
|
const folder = capability === 'product' && ['idea', 'research'].includes(name) ? 'discovery' : capability;
|
|
273
|
-
const
|
|
305
|
+
const outputs = loadArtifactContract(root).artifacts.outputs;
|
|
306
|
+
const routeCapability = Object.values(outputs).some(a => a.kind === 'docs' && (a.capability ?? 'product') === capability) ? capability : owner;
|
|
307
|
+
const route = docRoute(root, routeCapability, name);
|
|
274
308
|
if (route?.external) return { status: 'planned', type: name, capability, destination: route.destination,
|
|
275
309
|
write_policy: route.write_policy, executed: false,
|
|
276
310
|
template: content,
|
|
@@ -294,5 +328,26 @@ export function doctor(root) {
|
|
|
294
328
|
}
|
|
295
329
|
const state = check(root), environment = environmentReport(root);
|
|
296
330
|
return { ...state, status: state.status !== 'ok' || environment.problems.length ? 'error' : 'ok',
|
|
297
|
-
tools, environment, artifacts: artifactReport(root), note: 'Tool presence only; no commands executed or compatibility claim made. Configured project tools take precedence over generic PATH hints.' };
|
|
331
|
+
tools, environment, claude: claudeReport(root), artifacts: artifactReport(root), note: 'Tool presence only; no commands executed or compatibility claim made. Configured project tools take precedence over generic PATH hints.' };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function saveCheckpoint(root, id, report, expected) {
|
|
335
|
+
return guard(root, () => {
|
|
336
|
+
if (check(root).status !== 'ok') throw new HarnessError('Fix installation drift before checkpointing');
|
|
337
|
+
const name = checkpointPath(id);
|
|
338
|
+
if (!stat(safe(root, 'openspec/changes/' + id + '/tasks.md'))) throw new HarnessError('Checkpoint requires an existing change');
|
|
339
|
+
const row = validateCheckpoint(root, id, readConfig(root, report));
|
|
340
|
+
const destination = safe(root, name), previous = stat(destination) ? fs.readFileSync(destination) : null;
|
|
341
|
+
if (previous && (!/^[a-f0-9]{64}$/.test(expected ?? '') || digest(previous) !== expected)) throw new HarnessError('Checkpoint exists or changed; resume, reconcile, then supply --expected digest');
|
|
342
|
+
if (!previous && expected !== undefined) throw new HarnessError('Expected checkpoint is missing');
|
|
343
|
+
const changes = { [name]: encode(row) };
|
|
344
|
+
if (previous) {
|
|
345
|
+
const history = 'openspec/changes/' + id + '/checkpoint-history/' + digest(previous) + '.json';
|
|
346
|
+
const p = safe(root, history);
|
|
347
|
+
if (stat(p) && !fs.readFileSync(p).equals(previous)) throw new HarnessError('Checkpoint history collision');
|
|
348
|
+
if (!stat(p)) changes[history] = previous;
|
|
349
|
+
}
|
|
350
|
+
transaction(root, changes);
|
|
351
|
+
return { status: 'ok', checkpoint: name, checkpoint_sha256: digest(encode(row)), executed: false, note: 'Saved project-owned handoff/evidence declarations. Execution and remote state still require host verification.' };
|
|
352
|
+
});
|
|
298
353
|
}
|
package/cli/main.js
CHANGED
|
@@ -4,16 +4,21 @@ import { resolveModules } from './composition.js';
|
|
|
4
4
|
import { parseArgs } from 'node:util';
|
|
5
5
|
import { setupPlan } from './environment.js';
|
|
6
6
|
import { ready } from './readiness.js';
|
|
7
|
+
import { resumePlan } from './checkpoint.js';
|
|
8
|
+
import { agentPlan } from './agents.js';
|
|
7
9
|
import { adaptiveVerification } from './verification.js';
|
|
8
10
|
import { reviewVerification } from './verification-report.js';
|
|
9
11
|
import { artifactReport } from './artifacts.js';
|
|
10
|
-
import { SOURCE, readJson, targetRoot, install, check, doctor, newChange, newDoc, loadLock } from './harness.js';
|
|
12
|
+
import { SOURCE, readJson, targetRoot, install, check, doctor, newChange, newDoc, loadLock, saveCheckpoint } from './harness.js';
|
|
11
13
|
import path from 'node:path';
|
|
12
14
|
|
|
13
15
|
const HELP = `aih — install reusable AI development rules into a project
|
|
14
16
|
|
|
15
17
|
Usage: aih <command> [options]
|
|
16
18
|
list List available adapters and capabilities
|
|
19
|
+
checkpoint <id> Save a validated --report handoff; replace with --expected digest
|
|
20
|
+
resume <id> Inspect checkpoint freshness and side effects before continuation
|
|
21
|
+
agents Show installed agent contracts and declared model/skill routing
|
|
17
22
|
skills List installed skill names/descriptions without full content
|
|
18
23
|
skill <name> Load one installed skill on demand
|
|
19
24
|
detect Suggest adapters with evidence from target files
|
|
@@ -36,10 +41,13 @@ Options:
|
|
|
36
41
|
--stage <type> ready only: implement (default) or review
|
|
37
42
|
--capability <id> new-doc only: installed template owner (default: product)
|
|
38
43
|
--agent-preset <id> init only: recommended (default), economy, custom; no overwrite
|
|
39
|
-
--
|
|
44
|
+
--link-claude init/update: opt in to a managed CLAUDE.md import
|
|
45
|
+
--claude-skills init/update: opt in to canonical .claude/skills wrappers
|
|
46
|
+
--host <id> init/setup/agents: codex,claude-code,gemini-cli,cursor,copilot or custom host ID
|
|
40
47
|
--scope <type> verify/eval: adapters (default), capabilities, core, all
|
|
41
48
|
--adaptive verify: discover a read-only project verification plan
|
|
42
|
-
--
|
|
49
|
+
--expected <sha> checkpoint: observed prior checkpoint digest for replacement
|
|
50
|
+
--report <file> checkpoint: save handoff; eval: score; verify: validate evidence; new-doc techspec: link evidence
|
|
43
51
|
--adapters <a,b> init/resolve: java-spring,react-nextjs,react-native,ai-python,kubernetes,astro
|
|
44
52
|
--capabilities <a,b> init/resolve: uiux,product,architecture,delivery,devops,monetization
|
|
45
53
|
--source <directory> init/update/list/detect/resolve: trusted alternate bundle directory
|
|
@@ -88,8 +96,8 @@ export function main(argv = process.argv.slice(2)) {
|
|
|
88
96
|
try {
|
|
89
97
|
args = parseArgs({ args: argv, allowPositionals: true, strict: true, tokens: true, options: {
|
|
90
98
|
target: { type: 'string' }, adapters: { type: 'string' }, capabilities: { type: 'string' }, capability: { type: 'string' }, source: { type: 'string' },
|
|
91
|
-
'agent-preset': { type: 'string' }, host: { type: 'string' },
|
|
92
|
-
adaptive: { type: 'boolean' }, detect: { type: 'boolean' }, report: { type: 'string' }, scope: { type: 'string' }, stage: { type: 'string' },
|
|
99
|
+
'agent-preset': { type: 'string' }, host: { type: 'string' }, expected: { type: 'string' },
|
|
100
|
+
'link-claude': { type: 'boolean' }, 'claude-skills': { type: 'boolean' }, adaptive: { type: 'boolean' }, detect: { type: 'boolean' }, report: { type: 'string' }, scope: { type: 'string' }, stage: { type: 'string' },
|
|
93
101
|
'dry-run': { type: 'boolean' }, 'allow-major': { type: 'boolean' },
|
|
94
102
|
help: { type: 'boolean', short: 'h' }, version: { type: 'boolean', short: 'v' },
|
|
95
103
|
} });
|
|
@@ -101,15 +109,17 @@ export function main(argv = process.argv.slice(2)) {
|
|
|
101
109
|
if (args.values.help || !argv.length) { console.log(HELP); return 0; }
|
|
102
110
|
if (args.values.version) { console.log(readJson(path.join(SOURCE, 'package.json')).version); return 0; }
|
|
103
111
|
const [command, name, ...extra] = args.positionals;
|
|
104
|
-
if (!['init', 'update', 'check', 'doctor', 'new-change', 'new-doc', 'list', 'detect', 'resolve', 'verify', 'eval', 'skills', 'skill', 'setup', 'ready', 'artifacts'].includes(command)) throw new Error(`Unknown command: ${command}`);
|
|
105
|
-
if (extra.length || (!['new-change', 'new-doc', 'skill', 'ready'].includes(command) && name !== undefined) || (['new-change', 'new-doc', 'skill', 'ready'].includes(command) && name === undefined)) throw new Error('Unexpected or missing positional argument; run aih --help');
|
|
112
|
+
if (!['init', 'update', 'check', 'doctor', 'new-change', 'new-doc', 'list', 'detect', 'resolve', 'verify', 'eval', 'skills', 'skill', 'setup', 'ready', 'artifacts', 'agents', 'checkpoint', 'resume'].includes(command)) throw new Error(`Unknown command: ${command}`);
|
|
113
|
+
if (extra.length || (!['new-change', 'new-doc', 'skill', 'ready', 'checkpoint', 'resume'].includes(command) && name !== undefined) || (['new-change', 'new-doc', 'skill', 'ready', 'checkpoint', 'resume'].includes(command) && name === undefined)) throw new Error('Unexpected or missing positional argument; run aih --help');
|
|
106
114
|
const allowed = new Set(['target', ...(['init', 'update'].includes(command) ? ['source', 'dry-run', 'allow-major'] : []), ...(['init', 'resolve'].includes(command) ? ['adapters', 'capabilities'] : []), ...(command === 'init' ? ['detect'] : []), ...(['list', 'detect', 'resolve'].includes(command) ? ['source'] : []), ...(command === 'eval' ? ['report'] : []), ...(command === 'ready' ? ['stage'] : []), ...(['verify', 'eval'].includes(command) ? ['scope'] : [])]);
|
|
115
|
+
if (command === 'checkpoint') { allowed.add('report'); allowed.add('expected'); if (!args.values.report) throw new Error('checkpoint requires --report'); }
|
|
107
116
|
if (command === 'new-doc') { allowed.add('capability'); allowed.add('report'); }
|
|
108
117
|
if (command === 'verify') { allowed.add('adaptive'); allowed.add('report'); }
|
|
109
118
|
if (args.values.adaptive && args.values.report) throw new Error('Choose --adaptive or --report');
|
|
110
119
|
if (args.values.scope && (args.values.adaptive || (command === 'verify' && args.values.report))) throw new Error('--scope applies only to advisory checks');
|
|
111
120
|
if (command === 'init') allowed.add('agent-preset');
|
|
112
|
-
if (
|
|
121
|
+
if (['setup','agents','init'].includes(command)) allowed.add('host');
|
|
122
|
+
if (['init','update'].includes(command)) { allowed.add('link-claude'); allowed.add('claude-skills'); }
|
|
113
123
|
for (const key of Object.keys(args.values)) if (!allowed.has(key)) throw new Error(`--${key} is not supported for ${command}`);
|
|
114
124
|
if (args.values.detect && args.values.adapters !== undefined) throw new Error('Choose either --detect or --adapters');
|
|
115
125
|
for (const key of ['target', 'source', 'adapters', 'capabilities', 'capability', 'report', 'agent-preset', 'host']) if (args.values[key] !== undefined && !args.values[key].trim()) throw new Error(`--${key} cannot be empty`);
|
|
@@ -124,7 +134,7 @@ export function main(argv = process.argv.slice(2)) {
|
|
|
124
134
|
if (values.detect && !selection.length) throw new Error('No stack detected; choose --adapters explicitly or omit --detect for core only');
|
|
125
135
|
result = install(root, source, {
|
|
126
136
|
command, adapters: selection ?? (values.adapters === undefined ? undefined : values.adapters.split(',').map(v => v.trim())),
|
|
127
|
-
agentPreset: values['agent-preset'],
|
|
137
|
+
agentPreset: values['agent-preset'], host: values.host, linkClaude: values['link-claude'], claudeSkills: values['claude-skills'],
|
|
128
138
|
capabilities: values.capabilities === undefined ? undefined : values.capabilities.split(',').map(v => v.trim()),
|
|
129
139
|
dryRun: values['dry-run'], allowMajor: values['allow-major'],
|
|
130
140
|
});
|
|
@@ -145,6 +155,9 @@ export function main(argv = process.argv.slice(2)) {
|
|
|
145
155
|
result = values.report ? reviewVerification(root, values.report) : values.adaptive ? adaptiveVerification(root, SOURCE) : verify(root, values.scope ?? 'adapters');
|
|
146
156
|
}
|
|
147
157
|
else if (command === 'eval') result = evaluate(root, values.report, values.scope ?? 'adapters');
|
|
158
|
+
else if (command === 'checkpoint') result = saveCheckpoint(root, name, values.report, values.expected);
|
|
159
|
+
else if (command === 'resume') { if (check(root).status !== 'ok') throw new Error('Fix installation drift before resume'); result = resumePlan(root, name); }
|
|
160
|
+
else if (command === 'agents') { if (check(root).status !== 'ok') throw new Error('Fix installation drift before reading agents'); result = agentPlan(root, values.host); }
|
|
148
161
|
else if (command === 'artifacts') {
|
|
149
162
|
if (check(root).status !== 'ok') throw new Error('Fix installation drift before reading artifact declarations');
|
|
150
163
|
result = artifactReport(root);
|
package/cli/maven.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { safe, stat, HarnessError } from './files.js';
|
|
4
|
+
|
|
5
|
+
// A bounded metadata reader, not an XML runtime. No entities, interpolation or parent traversal.
|
|
6
|
+
export function mavenModules(root) {
|
|
7
|
+
const files=[],warnings=[],visited=new Set();let total=0;
|
|
8
|
+
function visit(file,depth){
|
|
9
|
+
if(visited.has(file))return;visited.add(file);
|
|
10
|
+
if(visited.size>64)throw new HarnessError('Maven discovery exceeds 64 POM files');
|
|
11
|
+
const p=safe(root,file),info=stat(p);if(!info){if(depth>0)warnings.push({file,reason:'Declared POM is missing'});return;}
|
|
12
|
+
if(info.size>1024*1024)throw new HarnessError('Maven POM exceeds 1 MiB: '+file);
|
|
13
|
+
total+=info.size;if(total>4*1024*1024)throw new HarnessError('Maven discovery exceeds 4 MiB total');
|
|
14
|
+
const body=fs.readFileSync(p,'utf8').replace(/<!--[\s\S]*?-->/g,'');
|
|
15
|
+
if(/<!DOCTYPE|<!ENTITY|<!\[CDATA\[/i.test(body))throw new HarnessError('Unsupported XML declarations in '+file);
|
|
16
|
+
files.push({file,body,depth});
|
|
17
|
+
if(depth>=2){if(/<(?:[\w.-]+:)?modules\b/.test(body))warnings.push({file,reason:'Depth limit reached; deeper modules not scanned'});return;}
|
|
18
|
+
for(const section of body.matchAll(/<(?:[\w.-]+:)?modules\b[^>]*>([\s\S]*?)<\/(?:[\w.-]+:)?modules\s*>/g)){
|
|
19
|
+
for(const item of section[1].matchAll(/<(?:[\w.-]+:)?module\b[^>]*>([^<]*)<\/(?:[\w.-]+:)?module\s*>/g)){
|
|
20
|
+
const value=item[1].trim();
|
|
21
|
+
if(!value || /[$&\\]/.test(value) || value.split('/').some(x=>x==='..'||x==='.') || value.startsWith('/'))
|
|
22
|
+
throw new HarnessError('Unsupported or unsafe Maven module path in '+file);
|
|
23
|
+
const child=path.posix.join(path.posix.dirname(file),value,'pom.xml');safe(root,child);visit(child,depth+1);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
visit('pom.xml',0);return {files,warnings};
|
|
28
|
+
}
|
|
@@ -11,7 +11,7 @@ const errorKinds = ['timeout', 'network', 'http', 'application', 'assertion', 'e
|
|
|
11
11
|
const finite = n => typeof n === 'number' && Number.isFinite(n) && n >= 0;
|
|
12
12
|
const iso = s => text(s) && /^\d{4}-\d{2}-\d{2}T.*(?:Z|[+-]\d{2}:\d{2})$/.test(s) && Number.isFinite(Date.parse(s));
|
|
13
13
|
const secretKey = /^(password|passwd|secret|api[_-]?key|access[_-]?token|refresh[_-]?token|authorization|cookie|credentials?)$/i;
|
|
14
|
-
function redacted(value) {
|
|
14
|
+
export function redacted(value) {
|
|
15
15
|
if (typeof value === 'string') {
|
|
16
16
|
assert(!/Bearer\s+[A-Za-z0-9._~-]{8,}/i.test(value), 'Redact bearer credentials before verification');
|
|
17
17
|
return;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"provider": "anthropic",
|
|
4
|
+
"defaultModel": "sonnet",
|
|
5
|
+
"defaultReasoning": "medium",
|
|
6
|
+
"models": {
|
|
7
|
+
"economy": "haiku",
|
|
8
|
+
"balanced": "sonnet",
|
|
9
|
+
"premium": "opus"
|
|
10
|
+
},
|
|
11
|
+
"agents": {
|
|
12
|
+
"core:implementer": {
|
|
13
|
+
"model": "@balanced",
|
|
14
|
+
"reasoning": "medium"
|
|
15
|
+
},
|
|
16
|
+
"core:planner": {
|
|
17
|
+
"model": "@balanced",
|
|
18
|
+
"reasoning": "high"
|
|
19
|
+
},
|
|
20
|
+
"core:reviewer": {
|
|
21
|
+
"model": "@balanced",
|
|
22
|
+
"reasoning": "high"
|
|
23
|
+
},
|
|
24
|
+
"core:security-reviewer": {
|
|
25
|
+
"model": "@balanced",
|
|
26
|
+
"reasoning": "high"
|
|
27
|
+
},
|
|
28
|
+
"core:test-engineer": {
|
|
29
|
+
"model": "@balanced",
|
|
30
|
+
"reasoning": "medium"
|
|
31
|
+
},
|
|
32
|
+
"core:orchestrator": {
|
|
33
|
+
"model": "@balanced",
|
|
34
|
+
"reasoning": "high"
|
|
35
|
+
},
|
|
36
|
+
"core:documentation": {
|
|
37
|
+
"model": "@economy",
|
|
38
|
+
"reasoning": "low"
|
|
39
|
+
},
|
|
40
|
+
"product:business-analyst": {
|
|
41
|
+
"model": "@balanced",
|
|
42
|
+
"reasoning": "medium"
|
|
43
|
+
},
|
|
44
|
+
"product:product-manager": {
|
|
45
|
+
"model": "@balanced",
|
|
46
|
+
"reasoning": "medium"
|
|
47
|
+
},
|
|
48
|
+
"product:product-owner": {
|
|
49
|
+
"model": "@balanced",
|
|
50
|
+
"reasoning": "medium"
|
|
51
|
+
},
|
|
52
|
+
"product:project-manager": {
|
|
53
|
+
"model": "@balanced",
|
|
54
|
+
"reasoning": "medium"
|
|
55
|
+
},
|
|
56
|
+
"product:requirements-reviewer": {
|
|
57
|
+
"model": "@balanced",
|
|
58
|
+
"reasoning": "high"
|
|
59
|
+
},
|
|
60
|
+
"architecture:solution-architect": {
|
|
61
|
+
"model": "@balanced",
|
|
62
|
+
"reasoning": "high"
|
|
63
|
+
},
|
|
64
|
+
"architecture:architecture-reviewer": {
|
|
65
|
+
"model": "@balanced",
|
|
66
|
+
"reasoning": "high"
|
|
67
|
+
},
|
|
68
|
+
"delivery:project-manager": {
|
|
69
|
+
"model": "@balanced",
|
|
70
|
+
"reasoning": "medium"
|
|
71
|
+
},
|
|
72
|
+
"core:project-manager": {
|
|
73
|
+
"model": "@balanced",
|
|
74
|
+
"reasoning": "medium"
|
|
75
|
+
},
|
|
76
|
+
"analysis:business-analyst": {
|
|
77
|
+
"model": "@balanced",
|
|
78
|
+
"reasoning": "medium"
|
|
79
|
+
},
|
|
80
|
+
"analysis:requirements-reviewer": {
|
|
81
|
+
"model": "@balanced",
|
|
82
|
+
"reasoning": "high"
|
|
83
|
+
},
|
|
84
|
+
"discovery:product-manager": {
|
|
85
|
+
"model": "@balanced",
|
|
86
|
+
"reasoning": "medium"
|
|
87
|
+
},
|
|
88
|
+
"discovery:product-owner": {
|
|
89
|
+
"model": "@balanced",
|
|
90
|
+
"reasoning": "medium"
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
"execution": {
|
|
94
|
+
"mode": "hybrid",
|
|
95
|
+
"maxConcurrentAgents": 2,
|
|
96
|
+
"maxRounds": 3,
|
|
97
|
+
"autoEscalateToPremium": false
|
|
98
|
+
},
|
|
99
|
+
"hosts": {}
|
|
100
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
|
-
"provider": "
|
|
4
|
-
"defaultModel": "
|
|
3
|
+
"provider": "host",
|
|
4
|
+
"defaultModel": "host-default",
|
|
5
5
|
"defaultReasoning": "medium",
|
|
6
6
|
"models": {
|
|
7
|
-
"economy": "
|
|
8
|
-
"balanced": "
|
|
9
|
-
"premium": "
|
|
7
|
+
"economy": "host-default",
|
|
8
|
+
"balanced": "host-default",
|
|
9
|
+
"premium": "host-default"
|
|
10
10
|
},
|
|
11
11
|
"agents": {
|
|
12
12
|
"core:implementer": {
|
|
@@ -68,6 +68,26 @@
|
|
|
68
68
|
"delivery:project-manager": {
|
|
69
69
|
"model": "@balanced",
|
|
70
70
|
"reasoning": "medium"
|
|
71
|
+
},
|
|
72
|
+
"core:project-manager": {
|
|
73
|
+
"model": "@balanced",
|
|
74
|
+
"reasoning": "medium"
|
|
75
|
+
},
|
|
76
|
+
"analysis:business-analyst": {
|
|
77
|
+
"model": "@balanced",
|
|
78
|
+
"reasoning": "medium"
|
|
79
|
+
},
|
|
80
|
+
"analysis:requirements-reviewer": {
|
|
81
|
+
"model": "@balanced",
|
|
82
|
+
"reasoning": "high"
|
|
83
|
+
},
|
|
84
|
+
"discovery:product-manager": {
|
|
85
|
+
"model": "@balanced",
|
|
86
|
+
"reasoning": "medium"
|
|
87
|
+
},
|
|
88
|
+
"discovery:product-owner": {
|
|
89
|
+
"model": "@balanced",
|
|
90
|
+
"reasoning": "medium"
|
|
71
91
|
}
|
|
72
92
|
},
|
|
73
93
|
"execution": {
|