fix: {project}-ops repo — separate operations from code (#757) (#767)

Fixes #757

## Changes
Separate operations from code into {project}-ops repo pattern. Added OPS_REPO_ROOT infrastructure (env.sh, load-project.sh, formula-session.sh with ensure_ops_repo helper). Updated all 8 agent scripts and 7 formulas to read/write vault items, journals, evidence, prerequisites, RESOURCES.md, and knowledge from the ops repo. Added setup_ops_repo() to disinto init for automatic ops repo creation and seeding. Removed migrated data from code repo (vault data dirs, planner journal/memory/prerequisites, supervisor journal/best-practices, evidence, RESOURCES.md). Updated all documentation. 55 files changed, ShellCheck clean, all 38 phase tests pass.

Co-authored-by: openhands <openhands@all-hands.dev>
Reviewed-on: https://codeberg.org/johba/disinto/pulls/767
Reviewed-by: Disinto_bot <disinto_bot@noreply.codeberg.org>
This commit is contained in:
johba 2026-03-26 19:55:12 +01:00
parent a899fd0733
commit 71fe89cdd0
55 changed files with 421 additions and 932 deletions

View file

@ -14,7 +14,7 @@ sourced as needed.
| `lib/formula-session.sh` | `acquire_cron_lock()`, `check_memory()`, `load_formula()`, `build_context_block()`, `consume_escalation_reply()`, `start_formula_session()`, `formula_phase_callback()`, `build_prompt_footer()`, `build_graph_section()`, `run_formula_and_monitor(AGENT [TIMEOUT] [CALLBACK])` — shared helpers for formula-driven cron agents (lock, memory guard, formula loading, prompt assembly, tmux session, monitor loop, crash recovery). `build_graph_section()` generates the structural-analysis section (runs `lib/build-graph.py`, formats JSON output) — previously duplicated in planner-run.sh and predictor-run.sh, now shared here. `formula_phase_callback()` handles `PHASE:escalate` (unified escalation path — kills the session). `run_formula_and_monitor` accepts an optional CALLBACK (default: `formula_phase_callback`) so callers can install custom merge-through or escalation handlers. | planner-run.sh, predictor-run.sh, gardener-run.sh, supervisor-run.sh, dev-agent.sh, action-agent.sh |
| `lib/guard.sh` | `check_active(agent_name)` — reads `$FACTORY_ROOT/state/.{agent_name}-active`; exits 0 (skip) if the file is absent. Factory is off by default — state files must be created to enable each agent. **Logs a message to stderr** when skipping (`[check_active] SKIP: state file not found`), so agent dropout is visible in cron logs. Sourced by dev-poll.sh, review-poll.sh, action-poll.sh, predictor-run.sh, supervisor-run.sh. | cron entry points |
| `lib/mirrors.sh` | `mirror_push()` — pushes `$PRIMARY_BRANCH` + tags to all configured mirror remotes (fire-and-forget background pushes). Reads `MIRROR_NAMES` and `MIRROR_*` vars exported by `load-project.sh` from the `[mirrors]` TOML section. Failures are logged but never block the pipeline. Sourced by dev-poll.sh and dev/phase-handler.sh — called after every successful merge. | dev-poll.sh, phase-handler.sh |
| `lib/build-graph.py` | Python tool: parses VISION.md, prerequisite-tree.md, AGENTS.md, formulas/*.toml, evidence/, and forge issues/labels into a NetworkX DiGraph. Runs structural analyses (orphaned objectives, stale prerequisites, thin evidence, circular deps) and outputs a JSON report. Used by `review-pr.sh` (per-PR changed-file analysis) and `predictor-run.sh` (full-project analysis) to provide structural context to Claude. | review-pr.sh, predictor-run.sh |
| `lib/build-graph.py` | Python tool: parses VISION.md, prerequisites.md (from ops repo), AGENTS.md, formulas/*.toml, evidence/ (from ops repo), and forge issues/labels into a NetworkX DiGraph. Runs structural analyses (orphaned objectives, stale prerequisites, thin evidence, circular deps) and outputs a JSON report. Used by `review-pr.sh` (per-PR changed-file analysis) and `predictor-run.sh` (full-project analysis) to provide structural context to Claude. | review-pr.sh, predictor-run.sh |
| `lib/secret-scan.sh` | `scan_for_secrets()` — detects potential secrets (API keys, bearer tokens, private keys, URLs with embedded credentials) in text; returns 1 if secrets found. `redact_secrets()` — replaces detected secret patterns with `[REDACTED]`. | file-action-issue.sh, phase-handler.sh |
| `lib/file-action-issue.sh` | `file_action_issue()` — dedup check, secret scan, label lookup, and issue creation for formula-driven cron wrappers. Sets `FILED_ISSUE_NUM` on success. Returns 4 if secrets detected in body. | (available for future use) |
| `lib/tea-helpers.sh` | `tea_file_issue(title, body, labels...)` — create issue via tea CLI with secret scanning; sets `FILED_ISSUE_NUM`. `tea_relabel(issue_num, labels...)` — replace labels using tea's `edit` subcommand (not `label`). `tea_comment(issue_num, body)` — add comment with secret scanning. `tea_close(issue_num)` — close issue. All use `TEA_LOGIN` and `FORGE_REPO` from env.sh. Labels by name (no ID lookup). Tea binary download verified via sha256 checksum. Sourced by env.sh when `tea` binary is available. | env.sh (conditional) |

View file

@ -86,6 +86,13 @@ export TEA_LOGIN
export PROJECT_NAME="${PROJECT_NAME:-${FORGE_REPO##*/}}"
export PROJECT_REPO_ROOT="${PROJECT_REPO_ROOT:-/home/${USER}/${PROJECT_NAME}}"
export PRIMARY_BRANCH="${PRIMARY_BRANCH:-master}"
# Ops repo: operational data (vault items, journals, evidence, prerequisites).
# Default convention: sibling directory named {project}-ops.
export OPS_REPO_ROOT="${OPS_REPO_ROOT:-/home/${USER}/${PROJECT_NAME}-ops}"
# Forge repo slug for the ops repo (used by agents that commit to ops).
export FORGE_OPS_REPO="${FORGE_OPS_REPO:-${FORGE_REPO:+${FORGE_REPO}-ops}}"
export WOODPECKER_REPO_ID="${WOODPECKER_REPO_ID:-}"
export WOODPECKER_SERVER="${WOODPECKER_SERVER:-http://localhost:8000}"
export CLAUDE_TIMEOUT="${CLAUDE_TIMEOUT:-7200}"

View file

@ -67,37 +67,91 @@ load_formula() {
# build_context_block FILE [FILE ...]
# Reads each file from $PROJECT_REPO_ROOT and builds CONTEXT_BLOCK.
# Files prefixed with "ops:" are read from $OPS_REPO_ROOT instead.
build_context_block() {
CONTEXT_BLOCK=""
local ctx ctx_path
local ctx ctx_path ctx_label
for ctx in "$@"; do
ctx_path="${PROJECT_REPO_ROOT}/${ctx}"
case "$ctx" in
ops:*)
ctx_label="${ctx#ops:}"
ctx_path="${OPS_REPO_ROOT}/${ctx_label}"
;;
*)
ctx_label="$ctx"
ctx_path="${PROJECT_REPO_ROOT}/${ctx}"
;;
esac
if [ -f "$ctx_path" ]; then
CONTEXT_BLOCK="${CONTEXT_BLOCK}
### ${ctx}
### ${ctx_label}
$(cat "$ctx_path")
"
fi
done
}
# ── Escalation reply consumption ─────────────────────────────────────────
# ── Ops repo helpers ─────────────────────────────────────────────────
# consume_escalation_reply AGENT_NAME
# Atomically consumes /tmp/{agent}-escalation-reply if it exists.
# Sets ESCALATION_REPLY to the file contents (empty string if no reply).
consume_escalation_reply() {
local agent="$1"
local reply_file="/tmp/${agent}-escalation-reply"
ESCALATION_REPLY=""
if [ -s "$reply_file" ]; then
local tmp_file="${reply_file}.consumed.$$"
if mv "$reply_file" "$tmp_file" 2>/dev/null; then
ESCALATION_REPLY=$(cat "$tmp_file")
rm -f "$tmp_file"
log "Consumed escalation reply: $(echo "$ESCALATION_REPLY" | head -1)"
fi
# ensure_ops_repo
# Clones or pulls the ops repo so agents can read/write operational data.
# Requires: OPS_REPO_ROOT, FORGE_OPS_REPO, FORGE_URL, FORGE_TOKEN.
# No-op if OPS_REPO_ROOT already exists and is up-to-date.
ensure_ops_repo() {
local ops_root="${OPS_REPO_ROOT:-}"
[ -n "$ops_root" ] || return 0
if [ -d "${ops_root}/.git" ]; then
# Pull latest from primary branch
git -C "$ops_root" fetch origin "${PRIMARY_BRANCH}" --quiet 2>/dev/null || true
git -C "$ops_root" checkout "${PRIMARY_BRANCH}" --quiet 2>/dev/null || true
git -C "$ops_root" pull --ff-only origin "${PRIMARY_BRANCH}" --quiet 2>/dev/null || true
return 0
fi
# Clone from Forgejo
local ops_repo="${FORGE_OPS_REPO:-}"
[ -n "$ops_repo" ] || return 0
local forge_url="${FORGE_URL:-http://localhost:3000}"
local clone_url
if [ -n "${FORGE_TOKEN:-}" ]; then
local auth_url
auth_url=$(printf '%s' "$forge_url" | sed "s|://|://$(whoami):${FORGE_TOKEN}@|")
clone_url="${auth_url}/${ops_repo}.git"
else
clone_url="${forge_url}/${ops_repo}.git"
fi
log "Cloning ops repo: ${ops_repo} -> ${ops_root}"
if git clone --quiet "$clone_url" "$ops_root" 2>/dev/null; then
log "Ops repo cloned: ${ops_root}"
else
log "WARNING: failed to clone ops repo ${ops_repo} — creating local directory"
mkdir -p "$ops_root"
fi
}
# ops_commit_and_push MESSAGE [FILE ...]
# Stage, commit, and push changes in the ops repo.
# If no files specified, stages all changes.
ops_commit_and_push() {
local msg="$1"
shift
local ops_root="${OPS_REPO_ROOT:-}"
[ -d "${ops_root}/.git" ] || return 0
(
cd "$ops_root" || return
if [ $# -gt 0 ]; then
git add "$@"
else
git add -A
fi
if ! git diff --cached --quiet; then
git commit -m "$msg"
git push origin "${PRIMARY_BRANCH}" --quiet 2>/dev/null || true
fi
)
}
# ── Session management ───────────────────────────────────────────────────
@ -296,6 +350,7 @@ NEVER echo or include the actual token value in output — always reference \${F
## Environment
FACTORY_ROOT=${FACTORY_ROOT}
PROJECT_REPO_ROOT=${PROJECT_REPO_ROOT}
OPS_REPO_ROOT=${OPS_REPO_ROOT}
PRIMARY_BRANCH=${PRIMARY_BRANCH}
PHASE_FILE=${PHASE_FILE}

View file

@ -43,6 +43,10 @@ emit('FORGE_URL', cfg.get('forge_url', ''))
if 'repo_root' in cfg:
emit('PROJECT_REPO_ROOT', cfg['repo_root'])
if 'ops_repo_root' in cfg:
emit('OPS_REPO_ROOT', cfg['ops_repo_root'])
if 'ops_repo' in cfg:
emit('FORGE_OPS_REPO', cfg['ops_repo'])
if 'primary_branch' in cfg:
emit('PRIMARY_BRANCH', cfg['primary_branch'])
@ -99,4 +103,14 @@ if [ -z "${PROJECT_REPO_ROOT:-}" ] && [ -n "${PROJECT_NAME:-}" ]; then
export PROJECT_REPO_ROOT="/home/${USER}/${PROJECT_NAME}"
fi
# Derive OPS_REPO_ROOT if not explicitly set
if [ -z "${OPS_REPO_ROOT:-}" ] && [ -n "${PROJECT_NAME:-}" ]; then
export OPS_REPO_ROOT="/home/${USER}/${PROJECT_NAME}-ops"
fi
# Derive FORGE_OPS_REPO if not explicitly set
if [ -z "${FORGE_OPS_REPO:-}" ] && [ -n "${FORGE_REPO:-}" ]; then
export FORGE_OPS_REPO="${FORGE_REPO}-ops"
fi
unset _PROJECT_TOML _PROJECT_VARS _key _val