Merge pull request 'fix: feat: vault as procurement gate + RESOURCES.md capability inventory (#504)' (#520) from fix/issue-504 into main

This commit is contained in:
johba 2026-03-21 20:28:24 +01:00
commit 725c4d7334
6 changed files with 283 additions and 53 deletions

View file

@ -24,7 +24,7 @@ disinto/
│ preflight.sh — pre-flight data collection for supervisor formula │ preflight.sh — pre-flight data collection for supervisor formula
│ supervisor/journal/ — daily health logs from each run │ supervisor/journal/ — daily health logs from each run
│ supervisor-poll.sh — legacy bash orchestrator (superseded) │ supervisor-poll.sh — legacy bash orchestrator (superseded)
├── vault/ vault-poll.sh, vault-agent.sh, vault-fire.sh — action gating ├── vault/ vault-poll.sh, vault-agent.sh, vault-fire.sh — action gating + procurement
├── action/ action-poll.sh, action-agent.sh — operational task execution ├── action/ action-poll.sh, action-agent.sh — operational task execution
├── lib/ env.sh, agent-session.sh, ci-helpers.sh, ci-debug.sh, load-project.sh, parse-deps.sh, matrix_listener.sh ├── lib/ env.sh, agent-session.sh, ci-helpers.sh, ci-debug.sh, load-project.sh, parse-deps.sh, matrix_listener.sh
├── projects/ *.toml — per-project config ├── projects/ *.toml — per-project config
@ -78,7 +78,7 @@ bash dev/phase-test.sh
| Planner | `planner/` | Strategic planning | [planner/AGENTS.md](planner/AGENTS.md) | | Planner | `planner/` | Strategic planning | [planner/AGENTS.md](planner/AGENTS.md) |
| Predictor | `predictor/` | Infrastructure pattern detection | [predictor/AGENTS.md](predictor/AGENTS.md) | | Predictor | `predictor/` | Infrastructure pattern detection | [predictor/AGENTS.md](predictor/AGENTS.md) |
| Action | `action/` | Operational task execution | [action/AGENTS.md](action/AGENTS.md) | | Action | `action/` | Operational task execution | [action/AGENTS.md](action/AGENTS.md) |
| Vault | `vault/` | Safety gate for dangerous actions | [vault/AGENTS.md](vault/AGENTS.md) | | Vault | `vault/` | Action gating + resource procurement | [vault/AGENTS.md](vault/AGENTS.md) |
See [lib/AGENTS.md](lib/AGENTS.md) for the full shared helper reference. See [lib/AGENTS.md](lib/AGENTS.md) for the full shared helper reference.

View file

@ -212,12 +212,22 @@ Update the tree by applying these operations:
5. **Propose new capabilities**: If you identify a capability the factory 5. **Propose new capabilities**: If you identify a capability the factory
needs (e.g., "marketing formula, runs weekly"), add it to the tree as needs (e.g., "marketing formula, runs weekly"), add it to the tree as
a proposed prerequisite. Anything with recurring cost should note: a proposed prerequisite. Anything with recurring cost (new accounts,
"→ vault approval required" so the planner files it to vault next run. new infra, new cron entries, new formulas) should be procured through
the vault see the file-at-constraints step for how to file requests.
6. **Check vault decisions**: Read any open vault issues to see if 6. **Check vault state**: Scan vault directories for procurement status:
previously proposed capabilities have been approved or rejected. - `$FACTORY_ROOT/vault/pending/*.md` requests awaiting human action.
Update the tree accordingly. Any prerequisite that depends on a pending procurement request should
be marked: `[ ] <name> blocked-on-vault (vault/pending/<id>.md)`
- `$FACTORY_ROOT/vault/approved/*.md` fulfilled, being processed.
- `$FACTORY_ROOT/vault/fired/*.md` completed. Check if the resource
now appears in RESOURCES.md and mark the prerequisite resolved.
- Do NOT file issues for objectives blocked on pending vault items.
7. **Re-read RESOURCES.md**: Check for newly available capabilities that
were not present last run. If a new resource appears, mark the
corresponding prerequisite as resolved.
Write the updated tree to: $FACTORY_ROOT/planner/prerequisite-tree.md Write the updated tree to: $FACTORY_ROOT/planner/prerequisite-tree.md
Use this format: Use this format:
@ -228,7 +238,8 @@ Use this format:
## Objective: <name> (#issue or description) ## Objective: <name> (#issue or description)
- [x] Resolved prerequisite (reference) - [x] Resolved prerequisite (reference)
- [ ] Unresolved prerequisite (#issue or description) - [ ] Unresolved prerequisite (#issue or description)
Status: READY | BLOCKED <reason> | DONE - [ ] Resource need blocked-on-vault (vault/pending/<id>.md)
Status: READY | BLOCKED <reason> | BLOCKED awaiting vault | DONE
Keep the tree focused only include objectives from VISION.md milestones Keep the tree focused only include objectives from VISION.md milestones
and their genuine prerequisites. Do not inflate the tree with nice-to-haves. and their genuine prerequisites. Do not inflate the tree with nice-to-haves.
@ -293,9 +304,53 @@ Rules:
- When deploying/operating, reference the resource alias from RESOURCES.md - When deploying/operating, reference the resource alias from RESOURCES.md
- Promoted predictions from triage may become constraints if they block - Promoted predictions from triage may become constraints if they block
downstream objectives rank them the same way downstream objectives rank them the same way
- **Do NOT file issues for objectives blocked on pending vault items**
these are waiting for human procurement, not dev work
If all top 3 constraints already have open issues, note that the backlog ### Filing vault procurement requests
is aligned with the constraint focus. No new issues needed.
If a constraint requires a resource the factory does not have (check
RESOURCES.md), and that resource has recurring cost (account, infra,
domain, API key, new cron job), file a procurement request instead of
an issue:
1. Check if a request already exists in vault/pending/ or vault/approved/
for this resource (match by filename).
2. If no request exists, create a markdown file at:
$FACTORY_ROOT/vault/pending/<resource-id>.md
Format:
```
# Procurement Request: <human-readable name>
## What
<description of what's needed>
## Why
<why the factory needs this which objectives it enables>
## Unblocks
<list prerequisite tree objectives this unblocks, with issue numbers>
## Proposed RESOURCES.md Entry
## <resource-id>
- type: <social|compute|asset|communication|ci|source-control>
- capability: <what it can do>
- env: <ENV_VAR_NAME if secrets needed>
```
3. Mark the prerequisite in the tree as blocked-on-vault:
`[ ] <name> blocked-on-vault (vault/pending/<resource-id>.md)`
4. vault-poll.sh will notify the human automatically.
Procurement requests count toward the 3-item-per-run limit (issues +
procurement requests combined).
If all top 3 constraints already have open issues or pending vault
requests, note that the backlog is aligned with the constraint focus.
No new items needed.
""" """
needs = ["update-prerequisite-tree"] needs = ["update-prerequisite-tree"]

View file

@ -1,19 +1,35 @@
<!-- last-reviewed: 80a64cd3e4d2836bfab3c46230a780e3e233125d --> <!-- last-reviewed: 80a64cd3e4d2836bfab3c46230a780e3e233125d -->
# Vault Agent # Vault Agent
**Role**: Safety gate for dangerous or irreversible actions. Actions enter a **Role**: Dual-purpose gate — action safety classification and resource procurement.
pending queue and are classified by Claude via `vault-agent.sh`, which can
auto-approve (call `vault-fire.sh` directly), auto-reject (call **Pipeline A — Action Gating (*.json)**: Actions enter a pending queue and are
`vault-reject.sh`), or escalate to a human via Matrix for APPROVE/REJECT. classified by Claude via `vault-agent.sh`, which can auto-approve (call
`vault-fire.sh` directly), auto-reject (call `vault-reject.sh`), or escalate
to a human via Matrix for APPROVE/REJECT.
**Pipeline B — Procurement (*.md)**: The planner files resource requests as
markdown files in `vault/pending/`. `vault-poll.sh` notifies the human via
Matrix. The human fulfills the request (creates accounts, provisions infra,
adds secrets to `.env`) and moves the file to `vault/approved/`.
`vault-fire.sh` then extracts the proposed entry and appends it to
`RESOURCES.md`.
**Trigger**: `vault-poll.sh` runs every 30 min via cron. **Trigger**: `vault-poll.sh` runs every 30 min via cron.
**Key files**: **Key files**:
- `vault/vault-poll.sh` — Processes pending actions: retry approved, auto-reject after 48h timeout, invoke vault-agent for new items - `vault/vault-poll.sh` — Processes pending items: retry approved, auto-reject after 48h timeout, invoke vault-agent for JSON actions, notify human for procurement requests
- `vault/vault-agent.sh` — Classifies and routes pending actions via `claude -p`: auto-approve, auto-reject, or escalate to human - `vault/vault-agent.sh` — Classifies and routes pending JSON actions via `claude -p`: auto-approve, auto-reject, or escalate to human
- `vault/PROMPT.md` — System prompt for the vault agent's Claude invocation - `vault/PROMPT.md` — System prompt for the vault agent's Claude invocation
- `vault/vault-fire.sh` — Executes an approved action - `vault/vault-fire.sh` — Executes an approved action (JSON) or writes RESOURCES.md entry (procurement MD)
- `vault/vault-reject.sh` — Marks an action as rejected - `vault/vault-reject.sh` — Marks a JSON action as rejected
**Procurement flow**:
1. Planner drops `vault/pending/<name>.md` with what/why/proposed RESOURCES.md entry
2. `vault-poll.sh` notifies human via Matrix
3. Human fulfills: creates account, adds secrets to `.env`, moves file to `vault/approved/`
4. `vault-fire.sh` extracts proposed entry, appends to RESOURCES.md, moves to `vault/fired/`
5. Next planner run reads RESOURCES.md → new capability available → unblocks prerequisite tree
**Environment variables consumed**: **Environment variables consumed**:
- All from `lib/env.sh` - All from `lib/env.sh`

View file

@ -4,9 +4,24 @@ You are the vault agent for `$CODEBERG_REPO`. You were called by
`vault-poll.sh` because one or more actions in `vault/pending/` need `vault-poll.sh` because one or more actions in `vault/pending/` need
classification and routing. classification and routing.
## Your Job ## Two Pipelines
For each pending action, decide: **auto-approve**, **escalate**, or **reject**. The vault handles two kinds of items:
### A. Action Gating (*.json)
Actions from agents that need safety classification before execution.
You classify and route these: auto-approve, escalate, or reject.
### B. Procurement Requests (*.md)
Resource requests from the planner. These always escalate to the human —
you do NOT auto-approve or reject procurement requests. The human fulfills
the request (creates accounts, provisions infra, adds secrets to .env)
and moves the file from `vault/pending/` to `vault/approved/`.
`vault-fire.sh` then writes the RESOURCES.md entry.
## Your Job (Action Gating only)
For each pending JSON action, decide: **auto-approve**, **escalate**, or **reject**.
## Routing Table (risk × reversibility) ## Routing Table (risk × reversibility)
@ -28,6 +43,8 @@ For each pending action, decide: **auto-approve**, **escalate**, or **reject**.
4. **Malformed JSON → reject** with reason `malformed`. 4. **Malformed JSON → reject** with reason `malformed`.
5. **Payload validation:** Check that the payload has the minimum required 5. **Payload validation:** Check that the payload has the minimum required
fields for the action type. Missing fields → reject with reason. fields for the action type. Missing fields → reject with reason.
6. **Procurement requests (*.md) → skip.** These are handled by the human
directly. Do not attempt to classify, approve, or reject them.
## Action Type Defaults ## Action Type Defaults
@ -41,6 +58,29 @@ For each pending action, decide: **auto-approve**, **escalate**, or **reject**.
| `webhook-call` | medium | depends | | `webhook-call` | medium | depends |
| `stripe-charge` | high | no | | `stripe-charge` | high | no |
## Procurement Request Format (reference only)
Procurement requests dropped by the planner look like:
```markdown
# Procurement Request: <name>
## What
<description of what's needed>
## Why
<why the factory needs this>
## Unblocks
<which prerequisite tree objective(s) this unblocks>
## Proposed RESOURCES.md Entry
## <resource-id>
- type: <type>
- capability: <capabilities>
- env: <env var names if applicable>
```
## Available Tools ## Available Tools
You have shell access. Use these for routing decisions: You have shell access. Use these for routing decisions:
@ -83,8 +123,10 @@ ROUTE: <action-id> → <auto-approve|escalate|reject> — <reason>
## Important ## Important
- Process ALL pending actions in the batch. Never skip silently. - Process ALL pending JSON actions in the batch. Never skip silently.
- For auto-approved actions, fire them immediately via `vault-fire.sh`. - For auto-approved actions, fire them immediately via `vault-fire.sh`.
- For escalated actions, move to `vault/approved/` only AFTER human approval - For escalated actions, move to `vault/approved/` only AFTER human approval
(vault-poll handles this via matrix_listener dispatch). (vault-poll handles this via matrix_listener dispatch).
- Read the action JSON carefully. Check the payload, not just the metadata. - Read the action JSON carefully. Check the payload, not just the metadata.
- Ignore `.md` files in pending/ — those are procurement requests handled
separately by vault-poll.sh and the human.

View file

@ -1,11 +1,14 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# vault-fire.sh — Execute an approved vault action by ID # vault-fire.sh — Execute an approved vault item by ID
# #
# Two-phase: pending/ → approved/ → fired/ # Handles two pipelines:
# If action is in pending/, moves to approved/ first. # A. Action gating (*.json): pending/ → approved/ → fired/
# If action is already in approved/, fires directly (crash recovery). # B. Procurement (*.md): approved/ → fired/ (writes RESOURCES.md entry)
# #
# Usage: bash vault-fire.sh <action-id> # If item is in pending/, moves to approved/ first.
# If item is already in approved/, fires directly (crash recovery).
#
# Usage: bash vault-fire.sh <item-id>
set -euo pipefail set -euo pipefail
@ -15,27 +18,38 @@ source "${SCRIPT_DIR}/../lib/env.sh"
VAULT_DIR="${FACTORY_ROOT}/vault" VAULT_DIR="${FACTORY_ROOT}/vault"
LOCKS_DIR="${VAULT_DIR}/.locks" LOCKS_DIR="${VAULT_DIR}/.locks"
LOGFILE="${VAULT_DIR}/vault.log" LOGFILE="${VAULT_DIR}/vault.log"
RESOURCES_FILE="${PROJECT_REPO_ROOT:-${FACTORY_ROOT}}/RESOURCES.md"
log() { log() {
printf '[%s] vault-fire: %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" "$*" >> "$LOGFILE" printf '[%s] vault-fire: %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" "$*" >> "$LOGFILE"
} }
ACTION_ID="${1:?Usage: vault-fire.sh <action-id>}" ACTION_ID="${1:?Usage: vault-fire.sh <item-id>}"
# Locate the action file # =============================================================================
# Detect pipeline: procurement (.md) or action gating (.json)
# =============================================================================
IS_PROCUREMENT=false
ACTION_FILE="" ACTION_FILE=""
if [ -f "${VAULT_DIR}/approved/${ACTION_ID}.json" ]; then
if [ -f "${VAULT_DIR}/approved/${ACTION_ID}.md" ]; then
IS_PROCUREMENT=true
ACTION_FILE="${VAULT_DIR}/approved/${ACTION_ID}.md"
elif [ -f "${VAULT_DIR}/pending/${ACTION_ID}.md" ]; then
IS_PROCUREMENT=true
mv "${VAULT_DIR}/pending/${ACTION_ID}.md" "${VAULT_DIR}/approved/${ACTION_ID}.md"
ACTION_FILE="${VAULT_DIR}/approved/${ACTION_ID}.md"
log "$ACTION_ID: pending → approved (procurement)"
elif [ -f "${VAULT_DIR}/approved/${ACTION_ID}.json" ]; then
ACTION_FILE="${VAULT_DIR}/approved/${ACTION_ID}.json" ACTION_FILE="${VAULT_DIR}/approved/${ACTION_ID}.json"
elif [ -f "${VAULT_DIR}/pending/${ACTION_ID}.json" ]; then elif [ -f "${VAULT_DIR}/pending/${ACTION_ID}.json" ]; then
# Phase 1: move pending → approved
mv "${VAULT_DIR}/pending/${ACTION_ID}.json" "${VAULT_DIR}/approved/${ACTION_ID}.json" mv "${VAULT_DIR}/pending/${ACTION_ID}.json" "${VAULT_DIR}/approved/${ACTION_ID}.json"
ACTION_FILE="${VAULT_DIR}/approved/${ACTION_ID}.json" ACTION_FILE="${VAULT_DIR}/approved/${ACTION_ID}.json"
# Update status in the file
TMP=$(mktemp) TMP=$(mktemp)
jq '.status = "approved"' "$ACTION_FILE" > "$TMP" && mv "$TMP" "$ACTION_FILE" jq '.status = "approved"' "$ACTION_FILE" > "$TMP" && mv "$TMP" "$ACTION_FILE"
log "$ACTION_ID: pending → approved" log "$ACTION_ID: pending → approved"
else else
log "ERROR: action $ACTION_ID not found in pending/ or approved/" log "ERROR: item $ACTION_ID not found in pending/ or approved/"
exit 1 exit 1
fi fi
@ -52,7 +66,42 @@ fi
echo $$ > "$LOCKFILE" echo $$ > "$LOCKFILE"
trap 'rm -f "$LOCKFILE"' EXIT trap 'rm -f "$LOCKFILE"' EXIT
# Read action metadata # =============================================================================
# Pipeline A: Procurement — extract RESOURCES.md entry and append
# =============================================================================
if [ "$IS_PROCUREMENT" = true ]; then
log "$ACTION_ID: firing procurement request"
# Extract the proposed RESOURCES.md entry from the markdown file.
# Everything after the "## Proposed RESOURCES.md Entry" heading to EOF.
# Uses awk because the entry itself contains ## headings (## <resource-id>).
ENTRY=""
ENTRY=$(awk '/^## Proposed RESOURCES\.md Entry/{found=1; next} found{print}' "$ACTION_FILE" 2>/dev/null || true)
# Strip leading/trailing blank lines and markdown code fences
ENTRY=$(echo "$ENTRY" | sed '/^```/d' | sed -e '/./,$!d' -e :a -e '/^\n*$/{$d;N;ba;}')
if [ -z "$ENTRY" ]; then
log "ERROR: $ACTION_ID has no '## Proposed RESOURCES.md Entry' section"
matrix_send "vault" "❌ Procurement $ACTION_ID has no RESOURCES.md entry — cannot fire" 2>/dev/null || true
exit 1
fi
# Append entry to RESOURCES.md
printf '\n%s\n' "$ENTRY" >> "$RESOURCES_FILE"
log "$ACTION_ID: wrote RESOURCES.md entry"
# Move to fired/
mv "$ACTION_FILE" "${VAULT_DIR}/fired/${ACTION_ID}.md"
rm -f "${LOCKS_DIR}/${ACTION_ID}.notified"
log "$ACTION_ID: approved → fired (procurement)"
matrix_send "vault" "✅ Procurement fulfilled: ${ACTION_ID} — RESOURCES.md updated" 2>/dev/null || true
exit 0
fi
# =============================================================================
# Pipeline B: Action gating — dispatch to handler
# =============================================================================
ACTION_TYPE=$(jq -r '.type // ""' < "$ACTION_FILE") ACTION_TYPE=$(jq -r '.type // ""' < "$ACTION_FILE")
ACTION_SOURCE=$(jq -r '.source // ""' < "$ACTION_FILE") ACTION_SOURCE=$(jq -r '.source // ""' < "$ACTION_FILE")
PAYLOAD=$(jq -c '.payload // {}' < "$ACTION_FILE") PAYLOAD=$(jq -c '.payload // {}' < "$ACTION_FILE")
@ -64,9 +113,6 @@ fi
log "$ACTION_ID: firing type=$ACTION_TYPE source=$ACTION_SOURCE" log "$ACTION_ID: firing type=$ACTION_TYPE source=$ACTION_SOURCE"
# =============================================================================
# Dispatch to handler
# =============================================================================
FIRE_EXIT=0 FIRE_EXIT=0
case "$ACTION_TYPE" in case "$ACTION_TYPE" in

View file

@ -1,10 +1,15 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# vault-poll.sh — Vault gate agent: process pending actions, retry approved, timeout escalations # vault-poll.sh — Vault: process pending actions + procurement requests
# #
# Runs every 30min via cron. Processes actions through the vault pipeline: # Runs every 30min via cron. Two pipelines:
# 1. Retry any approved/ actions that weren't fired (crash recovery) # A. Action gating (*.json): auto-approve/escalate/reject via vault-agent.sh
# B. Procurement (*.md): notify human, fire approved requests via vault-fire.sh
#
# Phases:
# 1. Retry any approved/ items that weren't fired (crash recovery)
# 2. Auto-reject escalations with no reply for 48h # 2. Auto-reject escalations with no reply for 48h
# 3. Invoke vault-agent.sh for new pending/ actions # 3. Invoke vault-agent.sh for new pending JSON actions
# 4. Notify human about new pending procurement requests (.md)
# #
# Cron: */30 * * * * /path/to/disinto/vault/vault-poll.sh # Cron: */30 * * * * /path/to/disinto/vault/vault-poll.sh
# #
@ -67,9 +72,9 @@ unlock_action() {
} }
# ============================================================================= # =============================================================================
# PHASE 1: Retry approved actions (crash recovery) # PHASE 1: Retry approved items (crash recovery — JSON actions + MD procurement)
# ============================================================================= # =============================================================================
status "phase 1: retrying approved actions" status "phase 1: retrying approved items"
for action_file in "${VAULT_DIR}/approved/"*.json; do for action_file in "${VAULT_DIR}/approved/"*.json; do
[ -f "$action_file" ] || continue [ -f "$action_file" ] || continue
@ -92,6 +97,27 @@ for action_file in "${VAULT_DIR}/approved/"*.json; do
unlock_action "$ACTION_ID" unlock_action "$ACTION_ID"
done done
# Retry approved procurement requests (.md)
for req_file in "${VAULT_DIR}/approved/"*.md; do
[ -f "$req_file" ] || continue
REQ_ID=$(basename "$req_file" .md)
if ! lock_action "$REQ_ID"; then
log "skip procurement $REQ_ID — locked by another process"
continue
fi
log "retrying approved procurement: $REQ_ID"
if bash "${VAULT_DIR}/vault-fire.sh" "$REQ_ID" >> "$LOGFILE" 2>&1; then
log "fired procurement $REQ_ID (retry)"
else
log "ERROR: fire failed for procurement $REQ_ID (retry)"
matrix_send "vault" "❌ Vault fire failed on retry: ${REQ_ID} (procurement)" 2>/dev/null || true
fi
unlock_action "$REQ_ID"
done
# ============================================================================= # =============================================================================
# PHASE 2: Timeout escalations (48h no reply → auto-reject) # PHASE 2: Timeout escalations (48h no reply → auto-reject)
# ============================================================================= # =============================================================================
@ -122,7 +148,7 @@ for action_file in "${VAULT_DIR}/pending/"*.json; do
done done
# ============================================================================= # =============================================================================
# PHASE 3: Process new pending actions # PHASE 3: Process new pending actions (JSON — action gating)
# ============================================================================= # =============================================================================
status "phase 3: processing pending actions" status "phase 3: processing pending actions"
@ -152,17 +178,62 @@ for action_file in "${VAULT_DIR}/pending/"*.json; do
unlock_action "$ACTION_ID" unlock_action "$ACTION_ID"
done done
if [ "$PENDING_COUNT" -eq 0 ]; then if [ "$PENDING_COUNT" -gt 0 ]; then
status "all clear — no pending actions" log "found $PENDING_COUNT pending action(s), invoking vault-agent"
exit 0 status "invoking vault-agent for $PENDING_COUNT action(s)"
fi
log "found $PENDING_COUNT pending action(s), invoking vault-agent" bash "${VAULT_DIR}/vault-agent.sh" >> "$LOGFILE" 2>&1 || {
status "invoking vault-agent for $PENDING_COUNT action(s)"
bash "${VAULT_DIR}/vault-agent.sh" >> "$LOGFILE" 2>&1 || {
log "ERROR: vault-agent failed" log "ERROR: vault-agent failed"
matrix_send "vault" "❌ vault-agent.sh failed — check vault.log" 2>/dev/null || true matrix_send "vault" "❌ vault-agent.sh failed — check vault.log" 2>/dev/null || true
} }
fi
status "poll complete" # =============================================================================
# PHASE 4: Notify human about new pending procurement requests (.md)
# =============================================================================
status "phase 4: processing pending procurement requests"
PROCURE_COUNT=0
for req_file in "${VAULT_DIR}/pending/"*.md; do
[ -f "$req_file" ] || continue
REQ_ID=$(basename "$req_file" .md)
# Check if already notified (marker file)
if [ -f "${VAULT_DIR}/.locks/${REQ_ID}.notified" ]; then
continue
fi
if ! lock_action "$REQ_ID"; then
log "skip procurement $REQ_ID — locked"
continue
fi
PROCURE_COUNT=$((PROCURE_COUNT + 1))
# Extract title from first heading
REQ_TITLE=$(grep -m1 '^# ' "$req_file" | sed 's/^# //' || echo "$REQ_ID")
log "new procurement request: $REQ_ID$REQ_TITLE"
# Notify human via Matrix
matrix_send "vault" "🔑 PROCUREMENT REQUEST — ${REQ_TITLE}
ID: ${REQ_ID}
Action: review vault/pending/${REQ_ID}.md
To approve: fulfill the request, add secrets to .env, move file to vault/approved/
$(head -20 "$req_file")" 2>/dev/null || true
# Mark as notified so we don't re-send
mkdir -p "${VAULT_DIR}/.locks"
touch "${VAULT_DIR}/.locks/${REQ_ID}.notified"
unlock_action "$REQ_ID"
done
if [ "$PENDING_COUNT" -eq 0 ] && [ "$PROCURE_COUNT" -eq 0 ]; then
status "all clear — no pending items"
else
status "poll complete — ${PENDING_COUNT} action(s), ${PROCURE_COUNT} procurement request(s)"
fi