feat: harden OpenSpec agent resilience

This commit is contained in:
2026-08-26 10:11:02 +03:00
parent 6719160c13
commit 8462a3ba75
15 changed files with 798 additions and 320 deletions
+107 -1
View File
@@ -40,12 +40,26 @@ run_verifier() {
expect_failure() {
local change=$1
local expected=$2
local expected_status=${3:-1}
local output
local status
if output=$(run_verifier "$change" 2>&1); then
set +e
output=$(run_verifier "$change" 2>&1)
status=$?
set -e
if [[ $status -eq 0 ]]; then
printf 'expected planning verification to fail for %s\n' "$change" >&2
exit 1
fi
if [[ $status -ne $expected_status ]]; then
printf 'expected exit %s for %s, got %s:\n%s\n' "$expected_status" "$change" "$status" "$output" >&2
exit 1
fi
if [[ "$output" != *'Planning artifact-validation failed:'* ]]; then
printf 'expected artifact-validation category for %s, got:\n%s\n' "$change" "$output" >&2
exit 1
fi
if [[ "$output" != *"$expected"* ]]; then
printf 'expected failure containing %q, got:\n%s\n' "$expected" "$output" >&2
exit 1
@@ -60,6 +74,33 @@ proposal_output=$(run_verifier "$proposal_change")
[[ "$proposal_output" == *'Planning stage verified: proposal'* ]]
[[ "$proposal_output" == *'Full change validation pending: specs, design, tasks'* ]]
set +e
invocation_output=$(bash "$verifier" 'INVALID CHANGE' 2>&1)
invocation_status=$?
set -e
if [[ $invocation_status -ne 2 || "$invocation_output" != *'Planning invocation failed:'* ]]; then
printf 'expected invalid-name invocation failure, got exit %s:\n%s\n' "$invocation_status" "$invocation_output" >&2
exit 1
fi
set +e
missing_change_output=$(bash "$verifier" "$prefix-missing" 2>&1)
missing_change_status=$?
set -e
if [[ $missing_change_status -ne 2 || "$missing_change_output" != *'Planning invocation failed:'* ]]; then
printf 'expected missing-change invocation failure, got exit %s:\n%s\n' "$missing_change_status" "$missing_change_output" >&2
exit 1
fi
set +e
environment_output=$(PATH=/usr/bin:/bin bash "$verifier" "$proposal_change" 2>&1)
environment_status=$?
set -e
if [[ $environment_status -ne 3 || "$environment_output" != *'Planning environment failed:'* ]]; then
printf 'expected environment failure, got exit %s:\n%s\n' "$environment_status" "$environment_output" >&2
exit 1
fi
make_change skip-conflict
skip_conflict=$fixture_change
rm -rf "$changes_root/$skip_conflict/specs"
@@ -103,6 +144,56 @@ tasks_output=$(run_verifier "$tasks_change")
[[ "$tasks_output" == *'Delta requirement coverage verified: 9/9'* ]]
[[ "$tasks_output" == *'Full planning structure verified; semantic approval remains human.'* ]]
for artifact in proposal specs design tasks; do
instructions=$(cd "$workspace/specs" && openspec instructions "$artifact" --change "$tasks_change" --store wealth-ledger-specs --json)
node -e '
const result = JSON.parse(require("node:fs").readFileSync(0, "utf8"));
if (!result.instruction.trimStart().startsWith("Critical Rules:")) process.exit(1);
' <<< "$instructions"
done
specs_instructions=$(cd "$workspace/specs" && openspec instructions specs --change "$tasks_change" --store wealth-ledger-specs --json)
node -e '
const result = JSON.parse(require("node:fs").readFileSync(0, "utf8"));
const activeOperation = /^## (ADDED|MODIFIED|REMOVED|RENAMED) Requirements$/m;
if (activeOperation.test(result.template)) process.exit(1);
for (const token of ["Purpose", "ADDED", "MODIFIED", "REMOVED", "RENAMED"]) {
if (!result.template.includes(token)) process.exit(1);
}
' <<< "$specs_instructions"
operation_change="$prefix-operation-neutral"
cp -R "$script_dir/fixtures/operation-neutral-change" "$changes_root/$operation_change"
created_changes+=("$operation_change")
operation_output=$(run_verifier "$operation_change")
if [[ "$operation_output" != *'Planning stage verified: specs'* ]]; then
printf 'expected controlled operation fixture to verify, got:\n%s\n' "$operation_output" >&2
exit 1
fi
for operation in ADDED MODIFIED REMOVED RENAMED; do
if [[ $(grep -Fxc "## $operation Requirements" "$changes_root/$operation_change/specs/workspace-artifact-hygiene/spec.md") -ne 1 ]]; then
printf 'expected exactly one controlled %s operation section\n' "$operation" >&2
exit 1
fi
done
# A completed fixture receives OpenSpec's terminal archive message instead of
# the schema's Apply instruction, so leave one task pending for this contract.
perl -0pi -e 's/^- \[[xX]\]/- [ ]/m' "$changes_root/$tasks_change/tasks.md"
apply_instructions=$(cd "$workspace/specs" && openspec instructions apply --change "$tasks_change" --store wealth-ledger-specs --json)
node -e '
const result = JSON.parse(require("node:fs").readFileSync(0, "utf8"));
for (const token of ["contextFiles", "AGENTS.md", "planned Verification", "Feedback routing", "Механическая правка", "Requirement wording"]) {
if (!result.instruction.includes(token)) process.exit(1);
}
' <<< "$apply_instructions"
design_instructions=$(cd "$workspace/specs" && openspec instructions design --change "$tasks_change" --store wealth-ledger-specs --json)
node -e '
const result = JSON.parse(require("node:fs").readFileSync(0, "utf8"));
if (!result.instruction.includes("инвалидируй downstream approvals")) process.exit(1);
' <<< "$design_instructions"
make_change invalid-area
invalid_area=$fixture_change
perl -0pi -e 's/\[specs\]/[frontend]/' "$changes_root/$invalid_area/tasks.md"
@@ -123,6 +214,21 @@ missing_metadata=$fixture_change
perl -0pi -e 's/^ - \*\*Verification:\*\*.*\n//m' "$changes_root/$missing_metadata/tasks.md"
expect_failure "$missing_metadata" 'task 1.1 must contain exactly one Verification line'
make_change unresolved-na-placeholder
unresolved_na_placeholder=$fixture_change
perl -0pi -e 's/^ - \*\*Covers:\*\*.*$/ - **Covers:** `N\/A` — <!-- unresolved reason -->/m' "$changes_root/$unresolved_na_placeholder/tasks.md"
expect_failure "$unresolved_na_placeholder" 'task 1.1 has unresolved N/A rationale placeholder'
make_change empty-na
empty_na=$fixture_change
perl -0pi -e 's/^ - \*\*Covers:\*\*.*$/ - **Covers:** `N\/A` — /m' "$changes_root/$empty_na/tasks.md"
expect_failure "$empty_na" 'task 1.1 has empty N/A rationale'
make_change separator-na
separator_na=$fixture_change
perl -0pi -e 's/^ - \*\*Covers:\*\*.*$/ - **Covers:** `N\/A` — —/m' "$changes_root/$separator_na/tasks.md"
expect_failure "$separator_na" 'task 1.1 has empty N/A rationale'
make_change malformed-rollback
malformed_rollback=$fixture_change
sed -i '' '/| Status | `N\/A` |/d' "$changes_root/$malformed_rollback/design.md"