reverse-engineer
Inventory an authorized repo, binary, or product. Not for tracing your own repo; that is codebase-recon. Triggers: "reverse-engineer X", "tear down Y", "what should we steal from Z", "evaluate competitor/upstream", "should we fork/adopt/build-native".
Install
npx skills add https://github.com/boshu2/agentops/tree/main/skills/reverse-engineer
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install boshu2-agentops@llmmart
git clone https://github.com/boshu2/agentops.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole boshu2/agentops collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Reverse Engineer
Reverse-engineer an external system into two things: a mechanically-verifiable teardown (feature inventory + registry + specs, optionally a security audit) and a steal-map — what to adopt into our surfaces, what to leave behind. The teardown is the evidence; the steal-map is the decision. Separating them works because a decision row that must cite a registry entry can be re-checked by anyone, while a decision made from impressions cannot be re-checked by its own author. The original failure mode this skill exists to prevent: reading a competitor's README and "deciding" from vibes.
Triggers: "reverse-engineer X", "tear down Y", "what should we steal from Z", "evaluate competitor/upstream", "should we fork/adopt/build-native".
Prompt
Reverse-engineer the beads CLI (github.com/steveyegge/beads, tag v2.1.0)
in repo mode, then author steal-map.md comparing its dependency-graph
reconciler against our cli/internal/gates/ package. I own this analysis
and have authorization for the clone.
It's working if
Observable in the trace, without reading the prose:
feature-registry.yamlandclone-metadata.jsonland under.agents/scratch/reverse-engineer/<product>/with the resolved upstream commit recorded.- Every
steal-map.mdrow cites a teardown registry entry and our matching surface, using the fullhave/gap/steal/park/rejectset. bash skills/reverse-engineer/scripts/validate-output.sh --output-dir "$output_dir" --phase completeexits 0 before handoff.- A one-way-door adoption row is routed to Plan instead of decided
inside
steal-map.md.
⚠️ Constraints — Hard Guardrails (MANDATORY)
- Only operate on code/binaries you own or have explicit written authorization to analyze — this matters because unauthorized teardown is the legal/IP line.
- Do not provide steps to bypass protections/ToS or to extract proprietary source/system prompts.
- Do not output reconstructed proprietary source or embedded prompts (index only; redact in reports) — to prevent reproducing protected IP.
- Redact secrets/tokens/keys if encountered; run the secret-scan gate over outputs to prevent credential leakage.
- Always separate docs say vs code proves vs hosted/control-plane.
Phase 1 — Mechanical teardown (the script)
Produce evidence, not vibes. The script clones (pinned), scans CLI/config/artifact surface, and writes a feature inventory + machine-checkable registry + spec set.
python3 skills/reverse-engineer/scripts/reverse_engineer.py <product> --mode=repo \
--upstream-repo="https://github.com/org/repo.git" --upstream-ref=v1.0.0 \
--output-dir=".agents/scratch/reverse-engineer/<product>/"
Binary mode requires --authorized (see Invocation Contract + Self-Test). Use the bundled demo fixture if you lack authorization for a real binary.
Phase 2 — The steal-map (the decision)
Map each capability the teardown found onto our surfaces. This is the part that turns research into a decision. Emit .agents/scratch/reverse-engineer/<product>/steal-map.md with a table; every row cites the teardown evidence and the matching surface in our repo.
The mechanical script intentionally stops after validating Phase 1. It cannot
truthfully decide whether our live tree has, lacks, or should adopt a capability.
The caller authors steal-map.md from the generated registry plus a fresh read
of our repository, then runs the complete-output validator below. A missing or
malformed map is therefore an incomplete skill result, not a script success
silently relabelled as a decision.
| Their capability | Our surface today | Verdict |
|---|---|---|
<feature> |
<our file / skill / CLI, or "none"> |
have / gap / steal / park / reject |
Verdict rules (hard-won — apply them, do not skip):
- steal — we lack it and it advances our core. Steal the pattern, not the storage engine: re-express in our primitives, never vendor their runtime.
- park — real, but it's substrate we deliberately delegate (e.g. orchestration per ADR-0009) or downstream of an unproven bet. Name it, don't build it.
- reject — it conflicts with our doctrine (e.g. a self-reported completion edge where we require a verdict — "no verdict = not done").
- have — we already do this; confirm it still holds, move on.
- gap — we should have it and don't. These are the steal candidates.
Discipline that makes the map trustworthy:
- Independently checked, not self-report. Get facts on how they implement each capability from code, cross-checked by a fresh reader — never from a README or one context's summary. Model family is optional metadata, not a trust requirement.
- Probe the real state, don't argue from stale. Re-verify our side against the live tree before calling something a gap; every "X is missing" carries the search that proved it.
- The steal is the pattern, not the platform. Their robustness is usually one idea (unification, a gate, a reconcile loop). Steal the idea; leave the scaffolding.
Route one-way-door adoptions into planning
If adopting a steal is a one-way door (an architecture fork, a new bounded context, or a migration), do not decide it here. Hand the steal-map to Plan. Dueling Idea Genies or Premortem may challenge the choice as advisory evidence. Plan alone shapes the selected option in the existing intent source; neither strategy grants readiness or continuation authority.
Invocation Contract
Required: product_name. Common flags: --mode=repo|binary|both, --upstream-repo, --upstream-ref (requires the selected checkout to be at that exact commit and records its resolved SHA in clone-metadata.json), --local-clone-dir (selects that exact tree, including a non-Git tree; it never falls back to the caller's checkout), --output-dir (default .agents/scratch/reverse-engineer/<product>/), --security-audit, --materialize-archives (authorized-only opt-in; embedded-archive extraction is off/index-only by default), --authorized (mandatory for binary mode — refuses without it). Full list: python3 skills/reverse-engineer/scripts/reverse_engineer.py --help.
Output Specification
Phase-1 teardown under output_dir/: feature-inventory.md, feature-registry.yaml, feature-catalog.md, spec-architecture.md, spec-code-map.md, spec-clone-vs-use.md, spec-clone-mvp.md, plus spec-cli-surface.md only when a CLI is detected. clone-metadata.json is written whenever an upstream repo/ref is selected and binds the exact analyzed commit, including an already-present checkout. Security mode adds output_dir/security/: threat-model.md, attack-surface.md, dataflow.md, crypto-review.md, authn-authz.md, findings.md, reproducibility.md, validate-security-audit.sh. Phase-2 adds the caller-authored steal-map.md.
Artifact directory: the exact
--output-dir, defaulting to$REPO/.agents/scratch/reverse-engineer/<product>/.Filename convention: the fixed phase-1 and phase-2 names above; security files live only in the
security/child directory.Serialization/schema format: registry is YAML, clone metadata is one JSON object, and inventories/specs/steal-map are nonempty Markdown files.
Validator command: Phase 1 runs this automatically with
--phase teardown. After authoringsteal-map.md, validate the complete skill output with$output_dir,$security_audit,$sbom, and$upstream_ref_set(each numeric flag0|1):bash skills/reverse-engineer/scripts/validate-output.sh \ --output-dir "$output_dir" --phase complete \ --security-audit "$security_audit" --sbom "$sbom" \ --upstream-ref-set "$upstream_ref_set"Downstream handoff: give the validated
steal-map.mdto Plan for one-way-door candidates; ordinaryhave,park, andrejectdecisions remain evidence-backed terminal rows.
Earlier default compatibility
Existing teardowns under .agents/research/<product>/ remain in place and
usable. The script accepts that directory when it is passed explicitly with
--output-dir; that flag is caller authorization to write the teardown at the
exact selected path. It does not relocate or duplicate existing artifacts. An
invocation that omits the flag writes only to the current scratch default and
never creates output under the earlier root.
Consumers must retain the exact selected output_dir with their evidence
references instead of rediscovering outputs by globbing one root. This owning
skill contract is the compatibility authority; no separate migration receipt
is required.
Reproducibility + fixtures
--upstream-ref binds the selected checkout to one full commit: a new clone is
checked out detached at the fetched ref, while an existing checkout must already
match or the run refuses before analysis. clone-metadata.json records that
resolved commit. Regression test: bash skills/reverse-engineer/scripts/repo_fixture_test.sh. To update a fixture when contracts legitimately change, re-run with the new pinned ref, copy the contract files into fixtures/<product>/, and commit.
Self-Test (acceptance)
bash skills/reverse-engineer/scripts/self_test.sh
Must show: feature inventory and registry generated; the exact Phase-1 validator
passes; the complete validator rejects a missing and malformed steal-map and
accepts a valid caller-authored fixture; existing-checkout ref mismatch and
output symlinks fail closed; in security mode validate-security-audit.sh
exits 0 only after the scaffold is completed and the secret scan passes.
Examples
Reverse-engineer an OSS CLI (repo mode) → steal-map
Run Phase 1 for cc-sdd with --mode=repo --upstream-repo="https://github.com/gotalab/cc-sdd.git" --upstream-ref=v1.0.0. It clones the pinned source, scans the surface, writes inventory/registry/specs, and validates the teardown. Then inspect our live surfaces, author each have/gap/steal/park/reject row in steal-map.md, and run the complete-output validator. Supply selected steals to Plan.
Binary analysis with security audit
Run the skill for ao with --authorized --mode=binary --binary-path="$(command -v ao)" --security-audit. It performs authorized static analysis plus the security suite under output_dir/security/; the secret-scan check must pass.
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
| Refuses binary analysis | Missing --authorized |
Add --authorized (explicit written authorization required). |
No clone-metadata.json |
--upstream-repo not passed |
Pass --upstream-repo (and optionally --upstream-ref). |
| Fixture diff fails | Upstream changed / stale golden | Re-run pinned, refresh fixtures/, commit. |
Existing teardown is under .agents/research/ |
It used the earlier default | Pass that exact directory with --output-dir; new runs otherwise use the scratch default. |
spec-cli-surface.md missing |
No Node/Python/Go CLI detected | Surface is documented in spec-code-map.md instead. |
| Steal-map is all "steal" | Skipped the park/reject rules | Substrate we delegate is park; doctrine conflicts are reject — not everything novel is worth adopting. |
Quality Rubric
- Every steal-map row cites teardown evidence and our matching surface (or "none").
- Verdicts use the full set —
have/gap/steal/park/reject— not everything marked "steal". - Facts on how they implement come from code and a fresh independent check — not a README.
- One-way-door adoptions are supplied to Plan, not decided here.
- Secret-scan gate passed over all outputs; no proprietary source/prompts reproduced.
See Also
- plan — shape selected steals in the existing intent source
- idea-genie — optional advisory challenge (duel mode)
- premortem — optional advisory challenge of the exact plan
- research — general exploration; this is its external-system specialization
Reference Documents
- references/reverse-engineer.feature — executable spec: repo-mode feature catalog + code map, binary-mode security audit, durable spec artifacts
Files (agentops)
-
agents
-
openai.yaml 296 B
interface: display_name: Reverse Engineer short_description: Inspect an external system and compare adoption options default_prompt: Evaluate the authorized external system against the caller's question. Separate observed code behavior, documentation claims and unverified hosted features.
-
-
fixtures
-
cc-sdd-v2.1.0
-
cli-surface-contracts.txt 1.4 KB
# CLI surface contract assertions for cc-sdd v2.1.0 # Each non-comment line below must appear verbatim in the generated spec-cli-surface.md. # The check uses grep -F (fixed-string substring match), so leading spaces matter. # Lines starting with # are comments. # Package identity - Node package: `tools/cc-sdd` - package name: `cc-sdd` - version: `2.1.0` # Binary entrypoint - `cc-sdd` -> `./dist/cli.js` # Source entry heuristic - `tools/cc-sdd/src/cli.ts` (node shebang entry; typically calls `runCli`) # Key CLI flags (2-space indent as they appear inside the help text code block) --agent <claude-code|claude-code-agent|codex|cursor|github-copilot|gemini-cli|windsurf|qwen-code|opencode|opencode-agent> Select agent --lang <ja|en|zh-TW|zh|es|pt|de|fr|ru|it|ko|ar|el> Language --os <auto|mac|windows|linux> Target OS (auto uses runtime) --kiro-dir <path> Kiro root dir (default .kiro) --overwrite <prompt|skip|force> Overwrite policy (default: prompt) --dry-run Print plan only --yes, -y Skip prompts (prompt -> force) -h, --help Show help -v, --version Show version # Config surface - User config file: `.cc-sdd.json` (loaded from CWD). - Environment variables: `NO_COLOR` -
clone-metadata.json 156 B
{ "upstream_repo": "https://github.com/gotalab/cc-sdd.git", "upstream_ref": "v2.1.0", "resolved_commit": "6e972c064ac4723bc8ad0181871d07e199af6a9f" } -
docs-features.txt 454 B
docs/README/README_en docs/README/README_ja docs/README/README_zh-TW docs/README docs/RELEASE_NOTES/RELEASE_NOTES_en docs/RELEASE_NOTES/RELEASE_NOTES_ja docs/guides/claude-subagents docs/guides/command-reference docs/guides/customization-guide docs/guides/ja/claude-subagents docs/guides/ja/command-reference docs/guides/ja/customization-guide docs/guides/ja/migration-guide docs/guides/ja/spec-driven docs/guides/migration-guide docs/guides/spec-driven -
feature-registry.yaml 843 B
schema_version: 1 product_name: 'cc-sdd' docs_features_prefix: 'docs/' docs_features: - 'docs/README/README_en' - 'docs/README/README_ja' - 'docs/README/README_zh-TW' - 'docs/README' - 'docs/RELEASE_NOTES/RELEASE_NOTES_en' - 'docs/RELEASE_NOTES/RELEASE_NOTES_ja' - 'docs/guides/claude-subagents' - 'docs/guides/command-reference' - 'docs/guides/customization-guide' - 'docs/guides/ja/claude-subagents' - 'docs/guides/ja/command-reference' - 'docs/guides/ja/customization-guide' - 'docs/guides/ja/migration-guide' - 'docs/guides/ja/spec-driven' - 'docs/guides/migration-guide' - 'docs/guides/spec-driven' groups: README: impl: control-plane anchors: [] notes: "" RELEASE_NOTES: impl: control-plane anchors: [] notes: "" guides: impl: control-plane anchors: [] notes: ""
-
-
-
references
-
templates
-
security
-
attack-surface.md.tmpl 310 B · in bundle
-
authn-authz.md.tmpl 294 B · in bundle
-
crypto-review.md.tmpl 283 B · in bundle
-
dataflow.md.tmpl 298 B · in bundle
-
findings.md.tmpl 236 B · in bundle
-
reproducibility.md.tmpl 237 B · in bundle
-
threat-model.md.tmpl 331 B · in bundle
-
-
postmortem.md.tmpl 433 B · in bundle
-
spec-architecture.md.tmpl 918 B · in bundle
-
spec-clone-mvp.md.tmpl 545 B · in bundle
-
spec-clone-vs-use.md.tmpl 465 B · in bundle
-
spec-code-map.md.tmpl 624 B · in bundle
-
vibe-report.md.tmpl 404 B · in bundle
-
-
reverse-engineer.feature 2.4 KB · in bundle
-
-
scripts
-
binary
-
analyze_binary.sh 6 KB
#!/usr/bin/env bash set -euo pipefail if [[ $# -ne 2 ]]; then echo "usage: analyze_binary.sh <binary_path> <out_dir>" >&2 exit 2 fi BIN="$1" OUT="$2" mkdir -p "$OUT" if [[ ! -f "$BIN" ]]; then echo "error: binary not found: $BIN" >&2 exit 2 fi { echo "# Binary Analysis (Best-Effort)" echo echo "- Target: \`$BIN\`" echo "- Generated: $(date +%F)" echo echo "## file(1)" echo if command -v file >/dev/null 2>&1; then file "$BIN" || true else echo "_file not available_" fi echo echo "## Linked Libraries (best-effort)" echo if command -v otool >/dev/null 2>&1; then otool -L "$BIN" 2>/dev/null || true elif command -v ldd >/dev/null 2>&1; then ldd "$BIN" 2>/dev/null || true else echo "_otool/ldd not available_" fi echo echo "## Language Heuristics (best-effort)" echo if command -v strings >/dev/null 2>&1; then # Cache strings output to a temp file for multiple scans _STRINGS_FILE=$(mktemp) trap 'rm -f "$_STRINGS_FILE"' EXIT strings -a "$BIN" 2>/dev/null >"$_STRINGS_FILE" # Helper: search strings file with rg falling back to grep -E _str_match() { local pattern="$1" if command -v rg >/dev/null 2>&1; then rg -m 1 "$pattern" "$_STRINGS_FILE" 2>/dev/null else grep -E -m 1 "$pattern" "$_STRINGS_FILE" 2>/dev/null fi } # --- Go detection (broad markers for stripped binaries) --- GO_DETECTED=false GO_MARKER="" # Original markers (unstripped binaries) if _str_match 'runtime\.morestack|go\.buildid|Go build ID|type\.\*runtime\.' >/dev/null 2>&1; then GO_DETECTED=true; GO_MARKER="Go runtime markers" # Broader markers for stripped binaries (version strings, GOROOT, module paths) elif _str_match 'go1\.[0-9]|GOROOT|github\.com/|golang\.org/' >/dev/null 2>&1; then GO_DETECTED=true; GO_MARKER="Go version/module strings" fi # --- Python detection --- PYTHON_DETECTED=false if _str_match '__pycache__|\.pyc|Py_Initialize|libpython|python[0-9]\.[0-9]' >/dev/null 2>&1; then PYTHON_DETECTED=true fi # --- Report language --- if $GO_DETECTED && $PYTHON_DETECTED; then echo "- Likely language/runtime: Go + Python (Go binary embedding Python code)" echo " - Go detection: $GO_MARKER" elif $GO_DETECTED; then echo "- Likely language/runtime: Go (heuristic: $GO_MARKER)" elif $PYTHON_DETECTED; then echo "- Likely language/runtime: Python (heuristic: Python runtime markers in strings)" else echo "- Likely language/runtime: unknown (no Go or Python markers found)" fi # --- Go details (version, module, packages) --- if $GO_DETECTED; then echo echo "### Go Details" echo # Go version string (e.g. "go1.23.4") — match lines that ARE the version _go_ver=$({ if command -v rg >/dev/null 2>&1; then rg -m 1 -o '^go1\.[0-9]+\.[0-9]+$' "$_STRINGS_FILE" 2>/dev/null else grep -E -m 1 '^go1\.[0-9]+\.[0-9]+$' "$_STRINGS_FILE" 2>/dev/null fi } || true) if [[ -n "$_go_ver" ]]; then echo "- Go version: \`$_go_ver\`" else echo "- Go version: _not found (stripped)_" fi # Module path — prefer github.com/gitlab.com/golang.org paths first _go_mod=$({ if command -v rg >/dev/null 2>&1; then rg -m 1 -o '^(github|gitlab|bitbucket)\.com/[^\s]+' "$_STRINGS_FILE" 2>/dev/null \ || rg -m 1 -o '^golang\.org/[^\s]+' "$_STRINGS_FILE" 2>/dev/null \ || rg -m 1 -o '^[a-z][a-z0-9.-]+\.[a-z]{2,}/[^\s]+' "$_STRINGS_FILE" 2>/dev/null else grep -E -m 1 -o '^(github|gitlab|bitbucket)\.com/[^ ]+' "$_STRINGS_FILE" 2>/dev/null \ || grep -E -m 1 -o '^golang\.org/[^ ]+' "$_STRINGS_FILE" 2>/dev/null \ || grep -E -m 1 -o '^[a-z][a-z0-9.-]+\.[a-z]{2,}/[^ ]+' "$_STRINGS_FILE" 2>/dev/null fi } || true) if [[ -n "$_go_mod" ]]; then echo "- Module path: \`$_go_mod\`" else echo "- Module path: _not found_" fi # Internal package count (unique Go module-style paths) _go_pkgs=$({ if command -v rg >/dev/null 2>&1; then rg -o '^(github|gitlab|bitbucket)\.com/[^\s]+|^golang\.org/[^\s]+' "$_STRINGS_FILE" 2>/dev/null else grep -E -o '^(github|gitlab|bitbucket)\.com/[^ ]+|^golang\.org/[^ ]+' "$_STRINGS_FILE" 2>/dev/null fi } | sort -u | wc -l || echo 0) echo "- Internal packages (approx): ${_go_pkgs##* }" fi else echo "- strings not available; cannot run heuristics" fi echo echo "## Embedded Archive Signatures (ZIP, best-effort)" echo if command -v python3 >/dev/null 2>&1; then python3 - "$BIN" <<'PY' import sys from pathlib import Path p = Path(sys.argv[1]) data = p.read_bytes() sig = b"PK\x03\x04" hits = [] start = 0 while True: i = data.find(sig, start) if i < 0: break hits.append(i) start = i + 1 print(f"- ZIP local header occurrences: {len(hits)}") for i in hits[:10]: print(f" - offset: {i}") if len(hits) > 10: print(" - ...") PY else echo "_python3 not available_" fi } >"$OUT/binary-analysis.md" # Raw strings (kept under tmp out dir; do not copy into output_dir by default). if command -v strings >/dev/null 2>&1; then strings -a "$BIN" 2>/dev/null | head -2000 >"$OUT/strings.head.txt" || true if command -v rg >/dev/null 2>&1; then strings -a "$BIN" 2>/dev/null | rg -n -S 'mcp|prompt|system|tool|openai|anthropic|claude' >"$OUT/strings.ai-hits.txt" 2>/dev/null || true else strings -a "$BIN" 2>/dev/null | grep -E -in 'mcp|prompt|system|tool|openai|anthropic|claude' >"$OUT/strings.ai-hits.txt" 2>/dev/null || true fi fi # Optional disassembly snippet (bounded). Keep under tmp out dir; do not paste into reports by default. if command -v otool >/dev/null 2>&1; then otool -tvV "$BIN" 2>/dev/null | head -500 >"$OUT/disassembly.head.txt" || true elif command -v objdump >/dev/null 2>&1; then objdump -d "$BIN" 2>/dev/null | head -500 >"$OUT/disassembly.head.txt" || true fi -
capture_cli_help.sh 8.2 KB
#!/usr/bin/env bash # capture_cli_help.sh — Recursively capture --help output from a CLI binary. # # Usage: capture_cli_help.sh <binary_path> <out_dir> # # Writes: # <out_dir>/cli-help-tree.txt — Structured help output per command/subcommand # <out_dir>/cli-commands.txt — One command path per line # # Constraints: # - 5-second timeout per invocation # - 120-second total execution cap # - Max recursion depth: 3 # - Exit 0 always (best-effort) set -euo pipefail BINARY_PATH="${1:?Usage: capture_cli_help.sh <binary_path> <out_dir>}" OUT_DIR="${2:?Usage: capture_cli_help.sh <binary_path> <out_dir>}" BINARY_NAME="$(basename "$BINARY_PATH")" PER_CMD_TIMEOUT=5 TOTAL_TIMEOUT=120 MAX_DEPTH=3 # Help-like keywords that indicate valid help output. HELP_KEYWORDS="Usage|Commands|Available|Flags|Options|usage|commands|available|flags|options|USAGE|COMMANDS|AVAILABLE|FLAGS|OPTIONS|help|HELP|Synopsis|SYNOPSIS|Arguments|ARGUMENTS" # Resolve timeout command (GNU coreutils `timeout` or macOS `gtimeout`). TIMEOUT_CMD="" if command -v timeout &>/dev/null; then TIMEOUT_CMD="timeout" elif command -v gtimeout &>/dev/null; then TIMEOUT_CMD="gtimeout" fi mkdir -p "$OUT_DIR" TREE_FILE="$OUT_DIR/cli-help-tree.txt" CMDS_FILE="$OUT_DIR/cli-commands.txt" SEEN_PATHS_FILE="$OUT_DIR/.seen-command-paths.tmp" VISITED_PREFIX_FILE="$OUT_DIR/.visited-prefixes.tmp" # Start fresh. : > "$TREE_FILE" : > "$CMDS_FILE" : > "$SEEN_PATHS_FILE" : > "$VISITED_PREFIX_FILE" # Track total elapsed time. START_TIME="$(date +%s)" elapsed() { local now now="$(date +%s)" echo $(( now - START_TIME )) } budget_exceeded() { [ "$(elapsed)" -ge "$TOTAL_TIMEOUT" ] } # Run a command with per-invocation timeout. Captures stdout+stderr. # Returns the output; exit code 0 on success, non-zero on timeout/failure. run_with_timeout() { if [ -n "$TIMEOUT_CMD" ]; then "$TIMEOUT_CMD" "$PER_CMD_TIMEOUT" "$@" 2>&1 || true else # Fallback: no timeout command available, just run it. "$@" 2>&1 || true fi } # Check if text looks like help output. looks_like_help() { local text="$1" if [ -z "$text" ]; then return 1 fi if echo "$text" | grep -qE "$HELP_KEYWORDS"; then return 0 fi return 1 } seen_contains() { local file="$1" local key="$2" grep -Fqx -- "$key" "$file" 2>/dev/null } seen_add() { local file="$1" local key="$2" printf '%s\n' "$key" >> "$file" } record_command_path() { local path="$1" [ -z "$path" ] && return 0 if seen_contains "$SEEN_PATHS_FILE" "$path"; then return 0 fi seen_add "$SEEN_PATHS_FILE" "$path" echo "$path" >> "$CMDS_FILE" } extract_usage_path() { local help_text="$1" local usage_line usage_line="$(echo "$help_text" | awk ' /^Usage:/ { line=$0 sub(/^Usage:[[:space:]]*/, "", line) if (line != "") { print line exit } in_usage=1 next } in_usage { if ($0 ~ /^[[:space:]]*$/) { in_usage=0 next } line=$0 sub(/^[[:space:]]+/, "", line) if (line != "") { print line exit } } ')" [ -z "$usage_line" ] && return 0 echo "$usage_line" | awk ' { out="" for (i=1; i<=NF; i++) { t=$i first = substr(t, 1, 1) if (first == "[" || first == "<" || first == "-" || first == "(" || first == "{") break out = (out ? out " " : "") t } print out } ' } # Extract subcommand names from help output. # Looks for lines after "Commands:" or "Available Commands:" header, # matching pattern: leading whitespace, then a word (the subcommand name). extract_subcommands() { local help_text="$1" local in_commands_section=0 local subcmds=() while IFS= read -r line; do # Detect start of commands section. if echo "$line" | grep -qiE '^\s*(Available\s+)?Commands\s*:'; then in_commands_section=1 continue fi if [ "$in_commands_section" -eq 1 ]; then # Empty line or a new section header ends the commands block. if [ -z "$line" ] || echo "$line" | grep -qE '^[A-Z].*:$'; then in_commands_section=0 continue fi # Extract the first word (subcommand name) from indented lines. local cmd cmd="$(echo "$line" | sed -n 's/^[[:space:]]\{1,\}\([a-zA-Z0-9_-]\{1,\}\)[[:space:]].*/\1/p')" if [ -n "$cmd" ]; then # Skip common non-command words that appear in help sections. case "$cmd" in help|completion) ;; # skip meta-commands *) subcmds+=("$cmd") ;; esac fi fi done <<< "$help_text" # Output one per line. for sc in "${subcmds[@]+"${subcmds[@]}"}"; do echo "$sc" done } # Recursive help capture. # Args: depth cmd_prefix args... # depth — current recursion depth (0-based) # cmd_prefix — display prefix for tree (e.g., "forge transcript") # args... — actual command + args to run capture_help() { local depth="$1"; shift local cmd_prefix="$1"; shift # Remaining args are the command to execute. if seen_contains "$VISITED_PREFIX_FILE" "$cmd_prefix"; then return 0 fi seen_add "$VISITED_PREFIX_FILE" "$cmd_prefix" if budget_exceeded; then return 0 fi if [ "$depth" -gt "$MAX_DEPTH" ]; then return 0 fi local help_output help_output="$(run_with_timeout "$@" --help)" if ! looks_like_help "$help_output"; then if [ "$depth" -eq 0 ]; then # Top-level binary doesn't produce help. Write note and bail. echo "# CLI Help Tree" >> "$TREE_FILE" echo "" >> "$TREE_FILE" echo "NOTE: $BINARY_NAME --help did not produce recognizable help output." >> "$TREE_FILE" fi return 0 fi # Write to tree file. echo "## $cmd_prefix" >> "$TREE_FILE" echo "" >> "$TREE_FILE" echo "$help_output" >> "$TREE_FILE" echo "" >> "$TREE_FILE" # Resolve canonical path from Usage: for alias handling and de-noising. local usage_path="" usage_path="$(extract_usage_path "$help_output" || true)" # Write to commands file (skip top-level binary name alone). if [ "$depth" -gt 0 ]; then # Strip first token (binary executable/command name) to compare subcommand paths robustly. local subcmd_path="${cmd_prefix#* }" local canonical_subcmd_path="" if [ -n "$usage_path" ] && [ "$usage_path" != "${usage_path#* }" ]; then canonical_subcmd_path="${usage_path#* }" fi if [ -n "$canonical_subcmd_path" ]; then record_command_path "$canonical_subcmd_path" else record_command_path "$subcmd_path" fi # If Usage path subcommands differ from invocation subcommands, this likely hit # an alias/help redirect. Stop recursion to avoid fake paths like "mail inbox inbox". if [ -n "$canonical_subcmd_path" ] && [ "$canonical_subcmd_path" != "$subcmd_path" ]; then return 0 fi fi # Extract and recurse into subcommands. local subcmds subcmds="$(extract_subcommands "$help_output")" if [ -z "$subcmds" ]; then return 0 fi while IFS= read -r subcmd; do [ -z "$subcmd" ] && continue if budget_exceeded; then return 0 fi capture_help "$(( depth + 1 ))" "$cmd_prefix $subcmd" "$@" "$subcmd" done <<< "$subcmds" } # Write tree header. echo "# CLI Help Tree" >> "$TREE_FILE" echo "" >> "$TREE_FILE" # Start recursive capture from the top-level binary. capture_help 0 "$BINARY_NAME" "$BINARY_PATH" # If commands file is empty but tree has content, write the top-level command. if [ ! -s "$CMDS_FILE" ] && [ -s "$TREE_FILE" ]; then # No subcommands found; the binary itself is the only entry. : # cli-commands.txt stays empty — top-level is implicit. fi exit 0 -
extract_embedded_archives.py 4 KB
#!/usr/bin/env python3 from __future__ import annotations import argparse import hashlib import json import sys import zipfile from dataclasses import dataclass from io import BytesIO from pathlib import Path @dataclass(frozen=True) class Candidate: offset: int file_count: int score: int def _sha256_file(path: Path) -> str: h = hashlib.sha256() with path.open("rb") as f: for chunk in iter(lambda: f.read(1024 * 1024), b""): h.update(chunk) return h.hexdigest() def _find_offsets(data: bytes, max_hits: int = 5000) -> list[int]: sig = b"PK\x03\x04" hits: list[int] = [] start = 0 while len(hits) < max_hits: i = data.find(sig, start) if i < 0: break hits.append(i) start = i + 1 return hits def _score_names(names: list[str]) -> int: exts = {".py": 5, ".js": 4, ".ts": 4, ".go": 4, ".md": 2, ".yaml": 2, ".yml": 2, ".json": 2, ".toml": 2} score = 0 for n in names: for ext, w in exts.items(): if n.endswith(ext): score += w break # Reward file count lightly. score += min(len(names), 200) return score def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--binary", required=True) ap.add_argument("--out-dir", required=True, help="Directory to extract archives into.") ap.add_argument("--max-candidates", type=int, default=200) args = ap.parse_args() binary = Path(args.binary) out_dir = Path(args.out_dir) out_dir.mkdir(parents=True, exist_ok=True) data = binary.read_bytes() offsets = _find_offsets(data) cands: list[Candidate] = [] opened = 0 for off in offsets[: args.max_candidates]: try: with zipfile.ZipFile(BytesIO(data[off:])) as zf: names = zf.namelist() cands.append(Candidate(offset=off, file_count=len(names), score=_score_names(names))) opened += 1 except Exception: continue if not cands: (out_dir / "extract.NOOP.md").write_text( f"# Extract Embedded Archives (No-Op)\n\nNo embedded ZIP archives could be opened.\n\nBinary: `{binary}`\n", encoding="utf-8", ) return 0 best = sorted(cands, key=lambda c: (-c.score, -c.file_count, c.offset))[0] dest = out_dir / f"zip@{best.offset}" dest.mkdir(parents=True, exist_ok=True) with zipfile.ZipFile(BytesIO(data[best.offset:])) as zf: # Bound decompression against zip bombs: this extracts an archive carved # from attacker-controlled binary bytes. Refuse an oversized member or # total uncompressed size before writing anything to disk. max_member = 128 * 1024 * 1024 max_total = 512 * 1024 * 1024 total = 0 for info in zf.infolist(): total += info.file_size if info.file_size > max_member or total > max_total: print( f"refusing to extract embedded archive at offset {best.offset}: " "uncompressed size exceeds bounds (possible zip bomb)", file=sys.stderr, ) return 1 # Extract all files. This is an authorized-only operation; do not commit the result. zf.extractall(dest) names = zf.namelist() manifest = { "binary": str(binary), "binary_sha256": _sha256_file(binary), "selected_offset": best.offset, "selected_file_count": best.file_count, "selected_score": best.score, "filenames": names[:500], "note": "Do not paste or commit extracted content. Reports must reference paths/hashes only.", } (dest / "manifest.json").write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8") # Convenience pointer for downstream scripts. (out_dir / "PRIMARY.txt").write_text(str(dest), encoding="utf-8") print(f"OK: extracted {best.file_count} files to {dest}") return 0 if __name__ == "__main__": raise SystemExit(main()) -
list_embedded_archives.py 3.7 KB
#!/usr/bin/env python3 from __future__ import annotations import argparse import hashlib import json import zipfile from dataclasses import dataclass from io import BytesIO from pathlib import Path @dataclass(frozen=True) class ZipCandidate: offset: int file_count: int names: list[str] sha256: str def _sha256_bytes(b: bytes) -> str: h = hashlib.sha256() h.update(b) return h.hexdigest() def _find_zip_offsets(data: bytes, max_hits: int = 5000) -> list[int]: sig = b"PK\x03\x04" hits: list[int] = [] start = 0 while len(hits) < max_hits: i = data.find(sig, start) if i < 0: break hits.append(i) start = i + 1 return hits def _try_open_zip(data: bytes, offset: int) -> ZipCandidate | None: tail = data[offset:] # zipfile wants central directory present; if it's not, this will fail (that's fine). bio = BytesIO(tail) try: with zipfile.ZipFile(bio) as zf: names = zf.namelist() # Hash just the first ~4MB for stable fingerprint without storing full content. sha = _sha256_bytes(tail[: 4 * 1024 * 1024]) return ZipCandidate(offset=offset, file_count=len(names), names=names[:200], sha256=sha) except Exception: return None def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--binary", required=True) ap.add_argument("--out-json", required=True) ap.add_argument("--out-index-md", required=True) args = ap.parse_args() binary = Path(args.binary) data = binary.read_bytes() hits = _find_zip_offsets(data) cands: list[ZipCandidate] = [] # Try a limited number to keep runtime bounded. for off in hits[:200]: cand = _try_open_zip(data, off) if cand: cands.append(cand) out_json = Path(args.out_json) out_json.parent.mkdir(parents=True, exist_ok=True) out_json.write_text( json.dumps( { "binary": str(binary), "zip_header_hits": len(hits), "candidates": [ {"offset": c.offset, "file_count": c.file_count, "sha256_head_4mb": c.sha256, "names": c.names} for c in sorted(cands, key=lambda x: (-x.file_count, x.offset)) ], }, indent=2, sort_keys=True, ) + "\n", encoding="utf-8", ) out_md = Path(args.out_index_md) out_md.parent.mkdir(parents=True, exist_ok=True) lines: list[str] = [] lines.append("# Embedded Archive Index (Best-Effort)") lines.append("") lines.append("Guardrail: this index does not dump reconstructed source or prompts; it only inventories candidate archives.") lines.append("") lines.append(f"- Binary: `{binary}`") lines.append(f"- ZIP header hits: {len(hits)}") lines.append(f"- ZIP candidates opened: {len(cands)}") lines.append("") if not cands: lines.append("_No embedded ZIP archives could be opened via the central directory heuristic._") lines.append("") else: for i, c in enumerate(sorted(cands, key=lambda x: (-x.file_count, x.offset))[:5], start=1): lines.append(f"## Candidate {i}") lines.append("") lines.append(f"- Offset: `{c.offset}`") lines.append(f"- File count: `{c.file_count}`") lines.append(f"- SHA256(head_4mb): `{c.sha256}`") lines.append("") lines.append("Top filenames (truncated):") lines.append("") for n in c.names[:30]: lines.append(f"- `{n}`") lines.append("") out_md.write_text("\n".join(lines) + "\n", encoding="utf-8") return 0 if __name__ == "__main__": raise SystemExit(main())
-
-
security
-
generate_sbom.sh 1.3 KB
#!/usr/bin/env bash set -euo pipefail if [[ $# -ne 2 ]]; then echo "usage: generate_sbom.sh <analysis_root_dir> <security_out_dir>" >&2 exit 2 fi ROOT="$1" OUT="$2" mkdir -p "$OUT" report="$OUT/dep-risk-report.md" if command -v syft >/dev/null 2>&1; then # Best-effort. Avoid failing the whole workflow if syft has issues. if syft "dir:${ROOT}" -o spdx-json >"$OUT/sbom.spdx.json" 2>"$OUT/syft.stderr"; then cat >"$report" <<EOF # Dependency Risk Report (Best-Effort) - Generator: syft - Input: \`${ROOT}\` - Notes: This report is a stub. Pair SBOM output with a vuln scanner (e.g., grype) in an authorized environment. EOF exit 0 fi fi # Language-aware no-op outputs (still produces deterministic artifacts). if [[ -f "$ROOT/go.mod" ]]; then if command -v go >/dev/null 2>&1; then (cd "$ROOT" && go list -m -json all) >"$OUT/sbom.go-mod.modules.json" 2>"$OUT/go-list.stderr" || true fi fi cat >"$OUT/sbom.NOOP.md" <<EOF # SBOM (No-Op) No supported SBOM generator was available (or it failed). Input: \`${ROOT}\` Created: $(date +%F) EOF cat >"$report" <<EOF # Dependency Risk Report (No-Op) No dependency risk scan was performed (offline / tool unavailable). Recommended (authorized environments only): - Generate a real SBOM with syft - Run a vuln scan with grype / osv-scanner / etc. EOF -
scan_secrets.sh 1.7 KB
#!/usr/bin/env bash set -euo pipefail if [[ $# -ne 1 ]]; then echo "usage: scan_secrets.sh <dir>" >&2 exit 2 fi ROOT="$1" if [[ ! -d "$ROOT" ]]; then echo "error: not a directory: $ROOT" >&2 exit 2 fi # Conservative patterns. This will produce false positives; treat as a gate to review and redact. PATTERNS=( 'AKIA[0-9A-Z]{16}' 'ASIA[0-9A-Z]{16}' '-----BEGIN (RSA|EC|OPENSSH) PRIVATE KEY-----' 'xox[baprs]-[0-9A-Za-z-]{10,}' 'ghp_[0-9A-Za-z]{20,}' 'github_pat_[0-9A-Za-z_]{20,}' 'sk-[0-9A-Za-z]{20,}' 'AIza[0-9A-Za-z\\-_]{20,}' '-----BEGIN PGP PRIVATE KEY BLOCK-----' '(?i)client_secret\\s*[:=]\\s*[^\\s]+' '(?i)api[_-]?key\\s*[:=]\\s*[^\\s]+' '(?i)authorization\\s*:\\s*bearer\\s+[^\\s]+' ) TMP="$(mktemp -t re_rpi_secrets.XXXXXX)" trap 'rm -f "$TMP"' EXIT FAIL=0 for pat in "${PATTERNS[@]}"; do # ripgrep is faster and supports PCRE2 with -P. if command -v rg >/dev/null 2>&1; then # Use '--' so patterns beginning with '-' are not treated as flags. # Avoid self-matches: this validator embeds some of the patterns it is looking for. if rg -n -S -P --hidden --no-ignore \ --glob '!.git/**' \ --glob '!.tmp/**' \ --glob '!**/security/scan-secrets.sh' \ --glob '!**/security/scan_secrets.sh' \ --glob '!**/security/validate-security-audit.sh' \ --glob '!**/security/generate-sbom.sh' \ -- "$pat" "$ROOT" >>"$TMP"; then FAIL=1 fi else if grep -RInE -- "$pat" "$ROOT" >>"$TMP" 2>/dev/null; then FAIL=1 fi fi done if [[ $FAIL -ne 0 ]]; then echo "FAIL: potential secrets detected in $ROOT" >&2 # Print limited output to avoid copying secrets into logs. head -50 "$TMP" >&2 echo "..." >&2 exit 1 fi echo "OK: secret scan passed ($ROOT)" -
validate_security_audit.sh 2.4 KB
#!/usr/bin/env bash set -euo pipefail if [[ $# -lt 2 ]]; then echo "usage: validate_security_audit.sh <output_dir> (--sbom|--no-sbom)" >&2 exit 2 fi OUTDIR="$1" SBOM_FLAG="${2:-}" SEC="$OUTDIR/security" if [[ ! -d "$SEC" ]]; then echo "FAIL: missing security dir: $SEC" >&2 exit 1 fi req=( "$SEC/threat-model.md" "$SEC/attack-surface.md" "$SEC/dataflow.md" "$SEC/crypto-review.md" "$SEC/authn-authz.md" "$SEC/findings.md" "$SEC/reproducibility.md" "$SEC/validate-security-audit.sh" ) fail=0 for f in "${req[@]}"; do if [[ ! -f "$f" ]]; then echo "FAIL: missing required file: $f" >&2 fail=1 fi done if [[ $fail -ne 0 ]]; then exit 1 fi # Findings gate: each finding must have Evidence + Fix sections (simple heuristic). if ! rg -n -S '^## ' "$SEC/findings.md" >/dev/null 2>&1; then echo "FAIL: findings.md has no findings headers (expected '## ...')" >&2 exit 1 fi if ! rg -n -S '(?i)^Evidence:' "$SEC/findings.md" >/dev/null 2>&1; then echo "FAIL: findings.md missing Evidence: lines" >&2 exit 1 fi if ! rg -n -S '(?i)^(Fix|Remediation):' "$SEC/findings.md" >/dev/null 2>&1; then echo "FAIL: findings.md missing Fix:/Remediation: lines" >&2 exit 1 fi # Reject unfilled placeholders across the WHOLE required audit bundle: the # shipped templates supply Evidence/Fix/threat/dataflow content as literal _TBD # markers, and the presence-only greps above certify them as real. Any _TBD in # any required narrative file is an unfilled scaffold and must not certify green # (scanning only findings.md would let a _TBD threat-model or dataflow through). for f in "${req[@]}"; do case "$f" in *.md) ;; *) continue ;; esac if grep -Fq '_TBD' "$f"; then echo "FAIL: $(basename "$f") still contains _TBD placeholders — fill the audit before certifying" >&2 exit 1 fi done # Secret scan gate over outputs. SCANDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCANNER="$SCANDIR/scan_secrets.sh" if [[ ! -x "$SCANNER" ]]; then SCANNER="$SCANDIR/scan-secrets.sh" fi "$SCANNER" "$OUTDIR" if [[ "$SBOM_FLAG" == "--sbom" ]]; then if [[ ! -f "$SEC/sbom.spdx.json" && ! -f "$SEC/sbom.NOOP.md" ]]; then echo "FAIL: --sbom set but no sbom.spdx.json (or sbom.NOOP.md) found in $SEC" >&2 exit 1 fi if [[ ! -f "$SEC/dep-risk-report.md" ]]; then echo "FAIL: --sbom set but missing dep-risk-report.md in $SEC" >&2 exit 1 fi fi echo "OK: security audit validated ($OUTDIR)"
-
-
extract_docs_features.sh 839 B
#!/usr/bin/env bash set -euo pipefail if [[ $# -ne 2 ]]; then echo "usage: extract_docs_features.sh <paths.txt> <docs_features_prefix>" >&2 exit 2 fi PATHS_TXT="$1" PREFIX_RAW="$2" # Normalize prefix: "docs/features/" -> "/docs/features" PREFIX="/${PREFIX_RAW#/}" PREFIX="${PREFIX%/}" python3 - "$PATHS_TXT" "$PREFIX" <<'PY' import sys from pathlib import Path paths_txt = Path(sys.argv[1]) prefix = sys.argv[2] out = set() for line in paths_txt.read_text(encoding="utf-8", errors="replace").splitlines(): p = line.strip() if not p: continue if not p.startswith("/"): p = "/" + p if p.startswith(prefix + "/") or p == prefix: # Keep the path *under* docs/features as a slug, without leading slash. slug = p.lstrip("/") out.add(slug) for s in sorted(out): print(s) PY -
extract_sitemap_paths.sh 892 B
#!/usr/bin/env bash set -euo pipefail if [[ $# -ne 1 ]]; then echo "usage: extract_sitemap_paths.sh <sitemap.xml>" >&2 exit 2 fi SITEMAP_XML="$1" python3 - "$SITEMAP_XML" <<'PY' import sys import urllib.parse import xml.etree.ElementTree as ET from pathlib import Path src = Path(sys.argv[1]) data = src.read_text(encoding="utf-8", errors="replace") root = ET.fromstring(data) paths = set() for loc in root.iter(): if loc.tag.endswith("loc") and loc.text: u = loc.text.strip() p = urllib.parse.urlparse(u) path = p.path or "" if not path: continue # Normalize: ensure leading slash, drop trailing slash except root. if not path.startswith("/"): path = "/" + path if len(path) > 1 and path.endswith("/"): path = path[:-1] paths.add(path) for p in sorted(paths): print(p) PY -
fetch_url.py 853 B
#!/usr/bin/env python3 from __future__ import annotations import sys import urllib.parse import urllib.request from pathlib import Path def main() -> int: if len(sys.argv) != 3: print("usage: fetch_url.py <url> <out_path>", file=sys.stderr) return 2 url = sys.argv[1] out_path = Path(sys.argv[2]) out_path.parent.mkdir(parents=True, exist_ok=True) parsed = urllib.parse.urlparse(url) if parsed.scheme in ("file", ""): src = Path(parsed.path if parsed.scheme == "file" else url) out_path.write_bytes(src.read_bytes()) return 0 req = urllib.request.Request(url, headers={"User-Agent": "reverse-engineer/1.0"}) with urllib.request.urlopen(req, timeout=30) as resp: out_path.write_bytes(resp.read()) return 0 if __name__ == "__main__": raise SystemExit(main()) -
generate_feature_catalog_md.py 2.9 KB
#!/usr/bin/env python3 from __future__ import annotations import argparse import datetime as _dt from pathlib import Path def _parse_registry(path: Path) -> dict: data = {"docs_features_prefix": "docs/features/", "docs_features": [], "groups": {}} cur = None in_docs = False in_groups = False in_anchors = False for raw in path.read_text(encoding="utf-8", errors="replace").splitlines(): line = raw.rstrip("\n") if not line.strip() or line.lstrip().startswith("#"): continue if line.startswith("docs_features_prefix:"): data["docs_features_prefix"] = line.split(":", 1)[1].strip().strip("'\"") if line == "docs_features:": in_docs = True in_groups = False continue if line == "groups:": in_docs = False in_groups = True continue if in_docs and line.startswith(" - "): data["docs_features"].append(line[4:].strip().strip("'\"")) continue if in_groups: if line.startswith(" ") and not line.startswith(" ") and line.endswith(":"): name = line.strip()[:-1] cur = {"impl": None, "anchors": [], "notes": ""} data["groups"][name] = cur in_anchors = False continue if cur is None: continue s = line.strip() if s.startswith("impl:"): cur["impl"] = s.split(":", 1)[1].strip() elif s.startswith("anchors:"): in_anchors = True if s.endswith("[]"): cur["anchors"] = [] elif in_anchors and s.startswith("- "): cur["anchors"].append(s[2:].strip().strip("'\"")) elif s.startswith("notes:"): cur["notes"] = s.split(":", 1)[1].strip().strip("'\"") return data def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--registry", required=True) ap.add_argument("--out", required=True) args = ap.parse_args() reg = _parse_registry(Path(args.registry)) groups = reg["groups"] out = Path(args.out) out.parent.mkdir(parents=True, exist_ok=True) lines: list[str] = [] lines.append("# Feature Catalog") lines.append("") lines.append(f"- Generated: {_dt.date.today().isoformat()}") lines.append(f"- Groups: {len(groups)}") lines.append("") lines.append("| Group | impl | anchors | notes |") lines.append("|---|---|---:|---|") for g in sorted(groups.keys()): ent = groups[g] impl = ent.get("impl") or "" anchors = ent.get("anchors") or [] notes = (ent.get("notes") or "").replace("\n", " ") lines.append(f"| `{g}` | `{impl}` | {len(anchors)} | {notes} |") lines.append("") out.write_text("\n".join(lines) + "\n", encoding="utf-8") return 0 if __name__ == "__main__": raise SystemExit(main()) -
generate_feature_inventory_md.py 1.4 KB
#!/usr/bin/env python3 from __future__ import annotations import argparse import datetime as _dt from pathlib import Path def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--product-name", required=True) ap.add_argument("--docs-features", required=True, help="Text file: one docs/features slug per line (may be empty).") ap.add_argument("--out", required=True) args = ap.parse_args() slugs_path = Path(args.docs_features) slugs = [ln.strip() for ln in slugs_path.read_text(encoding="utf-8", errors="replace").splitlines() if ln.strip()] out = Path(args.out) out.parent.mkdir(parents=True, exist_ok=True) lines: list[str] = [] lines.append(f"# Feature Inventory: {args.product_name}") lines.append("") lines.append(f"- Generated: {_dt.date.today().isoformat()}") lines.append("- Source: docs sitemap inventory (if provided); otherwise empty/incomplete by design.") lines.append(f"- Count: {len(slugs)}") lines.append("") lines.append("## Docs Slugs") lines.append("") if slugs: for s in slugs: lines.append(f"- `{s}`") else: lines.append("_No docs sitemap provided (or no matching `docs/features/` entries)._") lines.append("") out.write_text("\n".join(lines) + "\n", encoding="utf-8") return 0 if __name__ == "__main__": raise SystemExit(main()) -
repo_fixture_test.sh 14.4 KB
#!/usr/bin/env bash # repo_fixture_test.sh — Golden fixture self-test for cc-sdd repo-mode analysis. # # Pins to cc-sdd v2.1.0 (commit 6e972c064ac4723bc8ad0181871d07e199af6a9f) and # runs repo-mode analysis, then compares key contracts against stored fixtures. # # Usage: # bash skills/reverse-engineer/scripts/repo_fixture_test.sh # # Exit codes: # 0 All fixture contracts match. # 1 One or more contracts drifted (diff output printed to stderr). # 2 Prerequisite missing or unexpected error. # # Requirements: # - Network access (to clone github.com/gotalab/cc-sdd at v2.1.0) # - git, python3 set -euo pipefail # --------------------------------------------------------------------------- # Paths # --------------------------------------------------------------------------- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SKILL_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" # ROOT = git repo root (trunks/), two levels up from the skill dir # (reverse-engineer -> skills -> trunks) ROOT="$(cd "$SKILL_DIR/../.." && pwd)" FIXTURES_DIR="$SKILL_DIR/fixtures/cc-sdd-v2.1.0" PINNED_REF="v2.1.0" PINNED_COMMIT="6e972c064ac4723bc8ad0181871d07e199af6a9f" UPSTREAM_REPO="https://github.com/gotalab/cc-sdd.git" TMP="$ROOT/.tmp/repo-fixture-test-cc-sdd" OUT="$TMP/out" CLONE_DIR="$TMP/local-clone" # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- FAILURES=0 _fail() { echo "FAIL: $1" >&2 FAILURES=$((FAILURES + 1)) } _ok() { echo "OK: $1" } _check_cmd() { if ! command -v "$1" >/dev/null 2>&1; then echo "error: required command not found: $1" >&2 exit 2 fi } # Normalize a YAML/text file: strip generated_at/clone_date/analysis_root lines # (which are volatile) so diff is stable across run dates. _normalize() { local f="$1" grep -v '^generated_at:' "$f" \ | grep -v '^ "clone_date":' \ | grep -v '^ "analysis_root":' \ | grep -v '^ "node_package_dir":' \ | grep -v '^analysis_root:' \ | sed 's|^- Date: .*|- Date: <DATE>|' \ | sed 's|^- Analysis root: .*|- Analysis root: <ROOT>|' \ | sed "s|$(echo "$ROOT" | sed 's|/|\\/|g')|<ROOT>|g" } # --------------------------------------------------------------------------- # Prerequisites # --------------------------------------------------------------------------- _check_cmd git _check_cmd python3 if [ ! -d "$FIXTURES_DIR" ]; then echo "error: fixtures directory not found: $FIXTURES_DIR" >&2 echo " Run with UPDATE_FIXTURES=1 to create it, or check the skill directory." >&2 exit 2 fi # --------------------------------------------------------------------------- # UPDATE_FIXTURES mode: regenerate and overwrite golden fixtures. # --------------------------------------------------------------------------- if [ "${UPDATE_FIXTURES:-0}" = "1" ]; then echo "=== UPDATE_FIXTURES=1: regenerating golden fixtures ===" rm -rf "$TMP" mkdir -p "$OUT" "$CLONE_DIR" "$FIXTURES_DIR" python3 "$SKILL_DIR/scripts/reverse_engineer.py" cc-sdd \ --mode=repo \ --upstream-repo="$UPSTREAM_REPO" \ --upstream-ref="$PINNED_REF" \ --local-clone-dir="$CLONE_DIR" \ --output-dir="$OUT" # Verify resolved commit matches pin. ACTUAL_COMMIT="$(python3 -c "import json; d=json.load(open('$OUT/clone-metadata.json')); print(d['resolved_commit'])")" if [ "$ACTUAL_COMMIT" != "$PINNED_COMMIT" ]; then echo "WARNING: resolved commit $ACTUAL_COMMIT does not match expected pin $PINNED_COMMIT" >&2 echo " Update PINNED_COMMIT in this script if the tag was force-pushed." >&2 fi # docs-features.txt — stable (content from repo tree). cp "$OUT/docs-features.txt" "$FIXTURES_DIR/docs-features.txt" # feature-registry.yaml — strip generated_at before storing. grep -v '^generated_at:' "$OUT/feature-registry.yaml" > "$FIXTURES_DIR/feature-registry.yaml" # clone-metadata.json — strip clone_date (volatile). python3 - "$OUT/clone-metadata.json" "$FIXTURES_DIR/clone-metadata.json" <<'PYEOF' import json, sys d = json.load(open(sys.argv[1])) d.pop("clone_date", None) open(sys.argv[2], "w").write(json.dumps({k: d[k] for k in ("upstream_repo", "upstream_ref", "resolved_commit")}, indent=2) + "\n") PYEOF # cli-surface-contracts.txt — extract key contract lines from spec-cli-surface.md. python3 - "$OUT/spec-cli-surface.md" "$FIXTURES_DIR/cli-surface-contracts.txt" <<'PYEOF' import sys, re text = open(sys.argv[1]).read() out_lines = [ "# CLI surface contract assertions for cc-sdd v2.1.0", "# Each line below must appear verbatim in the generated spec-cli-surface.md", "# (after stripping leading/trailing whitespace).", "# Lines starting with # are comments.", "", ] # Entrypoints section. out_lines.append("# Package identity") for pat in [r"- Node package: `.+`", r"- package name: `.+`", r"- version: `.+`"]: m = re.search(pat, text) if m: out_lines.append(m.group(0)) out_lines.append("") out_lines.append("# Binary entrypoint") m = re.search(r"- `cc-sdd` -> `.+`", text) if m: out_lines.append(m.group(0)) out_lines.append("") out_lines.append("# Source entry heuristic") m = re.search(r"- `tools/cc-sdd/src/cli\.ts`.+", text) if m: out_lines.append(m.group(0)) # Help text flags (extract lines from the code block). in_block = False flags = [] for line in text.splitlines(): if line.strip().startswith("```"): in_block = not in_block continue if in_block and (line.startswith(" -") or line.startswith("-")): flags.append(line.rstrip()) if flags: out_lines.append("") out_lines.append("# Key CLI flags present in help text") out_lines.extend(flags) # Config surface. out_lines.append("") out_lines.append("# Config surface") for pat in [r"- User config file: `.+`", r"- Environment variables: `.+`"]: m = re.search(pat, text) if m: out_lines.append(m.group(0)) open(sys.argv[2], "w").write("\n".join(out_lines) + "\n") print(f"Written {len(out_lines)} lines to {sys.argv[2]}") PYEOF echo "=== Fixtures updated in $FIXTURES_DIR ===" exit 0 fi # --------------------------------------------------------------------------- # Normal mode: run analysis and compare against golden fixtures. # --------------------------------------------------------------------------- echo "=== repo_fixture_test.sh: cc-sdd v2.1.0 golden fixture test ===" echo " Pinned commit: $PINNED_COMMIT" echo " Fixtures: $FIXTURES_DIR" echo "" # Clean output dir for reproducible run. The clone dir is preserved across runs # to avoid re-downloading (a shallow clone is ~5-10 MB and slow on first run). # However, we always delete the clone dir if the resolved SHA does not match the # pinned commit (guards against a force-pushed tag). if [ -d "$CLONE_DIR/.git" ]; then EXISTING_SHA="$(git -C "$CLONE_DIR" rev-parse HEAD 2>/dev/null || true)" if [ "$EXISTING_SHA" != "$PINNED_COMMIT" ]; then echo "--- Existing clone SHA ($EXISTING_SHA) != pin ($PINNED_COMMIT); re-cloning ---" rm -rf "$CLONE_DIR" else echo "--- Reusing cached clone at $PINNED_COMMIT ---" fi fi rm -rf "$OUT" mkdir -p "$OUT" if [ ! -d "$CLONE_DIR/.git" ]; then echo "--- Cloning cc-sdd at $PINNED_REF (network required) ---" mkdir -p "$CLONE_DIR" fi echo "--- Running repo-mode analysis ---" python3 "$SKILL_DIR/scripts/reverse_engineer.py" cc-sdd \ --mode=repo \ --upstream-repo="$UPSTREAM_REPO" \ --upstream-ref="$PINNED_REF" \ --local-clone-dir="$CLONE_DIR" \ --output-dir="$OUT" # clone-metadata.json is only written by reverse_engineer.py during the initial # clone. When reusing a cached clone, write it ourselves so downstream checks work. if [ ! -f "$OUT/clone-metadata.json" ]; then RESOLVED_SHA="$(git -C "$CLONE_DIR" rev-parse HEAD 2>/dev/null || echo "")" python3 - "$OUT/clone-metadata.json" "$UPSTREAM_REPO" "$PINNED_REF" "$RESOLVED_SHA" <<'PYEOF' import json, sys out_path, repo, ref, sha = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4] data = {"upstream_repo": repo, "upstream_ref": ref, "resolved_commit": sha, "clone_date": "cached"} open(out_path, "w").write(json.dumps(data, indent=2) + "\n") PYEOF fi echo "" echo "--- Verifying pinned commit SHA ---" ACTUAL_COMMIT="$(python3 -c "import json; d=json.load(open('$OUT/clone-metadata.json')); print(d['resolved_commit'])")" if [ "$ACTUAL_COMMIT" != "$PINNED_COMMIT" ]; then _fail "resolved commit mismatch: got $ACTUAL_COMMIT, expected $PINNED_COMMIT" echo " This means the tag was force-pushed or the fixture pin is stale." >&2 else _ok "resolved commit matches pin ($PINNED_COMMIT)" fi # --------------------------------------------------------------------------- # Contract 1: docs-features.txt (exact match) # --------------------------------------------------------------------------- echo "" echo "--- Contract 1: docs-features.txt ---" GOLDEN="$FIXTURES_DIR/docs-features.txt" ACTUAL="$OUT/docs-features.txt" if [ ! -f "$ACTUAL" ]; then _fail "docs-features.txt not generated" else DIFF_OUT="$(diff --unified=3 "$GOLDEN" "$ACTUAL" 2>&1 || true)" if [ -n "$DIFF_OUT" ]; then _fail "docs-features.txt drifted from golden fixture" echo "--- diff (golden vs actual) ---" >&2 echo "$DIFF_OUT" >&2 echo "---" >&2 else _ok "docs-features.txt matches golden fixture" fi fi # --------------------------------------------------------------------------- # Contract 2: feature-registry.yaml (normalized, strip generated_at) # --------------------------------------------------------------------------- echo "" echo "--- Contract 2: feature-registry.yaml (normalized) ---" GOLDEN="$FIXTURES_DIR/feature-registry.yaml" ACTUAL="$OUT/feature-registry.yaml" if [ ! -f "$ACTUAL" ]; then _fail "feature-registry.yaml not generated" else GOLDEN_NORM="$(mktemp)" ACTUAL_NORM="$(mktemp)" grep -v '^generated_at:' "$GOLDEN" > "$GOLDEN_NORM" grep -v '^generated_at:' "$ACTUAL" > "$ACTUAL_NORM" DIFF_OUT="$(diff --unified=3 "$GOLDEN_NORM" "$ACTUAL_NORM" 2>&1 || true)" rm -f "$GOLDEN_NORM" "$ACTUAL_NORM" if [ -n "$DIFF_OUT" ]; then _fail "feature-registry.yaml drifted from golden fixture" echo "--- diff (golden vs actual, generated_at stripped) ---" >&2 echo "$DIFF_OUT" >&2 echo "---" >&2 else _ok "feature-registry.yaml matches golden fixture (normalized)" fi fi # --------------------------------------------------------------------------- # Contract 3: cli-surface-contracts.txt (line-presence check in spec-cli-surface.md) # --------------------------------------------------------------------------- echo "" echo "--- Contract 3: spec-cli-surface.md contract lines ---" CLI_SURFACE="$OUT/spec-cli-surface.md" CONTRACTS="$FIXTURES_DIR/cli-surface-contracts.txt" if [ ! -f "$CLI_SURFACE" ]; then _fail "spec-cli-surface.md not generated" elif [ ! -f "$CONTRACTS" ]; then echo "SKIP: cli-surface-contracts.txt fixture not found (non-fatal)" else CONTRACT_FAILURES=0 while IFS= read -r line; do # Skip blank lines and comments. [[ -z "$line" || "$line" == \#* ]] && continue # Check verbatim line presence (fixed-string, -- prevents lines starting with # '-' being misinterpreted as grep flags). if ! grep -qF -- "$line" "$CLI_SURFACE" 2>/dev/null; then _fail "contract line not found in spec-cli-surface.md: $line" CONTRACT_FAILURES=$((CONTRACT_FAILURES + 1)) fi done < "$CONTRACTS" if [ "$CONTRACT_FAILURES" -eq 0 ]; then _ok "all spec-cli-surface.md contract lines present" fi fi # --------------------------------------------------------------------------- # Contract 4: clone-metadata.json (key fields) # --------------------------------------------------------------------------- echo "" echo "--- Contract 4: clone-metadata.json key fields ---" GOLDEN="$FIXTURES_DIR/clone-metadata.json" ACTUAL="$OUT/clone-metadata.json" if [ ! -f "$ACTUAL" ]; then _fail "clone-metadata.json not generated" elif [ ! -f "$GOLDEN" ]; then echo "SKIP: clone-metadata.json fixture not found (non-fatal)" else # Compare only the stable fields (upstream_repo, upstream_ref, resolved_commit). GOLDEN_STABLE="$(mktemp)" ACTUAL_STABLE="$(mktemp)" python3 - "$GOLDEN" "$GOLDEN_STABLE" <<'PYEOF' import json, sys d = json.load(open(sys.argv[1])) out = {k: d[k] for k in ("upstream_repo", "upstream_ref", "resolved_commit") if k in d} open(sys.argv[2], "w").write(json.dumps(out, indent=2, sort_keys=True) + "\n") PYEOF python3 - "$ACTUAL" "$ACTUAL_STABLE" <<'PYEOF' import json, sys d = json.load(open(sys.argv[1])) out = {k: d[k] for k in ("upstream_repo", "upstream_ref", "resolved_commit") if k in d} open(sys.argv[2], "w").write(json.dumps(out, indent=2, sort_keys=True) + "\n") PYEOF DIFF_OUT="$(diff --unified=3 "$GOLDEN_STABLE" "$ACTUAL_STABLE" 2>&1 || true)" rm -f "$GOLDEN_STABLE" "$ACTUAL_STABLE" if [ -n "$DIFF_OUT" ]; then _fail "clone-metadata.json stable fields drifted from golden fixture" echo "--- diff (golden vs actual, stable fields only) ---" >&2 echo "$DIFF_OUT" >&2 echo "---" >&2 else _ok "clone-metadata.json stable fields match golden fixture" fi fi # --------------------------------------------------------------------------- # Contract 5: required output files exist # --------------------------------------------------------------------------- echo "" echo "--- Contract 5: required output files exist ---" REQUIRED_FILES=( feature-inventory.md feature-registry.yaml feature-catalog.md spec-architecture.md spec-code-map.md spec-clone-vs-use.md spec-clone-mvp.md spec-cli-surface.md spec-artifact-surface.md artifact-registry.json clone-metadata.json docs-features.txt validate-feature-registry.py ) for f in "${REQUIRED_FILES[@]}"; do if [ ! -f "$OUT/$f" ]; then _fail "required output file missing: $f" else _ok "exists: $f" fi done # --------------------------------------------------------------------------- # Contract 6: feature registry validator passes # --------------------------------------------------------------------------- echo "" echo "--- Contract 6: feature registry validator ---" if python3 "$OUT/validate-feature-registry.py" 2>&1; then _ok "validate-feature-registry.py exit 0" else _fail "validate-feature-registry.py exited non-zero" fi # --------------------------------------------------------------------------- # Summary # --------------------------------------------------------------------------- echo "" if [ "$FAILURES" -gt 0 ]; then echo "RESULT: FAIL — $FAILURES contract(s) drifted. See diff output above." >&2 exit 1 else echo "RESULT: PASS — all golden fixture contracts match." exit 0 fi -
reverse_engineer.py 79.8 KB
#!/usr/bin/env python3 from __future__ import annotations import argparse import datetime as _dt import hashlib import json import os import re import shutil import stat import subprocess import sys from pathlib import Path REPO_ROOT = Path.cwd() SKILL_DIR = Path(__file__).resolve().parents[1] TEMPLATES_DIR = SKILL_DIR / "references" / "templates" IGNORED_REPO_SCAN_PARTS = { ".agents", ".git", ".hg", ".mypy_cache", ".next", ".pytest_cache", ".svn", ".tmp", ".venv", "__pycache__", "build", "coverage", "dist", "node_modules", "target", "tmp", "venv", "vendor", } def _die(msg: str, code: int = 2) -> None: print(f"error: {msg}", file=sys.stderr) raise SystemExit(code) def _run( cmd: list[str], *, cwd: Path | None = None, check: bool = True ) -> subprocess.CompletedProcess: return subprocess.run(cmd, cwd=str(cwd) if cwd else None, check=check) def _lexical_absolute(path: Path) -> Path: """Return an absolute normalized path without following filesystem links.""" return Path(os.path.abspath(os.fspath(path.expanduser()))) def _ensure_real_directory(path: Path) -> tuple[int, int]: """Create/traverse *path* one component at a time without following links. The returned device/inode pair lets the caller detect replacement of the selected output root after setup. Every existing component must be a real directory; a symlink or special file is a hard error. """ absolute = _lexical_absolute(path) flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) nofollow = getattr(os, "O_NOFOLLOW", 0) current_fd = os.open(absolute.anchor, flags) try: for part in absolute.parts[1:]: try: os.mkdir(part, mode=0o755, dir_fd=current_fd) except FileExistsError: pass try: next_fd = os.open(part, flags | nofollow, dir_fd=current_fd) except OSError as exc: _die(f"directory component is not a real directory: {absolute}: {exc}") os.close(current_fd) current_fd = next_fd info = os.fstat(current_fd) return info.st_dev, info.st_ino finally: os.close(current_fd) def _assert_directory_identity( path: Path, identity: tuple[int, int], label: str ) -> None: try: info = os.lstat(path) except OSError as exc: _die(f"{label} disappeared during the run: {path}: {exc}") if stat.S_ISLNK(info.st_mode) or not stat.S_ISDIR(info.st_mode): _die(f"{label} is no longer a real directory: {path}") if (info.st_dev, info.st_ino) != identity: _die(f"{label} was replaced during the run: {path}") def _assert_no_symlinks(root: Path) -> None: """Reject pre-existing or concurrently introduced links below *root*.""" if not root.exists(): return root_info = os.lstat(root) if stat.S_ISLNK(root_info.st_mode) or not stat.S_ISDIR(root_info.st_mode): _die(f"output root must be a real directory: {root}") for directory, dirnames, filenames in os.walk(root, followlinks=False): base = Path(directory) for name in [*dirnames, *filenames]: child = base / name info = os.lstat(child) if stat.S_ISLNK(info.st_mode): _die(f"refusing symlink inside managed output tree: {child}") def _ensure_dirs(paths: list[Path]) -> None: for p in paths: _ensure_real_directory(p) def _today_ymd() -> str: return _dt.date.today().isoformat() def _slugify(s: str) -> str: out = [] for ch in s.strip().lower(): if ch.isalnum(): out.append(ch) elif ch in (" ", "-", "_", "/"): out.append("-") slug = "".join(out) while "--" in slug: slug = slug.replace("--", "-") return slug.strip("-") or "product" def _detect_docs_prefix_for_repo(analysis_root: Path) -> str: """ Choose a sensible docs slug prefix for repos that do not use docs/features/. Returns a prefix with trailing slash. """ candidates = [ "docs/features/", "docs/code-map/", "docs/workflows/", "docs/levels/", "docs/", ] best = "docs/features/" best_count = -1 for cand in candidates: base = analysis_root / cand.strip("/") if not base.exists() or not base.is_dir(): continue count = 0 for p in base.rglob("*"): if p.is_file() and p.suffix.lower() in (".md", ".mdx"): count += 1 if count > best_count: best = cand best_count = count if best_count >= 0: return best return "docs/features/" def _detect_docs_prefix_from_paths(paths: list[str]) -> str: """ Choose docs prefix from sitemap-style path inventory. """ normalized: list[str] = [] for raw in paths: p = raw.strip() if not p: continue if not p.startswith("/"): p = "/" + p normalized.append(p) if not normalized: return "docs/features/" candidates = [ "docs/features/", "docs/code-map/", "docs/workflows/", "docs/levels/", "docs/", ] best = "docs/features/" best_count = -1 for cand in candidates: prefix = "/" + cand.strip("/").rstrip("/") count = sum(1 for p in normalized if p == prefix or p.startswith(prefix + "/")) if count > best_count: best = cand best_count = count return best def _render_template(src: Path, dst: Path, vars: dict[str, str]) -> None: text = src.read_text(encoding="utf-8") for k, v in vars.items(): text = text.replace("{{" + k + "}}", v) dst.write_text(text, encoding="utf-8") def _read_text(p: Path) -> str: return p.read_text(encoding="utf-8", errors="replace") def _should_skip_repo_scan_path(path: Path, repo_root: Path) -> bool: try: rel_parts = path.relative_to(repo_root).parts except ValueError: rel_parts = path.parts for part in rel_parts: if part in IGNORED_REPO_SCAN_PARTS: return True return False def _extract_ts_backtick_const(src: Path, const_name: str) -> str | None: # Best-effort: extract `const <name> = `...`;` blocks (common for CLI help text). if not src.exists(): return None text = _read_text(src) m = re.search( rf"\bconst\s+{re.escape(const_name)}\s*=\s*`([\s\S]*?)`;", text, flags=re.MULTILINE, ) return m.group(1) if m else None def _extract_ts_string_const(src: Path, const_name: str) -> str | None: if not src.exists(): return None text = _read_text(src) m = re.search(rf"\b{re.escape(const_name)}\s*=\s*'([^']*)';", text) if m: return m.group(1) m = re.search(rf'\b{re.escape(const_name)}\s*=\s*"([^"]*)";', text) if m: return m.group(1) return None def _extract_agents_from_registry_ts( registry_ts: Path, ) -> tuple[list[str], list[str]] | None: """ Best-effort parser for agent keys + alias flags from a TS registry. Intended to resolve help text interpolations like `${agentKeys.join('|')}`. """ if not registry_ts.exists(): return None text = _read_text(registry_ts) start = text.find("export const agentDefinitions") if start < 0: return None tail = text[start:] # Limit to the agentDefinitions object body to reduce false matches. end = tail.find("} as const") if end > 0: tail = tail[:end] agent_keys: list[str] = [] seen_keys: set[str] = set() for line in tail.splitlines(): # Top-level agent keys in the registry are consistently 2-space indented. This avoids # accidentally matching nested object keys like `layout:` or `commands:`. m = re.match(r"^ (?:'([^']+)'|([A-Za-z0-9_-]+))\s*:\s*\{\s*$", line) if not m: continue key = (m.group(1) or m.group(2) or "").strip() if not key: continue if key not in seen_keys: agent_keys.append(key) seen_keys.add(key) alias_flags: set[str] = set() for m in re.finditer(r"aliasFlags:\s*\[([^\]]*)\]", tail, flags=re.MULTILINE): blob = m.group(1) for s in re.findall(r"'([^']+)'", blob): alias_flags.add(s) for s in re.findall(r"\"([^\"]+)\"", blob): alias_flags.add(s) return agent_keys, sorted(alias_flags) def _find_node_cli_package( repo_root: Path, product_slug: str, product_name: str ) -> dict[str, object] | None: # Detect Node CLI packages by locating a package.json with a "bin" field and matching name/bin key. product_name_lc = product_name.strip().lower() candidates: list[tuple[int, Path, dict[str, object]]] = [] for pkg_json in sorted(repo_root.rglob("package.json")): if _should_skip_repo_scan_path(pkg_json, repo_root): continue try: data = json.loads(_read_text(pkg_json)) except Exception: continue bin_field = data.get("bin") if not bin_field: continue name = str(data.get("name") or "") score = 0 if name.lower() == product_slug or name.lower() == product_name_lc: score += 100 # Normalize bin mapping. bin_map: dict[str, str] = {} if isinstance(bin_field, str): if name: bin_map[name] = bin_field elif isinstance(bin_field, dict): for k, v in bin_field.items(): if isinstance(k, str) and isinstance(v, str): bin_map[k] = v if product_slug in bin_map: score += 80 if product_name_lc in (k.lower() for k in bin_map.keys()): score += 60 # Prefer shallower packages when score ties (often the main package vs nested deps). depth = len(pkg_json.relative_to(repo_root).parts) score -= depth candidates.append((score, pkg_json, data)) if not candidates: return None candidates.sort(key=lambda t: t[0], reverse=True) score, pkg_json, data = candidates[0] # Return a normalized payload for downstream rendering. bin_field = data.get("bin") bin_map: dict[str, str] = {} if isinstance(bin_field, str): name = str(data.get("name") or "") if name: bin_map[name] = bin_field elif isinstance(bin_field, dict): for k, v in bin_field.items(): if isinstance(k, str) and isinstance(v, str): bin_map[k] = v return { "score": score, "package_json": str(pkg_json), "package_dir": str(pkg_json.parent), "name": str(data.get("name") or ""), "version": str(data.get("version") or ""), "bin": bin_map, } def _find_python_cli(repo_root: Path) -> dict[str, object] | None: """Detect Python CLI packages via pyproject.toml or setup.cfg entry_points.""" result: dict[str, object] = { "language": "python", "bin": {}, "framework": None, "entry_module": None, } # Try pyproject.toml first (modern standard). for pyproject in sorted(repo_root.rglob("pyproject.toml")): if _should_skip_repo_scan_path(pyproject, repo_root): continue text = _read_text(pyproject) # [project.scripts] section (PEP 621). m = re.search(r"\[project\.scripts\]\s*\n((?:[^\[].+\n)*)", text) if m: for line in m.group(1).strip().splitlines(): parts = line.split("=", 1) if len(parts) == 2: name = parts[0].strip().strip('"').strip("'") entry = parts[1].strip().strip('"').strip("'") result["bin"][name] = entry # type: ignore[index] if not result["entry_module"]: result["entry_module"] = ( entry.split(":")[0] if ":" in entry else entry ) # [tool.poetry.scripts] section. m2 = re.search(r"\[tool\.poetry\.scripts\]\s*\n((?:[^\[].+\n)*)", text) if m2: for line in m2.group(1).strip().splitlines(): parts = line.split("=", 1) if len(parts) == 2: name = parts[0].strip().strip('"').strip("'") entry = parts[1].strip().strip('"').strip("'") result["bin"][name] = entry # type: ignore[index] if result["bin"]: break # Try setup.cfg if pyproject didn't find scripts. if not result["bin"]: for setup_cfg in sorted(repo_root.rglob("setup.cfg")): if _should_skip_repo_scan_path(setup_cfg, repo_root): continue text = _read_text(setup_cfg) m = re.search( r"\[options\.entry_points\]\s*\nconsole_scripts\s*=\s*\n((?:\s+.+\n)*)", text, ) if m: for line in m.group(1).strip().splitlines(): parts = line.strip().split("=", 1) if len(parts) == 2: result["bin"][parts[0].strip()] = parts[1].strip() # type: ignore[index] if result["bin"]: break if not result["bin"]: return None # Detect CLI framework via source scan (best-effort, cap file count). scanned = 0 for py_file in sorted(repo_root.rglob("*.py")): if _should_skip_repo_scan_path(py_file, repo_root): continue scanned += 1 if scanned > 200: break text = _read_text(py_file) if "@click.command" in text or "@click.group" in text: result["framework"] = "click" break if "typer.Typer" in text or "@app.command" in text: result["framework"] = "typer" break if "ArgumentParser(" in text and "add_argument" in text: result["framework"] = "argparse" return result def _find_go_cli(repo_root: Path) -> dict[str, object] | None: """Detect Go CLI packages via go.mod + main.go + flag/cobra usage.""" result: dict[str, object] = { "language": "go", "bin": {}, "framework": None, "module": None, } # Find go.mod for module name. go_mod = repo_root / "go.mod" if not go_mod.exists(): # Check one level deeper (monorepo). for gm in sorted(repo_root.rglob("go.mod")): if _should_skip_repo_scan_path(gm, repo_root): continue go_mod = gm break if go_mod.exists(): text = _read_text(go_mod) m = re.search(r"^module\s+(.+)$", text, re.MULTILINE) if m: result["module"] = m.group(1).strip() # Find main.go files (entry points). main_files: list[Path] = [] for mg in sorted(repo_root.rglob("main.go")): if _should_skip_repo_scan_path(mg, repo_root) or "testdata" in mg.parts: continue main_files.append(mg) if not main_files and not result["module"]: return None # Derive binary names from cmd/ pattern or root main.go. for mf in main_files: rel = mf.relative_to(repo_root) parts = rel.parts if len(parts) >= 3 and parts[-3] == "cmd": # cmd/<name>/main.go pattern. result["bin"][parts[-2]] = str(rel) # type: ignore[index] elif len(parts) == 1: # Root main.go — use module basename or directory name. mod = str(result.get("module") or "") name = mod.rsplit("/", 1)[-1] if mod else repo_root.name result["bin"][name] = str(rel) # type: ignore[index] if not result["bin"]: return None # Detect CLI framework (cobra vs stdlib flag). scanned = 0 for go_file in sorted(repo_root.rglob("*.go")): if ( _should_skip_repo_scan_path(go_file, repo_root) or "testdata" in go_file.parts ): continue scanned += 1 if scanned > 200: break text = _read_text(go_file) if "cobra.Command" in text or '"github.com/spf13/cobra"' in text: result["framework"] = "cobra" break if "flag.String" in text or "flag.Bool" in text or "flag.Int" in text: result["framework"] = "flag" return result def _sha256_file(p: Path) -> str: h = hashlib.sha256() with p.open("rb") as f: for chunk in iter(lambda: f.read(1024 * 1024), b""): h.update(chunk) return h.hexdigest() def _render_placeholders(s: str, vars: dict[str, str]) -> str: out = s for k, v in vars.items(): out = out.replace("{{" + k + "}}", v) return out def _enrich_registry_with_binary_evidence( registry_yaml: Path, tmp_dir: Path, output_dir: Path, *, product_name: str, date: str, ) -> bool: """Enrich feature-registry.yaml with binary string evidence. Reads cli-commands.txt and binary strings to create evidence-backed groups. Also generates binary-symbols.txt in the output dir. Returns True if enrichment was applied. """ commands_file = tmp_dir / "binary" / "cli-commands.txt" _strings_file = tmp_dir / "binary" / "strings.head.txt" _ba_file = tmp_dir / "binary" / "binary-analysis.md" # Generate binary-symbols.txt from strings full_strings = tmp_dir / "binary" / "strings.head.txt" symbols_out = output_dir / "binary-symbols.txt" if full_strings.exists() and not symbols_out.exists(): shutil.copyfile(full_strings, symbols_out) # Gather command groups from cli-commands.txt cmd_groups: dict[str, list[str]] = {} if commands_file.exists(): for line in commands_file.read_text(encoding="utf-8").splitlines(): line = line.strip() if not line: continue parts = line.split() group = parts[0] cmd_groups.setdefault(group, []).append(line) if not cmd_groups: return False # Try to load the existing registry (manual parse, no yaml dep) reg: dict = {"groups": {}} try: text = registry_yaml.read_text(encoding="utf-8") for raw_line in text.splitlines(): stripped = raw_line.strip() if stripped.startswith("docs_features_prefix:"): reg["docs_features_prefix"] = ( stripped.split(":", 1)[1].strip().strip("'\"") ) elif stripped.startswith("docs_features:"): reg.setdefault("docs_features", []) elif ( raw_line.startswith(" - ") and "docs_features" in reg and "groups" not in text.split(raw_line)[0].rsplit("docs_features:", 1)[-1] ): reg.setdefault("docs_features", []).append( stripped[2:].strip().strip("'\"") ) # Parse groups using the same logic as the validator cur = None in_groups = False in_anchors = False for raw_line in text.splitlines(): line = raw_line.rstrip() if not line.strip() or line.lstrip().startswith("#"): continue if line == "groups:": in_groups = True continue if not in_groups: continue if ( line.startswith(" ") and not line.startswith(" ") and line.endswith(":") ): name = line.strip()[:-1] cur = {"impl": None, "anchors": [], "notes": ""} reg["groups"][name] = cur in_anchors = False continue if cur is None: continue s = line.strip() if s.startswith("impl:"): cur["impl"] = s.split(":", 1)[1].strip() elif s.startswith("anchors:"): in_anchors = True if s.endswith("[]"): cur["anchors"] = [] elif in_anchors and s.startswith("- "): cur["anchors"].append(s[2:].strip().strip("'\"")) elif s.startswith("notes:"): cur["notes"] = s.split(":", 1)[1].strip().strip("'\"") except Exception: return False groups = reg.get("groups", {}) # Check if registry is already populated (has non-empty groups with notes) has_content = any(g.get("notes") for g in groups.values()) if groups else False if has_content: # Already enriched or populated — don't overwrite return False # Build new groups from binary command data new_groups: dict[str, dict] = {} for grp_name, cmds in sorted(cmd_groups.items()): slug = grp_name.replace("-", "_") subcmds = [c for c in cmds if c != grp_name] sub_str = ", ".join(subcmds) if subcmds else "no subcommands" new_groups[slug] = { "impl": "client", "anchors": ["binary-symbols.txt"], "notes": f"{grp_name} ({len(cmds)} commands: {sub_str})", } # Write registry in the manual format expected by validate_feature_registry.py lines: list[str] = [] lines.append("schema_version: 1") lines.append(f"product_name: {product_name!r}") lines.append(f"generated_at: {date!r}") lines.append("evidence_source: 'binary --help + string extraction'") # Preserve docs_features_prefix if present dfp = reg.get("docs_features_prefix", "docs/features/") lines.append(f"docs_features_prefix: {dfp!r}") # Preserve docs_features list if present docs_feats = reg.get("docs_features", []) if docs_feats: lines.append("docs_features:") for df in docs_feats: lines.append(f" - {df!r}") lines.append("groups:") for slug, grp in new_groups.items(): lines.append(f" {slug}:") lines.append(f" impl: {grp['impl']}") lines.append(" anchors:") for a in grp["anchors"]: lines.append(f" - {a}") lines.append(f" notes: {grp['notes']!r}") registry_yaml.write_text("\n".join(lines) + "\n", encoding="utf-8") return True def _write_binary_cli_surface_spec( output_dir: Path, tmp_dir: Path, *, product_name: str, date: str, ) -> bool: """Write spec-cli-surface.md from binary --help output or binary strings. Returns True if a spec was written. """ help_tree = tmp_dir / "binary" / "cli-help-tree.txt" commands_file = tmp_dir / "binary" / "cli-commands.txt" strings_file = tmp_dir / "binary" / "strings.head.txt" lines: list[str] = [] lines.append(f"# CLI Surface Spec: {product_name}") lines.append("") lines.append(f"- Date: {date}") lines.append( "- Source: binary --help output" if help_tree.exists() else "- Source: binary string extraction" ) lines.append("") cmd_count = 0 if commands_file.exists(): cmds = [ c.strip() for c in commands_file.read_text(encoding="utf-8").splitlines() if c.strip() ] cmd_count = len(cmds) if help_tree.exists(): tree_text = help_tree.read_text(encoding="utf-8") lines.append("## Command Count") lines.append("") lines.append( f"- **{cmd_count} commands** discovered via recursive `--help` execution" ) lines.append("") # Extract top-level commands and subcommands if commands_file.exists(): top_level = sorted(set(c.split()[0] for c in cmds if c.strip())) lines.append("## Top-Level Commands") lines.append("") lines.append("| Command | Subcommands |") lines.append("|---------|-------------|") for top in top_level: subs = [c for c in cmds if c.startswith(top + " ") and c != top] sub_names = [c.split(maxsplit=1)[1] if " " in c else "" for c in subs] sub_str = ( ", ".join(f"`{s}`" for s in sub_names if s) if sub_names else "—" ) lines.append(f"| `{top}` | {sub_str} |") lines.append("") lines.append("## Full Help Tree") lines.append("") lines.append("```") # Truncate to avoid massive output tree_lines = tree_text.splitlines() if len(tree_lines) > 500: lines.extend(tree_lines[:500]) lines.append(f"... ({len(tree_lines) - 500} more lines)") else: lines.extend(tree_lines) lines.append("```") lines.append("") elif strings_file.exists(): # Fallback: extract command-like patterns from strings raw = strings_file.read_text(encoding="utf-8", errors="replace") usage_lines = [ line.strip() for line in raw.splitlines() if "usage" in line.lower() or "Usage" in line ] lines.append("## CLI Surface (from binary strings, best-effort)") lines.append("") if usage_lines: for u in usage_lines[:20]: lines.append(f"- `{u[:200]}`") else: lines.append("_No usage patterns found in binary strings._") lines.append("") else: return False out = output_dir / "spec-cli-surface.md" out.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8") return True def _write_cli_surface_spec( output_dir: Path, *, product_name: str, product_slug: str, date: str, analysis_root: Path, ) -> bool: """ Return True if a CLI was detected and spec-cli-surface.md was written. Repo-mode only. This is best-effort and aims to capture a mechanically-verifiable contract: - entrypoints (package.json bin) - help/usage text (static extraction, with interpolation resolved when possible) - config/env surface """ if not analysis_root.exists(): return False node_cli = _find_node_cli_package(analysis_root, product_slug, product_name) python_cli = _find_python_cli(analysis_root) if not node_cli else None go_cli = _find_go_cli(analysis_root) if not node_cli and not python_cli else None if not node_cli and not python_cli and not go_cli: return False # If Python or Go CLI detected (non-Node), write a language-appropriate spec. if python_cli or go_cli: cli_info = python_cli or go_cli assert cli_info is not None out = output_dir / "spec-cli-surface.md" lines: list[str] = [] lang = str(cli_info["language"]).capitalize() lines.append(f"# CLI Surface Spec: {product_name}") lines.append("") lines.append(f"- Date: {date}") lines.append(f"- Language: {lang}") lines.append(f"- Analysis root: `{analysis_root}`") if cli_info.get("framework"): lines.append(f"- Framework: {cli_info['framework']}") if cli_info.get("module"): lines.append(f"- Module: `{cli_info['module']}`") if cli_info.get("entry_module"): lines.append(f"- Entry module: `{cli_info['entry_module']}`") lines.append("") lines.append("## Entrypoints (Code-Proven)") lines.append("") bin_map = cli_info.get("bin") or {} if isinstance(bin_map, dict) and bin_map: for k in sorted(bin_map.keys()): lines.append(f"- `{k}` -> `{bin_map[k]}`") else: lines.append("- _No entrypoints extracted._") lines.append("") lines.append("## Notes For 1:1 Fidelity") lines.append("") lines.append( "- Run `<binary> --help` to capture the full CLI contract as a golden test fixture." ) if lang == "Python": lines.append( "- For Click/Typer apps, consider `<binary> --help` per subcommand for full coverage." ) elif lang == "Go": lines.append( "- For Cobra apps, consider `<binary> help <subcommand>` for full coverage." ) out.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8") return True out = output_dir / "spec-cli-surface.md" pkg_dir = Path(str(node_cli["package_dir"])) pkg_json_rel = ( Path(str(node_cli["package_json"])).relative_to(analysis_root).as_posix() ) src_index = pkg_dir / "src" / "index.ts" src_cli = pkg_dir / "src" / "cli.ts" src_store = pkg_dir / "src" / "cli" / "store.ts" src_agents = pkg_dir / "src" / "agents" / "registry.ts" help_text = _extract_ts_backtick_const(src_index, "helpText") config_file = _extract_ts_string_const(src_store, "CONFIG_FILE") # Resolve common interpolations in helpText for higher-fidelity output. if help_text and src_agents.exists(): extracted = _extract_agents_from_registry_ts(src_agents) if extracted: agent_keys, alias_flags = extracted if agent_keys: help_text = help_text.replace( "${agentKeys.join('|')}", "|".join(agent_keys) ) alias_line = "" if alias_flags: alias_line = f" {' | '.join(alias_flags)} Agent alias flags\n" help_text = help_text.replace("${agentAliasLine}", alias_line) # Env vars: scan the src tree for process.env.<NAME> patterns. env_vars: list[str] = [] src_root = pkg_dir / "src" if src_root.exists(): pat = re.compile(r"\bprocess\.env\.([A-Z][A-Z0-9_]*)\b") found = set() for p in sorted(src_root.rglob("*")): if not p.is_file() or p.suffix.lower() not in ( ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ): continue for m in pat.finditer(_read_text(p)): found.add(m.group(1)) env_vars = sorted(found) lines: list[str] = [] lines.append(f"# CLI Surface Spec: {product_name}") lines.append("") lines.append(f"- Date: {date}") lines.append(f"- Analysis root: `{analysis_root}`") lines.append("") lines.append("## Entrypoints (Code-Proven)") lines.append("") lines.append(f"- Node package: `{pkg_dir.relative_to(analysis_root).as_posix()}`") lines.append(f"- package.json: `{pkg_json_rel}`") if node_cli.get("name"): lines.append(f"- package name: `{node_cli['name']}`") if node_cli.get("version"): lines.append(f"- version: `{node_cli['version']}`") lines.append("") lines.append("### Binaries") lines.append("") bin_map = node_cli.get("bin") or {} if isinstance(bin_map, dict) and bin_map: for k in sorted(bin_map.keys()): v = str(bin_map[k]) lines.append(f"- `{k}` -> `{v}`") else: lines.append("- _No `bin` mapping extracted (unexpected)._") if src_cli.exists(): lines.append("") lines.append("### Source Entry (Heuristic)") lines.append("") lines.append( f"- `{src_cli.relative_to(analysis_root).as_posix()}` (node shebang entry; typically calls `runCli`)" ) lines.append("") lines.append("## Usage / Help (Code-Proven Where Possible)") lines.append("") if help_text: lines.append("```text") lines.append(help_text.rstrip("\n")) lines.append("```") lines.append("") lines.append("Evidence:") lines.append( f"- `{src_index.relative_to(analysis_root).as_posix()}` (`helpText`)" ) else: lines.append("- _Help text not extracted (pattern not found)._") lines.append("Evidence:") lines.append(f"- `{src_index.relative_to(analysis_root).as_posix()}`") lines.append("") lines.append("## Config / Env (Code-Proven Where Possible)") lines.append("") wrote_any = False if config_file: lines.append(f"- User config file: `{config_file}` (loaded from CWD).") lines.append(f" Evidence: `{src_store.relative_to(analysis_root).as_posix()}`") wrote_any = True if env_vars: lines.append(f"- Environment variables: `{', '.join(env_vars)}`") lines.append( f" Evidence: scan of `{src_root.relative_to(analysis_root).as_posix()}` for `process.env.<NAME>`." ) wrote_any = True if not wrote_any: lines.append("- _No config/env surface extracted._") lines.append("") lines.append("## Notes For 1:1 Fidelity") lines.append("") lines.append( "- Treat `--help` output as the CLI contract; include it as a golden test fixture for regressions." ) lines.append( "- If the repo does not ship built artifacts (ex: `dist/`), building may be required to execute the CLI directly." ) out.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8") return True def _write_artifact_surface_spec( output_dir: Path, *, product_name: str, product_slug: str, date: str, analysis_root: Path, ) -> None: """ Higher-fidelity extraction of "what the product writes/installs" for template-driven CLIs. Emits: - spec-artifact-surface.md (human summary) - artifact-registry.json (machine-usable: manifests + template file hashes) """ out_md = output_dir / "spec-artifact-surface.md" out_json = output_dir / "artifact-registry.json" if not analysis_root.exists(): out_md.write_text( f"# Artifact Surface Spec: {product_name}\n\n- Date: {date}\n\n- _No repo content available to analyze._\n", encoding="utf-8", ) return node_cli = _find_node_cli_package(analysis_root, product_slug, product_name) if not node_cli: out_md.write_text( f"# Artifact Surface Spec: {product_name}\n\n- Date: {date}\n\n- _No Node CLI package detected; artifact extraction not implemented for this repo._\n", encoding="utf-8", ) return pkg_dir = Path(str(node_cli["package_dir"])) manifests_dir = pkg_dir / "templates" / "manifests" if not manifests_dir.exists(): out_md.write_text( f"# Artifact Surface Spec: {product_name}\n\n- Date: {date}\n\n" f"- _No `templates/manifests/` directory found under `{pkg_dir.relative_to(analysis_root).as_posix()}`._\n", encoding="utf-8", ) return manifest_files = sorted(manifests_dir.glob("*.json")) manifests: list[dict[str, object]] = [] resolved_sources: list[dict[str, object]] = [] for mf in manifest_files: try: data = json.loads(_read_text(mf)) except Exception: continue agent = None artifacts = data.get("artifacts") if isinstance(data, dict) else None if isinstance(artifacts, list): for a in artifacts: if isinstance(a, dict): when = a.get("when") if isinstance(when, dict) and isinstance(when.get("agent"), str): agent = when.get("agent") break manifests.append( { "path": mf.relative_to(analysis_root).as_posix(), "agent": agent, "raw": data, } ) # Build resolved source inventory (what files are copied from templates). if not isinstance(artifacts, list): continue placeholder_vars = {"AGENT": agent} if isinstance(agent, str) else {} for a in artifacts: if not isinstance(a, dict): continue source = a.get("source") if not isinstance(source, dict): continue stype = source.get("type") if stype == "templateDir": from_dir = source.get("fromDir") if not isinstance(from_dir, str): continue from_dir_res = ( _render_placeholders(from_dir, placeholder_vars) if placeholder_vars else from_dir ) abs_from = pkg_dir / from_dir_res if abs_from.exists() and abs_from.is_dir(): for fp in sorted(abs_from.rglob("*")): if not fp.is_file(): continue resolved_sources.append( { "manifest": mf.relative_to(analysis_root).as_posix(), "artifact_id": a.get("id"), "source_type": "templateDir", "from": from_dir_res, "file": fp.relative_to(pkg_dir).as_posix(), "sha256": _sha256_file(fp), } ) elif stype == "templateFile": from_file = source.get("from") if not isinstance(from_file, str): continue from_file_res = ( _render_placeholders(from_file, placeholder_vars) if placeholder_vars else from_file ) abs_from = pkg_dir / from_file_res if abs_from.exists() and abs_from.is_file(): resolved_sources.append( { "manifest": mf.relative_to(analysis_root).as_posix(), "artifact_id": a.get("id"), "source_type": "templateFile", "from": from_file_res, "file": abs_from.relative_to(pkg_dir).as_posix(), "sha256": _sha256_file(abs_from), } ) out_json.write_text( json.dumps( { "schema_version": 1, "product_name": product_name, "generated_at": date, "analysis_root": str(analysis_root), "node_package_dir": pkg_dir.relative_to(analysis_root).as_posix(), "manifests": manifests, "resolved_template_files": resolved_sources, }, indent=2, sort_keys=True, ) + "\n", encoding="utf-8", ) lines: list[str] = [] lines.append(f"# Artifact Surface Spec: {product_name}") lines.append("") lines.append(f"- Date: {date}") lines.append(f"- Analysis root: `{analysis_root}`") lines.append(f"- Node package: `{pkg_dir.relative_to(analysis_root).as_posix()}`") lines.append( f"- Manifests dir: `{manifests_dir.relative_to(analysis_root).as_posix()}`" ) lines.append(f"- Machine registry: `{out_json.relative_to(output_dir).as_posix()}`") lines.append("") lines.append("## Manifest Inventory (Code-Proven)") lines.append("") if manifest_files: for mf in manifest_files: rel = mf.relative_to(analysis_root).as_posix() agent = None for m in manifests: if m.get("path") == rel: agent = m.get("agent") break agent_note = f" (agent={agent})" if agent else "" lines.append(f"- `{rel}`{agent_note}") else: lines.append("- _No manifest JSON files found._") lines.append("") lines.append("## Template Source File Inventory (Hashed)") lines.append("") lines.append(f"- Files hashed: `{len(resolved_sources)}`") lines.append( "- Use `artifact-registry.json` as the source of truth for 1:1 template content equivalence." ) out_md.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8") def _get_upstream_commit(analysis_root: Path) -> str | None: """Return the HEAD commit SHA if analysis_root is a git repo, else None.""" git_dir = analysis_root / ".git" if not git_dir.exists(): return None try: sha = subprocess.check_output( ["git", "-C", str(analysis_root), "rev-parse", "HEAD"], text=True, stderr=subprocess.DEVNULL, ).strip() return sha if sha else None except Exception: return None def _collect_env_vars_with_evidence( analysis_root: Path, ) -> list[dict[str, object]]: """ Scan source files for environment variable references and return a sorted list with per-var file evidence. Covers: - TypeScript/JavaScript: process.env.VAR_NAME - Python: os.environ['VAR'] / os.environ.get('VAR') / os.getenv('VAR') - Go: os.Getenv("VAR") / os.LookupEnv("VAR") - Shell: $VAR_NAME (upper-snake only, cap at 300 files) """ var_files: dict[str, set[str]] = {} patterns: list[tuple[re.Pattern[str], set[str]]] = [ ( re.compile(r"\bprocess\.env\.([A-Z][A-Z0-9_]+)\b"), {".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"}, ), ( re.compile( r"""os\.environ(?:\.get)?\s*\(\s*['"]([A-Z][A-Z0-9_]+)['"]\s*\)""" ), {".py"}, ), ( re.compile(r"""\bos\.getenv\s*\(\s*['"]([A-Z][A-Z0-9_]+)['"]\s*\)"""), {".py"}, ), ( re.compile( r"""\bos\.(?:Getenv|LookupEnv)\s*\(\s*"([A-Z][A-Z0-9_]+)"\s*\)""" ), {".go"}, ), ( re.compile(r"\$\{?([A-Z][A-Z0-9_]{2,})\}?"), {".sh", ".bash", ".env", ".envrc"}, ), ] scanned = 0 for p in sorted(analysis_root.rglob("*")): if not p.is_file(): continue # Skip irrelevant dirs skip_dirs = { "node_modules", ".git", ".venv", "vendor", "testdata", "__pycache__", } if any(part in skip_dirs for part in p.parts): continue suffix = p.suffix.lower() matching_pats = [pat for pat, suffixes in patterns if suffix in suffixes] if not matching_pats: continue scanned += 1 if scanned > 500: break try: text = _read_text(p) except Exception: continue rel = p.relative_to(analysis_root).as_posix() for pat in matching_pats: for m in pat.finditer(text): name = m.group(1) var_files.setdefault(name, set()).add(rel) result: list[dict[str, object]] = [] for var_name in sorted(var_files.keys()): result.append( { "name": var_name, "files": sorted(var_files[var_name]), } ) return result def _collect_schema_files(analysis_root: Path) -> list[str]: """ Return sorted relative paths of schema-like files in the repo. Matches: *.schema.json, *schema*.json, openapi*.json/yaml, swagger*.json/yaml, *.proto, *.avsc, *.thrift, graphql schema files. """ schema_patterns = [ "**/*.schema.json", "**/*schema*.json", "**/openapi*.json", "**/openapi*.yaml", "**/openapi*.yml", "**/swagger*.json", "**/swagger*.yaml", "**/swagger*.yml", "**/*.proto", "**/*.avsc", "**/*.thrift", "**/schema.graphql", "**/*.graphql", ] skip_dirs = {"node_modules", ".git", ".venv", "vendor", "testdata", "__pycache__"} found: set[str] = set() for pattern in schema_patterns: for p in analysis_root.glob(pattern): if not p.is_file(): continue if any(part in skip_dirs for part in p.relative_to(analysis_root).parts): continue found.add(p.relative_to(analysis_root).as_posix()) return sorted(found) def _collect_config_files(analysis_root: Path) -> list[str]: """ Return sorted relative paths of config files commonly read at runtime. Matches common config naming patterns at any depth (capped at 300 files). """ config_name_patterns = re.compile( r"^(config|configuration|settings|\.env|app\.config|appsettings" r"|pyproject|setup\.cfg|cargo\.toml|go\.mod|tsconfig|jest\.config" r"|webpack\.config|vite\.config|babel\.config|eslint.*|\.eslintrc.*" r"|prettier.*|\.prettierrc.*)(\.(json|yaml|yml|toml|ini|cfg|js|ts|cjs|mjs))?$", re.IGNORECASE, ) skip_dirs = {"node_modules", ".git", ".venv", "vendor", "testdata", "__pycache__"} found: set[str] = set() count = 0 for p in sorted(analysis_root.rglob("*")): if not p.is_file(): continue if any(part in skip_dirs for part in p.relative_to(analysis_root).parts): continue if config_name_patterns.match(p.name): found.add(p.relative_to(analysis_root).as_posix()) count += 1 if count >= 300: break return sorted(found) def _write_repo_contract_json( output_dir: Path, analysis_root: Path, *, product_name: str, product_slug: str, ) -> Path: """ Write a deterministic, machine-checkable contract JSON to output_dir/contracts/repo-contract.json. Contract includes: - upstream_commit (if analysis_root is a git repo) - cli surface: bin map, help text (static extraction), config file, env vars with file evidence - manifest inventory + template file hashes (from artifact-registry.json if present) - schema-like files - config files discovered in repo No absolute paths, no dates — stable across runs on the same commit. """ contracts_dir = output_dir / "contracts" contracts_dir.mkdir(parents=True, exist_ok=True) out_path = contracts_dir / "repo-contract.json" contract: dict[str, object] = { "schema_version": 1, "product_name": product_name, } # upstream_commit upstream_commit = _get_upstream_commit(analysis_root) if upstream_commit: contract["upstream_commit"] = upstream_commit # --- CLI surface --- cli_surface: dict[str, object] = {} node_cli = _find_node_cli_package(analysis_root, product_slug, product_name) python_cli_info = _find_python_cli(analysis_root) if node_cli is None else None go_cli_info = ( _find_go_cli(analysis_root) if node_cli is None and python_cli_info is None else None ) if node_cli: pkg_dir = Path(str(node_cli["package_dir"])) # bin map with relative paths raw_bin = node_cli.get("bin") or {} bin_map: dict[str, str] = {} if isinstance(raw_bin, dict): for k, v in raw_bin.items(): bin_map[k] = v cli_surface["language"] = "node" cli_surface["package_json"] = ( Path(str(node_cli["package_json"])).relative_to(analysis_root).as_posix() ) cli_surface["package_dir"] = pkg_dir.relative_to(analysis_root).as_posix() cli_surface["package_name"] = str(node_cli.get("name") or "") cli_surface["bin"] = {k: bin_map[k] for k in sorted(bin_map)} # Help text (static extraction) src_index = pkg_dir / "src" / "index.ts" src_agents = pkg_dir / "src" / "agents" / "registry.ts" help_text = _extract_ts_backtick_const(src_index, "helpText") if help_text and src_agents.exists(): extracted = _extract_agents_from_registry_ts(src_agents) if extracted: agent_keys, alias_flags = extracted if agent_keys: help_text = help_text.replace( "${agentKeys.join('|')}", "|".join(agent_keys) ) alias_line = "" if alias_flags: alias_line = f" {' | '.join(alias_flags)} Agent alias flags\n" help_text = help_text.replace("${agentAliasLine}", alias_line) if help_text is not None: cli_surface["help_text"] = help_text cli_surface["help_text_source"] = ( src_index.relative_to(analysis_root).as_posix() if src_index.exists() else None ) # Config file from store.ts src_store = pkg_dir / "src" / "cli" / "store.ts" config_file = _extract_ts_string_const(src_store, "CONFIG_FILE") if config_file: cli_surface["config_file"] = config_file cli_surface["config_file_source"] = ( src_store.relative_to(analysis_root).as_posix() if src_store.exists() else None ) elif python_cli_info: raw_bin_py = python_cli_info.get("bin") or {} cli_surface["language"] = "python" cli_surface["framework"] = python_cli_info.get("framework") cli_surface["entry_module"] = python_cli_info.get("entry_module") cli_surface["bin"] = ( {k: str(raw_bin_py[k]) for k in sorted(raw_bin_py)} if isinstance(raw_bin_py, dict) else {} ) elif go_cli_info: raw_bin_go = go_cli_info.get("bin") or {} cli_surface["language"] = "go" cli_surface["framework"] = go_cli_info.get("framework") cli_surface["module"] = go_cli_info.get("module") cli_surface["bin"] = ( {k: str(raw_bin_go[k]) for k in sorted(raw_bin_go)} if isinstance(raw_bin_go, dict) else {} ) contract["cli"] = cli_surface # --- Env vars with per-var file evidence --- contract["env_vars"] = _collect_env_vars_with_evidence(analysis_root) # --- Manifest inventory + template file hashes --- artifact_registry_path = output_dir / "artifact-registry.json" if artifact_registry_path.exists(): try: artifact_data = json.loads(_read_text(artifact_registry_path)) manifests_raw = artifact_data.get("manifests") or [] template_files_raw = artifact_data.get("resolved_template_files") or [] # Manifests: keep only path and agent (drop raw JSON for contract stability) manifests_clean: list[dict[str, object]] = [] for m in manifests_raw: entry: dict[str, object] = {"path": m.get("path")} if m.get("agent"): entry["agent"] = m["agent"] manifests_clean.append(entry) # Template files: keep path, sha256 (no absolute paths; already relative in artifact-registry) template_hashes: list[dict[str, object]] = [] for tf in template_files_raw: template_hashes.append( { "file": tf.get("file"), "manifest": tf.get("manifest"), "sha256": tf.get("sha256"), "source_type": tf.get("source_type"), } ) contract["manifests"] = sorted( manifests_clean, key=lambda x: str(x.get("path", "")) ) contract["template_files"] = sorted( template_hashes, key=lambda x: str(x.get("file", "")) ) except Exception: pass # --- Schema-like files --- contract["schema_files"] = _collect_schema_files(analysis_root) # --- Config files --- contract["config_files"] = _collect_config_files(analysis_root) out_path.write_text( json.dumps(contract, indent=2, sort_keys=True) + "\n", encoding="utf-8", ) return out_path def _write_comparison_report( output_dir: Path, tmp_dir: Path, *, product_name: str, date: str, ) -> bool: """Write comparison-report.md contrasting binary vs repo analysis results. Returns True if a report was written. """ # --- Command discovery --- binary_cmds: list[str] = [] commands_file = tmp_dir / "binary" / "cli-commands.txt" if commands_file.exists(): binary_cmds = [ c.strip() for c in commands_file.read_text(encoding="utf-8").splitlines() if c.strip() ] repo_cmds: list[str] = [] repo_cli_spec = output_dir / "spec-cli-surface.md" if repo_cli_spec.exists(): # Extract command names from the table rows (| `cmd` | ... |) text = repo_cli_spec.read_text(encoding="utf-8") for m in re.finditer(r"^\|\s*`([^`]+)`\s*\|", text, re.MULTILINE): cmd = m.group(1).strip() if cmd and cmd not in ("Command",): repo_cmds.append(cmd) binary_set = set(binary_cmds) repo_set = set(repo_cmds) only_binary = sorted(binary_set - repo_set) only_repo = sorted(repo_set - binary_set) delta = len(binary_cmds) - len(repo_cmds) # --- Registry groups --- binary_groups = 0 _repo_groups = 0 registry_yaml = output_dir / "feature-registry.yaml" if registry_yaml.exists(): text = registry_yaml.read_text(encoding="utf-8") in_groups = False for raw_line in text.splitlines(): line = raw_line.rstrip() if line == "groups:": in_groups = True continue if not in_groups: continue # Group entries are 2-space indented, end with ':' if ( line.startswith(" ") and not line.startswith(" ") and line.rstrip().endswith(":") ): # Determine source from notes field binary_groups += 1 # For the comparison we count total groups; binary-enriched have "binary-symbols.txt" anchor binary_enriched = 0 repo_scaffold = 0 for raw_line in text.splitlines(): stripped = raw_line.strip() if stripped == "- binary-symbols.txt": binary_enriched += 1 # Groups without binary anchor are repo-scaffolded repo_scaffold = binary_groups - binary_enriched # --- Coverage percentage --- if repo_cmds: coverage_pct = round(len(binary_set & repo_set) / len(repo_set) * 100) coverage_line = ( f"Binary analysis found {coverage_pct}% of repo-discovered commands." ) elif binary_cmds: coverage_line = f"Binary analysis found {len(binary_cmds)} commands; repo analysis found none (no CLI detected in repo)." else: coverage_line = "Neither source discovered CLI commands." # --- Write report --- lines: list[str] = [] lines.append(f"# Comparison Report: {product_name}") lines.append("") lines.append(f"**Date:** {date}") lines.append("**Mode:** both (binary + repo)") lines.append("") lines.append("## Command Discovery") lines.append("") lines.append("| Source | Commands Found |") lines.append("|--------|---------------|") lines.append(f"| Binary --help | {len(binary_cmds)} |") lines.append(f"| Repo analysis | {len(repo_cmds)} |") delta_str = f"+{delta}" if delta > 0 else str(delta) lines.append(f"| Delta | {delta_str} |") lines.append("") lines.append("## Commands Only in Binary") lines.append("") if only_binary: for cmd in only_binary: lines.append(f"- `{cmd}`") else: lines.append("_None._") lines.append("") lines.append("## Commands Only in Repo") lines.append("") if only_repo: for cmd in only_repo: lines.append(f"- `{cmd}`") else: lines.append("_None._") lines.append("") lines.append("## Registry Groups") lines.append("") lines.append("| Source | Groups |") lines.append("|--------|--------|") lines.append(f"| Binary enriched | {binary_enriched} |") lines.append(f"| Repo scaffold | {repo_scaffold} |") lines.append("") lines.append("## Summary") lines.append("") lines.append(coverage_line) out = output_dir / "comparison-report.md" out.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8") return True def _write_wrapper_validate_feature_registry(output_dir: Path) -> None: skill_validate_path = ( SKILL_DIR / "scripts" / "validate_feature_registry.py" ).resolve() wrapper = output_dir / "validate-feature-registry.py" wrapper.write_text( f"""#!/usr/bin/env python3 from __future__ import annotations import os import subprocess import sys from pathlib import Path HERE = Path(__file__).resolve().parent SKILL_VALIDATE_CANDIDATES = [ Path({str(skill_validate_path)!r}), Path(__file__).resolve().parents[3] / "skills" / "reverse-engineer" / "scripts" / "validate_feature_registry.py", Path(__file__).resolve().parents[2] / "skills" / "reverse-engineer" / "scripts" / "validate_feature_registry.py", Path.cwd() / "skills" / "reverse-engineer" / "scripts" / "validate_feature_registry.py", ] def _resolve_validator() -> Path: for cand in SKILL_VALIDATE_CANDIDATES: if cand.exists(): return cand raise FileNotFoundError("Could not locate validate_feature_registry.py") def main() -> int: # Delegate to the canonical validator, but default paths to this output dir. args = sys.argv[1:] if not args: root_path = HERE / "analysis-root-path.txt" local_root = (root_path.read_text(encoding="utf-8").strip() if root_path.exists() else str(HERE / "analysis-root")) args = [ "--feature-registry", str(HERE / "feature-registry.yaml"), "--docs-features", str(HERE / "docs-features.txt"), "--local-clone-dir", local_root, ] validator = _resolve_validator() p = subprocess.run([sys.executable, str(validator), *args]) return p.returncode if __name__ == "__main__": raise SystemExit(main()) """, encoding="utf-8", ) wrapper.chmod(0o755) def _copy_security_validators(output_dir: Path) -> None: sec_dir = output_dir / "security" _ensure_dirs([sec_dir]) # Copy validator + secret scan + sbom generator so the audit folder is self-validating. for rel in [ "scripts/security/validate_security_audit.sh", "scripts/security/scan_secrets.sh", "scripts/security/generate_sbom.sh", ]: src = SKILL_DIR / rel dst = sec_dir / Path(rel).name.replace("_", "-") dst.write_text(src.read_text(encoding="utf-8"), encoding="utf-8") dst.chmod(0o755) def _git_text(repo: Path, *args: str) -> str: return subprocess.check_output( ["git", "-C", str(repo), *args], text=True, stderr=subprocess.STDOUT ).strip() def _is_git_checkout(path: Path) -> bool: try: return _git_text(path, "rev-parse", "--is-inside-work-tree") == "true" except (OSError, subprocess.CalledProcessError): return False def _write_source_metadata( output_dir: Path, *, upstream_repo: str | None, upstream_ref: str | None, resolved_commit: str, source_kind: str, ) -> None: payload = { "upstream_repo": upstream_repo, "upstream_ref": upstream_ref, "resolved_commit": resolved_commit, "source_kind": source_kind, "clone_date": _today_ymd(), } (output_dir / "clone-metadata.json").write_te -
scaffold_feature_registry.py 2.2 KB
#!/usr/bin/env python3 from __future__ import annotations import argparse import datetime as _dt from pathlib import Path def _group_from_slug(slug: str, docs_features_prefix: str) -> str | None: prefix = docs_features_prefix.strip("/").rstrip("/") + "/" s = slug.strip().lstrip("/") if not s.startswith(prefix): return None rest = s[len(prefix) :] if not rest: return None group = rest.split("/", 1)[0].strip() return group or None def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--product-name", required=True) ap.add_argument("--docs-features-prefix", required=True) ap.add_argument("--docs-features", required=True) ap.add_argument("--out", required=True) args = ap.parse_args() docs_features_prefix = args.docs_features_prefix slugs = [ln.strip() for ln in Path(args.docs_features).read_text(encoding="utf-8", errors="replace").splitlines() if ln.strip()] groups: list[str] = [] seen = set() for slug in slugs: g = _group_from_slug(slug, docs_features_prefix) if not g: continue if g not in seen: groups.append(g) seen.add(g) out = Path(args.out) out.parent.mkdir(parents=True, exist_ok=True) # Minimal YAML that is still easy to mechanically validate. lines: list[str] = [] lines.append("schema_version: 1") lines.append(f"product_name: {args.product_name!r}") lines.append(f"generated_at: {_dt.date.today().isoformat()!r}") lines.append(f"docs_features_prefix: {docs_features_prefix!r}") lines.append("docs_features:") for s in slugs: lines.append(f" - {s!r}") lines.append("groups:") if not groups and slugs: # Slugs existed but no groups parsed; keep explicit empty mapping to fail validation loudly later. lines.append(" {}") elif not groups: lines.append(" {}") else: for g in groups: lines.append(f" {g!s}:") lines.append(" impl: control-plane") lines.append(" anchors: []") lines.append(" notes: \"\"") out.write_text("\n".join(lines) + "\n", encoding="utf-8") return 0 if __name__ == "__main__": raise SystemExit(main()) -
self_test.sh 14.5 KB
#!/usr/bin/env bash set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" SKILL="$ROOT/skills/reverse-engineer" if ! command -v go >/dev/null 2>&1; then echo "error: go is required for the demo fixture build" >&2 exit 2 fi TMP="$ROOT/.tmp/reverse-engineer-self-test" OUT1="$TMP/out-core" OUT2="$TMP/out-sec" SRC="$TMP/fixture-src" BIN="$TMP/demo_bin" SITEMAP="$TMP/sitemap.xml" rm -rf "$TMP" mkdir -p "$SRC" "$OUT1" "$OUT2" HELP="$(python3 "$SKILL/scripts/reverse_engineer.py" --help)" grep -Fq '.agents/scratch/reverse-engineer/<product>/' <<<"$HELP" grep -Fq '.agents/research/<product>/ path remains' <<<"$HELP" grep -Fq 'are never moved automatically.' <<<"$HELP" grep -Fq -- "- '.agents/scratch/reverse-engineer/*/'" "$SKILL/SKILL.md" echo "OK: output-path migration contract is visible in --help" python3 - "$SRC" <<'PY' import sys, zipfile from pathlib import Path src = Path(sys.argv[1]) (src / "payload.zip").parent.mkdir(parents=True, exist_ok=True) with zipfile.ZipFile(src / "payload.zip", "w", compression=zipfile.ZIP_DEFLATED) as zf: zf.writestr("agent/main.py", "print('hello from demo agent')\n") zf.writestr("agent/README.md", "# Demo Agent\n") zf.writestr("agent/SYSTEM_PROMPT.txt", "DEMO PROMPT (do not dump in reports)\n") PY cat >"$SRC/main.go" <<'EOF' package main import _ "embed" import "fmt" //go:embed payload.zip var payload []byte func main() { // Ensure the bytes are referenced so the ZIP signature is present in the binary. fmt.Printf("demo binary; embedded payload bytes=%d\n", len(payload)) } EOF (cat >"$SRC/go.mod" <<'EOF' module demo_embedded_zip go 1.22 EOF ) (cd "$SRC" && go build -o "$BIN" .) cat >"$SITEMAP" <<'EOF' <?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <url><loc>https://example.test/docs/features/alpha/overview</loc></url> <url><loc>https://example.test/docs/features/alpha/howto</loc></url> <url><loc>https://example.test/docs/features/beta/overview</loc></url> </urlset> EOF python3 "$SKILL/scripts/reverse_engineer.py" demo \ --authorized \ --mode=binary \ --binary-path="$BIN" \ --docs-sitemap-url="file://$SITEMAP" \ --materialize-archives \ --local-clone-dir="$TMP/local-demo" \ --output-dir="$OUT1" python3 "$OUT1/validate-feature-registry.py" VALIDATE_OUTPUT="$SKILL/scripts/validate-output.sh" "$VALIDATE_OUTPUT" --output-dir "$OUT1" --phase teardown \ --security-audit 0 --sbom 0 --upstream-ref-set 0 if "$VALIDATE_OUTPUT" --output-dir "$OUT1" --phase complete \ --security-audit 0 --sbom 0 --upstream-ref-set 0 >/dev/null 2>&1; then echo "FAIL: complete validator accepted a missing steal-map.md" >&2 exit 1 fi cat >"$OUT1/steal-map.md" <<'EOF' # Steal map: demo | Their capability | Our surface today | Verdict | |---|---|---| | Embedded archive inventory (`feature-registry.yaml`) | `skills/reverse-engineer/` | **have** | EOF "$VALIDATE_OUTPUT" --output-dir "$OUT1" --phase complete \ --security-audit 0 --sbom 0 --upstream-ref-set 0 cp "$OUT1/steal-map.md" "$OUT1/steal-map.valid" printf '# malformed map\n' >"$OUT1/steal-map.md" if "$VALIDATE_OUTPUT" --output-dir "$OUT1" --phase complete \ --security-audit 0 --sbom 0 --upstream-ref-set 0 >/dev/null 2>&1; then echo "FAIL: complete validator accepted a malformed steal-map.md" >&2 exit 1 fi mv "$OUT1/steal-map.valid" "$OUT1/steal-map.md" echo "OK: exact output validator distinguishes teardown from complete decision output" # --- Binary mode capability assertions --- echo "--- binary mode capability checks ---" # 1. Help capture output exists (may be empty if binary doesn't support --help) if [ ! -f "$OUT1/cli-commands.txt" ]; then echo "FAIL: cli-commands.txt not created by binary mode" >&2 exit 1 fi echo "OK: cli-commands.txt exists" # 2. CLI surface spec exists (generated from --help tree or binary strings fallback) if [ ! -f "$OUT1/spec-cli-surface.md" ]; then echo "FAIL: spec-cli-surface.md not created by binary mode" >&2 exit 1 fi echo "OK: spec-cli-surface.md exists" # 3. binary-symbols.txt exists if [ ! -f "$OUT1/binary-symbols.txt" ]; then echo "FAIL: binary-symbols.txt not created by binary mode" >&2 exit 1 fi echo "OK: binary-symbols.txt exists" # 4. Registry enrichment: if cli-commands.txt has content, groups should have impl: client if [ -s "$OUT1/cli-commands.txt" ]; then if ! grep -q 'impl: client' "$OUT1/feature-registry.yaml"; then echo "FAIL: feature-registry.yaml should contain 'impl: client' when CLI commands are found" >&2 exit 1 fi echo "OK: feature-registry.yaml enriched with impl: client" else echo "OK: cli-commands.txt empty (demo binary has no subcommands); skipping impl: client check" fi python3 "$SKILL/scripts/reverse_engineer.py" demo \ --authorized \ --mode=binary \ --binary-path="$BIN" \ --docs-sitemap-url="file://$SITEMAP" \ --output-dir="$OUT2" \ --materialize-archives \ --local-clone-dir="$TMP/local-demo" \ --security-audit \ --sbom # The freshly generated audit is a scaffold whose files carry _TBD # placeholders; the gate must now REFUSE to certify it (fail-closed). if "$OUT2/security/validate-security-audit.sh" "$OUT2" --sbom >/dev/null 2>&1; then echo "FAIL: security gate certified an unfilled _TBD scaffold (should fail-closed)" >&2 exit 1 fi echo "OK: security gate rejects the unfilled _TBD scaffold" # Fill EVERY required narrative file (no placeholders). The other files just # need real content; findings.md additionally needs the Evidence/Fix shape. for name in threat-model attack-surface dataflow crypto-review authn-authz reproducibility; do printf '# %s\n\nReviewed for the demo binary; no items of concern.\n' "$name" > "$OUT2/security/$name.md" done cat >"$OUT2/security/findings.md" <<'EOF' # Findings: demo - Date: self-test ## Finding F-001: Embedded demo prompt present in binary Severity: Low Impact: Informational; the embedded demo prompt is not a secret. Likelihood: Low Evidence: payload.zip/agent/SYSTEM_PROMPT.txt embedded via go:embed (see binary-embedded-archives.md). Fix: None required for the demo; production binaries should not embed plaintext prompts. Validation: Re-ran the secret scan over outputs; no credentials present. EOF "$OUT2/security/validate-security-audit.sh" "$OUT2" --sbom echo "OK: security gate certifies a completed audit" # Prove the _TBD gate scans BEYOND findings.md: seed a placeholder into a # different required file and the gate must fail-closed again. printf '# threat-model\n\n- _TBD_\n' > "$OUT2/security/threat-model.md" if "$OUT2/security/validate-security-audit.sh" "$OUT2" --sbom >/dev/null 2>&1; then echo "FAIL: security gate certified an audit with _TBD in threat-model.md (should fail-closed)" >&2 exit 1 fi echo "OK: security gate rejects _TBD in a non-findings required file" # --- Negative tests --- # Test: invalid --mode should fail echo "--- negative test: invalid --mode ---" if python3 "$SKILL/scripts/reverse_engineer.py" demo --mode=invalid --output-dir="$TMP/out-neg" 2>/dev/null; then echo "FAIL: expected non-zero exit for --mode=invalid" >&2 exit 1 fi echo "OK: invalid --mode correctly rejected" # --- Upstream ref pinning test --- echo "--- upstream-ref pinning test ---" OUT_REF="$TMP/out-ref" mkdir -p "$OUT_REF" # Use file:// protocol on the current repo to avoid network dependency. REPO_URL="file://$ROOT" python3 "$SKILL/scripts/reverse_engineer.py" self-ref-test \ --mode=repo \ --upstream-repo="$REPO_URL" \ --upstream-ref=HEAD \ --local-clone-dir="$TMP/local-ref" \ --output-dir="$OUT_REF" if [ ! -f "$OUT_REF/clone-metadata.json" ]; then echo "FAIL: clone-metadata.json not created with --upstream-ref" >&2 exit 1 fi echo "OK: clone-metadata.json created with --upstream-ref" echo "--- existing-checkout ref mismatch test ---" WRONG_REPO="$TMP/local-wrong-ref" WRONG_OUT="$TMP/out-wrong-ref" mkdir -p "$WRONG_REPO" git -C "$WRONG_REPO" init -q git -C "$WRONG_REPO" config user.name reverse-self-test git -C "$WRONG_REPO" config user.email reverse-self-test@example.invalid printf 'one\n' >"$WRONG_REPO/unique.txt" git -C "$WRONG_REPO" add unique.txt git -C "$WRONG_REPO" commit -qm one first_commit="$(git -C "$WRONG_REPO" rev-parse HEAD)" printf 'two\n' >"$WRONG_REPO/unique.txt" git -C "$WRONG_REPO" commit -qam two second_commit="$(git -C "$WRONG_REPO" rev-parse HEAD)" git -C "$WRONG_REPO" checkout -q --detach "$first_commit" if python3 "$SKILL/scripts/reverse_engineer.py" wrong-ref \ --mode=repo --local-clone-dir="$WRONG_REPO" \ --upstream-ref="$second_commit" --output-dir="$WRONG_OUT" >/dev/null 2>&1; then echo "FAIL: existing checkout at the wrong commit was analyzed" >&2 exit 1 fi if [ -e "$WRONG_OUT/feature-registry.yaml" ]; then echo "FAIL: ref mismatch wrote trusted teardown artifacts" >&2 exit 1 fi echo "OK: existing checkout must match the requested ref" echo "--- explicit non-Git root test ---" EXPLICIT_TREE="$TMP/explicit-nongit" EXPLICIT_OUT="$TMP/out-explicit-nongit" mkdir -p "$EXPLICIT_TREE" printf 'only-in-explicit-tree\n' >"$EXPLICIT_TREE/unique-source.txt" python3 "$SKILL/scripts/reverse_engineer.py" explicit-nongit \ --mode=repo --local-clone-dir="$EXPLICIT_TREE" --output-dir="$EXPLICIT_OUT" if ! grep -Fqx "$EXPLICIT_TREE" "$EXPLICIT_OUT/analysis-root-path.txt"; then echo "FAIL: explicit non-Git tree was replaced by the caller checkout" >&2 exit 1 fi echo "OK: explicit non-Git analysis root wins" echo "--- output symlink refusal tests ---" SYMLINK_CASE="$TMP/symlink-case" SYMLINK_OUTSIDE="$TMP/symlink-outside" mkdir -p "$SYMLINK_CASE/.agents" "$SYMLINK_OUTSIDE" "$SYMLINK_CASE/local" printf 'outside sentinel\n' >"$SYMLINK_OUTSIDE/sentinel" ln -s "$SYMLINK_OUTSIDE" "$SYMLINK_CASE/.agents/scratch" if ( cd "$SYMLINK_CASE" python3 "$SKILL/scripts/reverse_engineer.py" escaped \ --mode=repo --local-clone-dir="$SYMLINK_CASE/local" >/dev/null 2>&1 ); then echo "FAIL: default output followed a symlinked scratch parent" >&2 exit 1 fi if ! grep -Fqx 'outside sentinel' "$SYMLINK_OUTSIDE/sentinel" \ || [ -e "$SYMLINK_OUTSIDE/reverse-engineer" ]; then echo "FAIL: symlinked parent allowed an outside write" >&2 exit 1 fi MANAGED_OUT="$TMP/out-managed-link" MANAGED_OUTSIDE="$TMP/managed-outside.yaml" mkdir -p "$MANAGED_OUT" printf 'outside registry\n' >"$MANAGED_OUTSIDE" ln -s "$MANAGED_OUTSIDE" "$MANAGED_OUT/feature-registry.yaml" if python3 "$SKILL/scripts/reverse_engineer.py" managed-link \ --mode=repo --local-clone-dir="$EXPLICIT_TREE" \ --output-dir="$MANAGED_OUT" >/dev/null 2>&1; then echo "FAIL: managed artifact symlink was followed" >&2 exit 1 fi if ! grep -Fqx 'outside registry' "$MANAGED_OUTSIDE"; then echo "FAIL: managed artifact symlink changed the outside target" >&2 exit 1 fi echo "OK: output parent and managed-file symlinks fail closed" # --- Multi-language CLI graceful degradation test --- echo "--- multi-language CLI degradation test ---" OUT_NONCLI="$TMP/out-noncli" mkdir -p "$OUT_NONCLI" "$TMP/local-noncli" # Create a minimal repo with no CLI markers. mkdir -p "$TMP/local-noncli/.git" touch "$TMP/local-noncli/README.md" python3 "$SKILL/scripts/reverse_engineer.py" no-cli-demo \ --mode=repo \ --local-clone-dir="$TMP/local-noncli" \ --output-dir="$OUT_NONCLI" \ --docs-sitemap-url="file://$SITEMAP" # spec-cli-surface.md should NOT exist (no CLI detected), and the note should be in spec-code-map.md if [ -f "$OUT_NONCLI/spec-cli-surface.md" ]; then echo "FAIL: spec-cli-surface.md should not exist for non-CLI repo" >&2 exit 1 fi if ! grep -q "no CLI surface detected" "$OUT_NONCLI/spec-code-map.md" 2>/dev/null; then echo "FAIL: spec-code-map.md should note that no CLI surface was detected" >&2 exit 1 fi echo "OK: multi-language CLI graceful degradation works" echo "--- default output-path parity test ---" DEFAULT_OUT="$TMP/.agents/scratch/reverse-engineer/default-demo" ( cd "$TMP" python3 "$SKILL/scripts/reverse_engineer.py" default-demo \ --mode=repo \ --local-clone-dir="$TMP/local-noncli" \ --docs-sitemap-url="file://$SITEMAP" ) if [ ! -s "$DEFAULT_OUT/feature-registry.yaml" ] \ || [ ! -s "$DEFAULT_OUT/contracts/repo-contract.json" ] \ || [ ! -s "$DEFAULT_OUT/reports/$(date +%F)-vibe-default-demo.md" ] \ || [ ! -s "$DEFAULT_OUT/docs-features.txt" ] \ || [ ! -s "$DEFAULT_OUT/validate-feature-registry.py" ]; then echo "FAIL: executable default did not emit the declared product output directory" >&2 exit 1 fi echo "OK: frontmatter output directory matches the executable default" echo "--- earlier output-path compatibility test ---" LEGACY_OUT="$TMP/.agents/research/legacy-demo" LEGACY_EXPECTED="$TMP/legacy-sentinel.expected" LEGACY_DEFAULT="$TMP/.agents/scratch/reverse-engineer/legacy-demo" mkdir -p "$LEGACY_OUT" printf 'caller-owned sentinel\n\n' > "$LEGACY_OUT/caller-sentinel.txt" cp "$LEGACY_OUT/caller-sentinel.txt" "$LEGACY_EXPECTED" ( cd "$TMP" python3 "$SKILL/scripts/reverse_engineer.py" legacy-demo \ --mode=repo \ --local-clone-dir="$TMP/local-noncli" \ --output-dir="$LEGACY_OUT" \ --docs-sitemap-url="file://$SITEMAP" ) if [ ! -s "$LEGACY_OUT/feature-registry.yaml" ]; then echo "FAIL: explicit earlier-default output directory was not honored" >&2 exit 1 fi if ! cmp -s "$LEGACY_EXPECTED" "$LEGACY_OUT/caller-sentinel.txt"; then echo "FAIL: explicit earlier-default invocation changed a pre-existing artifact" >&2 exit 1 fi if [ -e "$LEGACY_DEFAULT" ]; then echo "FAIL: explicit earlier-default invocation also wrote to the scratch default" >&2 exit 1 fi echo "OK: explicit earlier-default output directory remains supported" echo "--- generated-tree hygiene regression test ---" HYGIENE_REPO="$TMP/local-hygiene" HYGIENE_OUT="$TMP/out-hygiene" mkdir -p "$HYGIENE_REPO/.tmp/compound-engineer" "$HYGIENE_OUT" (cd "$HYGIENE_REPO" && git init >/dev/null 2>&1) cat >"$HYGIENE_REPO/package.json" <<'EOF' { "name": "agentops", "version": "0.0.1", "bin": { "agentops": "bin/agentops.js" } } EOF cat >"$HYGIENE_REPO/.tmp/compound-engineer/package.json" <<'EOF' { "name": "@every-env/compound-plugin", "version": "9.9.9", "bin": { "compound-plugin": "bin/index.js" } } EOF python3 "$SKILL/scripts/reverse_engineer.py" agentops \ --mode=repo \ --local-clone-dir="$HYGIENE_REPO" \ --output-dir="$HYGIENE_OUT" if grep -q "\.tmp/compound-engineer" "$HYGIENE_OUT/spec-cli-surface.md"; then echo "FAIL: generated-tree package leaked into CLI surface spec" >&2 exit 1 fi if ! grep -q "package name: \`agentops\`" "$HYGIENE_OUT/spec-cli-surface.md"; then echo "FAIL: root package did not win CLI surface detection" >&2 exit 1 fi echo "OK: generated-tree hygiene regression holds" echo "OK: self-test passed (all positive + negative tests)" -
validate-output.sh 4.2 KB
#!/usr/bin/env bash set -euo pipefail usage() { cat >&2 <<'EOF' usage: validate-output.sh --output-dir DIR [--phase teardown|complete] [--security-audit 0|1] [--sbom 0|1] [--upstream-ref-set 0|1] EOF exit 2 } output_dir="" phase="complete" security_audit=0 sbom=0 upstream_ref_set=0 while (($#)); do case "$1" in --output-dir) (($# >= 2)) || usage; output_dir=$2; shift 2 ;; --phase) (($# >= 2)) || usage; phase=$2; shift 2 ;; --security-audit) (($# >= 2)) || usage; security_audit=$2; shift 2 ;; --sbom) (($# >= 2)) || usage; sbom=$2; shift 2 ;; --upstream-ref-set) (($# >= 2)) || usage; upstream_ref_set=$2; shift 2 ;; -h|--help) usage ;; *) usage ;; esac done [[ -n "$output_dir" ]] || usage [[ "$phase" == teardown || "$phase" == complete ]] || usage [[ "$security_audit" =~ ^[01]$ ]] || usage [[ "$sbom" =~ ^[01]$ ]] || usage [[ "$upstream_ref_set" =~ ^[01]$ ]] || usage [[ -d "$output_dir" && ! -L "$output_dir" ]] || { echo "error: output directory must be a real directory: $output_dir" >&2 exit 1 } required=( feature-inventory.md feature-registry.yaml feature-catalog.md spec-architecture.md spec-code-map.md spec-clone-vs-use.md spec-clone-mvp.md analysis-root-path.txt validate-feature-registry.py ) for name in "${required[@]}"; do path="$output_dir/$name" [[ -f "$path" && ! -L "$path" && -s "$path" ]] || { echo "error: required regular nonempty artifact missing: $path" >&2 exit 1 } done [[ -f "$output_dir/docs-features.txt" && ! -L "$output_dir/docs-features.txt" ]] || { echo "error: docs-features.txt must be a regular file" >&2 exit 1 } if [[ -e "$output_dir/spec-cli-surface.md" || -L "$output_dir/spec-cli-surface.md" ]]; then [[ -f "$output_dir/spec-cli-surface.md" && ! -L "$output_dir/spec-cli-surface.md" && -s "$output_dir/spec-cli-surface.md" ]] || { echo "error: spec-cli-surface.md must be a regular nonempty file when present" >&2 exit 1 } fi python3 "$output_dir/validate-feature-registry.py" if [[ "$upstream_ref_set" == 1 ]]; then metadata="$output_dir/clone-metadata.json" [[ -f "$metadata" && ! -L "$metadata" && -s "$metadata" ]] || { echo "error: --upstream-ref requires clone-metadata.json" >&2 exit 1 } python3 - "$metadata" <<'PY' import json, pathlib, re, sys path = pathlib.Path(sys.argv[1]) data = json.loads(path.read_text(encoding="utf-8")) if not isinstance(data, dict): raise SystemExit("clone metadata must be an object") commit = data.get("resolved_commit") if not isinstance(commit, str) or not re.fullmatch(r"[0-9a-fA-F]{40,64}", commit): raise SystemExit("clone metadata lacks a full resolved commit OID") if not data.get("upstream_ref"): raise SystemExit("clone metadata lacks upstream_ref") PY fi if [[ "$phase" == complete ]]; then steal_map="$output_dir/steal-map.md" [[ -f "$steal_map" && ! -L "$steal_map" && -s "$steal_map" ]] || { echo "error: complete output requires a regular nonempty steal-map.md" >&2 exit 1 } grep -Fqx '| Their capability | Our surface today | Verdict |' "$steal_map" || { echo "error: steal-map.md lacks the required table header" >&2 exit 1 } awk -F'|' ' BEGIN { found = 0 } /^\|/ { capability=$2; ours=$3; verdict=$4 gsub(/^[[:space:]]+|[[:space:]]+$/, "", capability) gsub(/^[[:space:]]+|[[:space:]]+$/, "", ours) gsub(/^[[:space:]]+|[[:space:]]+$/, "", verdict) gsub(/\*\*/, "", verdict) if (capability != "" && capability != "Their capability" && capability !~ /^-+$/ && ours != "" && verdict ~ /^(have|gap|steal|park|reject)$/) found = 1 } END { exit found ? 0 : 1 } ' "$steal_map" || { echo "error: steal-map.md needs at least one nonempty row with a valid verdict" >&2 exit 1 } fi if [[ "$security_audit" == 1 ]]; then gate="$output_dir/security/validate-security-audit.sh" [[ -x "$gate" && ! -L "$gate" ]] || { echo "error: security validator is missing or unsafe" >&2 exit 1 } if [[ "$sbom" == 1 ]]; then "$gate" "$output_dir" --sbom else "$gate" "$output_dir" --no-sbom fi else [[ "$sbom" == 0 ]] || { echo "error: --sbom requires --security-audit 1" >&2 exit 1 } fi echo "PASS: reverse-engineer $phase output is structurally valid" -
validate.sh 2 KB
#!/usr/bin/env bash set -euo pipefail SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # Syntax-check the shipped Python without writing __pycache__/*.pyc into the # package (py_compile writes the default cfile even with cfile=None); ast.parse # validates syntax and writes nothing. python3 - \ "$SKILL_DIR/scripts/reverse_engineer.py" \ "$SKILL_DIR/scripts/fetch_url.py" \ "$SKILL_DIR/scripts/generate_feature_inventory_md.py" \ "$SKILL_DIR/scripts/scaffold_feature_registry.py" \ "$SKILL_DIR/scripts/generate_feature_catalog_md.py" \ "$SKILL_DIR/scripts/validate_feature_registry.py" \ "$SKILL_DIR/scripts/binary/list_embedded_archives.py" \ "$SKILL_DIR/scripts/binary/extract_embedded_archives.py" <<'PY' import ast import sys for path in sys.argv[1:]: with open(path, encoding="utf-8") as fh: ast.parse(fh.read(), filename=path) PY # Hermetic behavioral witness: the security-audit gate must fail-closed on an # unfilled findings scaffold. The shipped template supplies Evidence/Fix as # literal _TBD markers; a presence-only grep used to certify them green. Build a # minimal security dir whose findings.md still carries _TBD and assert the gate # refuses it. (No go, network, or scanners needed — the _TBD check trips before # the secret scan.) tmp="$(mktemp -d "${TMPDIR:-/tmp}/re-validate.XXXXXX")" trap 'rm -rf "$tmp"' EXIT sec="$tmp/security" mkdir -p "$sec" for name in threat-model attack-surface dataflow crypto-review authn-authz reproducibility; do printf '# %s\n' "$name" > "$sec/$name.md" done cp "$SKILL_DIR/scripts/security/validate_security_audit.sh" "$sec/validate-security-audit.sh" chmod +x "$sec/validate-security-audit.sh" printf '## Finding F-1: scaffold\nEvidence: _TBD_\nFix: _TBD_\n' > "$sec/findings.md" if bash "$sec/validate-security-audit.sh" "$tmp" --no-sbom >/dev/null 2>&1; then echo "FAIL: security-audit gate certified an unfilled _TBD scaffold (should fail-closed)" >&2 exit 1 fi echo "OK: reverse-engineer validate.sh passed (syntax + security gate rejects _TBD scaffold)" -
validate_feature_registry.py 5.8 KB
#!/usr/bin/env python3 from __future__ import annotations import argparse import os import re import sys from pathlib import Path ALLOWED_IMPL = {"client", "mixed", "control-plane"} def _group_from_slug(slug: str, docs_features_prefix: str) -> str | None: prefix = docs_features_prefix.strip("/").rstrip("/") + "/" s = slug.strip().lstrip("/") if not s.startswith(prefix): return None rest = s[len(prefix) :] if not rest: return None return rest.split("/", 1)[0] or None def _parse_registry(path: Path) -> dict: data = {"docs_features_prefix": "docs/features/", "groups": {}} cur = None in_groups = False in_anchors = False for raw in path.read_text(encoding="utf-8", errors="replace").splitlines(): line = raw.rstrip("\n") if not line.strip() or line.lstrip().startswith("#"): continue if line.startswith("docs_features_prefix:"): data["docs_features_prefix"] = line.split(":", 1)[1].strip().strip("'\"") if line == "groups:": in_groups = True continue if not in_groups: continue if line.startswith(" ") and not line.startswith(" ") and line.endswith(":"): name = line.strip()[:-1] cur = {"impl": None, "anchors": [], "notes": ""} data["groups"][name] = cur in_anchors = False continue if cur is None: continue s = line.strip() if s.startswith("impl:"): cur["impl"] = s.split(":", 1)[1].strip() elif s.startswith("anchors:"): in_anchors = True if s.endswith("[]"): cur["anchors"] = [] elif in_anchors and s.startswith("- "): cur["anchors"].append(s[2:].strip().strip("'\"")) elif s.startswith("notes:"): cur["notes"] = s.split(":", 1)[1].strip().strip("'\"") return data def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--feature-registry", required=True) ap.add_argument("--docs-features", required=True) ap.add_argument("--local-clone-dir", required=True) args = ap.parse_args() feature_registry_path = Path(args.feature_registry).resolve() artifact_dir = feature_registry_path.parent reg = _parse_registry(feature_registry_path) prefix = reg["docs_features_prefix"] groups = reg["groups"] docs_slugs = [ln.strip() for ln in Path(args.docs_features).read_text(encoding="utf-8", errors="replace").splitlines() if ln.strip()] root = Path(args.local_clone_dir).resolve() errs: list[str] = [] # Rule: every docs/features slug maps to a group. for slug in docs_slugs: g = _group_from_slug(slug, prefix) if not g: errs.append(f"docs slug not under prefix {prefix!r}: {slug!r}") continue if g not in groups: errs.append(f"docs slug group missing from registry: group={g!r} slug={slug!r}") # Rule: every group has impl; client/mixed must have anchors. for g, ent in groups.items(): impl = (ent.get("impl") or "").strip() if impl not in ALLOWED_IMPL: errs.append(f"group {g!r} has invalid impl {impl!r} (allowed: {sorted(ALLOWED_IMPL)})") anchors = ent.get("anchors") or [] if impl in ("client", "mixed") and len(anchors) < 1: errs.append(f"group {g!r} impl={impl!r} requires >=1 anchor") for a in anchors: # Allow line/col suffix like "path/to/file.py:123" p = a.split(":", 1)[0] if p.startswith("/"): abs_path = Path(p).resolve() if not abs_path.exists(): errs.append(f"group {g!r} anchor missing: {a!r} (checked {abs_path})") continue # Relative anchors may reference either the analysis root or the artifact bundle dir. candidates = [ (artifact_dir, (artifact_dir / p).resolve(), "artifact_dir"), (root, (root / p).resolve(), "analysis_root"), ] path_ok = False missing_paths: list[str] = [] for base, resolved, _label in candidates: base_resolved = base.resolve() if not (resolved == base_resolved or str(resolved).startswith(str(base_resolved) + os.sep)): continue if resolved.exists(): path_ok = True break missing_paths.append(str(resolved)) if not path_ok: checked = ", ".join(missing_paths) if missing_paths else "(no safe candidate paths)" errs.append(f"group {g!r} anchor missing: {a!r} (checked {checked})") # Completeness guard: if docs/ exists with markdown content, empty docs feature inventory is likely bad prefix selection. docs_dir = root / "docs" if docs_dir.exists(): has_docs_markdown = any(docs_dir.rglob("*.md")) or any(docs_dir.rglob("*.mdx")) if has_docs_markdown and len(docs_slugs) == 0: errs.append( "docs-features inventory is empty while docs/ contains markdown; " "likely wrong docs_features_prefix or extraction failure" ) # Completeness guard: reject unresolved placeholder code-map specs. spec_code_map = artifact_dir / "spec-code-map.md" if spec_code_map.exists(): text = spec_code_map.read_text(encoding="utf-8", errors="replace") if "_TBD_" in text or re.search(r"\|\s*_TBD_\s*\|", text): errs.append(f"spec-code-map contains unresolved placeholders: {spec_code_map}") if errs: for e in errs: print(f"FAIL: {e}", file=sys.stderr) return 1 print("OK: feature registry validated") return 0 if __name__ == "__main__": raise SystemExit(main())
-
-
.gitignore 19 B · in bundle
-
SKILL.md 13 KB
--- name: reverse-engineer description: 'Tear down an authorized competitor repo, binary or product into a feature inventory and adoption choices. Use when: comparing an external system; local questions go to Research.' practices: - legacy-code-seams - ddd-bounded-context - adr hexagonal_role: supporting consumes: [] produces: - '.agents/scratch/reverse-engineer/*/' context_rel: [] skill_api_version: 1 user-invocable: true context: window: fork intent: mode: task sections: exclude: - HISTORY metadata: dependencies: [] capabilities: [reverse_engineer] effects: [clone_upstream_repo, authorized_binary_execution, write_teardown_artifacts] canonical_status: canonical disposition: keep_specialist tier: execution internal: false output_contract: validated phase-1 teardown directory, followed by a caller-authored and validated phase-2 steal-map.md --- # Reverse Engineer Reverse-engineer an external system into two things: a **mechanically-verifiable teardown** (feature inventory + registry + specs, optionally a security audit) and a **steal-map** — what to adopt into our surfaces, what to leave behind. The teardown is the evidence; the steal-map is the decision. Separating them works because a decision row that must cite a registry entry can be re-checked by anyone, while a decision made from impressions cannot be re-checked by its own author. The original failure mode this skill exists to prevent: reading a competitor's README and "deciding" from vibes. **Triggers:** "reverse-engineer X", "tear down Y", "what should we steal from Z", "evaluate competitor/upstream", "should we fork/adopt/build-native". ## Prompt ```text Reverse-engineer the beads CLI (github.com/steveyegge/beads, tag v2.1.0) in repo mode, then author steal-map.md comparing its dependency-graph reconciler against our cli/internal/gates/ package. I own this analysis and have authorization for the clone. ``` ## It's working if Observable in the trace, without reading the prose: - `feature-registry.yaml` and `clone-metadata.json` land under `.agents/scratch/reverse-engineer/<product>/` with the resolved upstream commit recorded. - Every `steal-map.md` row cites a teardown registry entry and our matching surface, using the full `have`/`gap`/`steal`/`park`/`reject` set. - `bash skills/reverse-engineer/scripts/validate-output.sh --output-dir "$output_dir" --phase complete` exits 0 before handoff. - A one-way-door adoption row is routed to Plan instead of decided inside `steal-map.md`. ## ⚠️ Constraints — Hard Guardrails (MANDATORY) - Only operate on code/binaries you own or have **explicit written authorization** to analyze — this matters because unauthorized teardown is the legal/IP line. - Do not provide steps to bypass protections/ToS or to extract proprietary source/system prompts. - Do not output reconstructed proprietary source or embedded prompts (index only; redact in reports) — to prevent reproducing protected IP. - Redact secrets/tokens/keys if encountered; run the secret-scan gate over outputs to prevent credential leakage. - Always separate **docs say** vs **code proves** vs **hosted/control-plane**. ## Phase 1 — Mechanical teardown (the script) Produce evidence, not vibes. The script clones (pinned), scans CLI/config/artifact surface, and writes a feature inventory + machine-checkable registry + spec set. ```bash python3 skills/reverse-engineer/scripts/reverse_engineer.py <product> --mode=repo \ --upstream-repo="https://github.com/org/repo.git" --upstream-ref=v1.0.0 \ --output-dir=".agents/scratch/reverse-engineer/<product>/" ``` Binary mode requires `--authorized` (see Invocation Contract + Self-Test). Use the bundled demo fixture if you lack authorization for a real binary. ## Phase 2 — The steal-map (the decision) Map each capability the teardown found onto **our** surfaces. This is the part that turns research into a decision. Emit `.agents/scratch/reverse-engineer/<product>/steal-map.md` with a table; every row cites the teardown evidence **and** the matching surface in our repo. The mechanical script intentionally stops after validating Phase 1. It cannot truthfully decide whether our live tree has, lacks, or should adopt a capability. The caller authors `steal-map.md` from the generated registry plus a fresh read of our repository, then runs the complete-output validator below. A missing or malformed map is therefore an incomplete skill result, not a script success silently relabelled as a decision. | Their capability | Our surface today | Verdict | |---|---|---| | `<feature>` | `<our file / skill / CLI, or "none">` | **have** / **gap** / **steal** / **park** / **reject** | Verdict rules (hard-won — apply them, do not skip): - **steal** — we lack it and it advances our core. Steal the *pattern*, not the storage engine: re-express in our primitives, never vendor their runtime. - **park** — real, but it's substrate we deliberately delegate (e.g. orchestration per ADR-0009) or downstream of an unproven bet. Name it, don't build it. - **reject** — it conflicts with our doctrine (e.g. a self-reported completion edge where we require a verdict — "no verdict = not done"). - **have** — we already do this; confirm it still holds, move on. - **gap** — we should have it and don't. These are the steal candidates. Discipline that makes the map trustworthy: - **Independently checked, not self-report.** Get facts on *how* they implement each capability from code, cross-checked by a fresh reader — never from a README or one context's summary. Model family is optional metadata, not a trust requirement. - **Probe the real state, don't argue from stale.** Re-verify our side against the live tree before calling something a gap; every "X is missing" carries the search that proved it. - **The steal is the pattern, not the platform.** Their robustness is usually one idea (unification, a gate, a reconcile loop). Steal the idea; leave the scaffolding. ## Route one-way-door adoptions into planning If adopting a steal is a **one-way door** (an architecture fork, a new bounded context, or a migration), do not decide it here. Hand the steal-map to Plan. Dueling Idea Genies or Premortem may challenge the choice as advisory evidence. Plan alone shapes the selected option in the existing intent source; neither strategy grants readiness or continuation authority. ## Invocation Contract Required: `product_name`. Common flags: `--mode=repo|binary|both`, `--upstream-repo`, `--upstream-ref` (requires the selected checkout to be at that exact commit and records its resolved SHA in `clone-metadata.json`), `--local-clone-dir` (selects that exact tree, including a non-Git tree; it never falls back to the caller's checkout), `--output-dir` (default `.agents/scratch/reverse-engineer/<product>/`), `--security-audit`, `--materialize-archives` (authorized-only opt-in; embedded-archive extraction is off/index-only by default), `--authorized` (mandatory for binary mode — refuses without it). Full list: `python3 skills/reverse-engineer/scripts/reverse_engineer.py --help`. ## Output Specification Phase-1 teardown under `output_dir/`: `feature-inventory.md`, `feature-registry.yaml`, `feature-catalog.md`, `spec-architecture.md`, `spec-code-map.md`, `spec-clone-vs-use.md`, `spec-clone-mvp.md`, plus `spec-cli-surface.md` only when a CLI is detected. `clone-metadata.json` is written whenever an upstream repo/ref is selected and binds the exact analyzed commit, including an already-present checkout. Security mode adds `output_dir/security/`: `threat-model.md`, `attack-surface.md`, `dataflow.md`, `crypto-review.md`, `authn-authz.md`, `findings.md`, `reproducibility.md`, `validate-security-audit.sh`. Phase-2 adds the caller-authored `steal-map.md`. - **Artifact directory:** the exact `--output-dir`, defaulting to `$REPO/.agents/scratch/reverse-engineer/<product>/`. - **Filename convention:** the fixed phase-1 and phase-2 names above; security files live only in the `security/` child directory. - **Serialization/schema format:** registry is YAML, clone metadata is one JSON object, and inventories/specs/steal-map are nonempty Markdown files. - **Validator command:** Phase 1 runs this automatically with `--phase teardown`. After authoring `steal-map.md`, validate the complete skill output with `$output_dir`, `$security_audit`, `$sbom`, and `$upstream_ref_set` (each numeric flag `0|1`): ```bash bash skills/reverse-engineer/scripts/validate-output.sh \ --output-dir "$output_dir" --phase complete \ --security-audit "$security_audit" --sbom "$sbom" \ --upstream-ref-set "$upstream_ref_set" ``` - **Downstream handoff:** give the validated `steal-map.md` to Plan for one-way-door candidates; ordinary `have`, `park`, and `reject` decisions remain evidence-backed terminal rows. ### Earlier default compatibility Existing teardowns under `.agents/research/<product>/` remain in place and usable. The script accepts that directory when it is passed explicitly with `--output-dir`; that flag is caller authorization to write the teardown at the exact selected path. It does not relocate or duplicate existing artifacts. An invocation that omits the flag writes only to the current scratch default and never creates output under the earlier root. Consumers must retain the exact selected `output_dir` with their evidence references instead of rediscovering outputs by globbing one root. This owning skill contract is the compatibility authority; no separate migration receipt is required. ## Reproducibility + fixtures `--upstream-ref` binds the selected checkout to one full commit: a new clone is checked out detached at the fetched ref, while an existing checkout must already match or the run refuses before analysis. `clone-metadata.json` records that resolved commit. Regression test: `bash skills/reverse-engineer/scripts/repo_fixture_test.sh`. To update a fixture when contracts legitimately change, re-run with the new pinned ref, copy the contract files into `fixtures/<product>/`, and commit. ## Self-Test (acceptance) ```bash bash skills/reverse-engineer/scripts/self_test.sh ``` Must show: feature inventory and registry generated; the exact Phase-1 validator passes; the complete validator rejects a missing and malformed steal-map and accepts a valid caller-authored fixture; existing-checkout ref mismatch and output symlinks fail closed; in security mode `validate-security-audit.sh` exits 0 only after the scaffold is completed and the secret scan passes. ## Examples ### Reverse-engineer an OSS CLI (repo mode) → steal-map Run Phase 1 for `cc-sdd` with `--mode=repo --upstream-repo="https://github.com/gotalab/cc-sdd.git" --upstream-ref=v1.0.0`. It clones the pinned source, scans the surface, writes inventory/registry/specs, and validates the teardown. Then inspect our live surfaces, author each `have`/`gap`/`steal`/`park`/`reject` row in `steal-map.md`, and run the complete-output validator. Supply selected steals to Plan. ### Binary analysis with security audit Run the skill for `ao` with `--authorized --mode=binary --binary-path="$(command -v ao)" --security-audit`. It performs authorized static analysis plus the security suite under `output_dir/security/`; the secret-scan check must pass. ## Troubleshooting | Problem | Cause | Solution | |---|---|---| | Refuses binary analysis | Missing `--authorized` | Add `--authorized` (explicit written authorization required). | | No `clone-metadata.json` | `--upstream-repo` not passed | Pass `--upstream-repo` (and optionally `--upstream-ref`). | | Fixture diff fails | Upstream changed / stale golden | Re-run pinned, refresh `fixtures/`, commit. | | Existing teardown is under `.agents/research/` | It used the earlier default | Pass that exact directory with `--output-dir`; new runs otherwise use the scratch default. | | `spec-cli-surface.md` missing | No Node/Python/Go CLI detected | Surface is documented in `spec-code-map.md` instead. | | Steal-map is all "steal" | Skipped the park/reject rules | Substrate we delegate is **park**; doctrine conflicts are **reject** — not everything novel is worth adopting. | ## Quality Rubric - [ ] Every steal-map row cites teardown evidence **and** our matching surface (or "none"). - [ ] Verdicts use the full set — `have`/`gap`/`steal`/`park`/`reject` — not everything marked "steal". - [ ] Facts on *how* they implement come from code and a fresh independent check — not a README. - [ ] One-way-door adoptions are supplied to Plan, not decided here. - [ ] Secret-scan gate passed over all outputs; no proprietary source/prompts reproduced. ## See Also - [plan](../plan/SKILL.md) — shape selected steals in the existing intent source - [idea-genie](../idea-genie/SKILL.md) — optional advisory challenge (duel mode) - [premortem](../premortem/SKILL.md) — optional advisory challenge of the exact plan - [research](../research/SKILL.md) — general exploration; this is its external-system specialization ## Reference Documents - [references/reverse-engineer.feature](references/reverse-engineer.feature) — executable spec: repo-mode feature catalog + code map, binary-mode security audit, durable spec artifacts
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.