@pedyc/harness-core 1.0.1 → 1.1.1
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/LICENSE +21 -21
- package/README.md +55 -48
- package/dist/adapters/provider-runner.d.ts +18 -0
- package/dist/adapters/provider-runner.d.ts.map +1 -0
- package/dist/adapters/provider-runner.js +45 -0
- package/dist/adapters/provider-runner.js.map +1 -0
- package/dist/contracts/agent.d.ts +79 -0
- package/dist/contracts/agent.d.ts.map +1 -0
- package/dist/contracts/agent.js +2 -0
- package/dist/contracts/agent.js.map +1 -0
- package/dist/contracts/index.d.ts +7 -0
- package/dist/contracts/index.d.ts.map +1 -0
- package/dist/contracts/index.js +2 -0
- package/dist/contracts/index.js.map +1 -0
- package/dist/contracts/policy.d.ts +30 -0
- package/dist/contracts/policy.d.ts.map +1 -0
- package/dist/contracts/policy.js +2 -0
- package/dist/contracts/policy.js.map +1 -0
- package/dist/contracts/preset.d.ts +27 -0
- package/dist/contracts/preset.d.ts.map +1 -0
- package/dist/contracts/preset.js +2 -0
- package/dist/contracts/preset.js.map +1 -0
- package/dist/contracts/run.d.ts +41 -0
- package/dist/contracts/run.d.ts.map +1 -0
- package/dist/contracts/run.js +2 -0
- package/dist/contracts/run.js.map +1 -0
- package/dist/contracts/task.d.ts +35 -0
- package/dist/contracts/task.d.ts.map +1 -0
- package/dist/contracts/task.js +2 -0
- package/dist/contracts/task.js.map +1 -0
- package/dist/contracts/validation.d.ts +7 -0
- package/dist/contracts/validation.d.ts.map +1 -0
- package/dist/contracts/validation.js +2 -0
- package/dist/contracts/validation.js.map +1 -0
- package/dist/core/agent.d.ts +22 -0
- package/dist/core/agent.d.ts.map +1 -0
- package/dist/core/agent.js +47 -0
- package/dist/core/agent.js.map +1 -0
- package/dist/core/approval-gate.d.ts +17 -0
- package/dist/core/approval-gate.d.ts.map +1 -0
- package/dist/core/approval-gate.js +18 -0
- package/dist/core/approval-gate.js.map +1 -0
- package/dist/core/command.d.ts +14 -0
- package/dist/core/command.d.ts.map +1 -0
- package/dist/core/command.js +33 -0
- package/dist/core/command.js.map +1 -0
- package/dist/core/diff-inspector.d.ts +7 -0
- package/dist/core/diff-inspector.d.ts.map +1 -0
- package/dist/core/diff-inspector.js +27 -0
- package/dist/core/diff-inspector.js.map +1 -0
- package/dist/core/executor.d.ts +21 -0
- package/dist/core/executor.d.ts.map +1 -0
- package/dist/core/executor.js +142 -0
- package/dist/core/executor.js.map +1 -0
- package/dist/core/intake.d.ts +11 -0
- package/dist/core/intake.d.ts.map +1 -0
- package/dist/core/intake.js +33 -0
- package/dist/core/intake.js.map +1 -0
- package/dist/core/package-manager.d.ts +14 -0
- package/dist/core/package-manager.d.ts.map +1 -0
- package/dist/core/package-manager.js +20 -0
- package/dist/core/package-manager.js.map +1 -0
- package/dist/core/policy-engine.d.ts +14 -0
- package/dist/core/policy-engine.d.ts.map +1 -0
- package/dist/core/policy-engine.js +29 -0
- package/dist/core/policy-engine.js.map +1 -0
- package/dist/core/validator.d.ts +25 -0
- package/dist/core/validator.d.ts.map +1 -0
- package/dist/core/validator.js +24 -0
- package/dist/core/validator.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -13
- package/src/agent.mjs +0 -27
- package/src/command.mjs +0 -28
- package/src/index.mjs +0 -11
- package/src/intake.mjs +0 -23
- package/src/orchestrator.mjs +0 -119
- package/src/package-manager.mjs +0 -17
- package/src/policy.mjs +0 -19
- package/src/provider.mjs +0 -32
- package/src/schema.mjs +0 -24
- package/src/snapshots.mjs +0 -20
package/package.json
CHANGED
|
@@ -1,20 +1,54 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pedyc/harness-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Runtime primitives for Pedyc Harness",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
|
8
|
-
".":
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"./
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"./
|
|
17
|
-
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./package-manager": {
|
|
13
|
+
"types": "./dist/core/package-manager.d.ts",
|
|
14
|
+
"default": "./dist/core/package-manager.js"
|
|
15
|
+
},
|
|
16
|
+
"./intake": {
|
|
17
|
+
"types": "./dist/core/intake.d.ts",
|
|
18
|
+
"default": "./dist/core/intake.js"
|
|
19
|
+
},
|
|
20
|
+
"./command": {
|
|
21
|
+
"types": "./dist/core/command.d.ts",
|
|
22
|
+
"default": "./dist/core/command.js"
|
|
23
|
+
},
|
|
24
|
+
"./snapshots": {
|
|
25
|
+
"types": "./dist/core/diff-inspector.d.ts",
|
|
26
|
+
"default": "./dist/core/diff-inspector.js"
|
|
27
|
+
},
|
|
28
|
+
"./schema": {
|
|
29
|
+
"types": "./dist/core/validator.d.ts",
|
|
30
|
+
"default": "./dist/core/validator.js"
|
|
31
|
+
},
|
|
32
|
+
"./agent": {
|
|
33
|
+
"types": "./dist/core/agent.d.ts",
|
|
34
|
+
"default": "./dist/core/agent.js"
|
|
35
|
+
},
|
|
36
|
+
"./policy": {
|
|
37
|
+
"types": "./dist/core/policy-engine.d.ts",
|
|
38
|
+
"default": "./dist/core/policy-engine.js"
|
|
39
|
+
},
|
|
40
|
+
"./provider": {
|
|
41
|
+
"types": "./dist/adapters/provider-runner.d.ts",
|
|
42
|
+
"default": "./dist/adapters/provider-runner.js"
|
|
43
|
+
},
|
|
44
|
+
"./orchestrator": {
|
|
45
|
+
"types": "./dist/core/executor.d.ts",
|
|
46
|
+
"default": "./dist/core/executor.js"
|
|
47
|
+
},
|
|
48
|
+
"./contracts": {
|
|
49
|
+
"types": "./dist/contracts/index.d.ts",
|
|
50
|
+
"default": "./dist/contracts/index.js"
|
|
51
|
+
}
|
|
18
52
|
},
|
|
19
53
|
"repository": {
|
|
20
54
|
"type": "git",
|
|
@@ -41,7 +75,10 @@
|
|
|
41
75
|
"ajv": "^8.20.0"
|
|
42
76
|
},
|
|
43
77
|
"files": [
|
|
44
|
-
"
|
|
78
|
+
"dist",
|
|
45
79
|
"README.md"
|
|
46
|
-
]
|
|
80
|
+
],
|
|
81
|
+
"scripts": {
|
|
82
|
+
"build": "tsc -p tsconfig.json"
|
|
83
|
+
}
|
|
47
84
|
}
|
package/src/agent.mjs
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
export const parseAgentResponse = (name, stdout, validate, ajv) => {
|
|
2
|
-
const trimmed = stdout.trim()
|
|
3
|
-
if (!trimmed) return { ok: true, details: `${name} completed without a response payload.`, payload: {} }
|
|
4
|
-
try {
|
|
5
|
-
const payload = JSON.parse(trimmed)
|
|
6
|
-
if (!validate(payload)) {
|
|
7
|
-
return { ok: false, details: `${name} returned an invalid response: ${ajv.errorsText(validate.errors)}`, payload: {} }
|
|
8
|
-
}
|
|
9
|
-
return { ok: true, details: payload.details ?? `${name} returned a structured response.`, payload }
|
|
10
|
-
} catch {
|
|
11
|
-
return { ok: false, details: `${name} must return one JSON object on stdout.`, payload: {} }
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export const validateStageResponse = (name, payload) => {
|
|
16
|
-
if (name === 'planner' && (!Array.isArray(payload.implementationPlan) || payload.implementationPlan.length === 0)) {
|
|
17
|
-
return 'planner must return a non-empty implementationPlan.'
|
|
18
|
-
}
|
|
19
|
-
if (name === 'tester') {
|
|
20
|
-
if (typeof payload.approved !== 'boolean') return 'tester must return a boolean approved field.'
|
|
21
|
-
if (!Array.isArray(payload.evidence) || payload.evidence.length === 0) return 'tester must return non-empty evidence.'
|
|
22
|
-
}
|
|
23
|
-
if (name === 'reviewer' && typeof payload.approved !== 'boolean') {
|
|
24
|
-
return 'reviewer must return a boolean approved field.'
|
|
25
|
-
}
|
|
26
|
-
return null
|
|
27
|
-
}
|
package/src/command.mjs
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process'
|
|
2
|
-
|
|
3
|
-
const normalizeCommand = (command) => {
|
|
4
|
-
if (
|
|
5
|
-
process.platform === 'win32'
|
|
6
|
-
&& ['npm', 'npx', 'pnpm', 'yarn'].includes(command)
|
|
7
|
-
&& !command.endsWith('.cmd')
|
|
8
|
-
) {
|
|
9
|
-
return `${command}.cmd`
|
|
10
|
-
}
|
|
11
|
-
return command
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export const runCommand = (root, command, args = [], stdin = null) => new Promise((resolve) => {
|
|
15
|
-
const child = spawn(normalizeCommand(command), args, {
|
|
16
|
-
cwd: root,
|
|
17
|
-
shell: process.platform === 'win32',
|
|
18
|
-
windowsHide: true,
|
|
19
|
-
})
|
|
20
|
-
let stdout = ''
|
|
21
|
-
let stderr = ''
|
|
22
|
-
child.stdout.on('data', (chunk) => { stdout += chunk })
|
|
23
|
-
child.stderr.on('data', (chunk) => { stderr += chunk })
|
|
24
|
-
child.on('close', (code) => resolve({ code: code ?? 1, stdout, stderr }))
|
|
25
|
-
child.on('error', (error) => resolve({ code: 1, stdout, stderr: error.message }))
|
|
26
|
-
if (stdin !== null) child.stdin.write(`${JSON.stringify(stdin)}\n`)
|
|
27
|
-
child.stdin.end()
|
|
28
|
-
})
|
package/src/index.mjs
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export { detectPackageManager, packageScriptCommand } from './package-manager.mjs'
|
|
2
|
-
export { normalizeTask, readTaskFile } from './intake.mjs'
|
|
3
|
-
export { runCommand } from './command.mjs'
|
|
4
|
-
export { changedFiles, snapshotFiles } from './snapshots.mjs'
|
|
5
|
-
export { loadSchemas, createValidators, validationDetails } from './schema.mjs'
|
|
6
|
-
export { parseAgentResponse, validateStageResponse } from './agent.mjs'
|
|
7
|
-
export { validatePolicy, findOutOfScopeChanges, isCommandAllowed } from './policy.mjs'
|
|
8
|
-
export { createProviderRunner } from './provider.mjs'
|
|
9
|
-
export { runOrchestrator } from './orchestrator.mjs'
|
|
10
|
-
|
|
11
|
-
export const harnessCoreVersion = '1.0.0'
|
package/src/intake.mjs
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs'
|
|
2
|
-
|
|
3
|
-
const defaultChecks = ['pnpm run harness:verify', 'pnpm run type-check', 'pnpm run test:unit', 'pnpm run build']
|
|
4
|
-
|
|
5
|
-
export const normalizeTask = (task) => {
|
|
6
|
-
const normalized = {
|
|
7
|
-
feature: task.task?.trim() || task.feature?.trim() || '',
|
|
8
|
-
objective: task.goal?.trim() || task.objective?.trim() || '',
|
|
9
|
-
constraints: task.specialConstraints ?? task.constraints ?? [],
|
|
10
|
-
acceptanceCriteria: task.acceptance ?? task.acceptanceCriteria ?? [],
|
|
11
|
-
testHints: task.testHints ?? defaultChecks,
|
|
12
|
-
maxIterations: task.maxIterations ?? 3,
|
|
13
|
-
}
|
|
14
|
-
const questions = []
|
|
15
|
-
if (!normalized.feature) questions.push('要实现的任务或功能是什么?')
|
|
16
|
-
if (!normalized.objective) questions.push('任务的目标是什么?')
|
|
17
|
-
if (!normalized.acceptanceCriteria.length) questions.push('完成任务的验收标准是什么?')
|
|
18
|
-
return questions.length > 0
|
|
19
|
-
? { status: 'needs_input', questions, normalizedTask: normalized }
|
|
20
|
-
: { status: 'ready', normalizedTask: normalized, questions: [] }
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export const readTaskFile = (path) => normalizeTask(JSON.parse(readFileSync(path, 'utf8')))
|
package/src/orchestrator.mjs
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import { findOutOfScopeChanges } from './policy.mjs'
|
|
2
|
-
|
|
3
|
-
export const runOrchestrator = async ({
|
|
4
|
-
input,
|
|
5
|
-
policy,
|
|
6
|
-
dryRun,
|
|
7
|
-
snapshot,
|
|
8
|
-
changedFiles,
|
|
9
|
-
runAgent,
|
|
10
|
-
runVerification,
|
|
11
|
-
writeVerification = () => {},
|
|
12
|
-
}) => {
|
|
13
|
-
const maxIterations = Math.min(
|
|
14
|
-
Math.max(Number(input.maxIterations ?? policy.maxIterations ?? 3), 1),
|
|
15
|
-
policy.maxIterations ?? 3,
|
|
16
|
-
)
|
|
17
|
-
const phases = []
|
|
18
|
-
const issues = []
|
|
19
|
-
const fileChanges = []
|
|
20
|
-
const implementationPlan = [
|
|
21
|
-
`Analyze the requested feature: ${input.feature.trim()}.`,
|
|
22
|
-
`Implement the objective while satisfying ${input.acceptanceCriteria.length} acceptance criteria.`,
|
|
23
|
-
'Run configured verification gates and review the result before reporting completion.',
|
|
24
|
-
]
|
|
25
|
-
|
|
26
|
-
if (dryRun) {
|
|
27
|
-
// A dry run is a safe preview: no Agent Provider is invoked, no verification
|
|
28
|
-
// gate is executed, and no product file can change. Report the same four
|
|
29
|
-
// phases so callers can consume the result with the regular output contract.
|
|
30
|
-
for (const [name, details] of [
|
|
31
|
-
['planner', 'Dry run: planner execution skipped; no agent provider was invoked.'],
|
|
32
|
-
['coder', 'Dry run: coder execution skipped; no product files were changed.'],
|
|
33
|
-
['tester', 'Dry run: tester execution skipped; no verification gates were executed.'],
|
|
34
|
-
['reviewer', 'Dry run: reviewer execution skipped; no product files were changed.'],
|
|
35
|
-
]) {
|
|
36
|
-
phases.push({ name, iteration: 1, status: 'passed', details })
|
|
37
|
-
}
|
|
38
|
-
return { completed: true, implementationPlan, fileChanges, verification: [], issues, phases, iterations: 1 }
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const recordPhase = (name, status, details, iteration) => {
|
|
42
|
-
const phase = { name, status, details }
|
|
43
|
-
if (iteration) phase.iteration = iteration
|
|
44
|
-
phases.push(phase)
|
|
45
|
-
return phase
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
recordPhase('planner', 'running', 'Validating task input and preparing an implementation plan.')
|
|
49
|
-
const plan = await runAgent('planner', { phase: 'planner', input, implementationPlan })
|
|
50
|
-
phases[phases.length - 1] = { name: 'planner', status: plan.ok ? 'passed' : 'failed', details: plan.details }
|
|
51
|
-
if (plan.payload?.implementationPlan?.length) {
|
|
52
|
-
implementationPlan.splice(0, implementationPlan.length, ...plan.payload.implementationPlan)
|
|
53
|
-
}
|
|
54
|
-
if (!plan.ok) issues.push(plan.details)
|
|
55
|
-
|
|
56
|
-
let lastVerification = []
|
|
57
|
-
let completed = false
|
|
58
|
-
for (let iteration = 1; iteration <= maxIterations && issues.length === 0; iteration += 1) {
|
|
59
|
-
const before = snapshot()
|
|
60
|
-
recordPhase('coder', 'running', 'Applying the approved implementation plan.', iteration)
|
|
61
|
-
const coder = await runAgent('coder', { phase: 'coder', input, implementationPlan, iteration, previousVerification: lastVerification })
|
|
62
|
-
phases[phases.length - 1] = { name: 'coder', iteration, status: coder.ok ? 'passed' : 'failed', details: coder.details }
|
|
63
|
-
|
|
64
|
-
for (const file of changedFiles(before, snapshot())) {
|
|
65
|
-
fileChanges.push({ file, change: `Changed during coder iteration ${iteration}.` })
|
|
66
|
-
}
|
|
67
|
-
if (!coder.ok) {
|
|
68
|
-
issues.push(coder.details)
|
|
69
|
-
break
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
recordPhase('tester', 'running', 'Running required verification gates.', iteration)
|
|
73
|
-
const verification = await runVerification()
|
|
74
|
-
const externalTest = await runAgent('tester', { phase: 'tester', input, implementationPlan, verification, iteration })
|
|
75
|
-
if (!externalTest.ok) verification.push({ command: 'external tester', result: 'fail', details: externalTest.details })
|
|
76
|
-
lastVerification = verification
|
|
77
|
-
const testerOk = verification.every((check) => check.result === 'pass')
|
|
78
|
-
&& externalTest.ok
|
|
79
|
-
&& externalTest.payload.approved === true
|
|
80
|
-
phases[phases.length - 1] = {
|
|
81
|
-
name: 'tester',
|
|
82
|
-
iteration,
|
|
83
|
-
status: testerOk ? 'passed' : 'failed',
|
|
84
|
-
details: testerOk ? 'All required gates passed and tester approved the evidence.'
|
|
85
|
-
: externalTest.ok ? 'At least one required gate failed or tester rejected the evidence.' : externalTest.details,
|
|
86
|
-
}
|
|
87
|
-
writeVerification(iteration, verification)
|
|
88
|
-
if (!testerOk && iteration === maxIterations) {
|
|
89
|
-
issues.push('Verification did not pass before maxIterations was reached.')
|
|
90
|
-
break
|
|
91
|
-
}
|
|
92
|
-
if (!testerOk) continue
|
|
93
|
-
|
|
94
|
-
recordPhase('reviewer', 'running', 'Checking scope, output contract, and acceptance criteria.', iteration)
|
|
95
|
-
const outOfScopeChanges = findOutOfScopeChanges(fileChanges.map(({ file }) => file), policy)
|
|
96
|
-
const reviewer = await runAgent('reviewer', { phase: 'reviewer', input, implementationPlan, verification, fileChanges, iteration })
|
|
97
|
-
const reviewerApproved = reviewer.ok && reviewer.payload?.approved === true && outOfScopeChanges.length === 0
|
|
98
|
-
const reviewerDetails = outOfScopeChanges.length
|
|
99
|
-
? `Out-of-scope files changed: ${outOfScopeChanges.join(', ')}`
|
|
100
|
-
: reviewer.details
|
|
101
|
-
phases[phases.length - 1] = { name: 'reviewer', iteration, status: reviewerApproved ? 'passed' : 'failed', details: reviewerDetails }
|
|
102
|
-
if (!reviewerApproved) {
|
|
103
|
-
issues.push(reviewerDetails)
|
|
104
|
-
break
|
|
105
|
-
}
|
|
106
|
-
completed = true
|
|
107
|
-
break
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return {
|
|
111
|
-
completed,
|
|
112
|
-
implementationPlan,
|
|
113
|
-
fileChanges,
|
|
114
|
-
verification: lastVerification,
|
|
115
|
-
issues,
|
|
116
|
-
phases,
|
|
117
|
-
iterations: phases.filter((phase) => phase.name === 'coder').length,
|
|
118
|
-
}
|
|
119
|
-
}
|
package/src/package-manager.mjs
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs'
|
|
2
|
-
import { join } from 'node:path'
|
|
3
|
-
|
|
4
|
-
export const detectPackageManager = (root) => {
|
|
5
|
-
if (existsSync(join(root, 'pnpm-lock.yaml'))) return { name: 'pnpm', command: 'pnpm', args: ['run'] }
|
|
6
|
-
if (existsSync(join(root, 'yarn.lock'))) return { name: 'yarn', command: 'yarn', args: [] }
|
|
7
|
-
return { name: 'npm', command: 'npm', args: ['run'] }
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export const packageScriptCommand = (root, script) => {
|
|
11
|
-
const manager = detectPackageManager(root)
|
|
12
|
-
return {
|
|
13
|
-
...manager,
|
|
14
|
-
args: [...manager.args, script],
|
|
15
|
-
display: `${manager.command} ${[...manager.args, script].join(' ')}`,
|
|
16
|
-
}
|
|
17
|
-
}
|
package/src/policy.mjs
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
export const validatePolicy = (policy) => {
|
|
2
|
-
if (!policy || typeof policy !== 'object') return 'Policy must be an object.'
|
|
3
|
-
if (!Array.isArray(policy.allowedProductPaths) || policy.allowedProductPaths.length === 0) {
|
|
4
|
-
return 'Harness policy must define at least one allowedProductPaths entry.'
|
|
5
|
-
}
|
|
6
|
-
if (!Number.isInteger(policy.maxIterations) || policy.maxIterations < 1) {
|
|
7
|
-
return 'Harness policy maxIterations must be a positive integer.'
|
|
8
|
-
}
|
|
9
|
-
if (!Array.isArray(policy.protectedPaths)) return 'Harness policy protectedPaths must be an array.'
|
|
10
|
-
if (!Array.isArray(policy.requiredChecks)) return 'Harness policy requiredChecks must be an array.'
|
|
11
|
-
return null
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export const findOutOfScopeChanges = (files, policy) => files.filter((file) =>
|
|
15
|
-
!policy.allowedProductPaths.some((allowedPath) => file.startsWith(allowedPath)),
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
export const isCommandAllowed = (command, policy) =>
|
|
19
|
-
!policy.allowedAgentCommands?.length || policy.allowedAgentCommands.includes(command)
|
package/src/provider.mjs
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { parseAgentResponse, validateStageResponse } from './agent.mjs'
|
|
2
|
-
import { runCommand } from './command.mjs'
|
|
3
|
-
|
|
4
|
-
export const createProviderRunner = ({ root, agents, policy, validator, ajv }) => async (name, payload) => {
|
|
5
|
-
const config = agents[name]
|
|
6
|
-
if (!config || config.mode === 'internal') {
|
|
7
|
-
return { ok: true, details: `${name} completed using the built-in stage.`, payload: {} }
|
|
8
|
-
}
|
|
9
|
-
if (config.mode !== 'external' || typeof config.provider !== 'string') {
|
|
10
|
-
return { ok: false, details: `${name} requires a configured provider in .harness/agents.json.`, payload: {} }
|
|
11
|
-
}
|
|
12
|
-
const provider = agents.providers?.[config.provider]
|
|
13
|
-
if (!provider || typeof provider.command !== 'string' || !Array.isArray(provider.args)) {
|
|
14
|
-
return { ok: false, details: `${name} provider '${config.provider}' is not configured.`, payload: {} }
|
|
15
|
-
}
|
|
16
|
-
if (!isAllowed(provider.command, policy)) {
|
|
17
|
-
return { ok: false, details: `${name} provider command is not in policy.allowedAgentCommands.`, payload: {} }
|
|
18
|
-
}
|
|
19
|
-
const result = await runCommand(root, provider.command, provider.args, { ...payload, provider: config.provider })
|
|
20
|
-
if (result.code !== 0) {
|
|
21
|
-
return { ok: false, details: result.stderr.trim() || `${name} exited with code ${result.code}.`, payload: {} }
|
|
22
|
-
}
|
|
23
|
-
const response = parseAgentResponse(name, result.stdout, validator, ajv)
|
|
24
|
-
if (!response.ok) return response
|
|
25
|
-
const stageError = validateStageResponse(name, response.payload)
|
|
26
|
-
return stageError
|
|
27
|
-
? { ok: false, details: stageError, payload: response.payload }
|
|
28
|
-
: response
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const isAllowed = (command, policy) =>
|
|
32
|
-
!policy.allowedAgentCommands?.length || policy.allowedAgentCommands.includes(command)
|
package/src/schema.mjs
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import Ajv2020 from 'ajv/dist/2020.js'
|
|
2
|
-
import { readFileSync } from 'node:fs'
|
|
3
|
-
import { join } from 'node:path'
|
|
4
|
-
|
|
5
|
-
export const loadSchemas = (root) => {
|
|
6
|
-
const read = (name) => JSON.parse(readFileSync(join(root, '.harness', name), 'utf8'))
|
|
7
|
-
return {
|
|
8
|
-
input: read('input.schema.json'),
|
|
9
|
-
output: read('output.schema.json'),
|
|
10
|
-
agentResponse: read('agent-response.schema.json'),
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export const createValidators = (schemas) => {
|
|
15
|
-
const ajv = new Ajv2020({ allErrors: true, strict: false })
|
|
16
|
-
return {
|
|
17
|
-
ajv,
|
|
18
|
-
input: ajv.compile(schemas.input),
|
|
19
|
-
output: ajv.compile(schemas.output),
|
|
20
|
-
agentResponse: ajv.compile(schemas.agentResponse),
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export const validationDetails = (ajv, validator) => ajv.errorsText(validator.errors)
|
package/src/snapshots.mjs
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
|
2
|
-
import { join, relative } from 'node:path'
|
|
3
|
-
|
|
4
|
-
const listFiles = (root, directory = root, result = []) => {
|
|
5
|
-
if (!existsSync(directory)) return result
|
|
6
|
-
for (const entry of readdirSync(directory)) {
|
|
7
|
-
if (directory === root && ['node_modules', 'dist', '.git'].includes(entry)) continue
|
|
8
|
-
const path = join(directory, entry)
|
|
9
|
-
if (statSync(path).isDirectory()) listFiles(root, path, result)
|
|
10
|
-
else result.push(relative(root, path).replaceAll('\\', '/'))
|
|
11
|
-
}
|
|
12
|
-
return result
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export const snapshotFiles = (root) => new Map(
|
|
16
|
-
listFiles(root).map((file) => [file, readFileSync(join(root, file), 'utf8')]),
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
export const changedFiles = (before, after) => [...new Set([...before.keys(), ...after.keys()])]
|
|
20
|
-
.filter((file) => !before.has(file) || !after.has(file) || before.get(file) !== after.get(file))
|