opencode-goal-plugin 0.5.0 → 0.6.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/CHANGELOG.md +29 -4
- package/CONTRIBUTING.md +8 -7
- package/README.md +67 -39
- package/SECURITY.md +29 -1
- package/demo/README.md +86 -0
- package/demo/opencode.json +11 -0
- package/demo/package.json +10 -0
- package/demo/src/math.js +3 -0
- package/demo/test/math.test.js +11 -0
- package/docs/providers.md +84 -0
- package/index.d.ts +53 -5
- package/package.json +19 -6
- package/scripts/behavior-benchmark.mjs +272 -0
- package/scripts/packed-host-contract.mjs +160 -0
- package/scripts/verify.mjs +4 -2
- package/src/completion-claim.js +127 -0
- package/src/goal-plugin.js +1166 -304
- package/src/goal-tool-result.js +18 -0
- package/src/native-agent-config.js +73 -0
- package/src/opencode-session-api.js +110 -0
- package/src/persistence-lease.js +94 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const MAX_SUMMARY_LENGTH = 500
|
|
2
|
+
const MAX_CRITERIA = 20
|
|
3
|
+
const MAX_CHECKS = 20
|
|
4
|
+
const MAX_CHANGED_FILES = 100
|
|
5
|
+
const MAX_LIMITATIONS = 20
|
|
6
|
+
const MAX_CRITERION_LENGTH = 300
|
|
7
|
+
const MAX_ITEM_LENGTH = 500
|
|
8
|
+
const CHECK_RESULTS = new Set(["passed", "failed", "not-run"])
|
|
9
|
+
|
|
10
|
+
function cleanStringList(values, field) {
|
|
11
|
+
const cleaned = values.map((value) => (typeof value === "string" ? value.trim() : ""))
|
|
12
|
+
return cleaned.every((value) => value && value.length <= MAX_ITEM_LENGTH)
|
|
13
|
+
? { ok: true, values: cleaned }
|
|
14
|
+
: {
|
|
15
|
+
ok: false,
|
|
16
|
+
error: `${field} entries must be non-empty strings of ${MAX_ITEM_LENGTH} characters or fewer`,
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validate an untrusted completion claim and render the concise evidence text
|
|
22
|
+
* consumed by completion auditors. Validation belongs at this tool boundary so
|
|
23
|
+
* malformed claims never enter goal state or verifier prompts.
|
|
24
|
+
*/
|
|
25
|
+
export function serializeCompletionClaim(raw = {}) {
|
|
26
|
+
const claim = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {}
|
|
27
|
+
const summary = typeof claim.summary === "string" ? claim.summary.trim() : ""
|
|
28
|
+
if (!summary) return { ok: false, error: "summary must be a non-empty string" }
|
|
29
|
+
if (summary.length > MAX_SUMMARY_LENGTH) {
|
|
30
|
+
return { ok: false, error: `summary must be ${MAX_SUMMARY_LENGTH} characters or fewer` }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const criteria = claim.criteria === undefined ? [] : claim.criteria
|
|
34
|
+
const checks = claim.checks === undefined ? [] : claim.checks
|
|
35
|
+
const changedFiles = claim.changedFiles === undefined ? [] : claim.changedFiles
|
|
36
|
+
const knownLimitations = claim.knownLimitations === undefined ? [] : claim.knownLimitations
|
|
37
|
+
if (
|
|
38
|
+
!Array.isArray(criteria) ||
|
|
39
|
+
!Array.isArray(checks) ||
|
|
40
|
+
!Array.isArray(changedFiles) ||
|
|
41
|
+
!Array.isArray(knownLimitations)
|
|
42
|
+
) {
|
|
43
|
+
return {
|
|
44
|
+
ok: false,
|
|
45
|
+
error: "criteria, checks, changedFiles, and knownLimitations must be arrays when provided",
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (
|
|
49
|
+
criteria.length > MAX_CRITERIA ||
|
|
50
|
+
checks.length > MAX_CHECKS ||
|
|
51
|
+
changedFiles.length > MAX_CHANGED_FILES ||
|
|
52
|
+
knownLimitations.length > MAX_LIMITATIONS
|
|
53
|
+
) {
|
|
54
|
+
return { ok: false, error: "completion claim exceeds item limits" }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const cleanCriteria = []
|
|
58
|
+
for (const item of criteria) {
|
|
59
|
+
const criterion = typeof item?.criterion === "string" ? item.criterion.trim() : ""
|
|
60
|
+
const evidence = Array.isArray(item?.evidence)
|
|
61
|
+
? item.evidence.map((value) => (typeof value === "string" ? value.trim() : "")).filter(Boolean)
|
|
62
|
+
: []
|
|
63
|
+
if (!criterion || evidence.length === 0) {
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
error: "each criterion requires a non-empty criterion and at least one evidence item",
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (
|
|
70
|
+
criterion.length > MAX_CRITERION_LENGTH ||
|
|
71
|
+
evidence.some((value) => value.length > MAX_ITEM_LENGTH)
|
|
72
|
+
) {
|
|
73
|
+
return {
|
|
74
|
+
ok: false,
|
|
75
|
+
error: `criterion must be ${MAX_CRITERION_LENGTH} characters or fewer and evidence items ${MAX_ITEM_LENGTH} or fewer`,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
cleanCriteria.push({ criterion, evidence })
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const cleanChecks = []
|
|
82
|
+
for (const item of checks) {
|
|
83
|
+
const result = typeof item?.result === "string" ? item.result.trim() : ""
|
|
84
|
+
if (!CHECK_RESULTS.has(result)) {
|
|
85
|
+
return { ok: false, error: "each check result must be passed, failed, or not-run" }
|
|
86
|
+
}
|
|
87
|
+
if (result === "failed") return { ok: false, error: "completion cannot include a failed check" }
|
|
88
|
+
const command = typeof item?.command === "string" ? item.command.trim() : ""
|
|
89
|
+
const explanation = typeof item?.explanation === "string" ? item.explanation.trim() : ""
|
|
90
|
+
const exitCode = item?.exitCode
|
|
91
|
+
if (exitCode !== undefined && (!Number.isInteger(exitCode) || exitCode < 0)) {
|
|
92
|
+
return { ok: false, error: "check exitCode must be a non-negative integer" }
|
|
93
|
+
}
|
|
94
|
+
if (!command && !explanation) {
|
|
95
|
+
return { ok: false, error: "each check requires a command or explanation" }
|
|
96
|
+
}
|
|
97
|
+
if (command.length > MAX_ITEM_LENGTH || explanation.length > MAX_ITEM_LENGTH) {
|
|
98
|
+
return {
|
|
99
|
+
ok: false,
|
|
100
|
+
error: `check command and explanation must be ${MAX_ITEM_LENGTH} characters or fewer`,
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
cleanChecks.push({ command, result, exitCode, explanation })
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const files = cleanStringList(changedFiles, "changedFiles")
|
|
107
|
+
if (!files.ok) return files
|
|
108
|
+
const limitations = cleanStringList(knownLimitations, "knownLimitations")
|
|
109
|
+
if (!limitations.ok) return limitations
|
|
110
|
+
|
|
111
|
+
const lines = [`Summary: ${summary}`]
|
|
112
|
+
cleanCriteria.forEach(({ criterion, evidence }) => {
|
|
113
|
+
lines.push(`Criterion: ${criterion} | Evidence: ${evidence.join("; ")}`)
|
|
114
|
+
})
|
|
115
|
+
cleanChecks.forEach(({ command, result, exitCode, explanation }) => {
|
|
116
|
+
const subject = command || "manual check"
|
|
117
|
+
const details = [exitCode === undefined ? "" : `exit ${exitCode}`, explanation]
|
|
118
|
+
.filter(Boolean)
|
|
119
|
+
.join("; ")
|
|
120
|
+
lines.push(`Check: ${subject} | ${result}${details ? ` | ${details}` : ""}`)
|
|
121
|
+
})
|
|
122
|
+
if (files.values.length) lines.push(`Changed files: ${files.values.join(", ")}`)
|
|
123
|
+
if (limitations.values.length) {
|
|
124
|
+
lines.push(`Known limitations: ${limitations.values.join("; ")}`)
|
|
125
|
+
}
|
|
126
|
+
return { ok: true, evidence: lines.join("\n") }
|
|
127
|
+
}
|