Compare commits
No commits in common. "a8b85154b88ce6507820430c1fe5d5ba02c8044d" and "df0fd9a9bac1c9df9b554f5adf7cc6161288e891" have entirely different histories.
a8b85154b8
...
df0fd9a9ba
6 changed files with 10 additions and 231 deletions
|
|
@ -44,8 +44,6 @@ reforge-seed # create repos from the manifest, seed the charter
|
|||
reforge-kickoff # file the agenda as issues
|
||||
reforge-role security-lead # one terminal per role …
|
||||
reforge-orchestrator # … or let one agent drive the whole run
|
||||
reforge-harvest run-1 # export the run's record (issues, reviews,
|
||||
# charter tree) as markdown + JSON for triage
|
||||
```
|
||||
|
||||
Not on NixOS? The forge is `services.forgejo`, so declarative provisioning is
|
||||
|
|
@ -75,8 +73,8 @@ the matching `REFORGE_*` variable.
|
|||
|
||||
Extracted from the aiolabs machine config where it was first built and run.
|
||||
The engine is generic; the aiolabs runs (their real charter/agenda/manifest)
|
||||
stay private. Remaining follow-up: Forgejo Actions as a CI gate ahead of
|
||||
review (see `docs/architecture.md`).
|
||||
stay private. See `docs/reforge.md` for the follow-up ideas (markdown export
|
||||
of a run's issues at close; Forgejo Actions as a CI gate ahead of review).
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
|||
|
|
@ -135,17 +135,10 @@ open Phase B — that switch is a log-worthy moment of the run.
|
|||
human opens Phase B when Phase A's signal is banked
|
||||
5. compare reforge-compare
|
||||
iterate 4 ⇄ 5 until every targeted repo is IDENTICAL
|
||||
6. harvest reforge-harvest <run-name> [outdir]
|
||||
(exports every issue, comment, PR and review per repo as
|
||||
markdown + raw JSON, plus the charter tree — GAMEPLAN.md,
|
||||
CONTRACTS.md — so the record is readable outside the forge);
|
||||
commit the export next to the run config, then triage each
|
||||
<repo>.md against the real stack: LIVE / FIXED / NA per finding,
|
||||
still-live findings become real issues; **refine these
|
||||
instruction sources** (this doc, the agent briefs, the agenda,
|
||||
the scripts) with what the run taught — the simulation is meant
|
||||
to improve every iteration, and the harvest is the next run's
|
||||
agenda input
|
||||
6. harvest GAMEPLAN.md PR'd into charter; issues reviewed + triaged against
|
||||
the real stack; **refine these instruction sources** (this doc,
|
||||
the agent briefs, the agenda, the scripts) with what the run
|
||||
taught — the simulation is meant to improve every iteration
|
||||
7. close reforge-reset backup <run-name>
|
||||
(or go straight to the next run's reset, which archives too)
|
||||
```
|
||||
|
|
@ -153,12 +146,6 @@ open Phase B — that switch is a log-worthy moment of the run.
|
|||
Archives: `/var/lib/forgejo-sandbox-archive/<name>-<stamp>.tar.gz`; restore
|
||||
any of them with `reforge-reset restore <tarball>`.
|
||||
|
||||
**Harvest is not optional.** An archive preserves a run; only the harvest
|
||||
makes it *act* on the real stack. A run whose findings stay in the tarball
|
||||
has produced nothing — the aiolabs run #1 sat unharvested for three months
|
||||
with two Critical findings live in deployed forks. Harvest before close,
|
||||
every run.
|
||||
|
||||
## Autonomous mode (an agent as orchestrator)
|
||||
|
||||
Steps 3–5 (kickoff → rebuild → compare) can be driven by an agent itself
|
||||
|
|
|
|||
|
|
@ -49,8 +49,6 @@ let
|
|||
inherit tokensDir stateDir;
|
||||
configDir = toString cfg.configDir;
|
||||
agentsDir = if cfg.agentsDir == null then null else toString cfg.agentsDir;
|
||||
# Seeded stack repos get the same approver whitelist as the working repo.
|
||||
requiredApprovers = cfg.requiredApprovers;
|
||||
refsDir = if cfg.refsDir == null then null else toString cfg.refsDir;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@
|
|||
configDir,
|
||||
agentsDir ? null,
|
||||
refsDir ? null,
|
||||
requiredApprovers ? [ "security-lead" ],
|
||||
}:
|
||||
|
||||
let
|
||||
|
|
@ -89,7 +88,6 @@ stdenvNoCC.mkDerivation {
|
|||
--set-default REFORGE_CONFIG_DIR ${lib.escapeShellArg (toString configDir)} \
|
||||
--set-default REFORGE_SETTINGS_DIR "$SETTINGS" \
|
||||
--set-default REFORGE_AGENTS_DIR "$AGENTS" \
|
||||
--set-default REFORGE_REQUIRED_APPROVERS ${lib.escapeShellArg (lib.concatStringsSep " " requiredApprovers)} \
|
||||
${lib.optionalString (
|
||||
refsDir != null
|
||||
) "--set-default REFORGE_REFS_DIR ${lib.escapeShellArg (toString refsDir)}"}
|
||||
|
|
@ -98,7 +96,7 @@ stdenvNoCC.mkDerivation {
|
|||
'';
|
||||
|
||||
meta = with lib; {
|
||||
description = "reforge lifecycle CLI (seed/reset/compare/smoke/kickoff/role/orchestrator/fetch-targets/harvest)";
|
||||
description = "reforge lifecycle CLI (seed/reset/compare/smoke/kickoff/role/orchestrator/fetch-targets)";
|
||||
mainProgram = "reforge-seed";
|
||||
license = licenses.mit;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,193 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# Reforge lifecycle step 6 (docs/reforge.md): export a run's full forge
|
||||
# record — every issue with its comments, every PR with its reviews and
|
||||
# review comments, per repo in the org, plus a tree snapshot of the charter
|
||||
# repo (GAMEPLAN.md, CONTRACTS.md, ...) — as plain markdown + raw JSON.
|
||||
#
|
||||
# The archive tarball (reforge-reset backup) preserves the run; this makes
|
||||
# it READABLE outside the forge, so findings can be triaged against the
|
||||
# real stack, committed next to the run config, and fed into the next run's
|
||||
# agenda. Without this step a run's value stays locked in a root-owned
|
||||
# sqlite snapshot.
|
||||
#
|
||||
# reforge-harvest [name] [outdir]
|
||||
#
|
||||
# Defaults: name "run"; outdir $REFORGE_HARVEST_DIR/<name>-<stamp>
|
||||
# (REFORGE_HARVEST_DIR defaults to ~/reforge-harvest). Read-only on the
|
||||
# forge; safe to re-run at any point in a run.
|
||||
#
|
||||
# Output layout:
|
||||
# INDEX.md counts per repo + what each file is
|
||||
# <repo>.md rendered issues (with comments) + PRs (with reviews)
|
||||
# raw/<repo>.issues.json API objects, one array per repo (issues incl. PRs)
|
||||
# raw/<repo>.pulls.json
|
||||
# raw/<repo>.comments.json {issue_number -> [comments]}
|
||||
# raw/<repo>.reviews.json {pr_number -> [reviews with .comments]}
|
||||
# charter-tree/ shallow working tree of the charter repo's main
|
||||
set -euo pipefail
|
||||
|
||||
FORGE_URL=${REFORGE_FORGE_URL:-http://localhost:3030}
|
||||
ORG=${REFORGE_ORG:-sandbox-team}
|
||||
ADMIN_USER=${REFORGE_ADMIN_USER:-sandbox-admin}
|
||||
TOKENS_DIR=${REFORGE_TOKENS_DIR:-/var/lib/forgejo-sandbox/tokens}
|
||||
TOKEN_FILE=${REFORGE_ADMIN_TOKEN_FILE:-$TOKENS_DIR/${ADMIN_USER}.token}
|
||||
CHARTER_REPO=${REFORGE_CHARTER_REPO:-charter}
|
||||
|
||||
NAME=${1:-run}
|
||||
STAMP=$(date +%Y%m%d-%H%M%S)
|
||||
OUT=${2:-${REFORGE_HARVEST_DIR:-$HOME/reforge-harvest}/$NAME-$STAMP}
|
||||
|
||||
API="$FORGE_URL/api/v1"
|
||||
TOKEN=$(cat "$TOKEN_FILE")
|
||||
PAGE=50
|
||||
|
||||
WORK=$(mktemp -d)
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
RESP="$WORK/resp"
|
||||
|
||||
api() { # path -> echoes HTTP code, body in $RESP
|
||||
curl -sS -o "$RESP" -w '%{http_code}' \
|
||||
-H "Authorization: token $TOKEN" "$API$1"
|
||||
}
|
||||
|
||||
# Follow ?page= until a page comes back empty; concatenate arrays.
|
||||
api_all() { # path-with-query (no page param) -> JSON array on stdout
|
||||
local path=$1 page=1 sep code
|
||||
case $path in *\?*) sep='&' ;; *) sep='?' ;; esac
|
||||
echo '['
|
||||
local first=1
|
||||
while :; do
|
||||
code=$(api "$path${sep}limit=$PAGE&page=$page")
|
||||
[ "$code" = 200 ] || { echo "harvest: GET $path page $page -> HTTP $code" >&2; cat "$RESP" >&2; exit 1; }
|
||||
local n
|
||||
n=$(jq 'length' <"$RESP")
|
||||
[ "$n" -gt 0 ] || break
|
||||
if [ $first = 1 ]; then first=0; else echo ','; fi
|
||||
jq '.[]' <"$RESP" | jq -s '.[]' | jq -c . | paste -sd, -
|
||||
[ "$n" -lt "$PAGE" ] && break
|
||||
page=$((page + 1))
|
||||
done
|
||||
echo ']'
|
||||
}
|
||||
|
||||
auth_url() { # name
|
||||
echo "http://$ADMIN_USER:$TOKEN@${FORGE_URL#http://}/$ORG/$1.git"
|
||||
}
|
||||
|
||||
mkdir -p "$OUT/raw"
|
||||
echo "→ reforge harvest '$NAME' → $OUT"
|
||||
echo " forge $FORGE_URL org $ORG"
|
||||
echo
|
||||
|
||||
api_all "/orgs/$ORG/repos" | jq -r '.[].name' | sort >"$WORK/repos"
|
||||
[ -s "$WORK/repos" ] || { echo "harvest: no repos in org $ORG" >&2; exit 1; }
|
||||
|
||||
{
|
||||
echo "# Harvest — $NAME"
|
||||
echo
|
||||
echo "Exported $(date -Iseconds) from $FORGE_URL (org \`$ORG\`)."
|
||||
echo "Read-only snapshot of the forge record; the authoritative copy is the"
|
||||
echo "run archive (reforge-reset backup). Raw API objects are under \`raw/\`."
|
||||
echo
|
||||
echo "| repo | issues | open | PRs | merged | file |"
|
||||
echo "|---|---|---|---|---|---|"
|
||||
} >"$OUT/INDEX.md"
|
||||
|
||||
while read -r repo; do
|
||||
printf -- '-- %-16s' "$repo"
|
||||
issues="$OUT/raw/$repo.issues.json"
|
||||
pulls="$OUT/raw/$repo.pulls.json"
|
||||
comments="$OUT/raw/$repo.comments.json"
|
||||
reviews="$OUT/raw/$repo.reviews.json"
|
||||
|
||||
api_all "/repos/$ORG/$repo/issues?state=all&type=issues" | jq . >"$issues"
|
||||
api_all "/repos/$ORG/$repo/pulls?state=all" | jq . >"$pulls"
|
||||
|
||||
# comments: issue-level comments cover both issues and PR conversation
|
||||
{
|
||||
echo '{'
|
||||
first=1
|
||||
for n in $(jq -r '.[].number' "$issues" "$pulls" | sort -nu); do
|
||||
c=$(api_all "/repos/$ORG/$repo/issues/$n/comments")
|
||||
[ "$(echo "$c" | jq 'length')" -gt 0 ] || continue
|
||||
if [ $first = 1 ]; then first=0; else echo ','; fi
|
||||
printf '"%s": %s' "$n" "$c"
|
||||
done
|
||||
echo '}'
|
||||
} | jq . >"$comments"
|
||||
|
||||
# reviews (+ their inline comments) per PR
|
||||
{
|
||||
echo '{'
|
||||
first=1
|
||||
for n in $(jq -r '.[].number' "$pulls" | sort -n); do
|
||||
r=$(api_all "/repos/$ORG/$repo/pulls/$n/reviews")
|
||||
[ "$(echo "$r" | jq 'length')" -gt 0 ] || continue
|
||||
# attach inline comments to each review
|
||||
r=$(echo "$r" | jq -c '.[]' | while read -r rev; do
|
||||
id=$(echo "$rev" | jq -r .id)
|
||||
rc=$(api_all "/repos/$ORG/$repo/pulls/$n/reviews/$id/comments")
|
||||
echo "$rev" | jq --argjson c "$rc" '. + {comments: $c}'
|
||||
done | jq -s .)
|
||||
if [ $first = 1 ]; then first=0; else echo ','; fi
|
||||
printf '"%s": %s' "$n" "$r"
|
||||
done
|
||||
echo '}'
|
||||
} | jq . >"$reviews"
|
||||
|
||||
# ── render ───────────────────────────────────────────────────────────
|
||||
jq -r --arg repo "$repo" --slurpfile comments "$comments" --slurpfile reviews "$reviews" \
|
||||
--slurpfile pulls "$pulls" '
|
||||
def md(s): (s // "" | gsub("\r"; ""));
|
||||
def who(u): (u.login // "?");
|
||||
def when(t): (t // "" | .[0:16] | gsub("T"; " "));
|
||||
def cmts(n): ($comments[0][n | tostring] // []);
|
||||
def revs(n): ($reviews[0][n | tostring] // []);
|
||||
def render_comments(n):
|
||||
(cmts(n) | if length == 0 then "" else
|
||||
"\n#### Comments\n" + (map("- **" + who(.user) + "** (" + when(.created_at) + "):\n\n " + (md(.body) | gsub("\n"; "\n ")) + "\n") | join("\n")) end);
|
||||
def render_reviews(n):
|
||||
(revs(n) | if length == 0 then "" else
|
||||
"\n#### Reviews\n" + (map(
|
||||
"- **" + who(.user) + "** — " + (.state // "?") + " (" + when(.submitted_at) + ")" +
|
||||
(if (md(.body) | length) > 0 then ":\n\n " + (md(.body) | gsub("\n"; "\n ")) else "" end) + "\n" +
|
||||
((.comments // []) | map(" - `" + (.path // "?") + "`: " + (md(.body) | gsub("\n"; " "))) | join("\n")) + "\n"
|
||||
) | join("\n")) end);
|
||||
def entry(kind):
|
||||
"### " + kind + " #" + (.number | tostring) + " " + .title + "\n\n" +
|
||||
"_" + .state + (if (.pull_request.merged // .merged // false) then ", merged" else "" end) +
|
||||
" · " + who(.user) + " · " + when(.created_at) + "_" +
|
||||
(if (.labels // []) | length > 0 then " · labels: " + ((.labels | map(.name)) | join(", ")) else "" end) + "\n\n" +
|
||||
md(.body) + "\n";
|
||||
"# " + $repo + "\n\n" +
|
||||
"## Issues (" + (length | tostring) + ")\n\n" +
|
||||
(map(entry("Issue") + render_comments(.number) + "\n---\n") | join("\n")) +
|
||||
"\n## Pull requests (" + ($pulls[0] | length | tostring) + ")\n\n" +
|
||||
($pulls[0] | map(entry("PR") + render_comments(.number) + render_reviews(.number) + "\n---\n") | join("\n"))
|
||||
' "$issues" >"$OUT/$repo.md"
|
||||
|
||||
ni=$(jq 'length' "$issues"); no=$(jq '[.[] | select(.state=="open")] | length' "$issues")
|
||||
np=$(jq 'length' "$pulls"); nm=$(jq '[.[] | select(.merged==true)] | length' "$pulls")
|
||||
echo "| $repo | $ni | $no | $np | $nm | [$repo.md]($repo.md) |" >>"$OUT/INDEX.md"
|
||||
echo " issues $ni (open $no) PRs $np (merged $nm)"
|
||||
done <"$WORK/repos"
|
||||
|
||||
# ── charter tree: the run's synthesized deliverables live as files there ──
|
||||
if grep -qx "$CHARTER_REPO" "$WORK/repos"; then
|
||||
if git clone --quiet --depth 1 "$(auth_url "$CHARTER_REPO")" "$OUT/charter-tree" 2>/dev/null; then
|
||||
rm -rf "$OUT/charter-tree/.git"
|
||||
{
|
||||
echo
|
||||
echo "Charter repo tree (main, source-stripped): \`charter-tree/\` —"
|
||||
find "$OUT/charter-tree" -type f | sed "s|$OUT/charter-tree/||" | sort | sed 's/^/ - /'
|
||||
} >>"$OUT/INDEX.md"
|
||||
echo "-- charter tree -> charter-tree/"
|
||||
else
|
||||
echo "-- charter tree: $CHARTER_REPO has no main yet, skipped"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "harvest: $OUT"
|
||||
echo "next: triage <repo>.md against the real stack; commit the harvest next to"
|
||||
echo " your run config; carry still-live findings into the next agenda."
|
||||
|
|
@ -73,21 +73,12 @@ ensure_repo() { # name description
|
|||
fi
|
||||
}
|
||||
|
||||
# Approver whitelist: the module bakes reforge.requiredApprovers in as
|
||||
# REFORGE_REQUIRED_APPROVERS (space-separated); the standalone default
|
||||
# matches the module's default. Field name is singular (Forgejo API quirk).
|
||||
APPROVERS=${REFORGE_REQUIRED_APPROVERS:-security-lead}
|
||||
PROTECTION_JSON=$(jq -cn --arg a "$APPROVERS" \
|
||||
'{branch_name:"main",rule_name:"main",enable_push:false,required_approvals:1,
|
||||
enable_approvals_whitelist:true,
|
||||
approvals_whitelist_username:($a | split(" ") | map(select(length > 0))),
|
||||
block_on_rejected_reviews:true,dismiss_stale_approvals:true}')
|
||||
|
||||
protect_main() { # name (same rule the module puts on the working repo)
|
||||
if [ "$(api GET "/repos/$ORG/$1/branch_protections/main")" = 404 ]; then
|
||||
must "$(api POST "/repos/$ORG/$1/branch_protections" "$PROTECTION_JSON")" \
|
||||
must "$(api POST "/repos/$ORG/$1/branch_protections" \
|
||||
'{"branch_name":"main","rule_name":"main","enable_push":false,"required_approvals":1,"enable_approvals_whitelist":true,"approvals_whitelist_username":["security-lead"],"block_on_rejected_reviews":true,"dismiss_stale_approvals":true}')" \
|
||||
"protect $ORG/$1 main"
|
||||
echo " protected main (approvers: $APPROVERS)"
|
||||
echo " protected main"
|
||||
fi
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue