shipwright-cli 3.1.0 → 3.3.0
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/.claude/agents/code-reviewer.md +2 -0
- package/.claude/agents/devops-engineer.md +2 -0
- package/.claude/agents/doc-fleet-agent.md +2 -0
- package/.claude/agents/pipeline-agent.md +2 -0
- package/.claude/agents/shell-script-specialist.md +2 -0
- package/.claude/agents/test-specialist.md +2 -0
- package/.claude/hooks/agent-crash-capture.sh +32 -0
- package/.claude/hooks/post-tool-use.sh +3 -2
- package/.claude/hooks/pre-tool-use.sh +35 -3
- package/README.md +22 -8
- package/claude-code/hooks/config-change.sh +18 -0
- package/claude-code/hooks/instructions-reloaded.sh +7 -0
- package/claude-code/hooks/worktree-create.sh +25 -0
- package/claude-code/hooks/worktree-remove.sh +20 -0
- package/config/code-constitution.json +130 -0
- package/config/defaults.json +25 -2
- package/config/policy.json +1 -1
- package/dashboard/middleware/auth.ts +134 -0
- package/dashboard/middleware/constants.ts +21 -0
- package/dashboard/public/index.html +8 -6
- package/dashboard/public/styles.css +176 -97
- package/dashboard/routes/auth.ts +38 -0
- package/dashboard/server.ts +117 -25
- package/dashboard/services/config.ts +26 -0
- package/dashboard/services/db.ts +118 -0
- package/dashboard/src/canvas/pixel-agent.ts +298 -0
- package/dashboard/src/canvas/pixel-sprites.ts +440 -0
- package/dashboard/src/canvas/shipyard-effects.ts +367 -0
- package/dashboard/src/canvas/shipyard-scene.ts +616 -0
- package/dashboard/src/canvas/submarine-layout.ts +267 -0
- package/dashboard/src/components/header.ts +8 -7
- package/dashboard/src/core/api.ts +5 -0
- package/dashboard/src/core/router.ts +1 -0
- package/dashboard/src/design/submarine-theme.ts +253 -0
- package/dashboard/src/main.ts +2 -0
- package/dashboard/src/types/api.ts +12 -1
- package/dashboard/src/views/activity.ts +2 -1
- package/dashboard/src/views/metrics.ts +69 -1
- package/dashboard/src/views/shipyard.ts +39 -0
- package/dashboard/types/index.ts +166 -0
- package/docs/plans/2026-02-28-compound-audit-and-shipyard-design.md +186 -0
- package/docs/plans/2026-02-28-skipper-shipwright-implementation-plan.md +1182 -0
- package/docs/plans/2026-02-28-skipper-shipwright-integration-design.md +531 -0
- package/docs/plans/2026-03-01-ai-powered-skill-injection-design.md +298 -0
- package/docs/plans/2026-03-01-ai-powered-skill-injection-plan.md +1109 -0
- package/docs/plans/2026-03-01-capabilities-cleanup-plan.md +658 -0
- package/docs/plans/2026-03-01-clean-architecture-plan.md +924 -0
- package/docs/plans/2026-03-01-compound-audit-cascade-design.md +191 -0
- package/docs/plans/2026-03-01-compound-audit-cascade-plan.md +921 -0
- package/docs/plans/2026-03-01-deep-integration-plan.md +851 -0
- package/docs/plans/2026-03-01-pipeline-audit-trail-design.md +145 -0
- package/docs/plans/2026-03-01-pipeline-audit-trail-plan.md +770 -0
- package/docs/plans/2026-03-01-refined-depths-brand-design.md +382 -0
- package/docs/plans/2026-03-01-refined-depths-implementation.md +599 -0
- package/docs/plans/2026-03-01-skipper-kernel-integration-design.md +203 -0
- package/docs/plans/2026-03-01-unified-platform-design.md +272 -0
- package/docs/plans/2026-03-07-claude-code-feature-integration-design.md +189 -0
- package/docs/plans/2026-03-07-claude-code-feature-integration-plan.md +1165 -0
- package/docs/research/BACKLOG_QUICK_REFERENCE.md +352 -0
- package/docs/research/CUTTING_EDGE_RESEARCH_2026.md +546 -0
- package/docs/research/RESEARCH_INDEX.md +439 -0
- package/docs/research/RESEARCH_SOURCES.md +440 -0
- package/docs/research/RESEARCH_SUMMARY.txt +275 -0
- package/docs/superpowers/specs/2026-03-10-pipeline-quality-revolution-design.md +341 -0
- package/package.json +2 -2
- package/scripts/lib/adaptive-model.sh +427 -0
- package/scripts/lib/adaptive-timeout.sh +316 -0
- package/scripts/lib/audit-trail.sh +309 -0
- package/scripts/lib/auto-recovery.sh +471 -0
- package/scripts/lib/bandit-selector.sh +431 -0
- package/scripts/lib/bootstrap.sh +104 -2
- package/scripts/lib/causal-graph.sh +455 -0
- package/scripts/lib/compat.sh +126 -0
- package/scripts/lib/compound-audit.sh +337 -0
- package/scripts/lib/constitutional.sh +454 -0
- package/scripts/lib/context-budget.sh +359 -0
- package/scripts/lib/convergence.sh +594 -0
- package/scripts/lib/cost-optimizer.sh +634 -0
- package/scripts/lib/daemon-adaptive.sh +14 -2
- package/scripts/lib/daemon-dispatch.sh +106 -17
- package/scripts/lib/daemon-failure.sh +34 -4
- package/scripts/lib/daemon-patrol.sh +25 -4
- package/scripts/lib/daemon-poll-github.sh +361 -0
- package/scripts/lib/daemon-poll-health.sh +299 -0
- package/scripts/lib/daemon-poll.sh +27 -611
- package/scripts/lib/daemon-state.sh +119 -66
- package/scripts/lib/daemon-triage.sh +10 -0
- package/scripts/lib/dod-scorecard.sh +442 -0
- package/scripts/lib/error-actionability.sh +300 -0
- package/scripts/lib/formal-spec.sh +461 -0
- package/scripts/lib/helpers.sh +180 -5
- package/scripts/lib/intent-analysis.sh +409 -0
- package/scripts/lib/loop-convergence.sh +350 -0
- package/scripts/lib/loop-iteration.sh +682 -0
- package/scripts/lib/loop-progress.sh +48 -0
- package/scripts/lib/loop-restart.sh +185 -0
- package/scripts/lib/memory-effectiveness.sh +506 -0
- package/scripts/lib/mutation-executor.sh +352 -0
- package/scripts/lib/outcome-feedback.sh +521 -0
- package/scripts/lib/pipeline-cli.sh +336 -0
- package/scripts/lib/pipeline-commands.sh +1216 -0
- package/scripts/lib/pipeline-detection.sh +101 -3
- package/scripts/lib/pipeline-execution.sh +897 -0
- package/scripts/lib/pipeline-github.sh +28 -3
- package/scripts/lib/pipeline-intelligence-compound.sh +431 -0
- package/scripts/lib/pipeline-intelligence-scoring.sh +407 -0
- package/scripts/lib/pipeline-intelligence-skip.sh +181 -0
- package/scripts/lib/pipeline-intelligence.sh +104 -1138
- package/scripts/lib/pipeline-quality-bash-compat.sh +182 -0
- package/scripts/lib/pipeline-quality-checks.sh +17 -711
- package/scripts/lib/pipeline-quality-gates.sh +563 -0
- package/scripts/lib/pipeline-stages-build.sh +730 -0
- package/scripts/lib/pipeline-stages-delivery.sh +965 -0
- package/scripts/lib/pipeline-stages-intake.sh +1133 -0
- package/scripts/lib/pipeline-stages-monitor.sh +407 -0
- package/scripts/lib/pipeline-stages-review.sh +1022 -0
- package/scripts/lib/pipeline-stages.sh +161 -2901
- package/scripts/lib/pipeline-state.sh +36 -5
- package/scripts/lib/pipeline-util.sh +487 -0
- package/scripts/lib/policy-learner.sh +438 -0
- package/scripts/lib/process-reward.sh +493 -0
- package/scripts/lib/project-detect.sh +649 -0
- package/scripts/lib/quality-profile.sh +334 -0
- package/scripts/lib/recruit-commands.sh +885 -0
- package/scripts/lib/recruit-learning.sh +739 -0
- package/scripts/lib/recruit-roles.sh +648 -0
- package/scripts/lib/reward-aggregator.sh +458 -0
- package/scripts/lib/rl-optimizer.sh +362 -0
- package/scripts/lib/root-cause.sh +427 -0
- package/scripts/lib/scope-enforcement.sh +445 -0
- package/scripts/lib/session-restart.sh +493 -0
- package/scripts/lib/skill-memory.sh +300 -0
- package/scripts/lib/skill-registry.sh +775 -0
- package/scripts/lib/spec-driven.sh +476 -0
- package/scripts/lib/test-helpers.sh +18 -7
- package/scripts/lib/test-holdout.sh +429 -0
- package/scripts/lib/test-optimizer.sh +511 -0
- package/scripts/shipwright-file-suggest.sh +45 -0
- package/scripts/skills/adversarial-quality.md +61 -0
- package/scripts/skills/api-design.md +44 -0
- package/scripts/skills/architecture-design.md +50 -0
- package/scripts/skills/brainstorming.md +43 -0
- package/scripts/skills/data-pipeline.md +44 -0
- package/scripts/skills/deploy-safety.md +64 -0
- package/scripts/skills/documentation.md +38 -0
- package/scripts/skills/frontend-design.md +45 -0
- package/scripts/skills/generated/.gitkeep +0 -0
- package/scripts/skills/generated/_refinements/.gitkeep +0 -0
- package/scripts/skills/generated/_refinements/adversarial-quality.patch.md +3 -0
- package/scripts/skills/generated/_refinements/architecture-design.patch.md +3 -0
- package/scripts/skills/generated/_refinements/brainstorming.patch.md +3 -0
- package/scripts/skills/generated/cli-version-management.md +29 -0
- package/scripts/skills/generated/collection-system-validation.md +99 -0
- package/scripts/skills/generated/large-scale-c-refactoring-coordination.md +97 -0
- package/scripts/skills/generated/pattern-matching-similarity-scoring.md +195 -0
- package/scripts/skills/generated/test-parallelization-detection.md +65 -0
- package/scripts/skills/observability.md +79 -0
- package/scripts/skills/performance.md +48 -0
- package/scripts/skills/pr-quality.md +49 -0
- package/scripts/skills/product-thinking.md +43 -0
- package/scripts/skills/security-audit.md +49 -0
- package/scripts/skills/systematic-debugging.md +40 -0
- package/scripts/skills/testing-strategy.md +47 -0
- package/scripts/skills/two-stage-review.md +52 -0
- package/scripts/skills/validation-thoroughness.md +55 -0
- package/scripts/sw +9 -3
- package/scripts/sw-activity.sh +9 -8
- package/scripts/sw-adaptive.sh +8 -7
- package/scripts/sw-adversarial.sh +2 -1
- package/scripts/sw-architecture-enforcer.sh +3 -1
- package/scripts/sw-auth.sh +12 -2
- package/scripts/sw-autonomous.sh +5 -1
- package/scripts/sw-changelog.sh +4 -1
- package/scripts/sw-checkpoint.sh +2 -1
- package/scripts/sw-ci.sh +15 -6
- package/scripts/sw-cleanup.sh +4 -26
- package/scripts/sw-code-review.sh +45 -20
- package/scripts/sw-connect.sh +2 -1
- package/scripts/sw-context.sh +2 -1
- package/scripts/sw-cost.sh +107 -5
- package/scripts/sw-daemon.sh +71 -11
- package/scripts/sw-dashboard.sh +3 -1
- package/scripts/sw-db.sh +71 -20
- package/scripts/sw-decide.sh +8 -2
- package/scripts/sw-decompose.sh +360 -17
- package/scripts/sw-deps.sh +4 -1
- package/scripts/sw-developer-simulation.sh +4 -1
- package/scripts/sw-discovery.sh +378 -5
- package/scripts/sw-doc-fleet.sh +4 -1
- package/scripts/sw-docs-agent.sh +3 -1
- package/scripts/sw-docs.sh +2 -1
- package/scripts/sw-doctor.sh +453 -2
- package/scripts/sw-dora.sh +4 -1
- package/scripts/sw-durable.sh +12 -7
- package/scripts/sw-e2e-orchestrator.sh +17 -16
- package/scripts/sw-eventbus.sh +13 -4
- package/scripts/sw-evidence.sh +364 -12
- package/scripts/sw-feedback.sh +550 -9
- package/scripts/sw-fix.sh +20 -1
- package/scripts/sw-fleet-discover.sh +6 -2
- package/scripts/sw-fleet-viz.sh +9 -4
- package/scripts/sw-fleet.sh +5 -1
- package/scripts/sw-github-app.sh +18 -4
- package/scripts/sw-github-checks.sh +3 -2
- package/scripts/sw-github-deploy.sh +3 -2
- package/scripts/sw-github-graphql.sh +18 -7
- package/scripts/sw-guild.sh +5 -1
- package/scripts/sw-heartbeat.sh +5 -30
- package/scripts/sw-hello.sh +67 -0
- package/scripts/sw-hygiene.sh +10 -3
- package/scripts/sw-incident.sh +273 -5
- package/scripts/sw-init.sh +18 -2
- package/scripts/sw-instrument.sh +10 -2
- package/scripts/sw-intelligence.sh +44 -7
- package/scripts/sw-jira.sh +5 -1
- package/scripts/sw-launchd.sh +2 -1
- package/scripts/sw-linear.sh +4 -1
- package/scripts/sw-logs.sh +4 -1
- package/scripts/sw-loop.sh +436 -1076
- package/scripts/sw-memory.sh +357 -3
- package/scripts/sw-mission-control.sh +6 -1
- package/scripts/sw-model-router.sh +483 -27
- package/scripts/sw-otel.sh +15 -4
- package/scripts/sw-oversight.sh +14 -5
- package/scripts/sw-patrol-meta.sh +334 -0
- package/scripts/sw-pipeline-composer.sh +7 -1
- package/scripts/sw-pipeline-vitals.sh +12 -6
- package/scripts/sw-pipeline.sh +54 -2653
- package/scripts/sw-pm.sh +16 -8
- package/scripts/sw-pr-lifecycle.sh +2 -1
- package/scripts/sw-predictive.sh +17 -5
- package/scripts/sw-prep.sh +185 -2
- package/scripts/sw-ps.sh +5 -25
- package/scripts/sw-public-dashboard.sh +17 -4
- package/scripts/sw-quality.sh +14 -6
- package/scripts/sw-reaper.sh +8 -25
- package/scripts/sw-recruit.sh +156 -2303
- package/scripts/sw-regression.sh +19 -12
- package/scripts/sw-release-manager.sh +3 -1
- package/scripts/sw-release.sh +4 -1
- package/scripts/sw-remote.sh +3 -1
- package/scripts/sw-replay.sh +7 -1
- package/scripts/sw-retro.sh +158 -1
- package/scripts/sw-review-rerun.sh +3 -1
- package/scripts/sw-scale.sh +14 -5
- package/scripts/sw-security-audit.sh +6 -1
- package/scripts/sw-self-optimize.sh +173 -6
- package/scripts/sw-session.sh +9 -3
- package/scripts/sw-setup.sh +3 -1
- package/scripts/sw-stall-detector.sh +406 -0
- package/scripts/sw-standup.sh +15 -7
- package/scripts/sw-status.sh +3 -1
- package/scripts/sw-strategic.sh +14 -6
- package/scripts/sw-stream.sh +13 -4
- package/scripts/sw-swarm.sh +20 -7
- package/scripts/sw-team-stages.sh +13 -6
- package/scripts/sw-templates.sh +7 -31
- package/scripts/sw-testgen.sh +17 -6
- package/scripts/sw-tmux-pipeline.sh +4 -1
- package/scripts/sw-tmux-role-color.sh +2 -0
- package/scripts/sw-tmux-status.sh +1 -1
- package/scripts/sw-tmux.sh +37 -1
- package/scripts/sw-trace.sh +3 -1
- package/scripts/sw-tracker-github.sh +3 -0
- package/scripts/sw-tracker-jira.sh +3 -0
- package/scripts/sw-tracker-linear.sh +3 -0
- package/scripts/sw-tracker.sh +3 -1
- package/scripts/sw-triage.sh +3 -2
- package/scripts/sw-upgrade.sh +3 -1
- package/scripts/sw-ux.sh +5 -2
- package/scripts/sw-webhook.sh +5 -2
- package/scripts/sw-widgets.sh +9 -4
- package/scripts/sw-worktree.sh +15 -3
- package/scripts/test-skill-injection.sh +1233 -0
- package/templates/pipelines/autonomous.json +27 -3
- package/templates/pipelines/cost-aware.json +34 -8
- package/templates/pipelines/deployed.json +12 -0
- package/templates/pipelines/enterprise.json +12 -0
- package/templates/pipelines/fast.json +6 -0
- package/templates/pipelines/full.json +27 -3
- package/templates/pipelines/hotfix.json +6 -0
- package/templates/pipelines/standard.json +12 -0
- package/templates/pipelines/tdd.json +12 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Module guard - prevent double-sourcing
|
|
3
|
+
[[ -n "${_FORMAL_SPEC_LOADED:-}" ]] && return 0
|
|
4
|
+
_FORMAL_SPEC_LOADED=1
|
|
5
|
+
|
|
6
|
+
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
|
7
|
+
# ║ shipwright formal-spec — Lightweight Formal Specification System ║
|
|
8
|
+
# ║ Extract pre/post-conditions from docstrings, verify against code, ║
|
|
9
|
+
# ║ inject spec context into pipeline prompts for provable correctness ║
|
|
10
|
+
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
|
11
|
+
|
|
12
|
+
# shellcheck disable=SC2034
|
|
13
|
+
VERSION="3.3.0"
|
|
14
|
+
|
|
15
|
+
# ─── Output Helpers ──────────────────────────────────────────────────────────
|
|
16
|
+
[[ "$(type -t info 2>/dev/null)" == "function" ]] || info() { echo -e "\033[38;2;0;212;255m\033[1m▸\033[0m $*"; }
|
|
17
|
+
[[ "$(type -t success 2>/dev/null)" == "function" ]] || success() { echo -e "\033[38;2;74;222;128m\033[1m✓\033[0m $*"; }
|
|
18
|
+
[[ "$(type -t warn 2>/dev/null)" == "function" ]] || warn() { echo -e "\033[38;2;250;204;21m\033[1m⚠\033[0m $*"; }
|
|
19
|
+
[[ "$(type -t error 2>/dev/null)" == "function" ]] || error() { echo -e "\033[38;2;248;113;113m\033[1m✗\033[0m $*" >&2; }
|
|
20
|
+
if [[ "$(type -t now_iso 2>/dev/null)" != "function" ]]; then
|
|
21
|
+
now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
|
|
22
|
+
now_epoch() { date +%s; }
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
# ─── Configuration ───────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
FORMAL_SPECS_FILE="${FORMAL_SPECS_FILE:-.claude/formal-specs.json}"
|
|
28
|
+
FORMAL_SPEC_REPORT="${FORMAL_SPEC_REPORT:-.claude/pipeline-artifacts/formal-spec-report.json}"
|
|
29
|
+
|
|
30
|
+
# ─── Extract Specs ───────────────────────────────────────────────────────────
|
|
31
|
+
# Extract pre/post-conditions and invariants from code comments/docstrings.
|
|
32
|
+
# Input: $1 = file or directory to scan
|
|
33
|
+
# Output: JSON specs written to $FORMAL_SPECS_FILE, path echoed
|
|
34
|
+
|
|
35
|
+
formal_spec_extract() {
|
|
36
|
+
local target="${1:-.}"
|
|
37
|
+
local output_file="${2:-$FORMAL_SPECS_FILE}"
|
|
38
|
+
local tmp_file
|
|
39
|
+
tmp_file=$(mktemp 2>/dev/null || echo "${TMPDIR:-/tmp}/formal-spec-extract.$$.tmp")
|
|
40
|
+
|
|
41
|
+
# Ensure output directory exists
|
|
42
|
+
local out_dir
|
|
43
|
+
out_dir=$(dirname "$output_file")
|
|
44
|
+
mkdir -p "$out_dir" 2>/dev/null || true
|
|
45
|
+
|
|
46
|
+
echo '{"specs":[],"extracted_at":"'"$(now_iso)"'"}' > "$tmp_file"
|
|
47
|
+
|
|
48
|
+
local file_list
|
|
49
|
+
if [[ -d "$target" ]]; then
|
|
50
|
+
file_list=$(find "$target" -type f \( -name "*.js" -o -name "*.ts" -o -name "*.py" -o -name "*.sh" -o -name "*.go" -o -name "*.java" \) 2>/dev/null | head -200)
|
|
51
|
+
elif [[ -f "$target" ]]; then
|
|
52
|
+
file_list="$target"
|
|
53
|
+
else
|
|
54
|
+
echo "$output_file"
|
|
55
|
+
return 0
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
local specs_json='[]'
|
|
59
|
+
|
|
60
|
+
while IFS= read -r file; do
|
|
61
|
+
[[ -z "$file" || ! -f "$file" ]] && continue
|
|
62
|
+
|
|
63
|
+
local preconditions="" postconditions="" invariants=""
|
|
64
|
+
local func_name=""
|
|
65
|
+
|
|
66
|
+
# Extract JSDoc @precondition, @postcondition, @invariant tags
|
|
67
|
+
preconditions=$(grep -n '@precondition' "$file" 2>/dev/null || true)
|
|
68
|
+
postconditions=$(grep -n '@postcondition' "$file" 2>/dev/null || true)
|
|
69
|
+
invariants=$(grep -n '@invariant' "$file" 2>/dev/null || true)
|
|
70
|
+
|
|
71
|
+
# Extract Python docstring Precondition:, Postcondition:, Invariant: sections
|
|
72
|
+
if [[ -z "$preconditions" ]]; then
|
|
73
|
+
preconditions=$(grep -n 'Precondition:' "$file" 2>/dev/null || true)
|
|
74
|
+
fi
|
|
75
|
+
if [[ -z "$postconditions" ]]; then
|
|
76
|
+
postconditions=$(grep -n 'Postcondition:' "$file" 2>/dev/null || true)
|
|
77
|
+
fi
|
|
78
|
+
if [[ -z "$invariants" ]]; then
|
|
79
|
+
invariants=$(grep -n 'Invariant:' "$file" 2>/dev/null || true)
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
# Skip files with no specs
|
|
83
|
+
if [[ -z "$preconditions" && -z "$postconditions" && -z "$invariants" ]]; then
|
|
84
|
+
continue
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
# Build spec entries for this file
|
|
88
|
+
local line_num condition spec_type
|
|
89
|
+
|
|
90
|
+
# Process preconditions
|
|
91
|
+
while IFS= read -r line; do
|
|
92
|
+
[[ -z "$line" ]] && continue
|
|
93
|
+
line_num=$(echo "$line" | cut -d: -f1)
|
|
94
|
+
condition=$(echo "$line" | sed 's/^[0-9]*://' | sed 's/.*@precondition[[:space:]]*//' | sed 's/.*Precondition:[[:space:]]*//' | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*\*\///')
|
|
95
|
+
|
|
96
|
+
# Find nearest function name (look ahead up to 10 lines)
|
|
97
|
+
func_name=$(_find_function_name "$file" "$line_num")
|
|
98
|
+
|
|
99
|
+
specs_json=$(echo "$specs_json" | jq --arg file "$file" --arg fn "$func_name" \
|
|
100
|
+
--arg cond "$condition" --argjson ln "$line_num" --arg type "precondition" \
|
|
101
|
+
'. + [{"file":$file,"function":$fn,"type":$type,"condition":$cond,"line":$ln}]' 2>/dev/null || echo "$specs_json")
|
|
102
|
+
done <<< "$preconditions"
|
|
103
|
+
|
|
104
|
+
# Process postconditions
|
|
105
|
+
while IFS= read -r line; do
|
|
106
|
+
[[ -z "$line" ]] && continue
|
|
107
|
+
line_num=$(echo "$line" | cut -d: -f1)
|
|
108
|
+
condition=$(echo "$line" | sed 's/^[0-9]*://' | sed 's/.*@postcondition[[:space:]]*//' | sed 's/.*Postcondition:[[:space:]]*//' | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*\*\///')
|
|
109
|
+
|
|
110
|
+
func_name=$(_find_function_name "$file" "$line_num")
|
|
111
|
+
|
|
112
|
+
specs_json=$(echo "$specs_json" | jq --arg file "$file" --arg fn "$func_name" \
|
|
113
|
+
--arg cond "$condition" --argjson ln "$line_num" --arg type "postcondition" \
|
|
114
|
+
'. + [{"file":$file,"function":$fn,"type":$type,"condition":$cond,"line":$ln}]' 2>/dev/null || echo "$specs_json")
|
|
115
|
+
done <<< "$postconditions"
|
|
116
|
+
|
|
117
|
+
# Process invariants
|
|
118
|
+
while IFS= read -r line; do
|
|
119
|
+
[[ -z "$line" ]] && continue
|
|
120
|
+
line_num=$(echo "$line" | cut -d: -f1)
|
|
121
|
+
condition=$(echo "$line" | sed 's/^[0-9]*://' | sed 's/.*@invariant[[:space:]]*//' | sed 's/.*Invariant:[[:space:]]*//' | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*\*\///')
|
|
122
|
+
|
|
123
|
+
func_name=$(_find_function_name "$file" "$line_num")
|
|
124
|
+
|
|
125
|
+
specs_json=$(echo "$specs_json" | jq --arg file "$file" --arg fn "$func_name" \
|
|
126
|
+
--arg cond "$condition" --argjson ln "$line_num" --arg type "invariant" \
|
|
127
|
+
'. + [{"file":$file,"function":$fn,"type":$type,"condition":$cond,"line":$ln}]' 2>/dev/null || echo "$specs_json")
|
|
128
|
+
done <<< "$invariants"
|
|
129
|
+
|
|
130
|
+
done <<< "$file_list"
|
|
131
|
+
|
|
132
|
+
# Write final output
|
|
133
|
+
jq -n --argjson specs "$specs_json" --arg ts "$(now_iso)" \
|
|
134
|
+
'{"specs":$specs,"extracted_at":$ts,"count":($specs|length)}' > "$output_file" 2>/dev/null || {
|
|
135
|
+
echo '{"specs":[],"extracted_at":"'"$(now_iso)"'","count":0}' > "$output_file"
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
rm -f "$tmp_file" 2>/dev/null || true
|
|
139
|
+
|
|
140
|
+
if [[ "$(type -t emit_event 2>/dev/null)" == "function" ]]; then
|
|
141
|
+
local spec_count
|
|
142
|
+
spec_count=$(jq -r '.count // 0' "$output_file" 2>/dev/null || echo "0")
|
|
143
|
+
emit_event "formal_spec.extracted" "count=$spec_count" "target=$target" 2>/dev/null || true
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
echo "$output_file"
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
# Helper: find nearest function name from a line number
|
|
150
|
+
_find_function_name() {
|
|
151
|
+
local file="$1"
|
|
152
|
+
local line_num="$2"
|
|
153
|
+
local search_end=$((line_num + 15))
|
|
154
|
+
local fn_name="unknown"
|
|
155
|
+
|
|
156
|
+
# Look ahead for function declaration
|
|
157
|
+
local snippet
|
|
158
|
+
snippet=$(sed -n "${line_num},${search_end}p" "$file" 2>/dev/null || true)
|
|
159
|
+
|
|
160
|
+
# JS/TS: function name() or const name = or name() {
|
|
161
|
+
local match
|
|
162
|
+
match=$(echo "$snippet" | grep -oE '(function\s+[a-zA-Z_][a-zA-Z0-9_]*|const\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=|[a-zA-Z_][a-zA-Z0-9_]*\s*\()' | head -1 || true)
|
|
163
|
+
if [[ -n "$match" ]]; then
|
|
164
|
+
fn_name=$(echo "$match" | sed 's/function[[:space:]]*//' | sed 's/const[[:space:]]*//' | sed 's/[[:space:]]*=.*//' | sed 's/[[:space:]]*($//')
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
# Python: def name(
|
|
168
|
+
if [[ "$fn_name" == "unknown" ]]; then
|
|
169
|
+
match=$(echo "$snippet" | grep -oE 'def\s+[a-zA-Z_][a-zA-Z0-9_]*' | head -1 || true)
|
|
170
|
+
if [[ -n "$match" ]]; then
|
|
171
|
+
fn_name=$(echo "$match" | sed 's/def[[:space:]]*//')
|
|
172
|
+
fi
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
# Bash: name() {
|
|
176
|
+
if [[ "$fn_name" == "unknown" ]]; then
|
|
177
|
+
match=$(echo "$snippet" | grep -oE '^[a-zA-Z_][a-zA-Z0-9_]*\s*\(\)' | head -1 || true)
|
|
178
|
+
if [[ -n "$match" ]]; then
|
|
179
|
+
fn_name=$(echo "$match" | sed 's/[[:space:]]*()$//')
|
|
180
|
+
fi
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
echo "$fn_name"
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
# ─── Verify Specs ────────────────────────────────────────────────────────────
|
|
187
|
+
# Verify extracted specs against code behavior via grep/pattern matching.
|
|
188
|
+
# Input: $1 = specs file (JSON), $2 = project root
|
|
189
|
+
# Output: compliance report JSON, path echoed
|
|
190
|
+
|
|
191
|
+
formal_spec_verify() {
|
|
192
|
+
local specs_file="${1:-$FORMAL_SPECS_FILE}"
|
|
193
|
+
local project_root="${2:-.}"
|
|
194
|
+
local report_file="${3:-$FORMAL_SPEC_REPORT}"
|
|
195
|
+
|
|
196
|
+
if [[ ! -f "$specs_file" ]]; then
|
|
197
|
+
warn "No specs file found at $specs_file"
|
|
198
|
+
echo "$report_file"
|
|
199
|
+
return 0
|
|
200
|
+
fi
|
|
201
|
+
|
|
202
|
+
local out_dir
|
|
203
|
+
out_dir=$(dirname "$report_file")
|
|
204
|
+
mkdir -p "$out_dir" 2>/dev/null || true
|
|
205
|
+
|
|
206
|
+
local total=0 verified=0 violations=0 unchecked=0
|
|
207
|
+
local violations_json='[]'
|
|
208
|
+
|
|
209
|
+
local spec_count
|
|
210
|
+
spec_count=$(jq -r '.count // 0' "$specs_file" 2>/dev/null || echo "0")
|
|
211
|
+
|
|
212
|
+
if [[ "$spec_count" -eq 0 ]]; then
|
|
213
|
+
jq -n '{"total":0,"verified":0,"violations":0,"unchecked":0,"compliance_pct":100,"details":[],"verified_at":"'"$(now_iso)"'"}' > "$report_file" 2>/dev/null
|
|
214
|
+
echo "$report_file"
|
|
215
|
+
return 0
|
|
216
|
+
fi
|
|
217
|
+
|
|
218
|
+
# Process each spec
|
|
219
|
+
local i=0
|
|
220
|
+
while [[ "$i" -lt "$spec_count" ]]; do
|
|
221
|
+
total=$((total + 1))
|
|
222
|
+
local spec_file spec_fn spec_type spec_cond
|
|
223
|
+
spec_file=$(jq -r ".specs[$i].file // \"\"" "$specs_file" 2>/dev/null || true)
|
|
224
|
+
spec_fn=$(jq -r ".specs[$i].function // \"\"" "$specs_file" 2>/dev/null || true)
|
|
225
|
+
spec_type=$(jq -r ".specs[$i].type // \"\"" "$specs_file" 2>/dev/null || true)
|
|
226
|
+
spec_cond=$(jq -r ".specs[$i].condition // \"\"" "$specs_file" 2>/dev/null || true)
|
|
227
|
+
|
|
228
|
+
if [[ ! -f "$spec_file" ]]; then
|
|
229
|
+
unchecked=$((unchecked + 1))
|
|
230
|
+
i=$((i + 1))
|
|
231
|
+
continue
|
|
232
|
+
fi
|
|
233
|
+
|
|
234
|
+
local status="unchecked"
|
|
235
|
+
|
|
236
|
+
case "$spec_type" in
|
|
237
|
+
precondition)
|
|
238
|
+
# Check if function validates the precondition
|
|
239
|
+
# Look for validation patterns: if (!param), assert, throw, guard clauses
|
|
240
|
+
if _check_precondition "$spec_file" "$spec_fn" "$spec_cond"; then
|
|
241
|
+
status="verified"
|
|
242
|
+
verified=$((verified + 1))
|
|
243
|
+
else
|
|
244
|
+
status="violation"
|
|
245
|
+
violations=$((violations + 1))
|
|
246
|
+
violations_json=$(echo "$violations_json" | jq --arg file "$spec_file" --arg fn "$spec_fn" \
|
|
247
|
+
--arg type "$spec_type" --arg cond "$spec_cond" \
|
|
248
|
+
'. + [{"file":$file,"function":$fn,"type":$type,"condition":$cond,"status":"missing_validation"}]' 2>/dev/null || echo "$violations_json")
|
|
249
|
+
fi
|
|
250
|
+
;;
|
|
251
|
+
postcondition)
|
|
252
|
+
# Check if function has return value matching expected pattern
|
|
253
|
+
if _check_postcondition "$spec_file" "$spec_fn" "$spec_cond"; then
|
|
254
|
+
status="verified"
|
|
255
|
+
verified=$((verified + 1))
|
|
256
|
+
else
|
|
257
|
+
status="violation"
|
|
258
|
+
violations=$((violations + 1))
|
|
259
|
+
violations_json=$(echo "$violations_json" | jq --arg file "$spec_file" --arg fn "$spec_fn" \
|
|
260
|
+
--arg type "$spec_type" --arg cond "$spec_cond" \
|
|
261
|
+
'. + [{"file":$file,"function":$fn,"type":$type,"condition":$cond,"status":"missing_guarantee"}]' 2>/dev/null || echo "$violations_json")
|
|
262
|
+
fi
|
|
263
|
+
;;
|
|
264
|
+
invariant)
|
|
265
|
+
# Check for invariant violations in the code
|
|
266
|
+
if _check_invariant "$spec_file" "$spec_fn" "$spec_cond"; then
|
|
267
|
+
status="verified"
|
|
268
|
+
verified=$((verified + 1))
|
|
269
|
+
else
|
|
270
|
+
status="violation"
|
|
271
|
+
violations=$((violations + 1))
|
|
272
|
+
violations_json=$(echo "$violations_json" | jq --arg file "$spec_file" --arg fn "$spec_fn" \
|
|
273
|
+
--arg type "$spec_type" --arg cond "$spec_cond" \
|
|
274
|
+
'. + [{"file":$file,"function":$fn,"type":$type,"condition":$cond,"status":"invariant_broken"}]' 2>/dev/null || echo "$violations_json")
|
|
275
|
+
fi
|
|
276
|
+
;;
|
|
277
|
+
*)
|
|
278
|
+
unchecked=$((unchecked + 1))
|
|
279
|
+
;;
|
|
280
|
+
esac
|
|
281
|
+
|
|
282
|
+
i=$((i + 1))
|
|
283
|
+
done
|
|
284
|
+
|
|
285
|
+
# Calculate compliance percentage
|
|
286
|
+
local compliance_pct=100
|
|
287
|
+
if [[ "$total" -gt 0 ]]; then
|
|
288
|
+
compliance_pct=$(( (verified * 100) / total ))
|
|
289
|
+
fi
|
|
290
|
+
|
|
291
|
+
# Write report
|
|
292
|
+
jq -n --argjson total "$total" --argjson verified "$verified" \
|
|
293
|
+
--argjson violations "$violations" --argjson unchecked "$unchecked" \
|
|
294
|
+
--argjson pct "$compliance_pct" --argjson details "$violations_json" \
|
|
295
|
+
--arg ts "$(now_iso)" \
|
|
296
|
+
'{"total":$total,"verified":$verified,"violations":$violations,"unchecked":$unchecked,"compliance_pct":$pct,"details":$details,"verified_at":$ts}' \
|
|
297
|
+
> "$report_file" 2>/dev/null
|
|
298
|
+
|
|
299
|
+
if [[ "$(type -t emit_event 2>/dev/null)" == "function" ]]; then
|
|
300
|
+
emit_event "formal_spec.verified" \
|
|
301
|
+
"total=$total" "verified=$verified" "violations=$violations" \
|
|
302
|
+
"compliance_pct=$compliance_pct" 2>/dev/null || true
|
|
303
|
+
fi
|
|
304
|
+
|
|
305
|
+
echo "$report_file"
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
# ─── Verification Helpers ────────────────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
_check_precondition() {
|
|
311
|
+
local file="$1" fn="$2" cond="$3"
|
|
312
|
+
|
|
313
|
+
# Extract keywords from condition to search for validation
|
|
314
|
+
local keywords
|
|
315
|
+
keywords=$(echo "$cond" | grep -oE '[a-zA-Z_][a-zA-Z0-9_]*' | head -5 || true)
|
|
316
|
+
|
|
317
|
+
local fn_body
|
|
318
|
+
fn_body=$(_extract_function_body "$file" "$fn")
|
|
319
|
+
[[ -z "$fn_body" ]] && return 1
|
|
320
|
+
|
|
321
|
+
# Look for validation patterns: if, assert, throw, guard, check, validate
|
|
322
|
+
local has_validation=false
|
|
323
|
+
while IFS= read -r kw; do
|
|
324
|
+
[[ -z "$kw" ]] && continue
|
|
325
|
+
if echo "$fn_body" | grep -qE "(if.*${kw}|assert.*${kw}|throw.*${kw}|check.*${kw}|validate.*${kw}|guard.*${kw}|${kw}.*!=.*null|${kw}.*!==.*undefined)" 2>/dev/null; then
|
|
326
|
+
has_validation=true
|
|
327
|
+
break
|
|
328
|
+
fi
|
|
329
|
+
done <<< "$keywords"
|
|
330
|
+
|
|
331
|
+
[[ "$has_validation" == "true" ]]
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
_check_postcondition() {
|
|
335
|
+
local file="$1" fn="$2" cond="$3"
|
|
336
|
+
|
|
337
|
+
local fn_body
|
|
338
|
+
fn_body=$(_extract_function_body "$file" "$fn")
|
|
339
|
+
[[ -z "$fn_body" ]] && return 1
|
|
340
|
+
|
|
341
|
+
# Check that function has a return statement
|
|
342
|
+
if echo "$fn_body" | grep -qE '(return |echo |print\(|yield )' 2>/dev/null; then
|
|
343
|
+
return 0
|
|
344
|
+
fi
|
|
345
|
+
|
|
346
|
+
return 1
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
_check_invariant() {
|
|
350
|
+
local file="$1" fn="$2" cond="$3"
|
|
351
|
+
|
|
352
|
+
# Extract the invariant pattern (e.g., "counter >= 0" -> look for "counter < 0")
|
|
353
|
+
local negated
|
|
354
|
+
negated=$(_negate_condition "$cond")
|
|
355
|
+
|
|
356
|
+
if [[ -n "$negated" ]]; then
|
|
357
|
+
# If we find the negation in the code, invariant is broken
|
|
358
|
+
if grep -qE "$negated" "$file" 2>/dev/null; then
|
|
359
|
+
return 1
|
|
360
|
+
fi
|
|
361
|
+
fi
|
|
362
|
+
|
|
363
|
+
# No violation found = invariant holds
|
|
364
|
+
return 0
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
_negate_condition() {
|
|
368
|
+
local cond="$1"
|
|
369
|
+
|
|
370
|
+
# Simple negation patterns
|
|
371
|
+
if echo "$cond" | grep -qE '>= 0|>= zero|non-negative|not negative' 2>/dev/null; then
|
|
372
|
+
local var
|
|
373
|
+
var=$(echo "$cond" | grep -oE '[a-zA-Z_][a-zA-Z0-9_]*' | head -1 || true)
|
|
374
|
+
[[ -n "$var" ]] && echo "${var}[[:space:]]*<[[:space:]]*0" && return 0
|
|
375
|
+
fi
|
|
376
|
+
|
|
377
|
+
if echo "$cond" | grep -qE 'not null|non-null|!= null|!== null' 2>/dev/null; then
|
|
378
|
+
local var
|
|
379
|
+
var=$(echo "$cond" | grep -oE '[a-zA-Z_][a-zA-Z0-9_]*' | head -1 || true)
|
|
380
|
+
[[ -n "$var" ]] && echo "${var}[[:space:]]*=[[:space:]]*null" && return 0
|
|
381
|
+
fi
|
|
382
|
+
|
|
383
|
+
if echo "$cond" | grep -qE 'not empty|non-empty' 2>/dev/null; then
|
|
384
|
+
local var
|
|
385
|
+
var=$(echo "$cond" | grep -oE '[a-zA-Z_][a-zA-Z0-9_]*' | head -1 || true)
|
|
386
|
+
[[ -n "$var" ]] && echo "${var}[[:space:]]*=[[:space:]]*[\"']{2}" && return 0
|
|
387
|
+
fi
|
|
388
|
+
|
|
389
|
+
echo ""
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
_extract_function_body() {
|
|
393
|
+
local file="$1" fn="$2"
|
|
394
|
+
|
|
395
|
+
[[ -z "$fn" || "$fn" == "unknown" ]] && return 0
|
|
396
|
+
|
|
397
|
+
# Find the function start line and extract ~50 lines
|
|
398
|
+
local start_line
|
|
399
|
+
start_line=$(grep -n -E "(function[[:space:]]+${fn}|${fn}[[:space:]]*\(|def[[:space:]]+${fn}|${fn}[[:space:]]*\(\))" "$file" 2>/dev/null | head -1 | cut -d: -f1 || true)
|
|
400
|
+
|
|
401
|
+
if [[ -n "$start_line" ]]; then
|
|
402
|
+
local end_line=$((start_line + 50))
|
|
403
|
+
sed -n "${start_line},${end_line}p" "$file" 2>/dev/null || true
|
|
404
|
+
fi
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
# ─── Inject Specs ────────────────────────────────────────────────────────────
|
|
408
|
+
# Add formal spec context to pipeline prompts.
|
|
409
|
+
# Input: $1 = specs file, $2 = changed files (newline-separated)
|
|
410
|
+
# Output: prompt text string
|
|
411
|
+
|
|
412
|
+
formal_spec_inject() {
|
|
413
|
+
local specs_file="${1:-$FORMAL_SPECS_FILE}"
|
|
414
|
+
local changed_files="${2:-}"
|
|
415
|
+
|
|
416
|
+
if [[ ! -f "$specs_file" ]]; then
|
|
417
|
+
return 0
|
|
418
|
+
fi
|
|
419
|
+
|
|
420
|
+
local spec_count
|
|
421
|
+
spec_count=$(jq -r '.count // 0' "$specs_file" 2>/dev/null || echo "0")
|
|
422
|
+
[[ "$spec_count" -eq 0 ]] && return 0
|
|
423
|
+
|
|
424
|
+
local prompt_text=""
|
|
425
|
+
prompt_text+="## Formal Specifications"$'\n'
|
|
426
|
+
prompt_text+="The following functions have formal specifications. Ensure your changes maintain these contracts:"$'\n'$'\n'
|
|
427
|
+
|
|
428
|
+
local relevant_specs='[]'
|
|
429
|
+
|
|
430
|
+
if [[ -n "$changed_files" ]]; then
|
|
431
|
+
# Filter specs to only changed files
|
|
432
|
+
while IFS= read -r cf; do
|
|
433
|
+
[[ -z "$cf" ]] && continue
|
|
434
|
+
local file_specs
|
|
435
|
+
file_specs=$(jq --arg f "$cf" '[.specs[] | select(.file == $f)]' "$specs_file" 2>/dev/null || echo "[]")
|
|
436
|
+
if [[ "$file_specs" != "[]" ]]; then
|
|
437
|
+
relevant_specs=$(echo "$relevant_specs" "$file_specs" | jq -s 'add' 2>/dev/null || echo "$relevant_specs")
|
|
438
|
+
fi
|
|
439
|
+
done <<< "$changed_files"
|
|
440
|
+
else
|
|
441
|
+
relevant_specs=$(jq '.specs' "$specs_file" 2>/dev/null || echo "[]")
|
|
442
|
+
fi
|
|
443
|
+
|
|
444
|
+
local rel_count
|
|
445
|
+
rel_count=$(echo "$relevant_specs" | jq 'length' 2>/dev/null || echo "0")
|
|
446
|
+
[[ "$rel_count" -eq 0 ]] && return 0
|
|
447
|
+
|
|
448
|
+
local j=0
|
|
449
|
+
while [[ "$j" -lt "$rel_count" ]]; do
|
|
450
|
+
local s_file s_fn s_type s_cond
|
|
451
|
+
s_file=$(echo "$relevant_specs" | jq -r ".[$j].file // \"\"" 2>/dev/null || true)
|
|
452
|
+
s_fn=$(echo "$relevant_specs" | jq -r ".[$j].function // \"\"" 2>/dev/null || true)
|
|
453
|
+
s_type=$(echo "$relevant_specs" | jq -r ".[$j].type // \"\"" 2>/dev/null || true)
|
|
454
|
+
s_cond=$(echo "$relevant_specs" | jq -r ".[$j].condition // \"\"" 2>/dev/null || true)
|
|
455
|
+
|
|
456
|
+
prompt_text+="- **${s_file}::${s_fn}** — ${s_type}: ${s_cond}"$'\n'
|
|
457
|
+
j=$((j + 1))
|
|
458
|
+
done
|
|
459
|
+
|
|
460
|
+
echo "$prompt_text"
|
|
461
|
+
}
|
package/scripts/lib/helpers.sh
CHANGED
|
@@ -83,16 +83,24 @@ emit_event() {
|
|
|
83
83
|
json_fields="${json_fields},\"${key}\":\"${val}\""
|
|
84
84
|
fi
|
|
85
85
|
done
|
|
86
|
-
mkdir -p "${HOME}/.shipwright"
|
|
86
|
+
mkdir -p "${HOME}/.shipwright" 2>/dev/null || true
|
|
87
87
|
local _event_line="{\"ts\":\"$(now_iso)\",\"ts_epoch\":$(now_epoch),\"type\":\"${event_type}\"${json_fields}}"
|
|
88
88
|
# Use flock to prevent concurrent write corruption
|
|
89
|
-
|
|
89
|
+
# Lock file uses TMPDIR as fallback so sandbox-restricted envs don't block event logging
|
|
90
|
+
local _lock_file
|
|
91
|
+
if [[ -w "$(dirname "${EVENTS_FILE}")" ]] 2>/dev/null; then
|
|
92
|
+
_lock_file="${EVENTS_FILE}.lock"
|
|
93
|
+
else
|
|
94
|
+
_lock_file="${TMPDIR:-/tmp}/sw-events-$$.lock"
|
|
95
|
+
fi
|
|
90
96
|
(
|
|
91
97
|
if command -v flock >/dev/null 2>&1; then
|
|
92
|
-
flock -w 2 200 2>/dev/null
|
|
98
|
+
if ! flock -w 2 200 2>/dev/null; then
|
|
99
|
+
echo "WARN: emit_event lock timeout — concurrent write possible" >&2
|
|
100
|
+
fi
|
|
93
101
|
fi
|
|
94
102
|
echo "$_event_line" >> "$EVENTS_FILE"
|
|
95
|
-
) 200>"$_lock_file"
|
|
103
|
+
) 200>"$_lock_file" 2>/dev/null || true
|
|
96
104
|
|
|
97
105
|
# Schema validation — auto-detect config repo from BASH_SOURCE location
|
|
98
106
|
local _schema_dir="${_CONFIG_REPO_DIR:-}"
|
|
@@ -175,11 +183,160 @@ rotate_jsonl() {
|
|
|
175
183
|
current_lines=$(wc -l < "$file" 2>/dev/null | tr -d ' ')
|
|
176
184
|
if [[ "$current_lines" -gt "$max_lines" ]]; then
|
|
177
185
|
local tmp_rotate
|
|
178
|
-
tmp_rotate=$(mktemp)
|
|
186
|
+
tmp_rotate=$(mktemp "${TMPDIR:-/tmp}/sw-rotate.XXXXXX") || return 0
|
|
179
187
|
tail -n "$max_lines" "$file" > "$tmp_rotate" && mv "$tmp_rotate" "$file" || rm -f "$tmp_rotate"
|
|
180
188
|
fi
|
|
181
189
|
}
|
|
182
190
|
|
|
191
|
+
# ─── Atomic Write Helpers ────────────────────────────────────────
|
|
192
|
+
# atomic_write: Write data to a file atomically (write to tmp, validate, mv)
|
|
193
|
+
# Usage: atomic_write <target_file> <data>
|
|
194
|
+
atomic_write() {
|
|
195
|
+
local target="$1"
|
|
196
|
+
local data="$2"
|
|
197
|
+
|
|
198
|
+
[[ -z "$target" ]] && { error "atomic_write: target file not specified"; return 1; }
|
|
199
|
+
|
|
200
|
+
local tmp
|
|
201
|
+
tmp=$(mktemp "${target}.tmp.XXXXXX") || return 1
|
|
202
|
+
|
|
203
|
+
# Write to tmp file
|
|
204
|
+
echo -n "$data" > "$tmp" || { rm -f "$tmp"; return 1; }
|
|
205
|
+
|
|
206
|
+
# Atomically move into place
|
|
207
|
+
mv "$tmp" "$target" || { rm -f "$tmp"; return 1; }
|
|
208
|
+
|
|
209
|
+
return 0
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
# atomic_append: Append a line to a JSONL file atomically
|
|
213
|
+
# Usage: atomic_append <target_file> <json_line>
|
|
214
|
+
# Thread-safe via flock; validates line before appending
|
|
215
|
+
atomic_append() {
|
|
216
|
+
local target="$1"
|
|
217
|
+
local line="$2"
|
|
218
|
+
|
|
219
|
+
[[ -z "$target" ]] && { error "atomic_append: target file not specified"; return 1; }
|
|
220
|
+
[[ -z "$line" ]] && { error "atomic_append: line not specified"; return 1; }
|
|
221
|
+
|
|
222
|
+
# Validate JSON line
|
|
223
|
+
if ! echo "$line" | jq -e . >/dev/null 2>&1; then
|
|
224
|
+
error "atomic_append: invalid JSON: $line"
|
|
225
|
+
return 1
|
|
226
|
+
fi
|
|
227
|
+
|
|
228
|
+
local tmp lock_file
|
|
229
|
+
tmp=$(mktemp "${target}.tmp.XXXXXX") || return 1
|
|
230
|
+
lock_file="${target}.lock"
|
|
231
|
+
|
|
232
|
+
(
|
|
233
|
+
# Acquire exclusive lock with 5s timeout
|
|
234
|
+
if ! flock -w 5 200 2>/dev/null; then
|
|
235
|
+
error "atomic_append: failed to acquire lock on $target"
|
|
236
|
+
return 1
|
|
237
|
+
fi
|
|
238
|
+
|
|
239
|
+
# Append to tmp file
|
|
240
|
+
echo "$line" > "$tmp" || { rm -f "$tmp"; return 1; }
|
|
241
|
+
|
|
242
|
+
# Append tmp to target (atomic cat)
|
|
243
|
+
cat "$tmp" >> "$target" 2>/dev/null || { rm -f "$tmp"; return 1; }
|
|
244
|
+
|
|
245
|
+
rm -f "$tmp"
|
|
246
|
+
return 0
|
|
247
|
+
) 200>"$lock_file"
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
# ─── Tmpfile Tracking & Cleanup ──────────────────────────────────
|
|
251
|
+
# Registers a temp file for automatic cleanup on exit
|
|
252
|
+
# Usage: register_tmpfile <tmpfile_path>
|
|
253
|
+
# Set up trap handler: trap '_cleanup_tmpfiles' EXIT
|
|
254
|
+
_REGISTERED_TMPFILES=()
|
|
255
|
+
|
|
256
|
+
register_tmpfile() {
|
|
257
|
+
local tmpfile="$1"
|
|
258
|
+
[[ -z "$tmpfile" ]] && { error "register_tmpfile: path not specified"; return 1; }
|
|
259
|
+
_REGISTERED_TMPFILES+=("$tmpfile")
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
# Cleanup all registered temp files
|
|
263
|
+
_cleanup_tmpfiles() {
|
|
264
|
+
for f in "${_REGISTERED_TMPFILES[@]}"; do
|
|
265
|
+
[[ -f "$f" ]] && rm -f "$f"
|
|
266
|
+
[[ -d "$f" ]] && rm -rf "$f"
|
|
267
|
+
done
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
# ─── Disk Space Check ───────────────────────────────────────────
|
|
271
|
+
# Validates minimum free disk space before critical writes
|
|
272
|
+
# Usage: check_disk_space <path> [min_mb]
|
|
273
|
+
check_disk_space() {
|
|
274
|
+
local target_path="${1:-.}"
|
|
275
|
+
local min_mb="${2:-100}" # Default 100MB minimum
|
|
276
|
+
|
|
277
|
+
# Get available space in KB
|
|
278
|
+
local free_kb
|
|
279
|
+
free_kb=$(df -k "$target_path" 2>/dev/null | tail -1 | awk '{print $4}')
|
|
280
|
+
|
|
281
|
+
if [[ -z "$free_kb" ]] || [[ ! "$free_kb" =~ ^[0-9]+$ ]]; then
|
|
282
|
+
warn "Could not determine free disk space — proceeding anyway"
|
|
283
|
+
return 0
|
|
284
|
+
fi
|
|
285
|
+
|
|
286
|
+
local free_mb=$((free_kb / 1024))
|
|
287
|
+
if [[ "$free_mb" -lt "$min_mb" ]]; then
|
|
288
|
+
error "Insufficient disk space: ${free_mb}MB free, need ${min_mb}MB minimum"
|
|
289
|
+
return 1
|
|
290
|
+
fi
|
|
291
|
+
|
|
292
|
+
return 0
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
# ─── GitHub API Retry Helper ────────────────────────────────────
|
|
296
|
+
# Retries gh CLI calls with exponential backoff on 403 (rate limit)
|
|
297
|
+
# Usage: gh_with_retry <max_attempts> gh issue view <args>
|
|
298
|
+
# Returns: command output on success, empty on failure
|
|
299
|
+
gh_with_retry() {
|
|
300
|
+
local max_attempts="${1:-4}"
|
|
301
|
+
shift
|
|
302
|
+
local attempt=1
|
|
303
|
+
local backoff_secs=30
|
|
304
|
+
|
|
305
|
+
while [[ "$attempt" -le "$max_attempts" ]]; do
|
|
306
|
+
# Execute gh command
|
|
307
|
+
local output result
|
|
308
|
+
output=$("$@" 2>&1)
|
|
309
|
+
result=$?
|
|
310
|
+
|
|
311
|
+
# Success
|
|
312
|
+
if [[ "$result" -eq 0 ]]; then
|
|
313
|
+
echo "$output"
|
|
314
|
+
return 0
|
|
315
|
+
fi
|
|
316
|
+
|
|
317
|
+
# Check for rate limit (403) or API error
|
|
318
|
+
if echo "$output" | grep -qE "HTTP 403|API rate limit|rate limited|You have exceeded"; then
|
|
319
|
+
if [[ "$attempt" -lt "$max_attempts" ]]; then
|
|
320
|
+
warn "GitHub API rate limit detected — backing off ${backoff_secs}s (attempt $attempt/$max_attempts)"
|
|
321
|
+
emit_event "github.rate_limited" "attempt=$attempt" "backoff=$backoff_secs"
|
|
322
|
+
sleep "$backoff_secs"
|
|
323
|
+
backoff_secs=$((backoff_secs * 2))
|
|
324
|
+
[[ "$backoff_secs" -gt 300 ]] && backoff_secs=300
|
|
325
|
+
fi
|
|
326
|
+
else
|
|
327
|
+
# Non-rate-limit error — fail immediately
|
|
328
|
+
return "$result"
|
|
329
|
+
fi
|
|
330
|
+
|
|
331
|
+
attempt=$((attempt + 1))
|
|
332
|
+
done
|
|
333
|
+
|
|
334
|
+
# Exhausted all retries
|
|
335
|
+
error "GitHub API call failed after $max_attempts attempts: ${output##*$'\n'}"
|
|
336
|
+
emit_event "github.api_failed" "attempts=$max_attempts"
|
|
337
|
+
return 1
|
|
338
|
+
}
|
|
339
|
+
|
|
183
340
|
# ─── Project Identity ────────────────────────────────────────────
|
|
184
341
|
# Auto-detect GitHub owner/repo from git remote, with fallbacks
|
|
185
342
|
_sw_github_repo() {
|
|
@@ -210,3 +367,21 @@ _sw_github_url() {
|
|
|
210
367
|
echo "https://github.com/${repo}"
|
|
211
368
|
}
|
|
212
369
|
|
|
370
|
+
# ─── Secret Sanitization ─────────────────────────────────────────────
|
|
371
|
+
# Redacts sensitive data from strings before logging
|
|
372
|
+
# Redacts: ANTHROPIC_API_KEY, GITHUB_TOKEN, sk-* patterns, Bearer tokens
|
|
373
|
+
sanitize_secrets() {
|
|
374
|
+
local text="$1"
|
|
375
|
+
# Redact ANTHROPIC_API_KEY=... (until whitespace or quote)
|
|
376
|
+
text="$(echo "$text" | sed 's/ANTHROPIC_API_KEY=[^ "]*\|ANTHROPIC_API_KEY=[^ ]*/ANTHROPIC_API_KEY=***REDACTED***/g')"
|
|
377
|
+
# Redact GITHUB_TOKEN=... (until whitespace or quote)
|
|
378
|
+
text="$(echo "$text" | sed 's/GITHUB_TOKEN=[^ "]*\|GITHUB_TOKEN=[^ ]*/GITHUB_TOKEN=***REDACTED***/g')"
|
|
379
|
+
# Redact sk-* patterns (Anthropic API key format)
|
|
380
|
+
text="$(echo "$text" | sed 's/sk-[a-zA-Z0-9_-]*/sk-***REDACTED***/g')"
|
|
381
|
+
# Redact Bearer tokens
|
|
382
|
+
text="$(echo "$text" | sed 's/Bearer [a-zA-Z0-9_.-]*/Bearer ***REDACTED***/g')"
|
|
383
|
+
# Redact oauth tokens (gh_...)
|
|
384
|
+
text="$(echo "$text" | sed 's/gh_[a-zA-Z0-9_]*/gh_***REDACTED***/g')"
|
|
385
|
+
echo "$text"
|
|
386
|
+
}
|
|
387
|
+
|