feat: harden OpenSpec agent resilience
This commit is contained in:
@@ -1,15 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
printf 'Usage: %s <change-name>\n' "${0##*/}" >&2
|
||||
invocation_fail() {
|
||||
printf 'Planning invocation failed: %s\n' "$1" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
environment_fail() {
|
||||
printf 'Planning environment failed: %s\n' "$1" >&2
|
||||
exit 3
|
||||
}
|
||||
|
||||
artifact_fail() {
|
||||
printf 'Planning artifact-validation failed: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
invocation_fail "usage: ${0##*/} <change-name>"
|
||||
fi
|
||||
|
||||
change=$1
|
||||
if [[ ! "$change" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]]; then
|
||||
printf 'Invalid OpenSpec change name: %s\n' "$change" >&2
|
||||
exit 2
|
||||
invocation_fail "invalid OpenSpec change name: $change"
|
||||
fi
|
||||
|
||||
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)
|
||||
@@ -26,8 +39,7 @@ tasks_file="$change_dir/tasks.md"
|
||||
source "$script_dir/lib/delivery-map.sh"
|
||||
|
||||
fail() {
|
||||
printf 'Planning verification failed: %s\n' "$1" >&2
|
||||
exit 1
|
||||
artifact_fail "$1"
|
||||
}
|
||||
|
||||
require_file() {
|
||||
@@ -38,7 +50,10 @@ require_heading() {
|
||||
grep -Fqx -- "$2" "$1" || fail "${1#$workspace/} must contain heading: $2"
|
||||
}
|
||||
|
||||
bash "$script_dir/verify-openspec-version.sh" "$openspec_root/.cli-version" >/dev/null
|
||||
if ! version_output=$(bash "$script_dir/verify-openspec-version.sh" "$openspec_root/.cli-version" 2>&1); then
|
||||
environment_fail "$version_output"
|
||||
fi
|
||||
[[ -d "$change_dir" ]] || invocation_fail "change $change is absent from canonical store wealth-ledger-specs"
|
||||
require_file "$metadata_file"
|
||||
require_file "$proposal_file"
|
||||
|
||||
@@ -49,15 +64,17 @@ if ! grep -Eq '^goal: .+' "$metadata_file"; then
|
||||
fail '.openspec.yaml must contain a non-empty goal'
|
||||
fi
|
||||
|
||||
if ! (cd "$specs_root" && openspec status --change "$change" --json --store wealth-ledger-specs >/dev/null); then
|
||||
fail "change $change is not resolvable through canonical store wealth-ledger-specs"
|
||||
if ! status_output=$(cd "$specs_root" && openspec status --change "$change" --json --store wealth-ledger-specs 2>&1); then
|
||||
invocation_fail "change $change is not resolvable through canonical store wealth-ledger-specs: $status_output"
|
||||
fi
|
||||
|
||||
for heading in '## Why' '## What Changes' '## Capabilities' '### New Capabilities' '### Modified Capabilities' '## Impact'; do
|
||||
require_heading "$proposal_file" "$heading"
|
||||
done
|
||||
|
||||
temporary=$(mktemp -d)
|
||||
if ! temporary=$(mktemp -d); then
|
||||
environment_fail 'cannot create temporary planning-verification directory'
|
||||
fi
|
||||
trap 'rm -rf "$temporary"' EXIT
|
||||
declared_capabilities="$temporary/declared-capabilities.tsv"
|
||||
actual_capabilities="$temporary/actual-capabilities.txt"
|
||||
@@ -133,7 +150,10 @@ if [[ -f "$design_file" ]]; then
|
||||
for heading in '## Context' '## Goals / Non-Goals' '## Decisions' '## Delivery Map' '### Contract Gate Decision' '## Risks / Trade-offs' '## Migration Plan' '### Rollback Contract'; do
|
||||
require_heading "$design_file" "$heading"
|
||||
done
|
||||
wl_parse_delivery_map "$metadata_file" "$design_file" || exit 1
|
||||
delivery_map_error="$temporary/delivery-map-error.txt"
|
||||
if ! wl_parse_delivery_map "$metadata_file" "$design_file" 2>"$delivery_map_error"; then
|
||||
artifact_fail "$(<"$delivery_map_error")"
|
||||
fi
|
||||
|
||||
rollback_status_count=$(awk -F'|' '$2 ~ /^[[:space:]]*Status[[:space:]]*$/ { count++ } END { print count + 0 }' "$design_file")
|
||||
[[ "$rollback_status_count" -eq 1 ]] || fail 'Rollback Contract must contain exactly one Status row'
|
||||
@@ -203,6 +223,7 @@ if [[ -f "$tasks_file" ]]; then
|
||||
if [[ "$payload" == \`N/A\`\ —\ * ]]; then
|
||||
reason=${payload#\`N/A\`\ —\ }
|
||||
[[ -n "$reason" && "$reason" != '-' && "$reason" != '—' ]] || fail "task $task_number has empty N/A rationale"
|
||||
[[ "$reason" != *'<!--'* && "$reason" != *'-->'* ]] || fail "task $task_number has unresolved N/A rationale placeholder"
|
||||
covers_is_na=1
|
||||
else
|
||||
IFS=';' read -r -a references <<< "$payload"
|
||||
|
||||
Reference in New Issue
Block a user