@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.
Files changed (163) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +26 -38
  3. package/adapters/ai-python/adapter.yaml +1 -1
  4. package/adapters/ai-python/rules.md +3 -0
  5. package/adapters/ai-python/skills/harness-ai-python-rag-access/SKILL.md +1 -0
  6. package/adapters/ai-python/skills/harness-ai-python-tool-safety/SKILL.md +1 -0
  7. package/adapters/astro/adapter.yaml +3 -5
  8. package/adapters/astro/checks/verification.yaml +6 -6
  9. package/adapters/astro/evals/cases.json +5 -5
  10. package/adapters/astro/rules/architecture.md +2 -8
  11. package/adapters/astro/rules/deployment.md +2 -8
  12. package/adapters/astro/rules.md +5 -14
  13. package/adapters/astro/skills/harness-astro-islands/SKILL.md +3 -2
  14. package/adapters/astro/skills/harness-astro-performance/SKILL.md +1 -0
  15. package/adapters/astro/skills/harness-astro-project-architecture/SKILL.md +9 -19
  16. package/adapters/astro/skills/harness-astro-seo/SKILL.md +1 -0
  17. package/adapters/java-spring/adapter.yaml +2 -4
  18. package/adapters/java-spring/rules/database.md +2 -2
  19. package/adapters/java-spring/rules/performance.md +2 -2
  20. package/adapters/java-spring/rules.md +4 -1
  21. package/adapters/java-spring/skills/harness-java-spring-spring-api/SKILL.md +1 -0
  22. package/adapters/kubernetes/adapter.yaml +1 -1
  23. package/adapters/kubernetes/rules.md +3 -0
  24. package/adapters/kubernetes/skills/harness-kubernetes-helm-release/SKILL.md +1 -0
  25. package/adapters/kubernetes/skills/harness-kubernetes-rollout-safety/SKILL.md +1 -0
  26. package/adapters/kubernetes/skills/harness-kubernetes-workload-review/SKILL.md +1 -0
  27. package/adapters/react-native/adapter.yaml +1 -1
  28. package/adapters/react-native/rules.md +3 -0
  29. package/adapters/react-native/skills/harness-react-native-behavior-testing/SKILL.md +1 -0
  30. package/adapters/react-native/skills/harness-react-native-device-permissions/SKILL.md +1 -0
  31. package/adapters/react-native/skills/harness-react-native-offline-sync/SKILL.md +1 -0
  32. package/adapters/react-native/skills/harness-react-native-screen-delivery/SKILL.md +1 -0
  33. package/adapters/react-nextjs/adapter.yaml +1 -1
  34. package/adapters/react-nextjs/rules.md +3 -0
  35. package/adapters/react-nextjs/skills/harness-react-nextjs-accessible-ui/SKILL.md +1 -0
  36. package/adapters/react-nextjs/skills/harness-react-nextjs-behavior-testing/SKILL.md +1 -0
  37. package/adapters/react-nextjs/skills/harness-react-nextjs-feature-delivery/SKILL.md +1 -0
  38. package/adapters/react-nextjs/skills/harness-react-nextjs-server-boundaries/SKILL.md +1 -0
  39. package/capabilities/analysis/capability.json +48 -0
  40. package/capabilities/analysis/checks/verification.json +16 -0
  41. package/capabilities/analysis/evals/cases.json +15 -0
  42. package/capabilities/architecture/agents/architecture-reviewer.md +5 -0
  43. package/capabilities/architecture/agents/solution-architect.md +5 -0
  44. package/capabilities/architecture/capability.json +1 -1
  45. package/capabilities/architecture/checks/verification.json +11 -0
  46. package/capabilities/architecture/evals/cases.json +10 -0
  47. package/capabilities/architecture/skills/harness-architecture-change-impact/SKILL.md +5 -0
  48. package/capabilities/architecture/templates/project-map.md +5 -0
  49. package/capabilities/architecture/workflows/change-review.md +2 -0
  50. package/capabilities/delivery/agents/project-manager.md +10 -11
  51. package/capabilities/delivery/capability.json +8 -2
  52. package/capabilities/delivery/checks/verification.json +11 -0
  53. package/capabilities/delivery/evals/cases.json +10 -0
  54. package/capabilities/delivery/skills/harness-delivery-scope-change/SKILL.md +6 -0
  55. package/capabilities/delivery/templates/delivery-plan.md +2 -23
  56. package/capabilities/delivery/workflows/execution-change.md +2 -0
  57. package/capabilities/discovery/capability.json +37 -0
  58. package/capabilities/discovery/checks/verification.json +16 -0
  59. package/capabilities/discovery/evals/cases.json +15 -0
  60. package/capabilities/product/agents/business-analyst.md +5 -0
  61. package/capabilities/product/agents/product-manager.md +5 -0
  62. package/capabilities/product/agents/product-owner.md +5 -0
  63. package/capabilities/product/agents/project-manager.md +9 -14
  64. package/capabilities/product/agents/requirements-reviewer.md +5 -0
  65. package/capabilities/product/capability.json +8 -2
  66. package/capabilities/product/checks/verification.json +11 -0
  67. package/capabilities/product/common/document-contract.md +10 -0
  68. package/capabilities/product/evals/cases.json +21 -0
  69. package/capabilities/product/skills/harness-product-acceptance-criteria/SKILL.md +4 -7
  70. package/capabilities/product/skills/harness-product-backlog-prioritization/SKILL.md +9 -6
  71. package/capabilities/product/skills/harness-product-brd-generation/SKILL.md +2 -6
  72. package/capabilities/product/skills/harness-product-business-analysis/SKILL.md +12 -10
  73. package/capabilities/product/skills/harness-product-document-review/SKILL.md +2 -6
  74. package/capabilities/product/skills/harness-product-idea-discovery/SKILL.md +2 -6
  75. package/capabilities/product/skills/harness-product-knowledge-analysis/SKILL.md +2 -6
  76. package/capabilities/product/skills/harness-product-mvp-definition/SKILL.md +1 -2
  77. package/capabilities/product/skills/harness-product-prd-generation/SKILL.md +2 -6
  78. package/capabilities/product/skills/harness-product-product-outcomes/SKILL.md +1 -2
  79. package/capabilities/product/skills/harness-product-product-research/SKILL.md +2 -6
  80. package/capabilities/product/skills/harness-product-srs-generation/SKILL.md +2 -6
  81. package/capabilities/product/skills/harness-product-techspec/SKILL.md +1 -1
  82. package/capabilities/product/skills/harness-product-traceability/SKILL.md +9 -7
  83. package/capabilities/product/skills/harness-product-use-case-generation/SKILL.md +2 -6
  84. package/capabilities/product/skills/harness-product-user-stories/SKILL.md +2 -6
  85. package/capabilities/product/templates/backlog.md +7 -0
  86. package/capabilities/product/templates/delivery-plan.md +3 -17
  87. package/capabilities/product/templates/traceability.md +7 -0
  88. package/capabilities/product/workflows/change-impact-analysis.md +4 -2
  89. package/capabilities/product/workflows/techspec-maintenance.md +3 -1
  90. package/cli/adapters.js +9 -5
  91. package/cli/agents.js +45 -9
  92. package/cli/capabilities.js +39 -8
  93. package/cli/checkpoint.js +57 -0
  94. package/cli/claude.js +40 -0
  95. package/cli/comparison.js +46 -0
  96. package/cli/composition.js +1 -0
  97. package/cli/files.js +2 -1
  98. package/cli/harness.js +69 -14
  99. package/cli/main.js +22 -9
  100. package/cli/maven.js +28 -0
  101. package/cli/verification-report.js +1 -1
  102. package/config/agents.claude-code.json +100 -0
  103. package/config/agents.default.json +25 -5
  104. package/core/agents/documentation.md +5 -0
  105. package/core/agents/implementer.md +5 -0
  106. package/core/agents/orchestrator.md +5 -0
  107. package/core/agents/planner.md +5 -0
  108. package/core/agents/project-manager.md +20 -0
  109. package/core/agents/reviewer.md +5 -0
  110. package/core/agents/security-reviewer.md +5 -0
  111. package/core/agents/test-engineer.md +5 -0
  112. package/core/checks/verification.json +25 -0
  113. package/core/evals/cases.json +92 -0
  114. package/core/evals/composition.json +15 -0
  115. package/core/policies/permissions.md +3 -1
  116. package/core/rules/context.md +4 -0
  117. package/core/skills/harness-debugging/SKILL.md +6 -0
  118. package/core/skills/harness-review/SKILL.md +6 -0
  119. package/core/skills/harness-spec/SKILL.md +6 -0
  120. package/core/workflows/adaptive-verification.md +3 -0
  121. package/core/workflows/agent-routing.md +27 -26
  122. package/core/workflows/artifact-contract.md +5 -0
  123. package/core/workflows/continuous-development.md +101 -0
  124. package/core/workflows/spec-to-code.md +4 -1
  125. package/docs/README.md +5 -1
  126. package/docs/agent-configuration.md +9 -5
  127. package/docs/agent-first.md +66 -0
  128. package/docs/agent-handoffs.md +8 -0
  129. package/docs/architecture.md +8 -0
  130. package/docs/cli.md +4 -0
  131. package/docs/continuous-development.md +92 -0
  132. package/docs/documentation-workflows.md +2 -0
  133. package/docs/end-to-end-workflow.md +4 -0
  134. package/docs/examples/astro-micro-tools.md +1 -1
  135. package/docs/examples/checkpoint.json +20 -0
  136. package/docs/examples/continuous-development.md +40 -0
  137. package/docs/examples/spring-boot-api.md +2 -4
  138. package/docs/getting-started.md +5 -2
  139. package/docs/i18n/README.es.md +32 -0
  140. package/docs/i18n/README.vi.md +19 -42
  141. package/docs/i18n/README.zh-CN.md +32 -0
  142. package/docs/skill-matrix.md +11 -7
  143. package/docs/tool-setup/README.md +69 -67
  144. package/docs/tool-setup/claude-code.md +41 -49
  145. package/docs/tool-setup/codex.md +31 -43
  146. package/docs/tool-setup/copilot.md +35 -38
  147. package/docs/tool-setup/cursor.md +25 -40
  148. package/docs/tool-setup/gemini-cli.md +31 -35
  149. package/docs/tool-setup/kiro.md +55 -0
  150. package/docs/versioning.md +17 -0
  151. package/evals/README.md +4 -0
  152. package/evals/real-task-comparison.md +4 -0
  153. package/harness.yaml +4 -2
  154. package/package.json +3 -2
  155. package/scripts/compare-tasks.js +15 -0
  156. package/templates/AGENTS.md +19 -26
  157. package/templates/delivery-plan.md +25 -0
  158. package/templates/openspec/design.md +10 -0
  159. package/templates/openspec/tasks.md +8 -1
  160. package/adapters/astro/skills/harness-astro-cloudflare/SKILL.md +0 -27
  161. package/adapters/astro/skills/harness-astro-microfrontend/SKILL.md +0 -27
  162. package/adapters/java-spring/skills/harness-java-spring-database-migration/SKILL.md +0 -23
  163. 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
+ }
@@ -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
- if (Object.hasOwn(files, dest)) throw new HarnessError(`Skill collision: ${name}`);
78
- files[dest] = fs.readFileSync(safe(source, `capabilities/${c}/skills/${name}/SKILL.md`));
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
- if (!(lock.capabilities ?? []).includes(capability)) {
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 sourceName = `.harness/capabilities/${capability}/templates/${name}.md`;
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 && (capability !== 'product' || name !== 'techspec')) throw new HarnessError('--report is supported only for product techspec');
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 route = docRoute(root, capability, name);
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
- --host <id> setup only: codex,claude-code,gemini-cli,cursor,copilot or custom host ID
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
- --report <file> eval: score; verify: validate evidence; new-doc techspec: link evidence
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 (command === 'setup') allowed.add('host');
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": "openai",
4
- "defaultModel": "gpt-6.1-sol",
3
+ "provider": "host",
4
+ "defaultModel": "host-default",
5
5
  "defaultReasoning": "medium",
6
6
  "models": {
7
- "economy": "gpt-6-luna",
8
- "balanced": "gpt-6.1-sol",
9
- "premium": "gpt-6-astra"
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": {