@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.
Files changed (85) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +55 -48
  3. package/dist/adapters/provider-runner.d.ts +18 -0
  4. package/dist/adapters/provider-runner.d.ts.map +1 -0
  5. package/dist/adapters/provider-runner.js +45 -0
  6. package/dist/adapters/provider-runner.js.map +1 -0
  7. package/dist/contracts/agent.d.ts +79 -0
  8. package/dist/contracts/agent.d.ts.map +1 -0
  9. package/dist/contracts/agent.js +2 -0
  10. package/dist/contracts/agent.js.map +1 -0
  11. package/dist/contracts/index.d.ts +7 -0
  12. package/dist/contracts/index.d.ts.map +1 -0
  13. package/dist/contracts/index.js +2 -0
  14. package/dist/contracts/index.js.map +1 -0
  15. package/dist/contracts/policy.d.ts +30 -0
  16. package/dist/contracts/policy.d.ts.map +1 -0
  17. package/dist/contracts/policy.js +2 -0
  18. package/dist/contracts/policy.js.map +1 -0
  19. package/dist/contracts/preset.d.ts +27 -0
  20. package/dist/contracts/preset.d.ts.map +1 -0
  21. package/dist/contracts/preset.js +2 -0
  22. package/dist/contracts/preset.js.map +1 -0
  23. package/dist/contracts/run.d.ts +41 -0
  24. package/dist/contracts/run.d.ts.map +1 -0
  25. package/dist/contracts/run.js +2 -0
  26. package/dist/contracts/run.js.map +1 -0
  27. package/dist/contracts/task.d.ts +35 -0
  28. package/dist/contracts/task.d.ts.map +1 -0
  29. package/dist/contracts/task.js +2 -0
  30. package/dist/contracts/task.js.map +1 -0
  31. package/dist/contracts/validation.d.ts +7 -0
  32. package/dist/contracts/validation.d.ts.map +1 -0
  33. package/dist/contracts/validation.js +2 -0
  34. package/dist/contracts/validation.js.map +1 -0
  35. package/dist/core/agent.d.ts +22 -0
  36. package/dist/core/agent.d.ts.map +1 -0
  37. package/dist/core/agent.js +47 -0
  38. package/dist/core/agent.js.map +1 -0
  39. package/dist/core/approval-gate.d.ts +17 -0
  40. package/dist/core/approval-gate.d.ts.map +1 -0
  41. package/dist/core/approval-gate.js +18 -0
  42. package/dist/core/approval-gate.js.map +1 -0
  43. package/dist/core/command.d.ts +14 -0
  44. package/dist/core/command.d.ts.map +1 -0
  45. package/dist/core/command.js +33 -0
  46. package/dist/core/command.js.map +1 -0
  47. package/dist/core/diff-inspector.d.ts +7 -0
  48. package/dist/core/diff-inspector.d.ts.map +1 -0
  49. package/dist/core/diff-inspector.js +27 -0
  50. package/dist/core/diff-inspector.js.map +1 -0
  51. package/dist/core/executor.d.ts +21 -0
  52. package/dist/core/executor.d.ts.map +1 -0
  53. package/dist/core/executor.js +142 -0
  54. package/dist/core/executor.js.map +1 -0
  55. package/dist/core/intake.d.ts +11 -0
  56. package/dist/core/intake.d.ts.map +1 -0
  57. package/dist/core/intake.js +33 -0
  58. package/dist/core/intake.js.map +1 -0
  59. package/dist/core/package-manager.d.ts +14 -0
  60. package/dist/core/package-manager.d.ts.map +1 -0
  61. package/dist/core/package-manager.js +20 -0
  62. package/dist/core/package-manager.js.map +1 -0
  63. package/dist/core/policy-engine.d.ts +14 -0
  64. package/dist/core/policy-engine.d.ts.map +1 -0
  65. package/dist/core/policy-engine.js +29 -0
  66. package/dist/core/policy-engine.js.map +1 -0
  67. package/dist/core/validator.d.ts +25 -0
  68. package/dist/core/validator.d.ts.map +1 -0
  69. package/dist/core/validator.js +24 -0
  70. package/dist/core/validator.js.map +1 -0
  71. package/dist/index.d.ts +20 -0
  72. package/dist/index.d.ts.map +1 -0
  73. package/dist/index.js +15 -0
  74. package/dist/index.js.map +1 -0
  75. package/package.json +50 -13
  76. package/src/agent.mjs +0 -27
  77. package/src/command.mjs +0 -28
  78. package/src/index.mjs +0 -11
  79. package/src/intake.mjs +0 -23
  80. package/src/orchestrator.mjs +0 -119
  81. package/src/package-manager.mjs +0 -17
  82. package/src/policy.mjs +0 -19
  83. package/src/provider.mjs +0 -32
  84. package/src/schema.mjs +0 -24
  85. 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.0.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
- ".": "./src/index.mjs",
9
- "./package-manager": "./src/package-manager.mjs",
10
- "./intake": "./src/intake.mjs",
11
- "./command": "./src/command.mjs",
12
- "./snapshots": "./src/snapshots.mjs",
13
- "./schema": "./src/schema.mjs",
14
- "./agent": "./src/agent.mjs",
15
- "./policy": "./src/policy.mjs",
16
- "./provider": "./src/provider.mjs",
17
- "./orchestrator": "./src/orchestrator.mjs"
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
- "src",
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')))
@@ -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
- }
@@ -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))