chore: consolidate new foundation and archive v1 (#1495)

This commit is contained in:
2026-09-07 12:32:57 -05:00
3511 changed files with 727899 additions and 10 deletions
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root() {
git rev-parse --show-toplevel 2>/dev/null || pwd
}
ensure_repo_root() {
cd "$(repo_root)"
}
has_remote() {
git remote get-url origin >/dev/null 2>&1
}
run_step() {
local label="$1"
shift
echo "[agent-framework] $label"
"$@"
}
load_repo_hooks() {
local hooks_file=".mosaic/repo-hooks.sh"
if [[ -f "$hooks_file" ]]; then
# shellcheck disable=SC1090
source "$hooks_file"
fi
}
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./common.sh
source "$SCRIPT_DIR/common.sh"
ensure_repo_root
load_repo_hooks
if declare -F mosaic_hook_critical >/dev/null 2>&1; then
run_step "Run repo critical hook" mosaic_hook_critical
else
echo "[agent-framework] No repo critical hook configured (.mosaic/repo-hooks.sh)"
echo "[agent-framework] Define mosaic_hook_critical() for project-specific priority scans"
fi
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
set -euo pipefail
TITLE="${1:-}"
if [[ -z "$TITLE" ]]; then
echo "Usage: $0 \"Short limitation title\"" >&2
exit 1
fi
FILE="EVOLUTION.md"
if [[ ! -f "$FILE" ]]; then
echo "[agent-framework] $FILE not found. Create project-specific limitations log if needed."
exit 0
fi
if command -v rg >/dev/null 2>&1; then
last_num=$(rg -o "^### L-[0-9]{3}" "$FILE" | sed 's/^### L-//' | sort -n | tail -1)
else
last_num=$(grep -E "^### L-[0-9]{3}" "$FILE" | sed 's/^### L-//' | sort -n | tail -1)
fi
if [[ -z "$last_num" ]]; then
next_num="001"
else
next_num=$(printf "%03d" $((10#$last_num + 1)))
fi
entry_id="L-$next_num"
cat <<EOF2
### $entry_id: $TITLE
| Aspect | Details |
|--------|---------|
| **Pain** | TODO |
| **Impact** | TODO |
| **Frequency** | TODO |
| **Current Workaround** | TODO |
| **Proposed Solution** | TODO |
| **Platform Implication** | TODO |
EOF2
echo "[agent-framework] Suggested limitation ID: $entry_id"
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./common.sh
source "$SCRIPT_DIR/common.sh"
ensure_repo_root
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
ORCH_DIR=".mosaic/orchestrator"
PID_FILE="$ORCH_DIR/orchestrator.pid"
LOG_FILE="$ORCH_DIR/logs/daemon.log"
usage() {
cat <<USAGE
Usage: $(basename "$0") <start|drain|stop|status> [--poll-sec N] [--no-sync]
Commands:
start Run orchestrator drain loop in background (detached)
drain Run orchestrator drain loop in foreground (until queue drained)
stop Stop background orchestrator if running
status Show background orchestrator status
Options:
--poll-sec N Poll interval (default: 15)
--no-sync Skip docs/TASKS.md -> orchestrator queue sync before run
USAGE
}
cmd="${1:-status}"
if [[ $# -gt 0 ]]; then
shift
fi
poll_sec=15
sync_arg=""
while [[ $# -gt 0 ]]; do
case "$1" in
--poll-sec)
poll_sec="${2:-15}"
shift 2
;;
--no-sync)
sync_arg="--no-sync"
shift
;;
*)
echo "[agent-framework] unknown argument: $1" >&2
usage
exit 1
;;
esac
done
mkdir -p "$ORCH_DIR/logs" "$ORCH_DIR/results"
is_running() {
[[ -f "$PID_FILE" ]] || return 1
local pid
pid="$(cat "$PID_FILE" 2>/dev/null || true)"
[[ -n "$pid" ]] || return 1
kill -0 "$pid" 2>/dev/null
}
case "$cmd" in
start)
if is_running; then
echo "[agent-framework] orchestrator already running (pid=$(cat "$PID_FILE"))"
exit 0
fi
nohup "$MOSAIC_HOME/tools/_scripts/mosaic-orchestrator-drain" --poll-sec "$poll_sec" $sync_arg >"$LOG_FILE" 2>&1 &
echo "$!" > "$PID_FILE"
echo "[agent-framework] orchestrator started (pid=$!, log=$LOG_FILE)"
;;
drain)
exec "$MOSAIC_HOME/tools/_scripts/mosaic-orchestrator-drain" --poll-sec "$poll_sec" $sync_arg
;;
stop)
if ! is_running; then
echo "[agent-framework] orchestrator not running"
rm -f "$PID_FILE"
exit 0
fi
pid="$(cat "$PID_FILE")"
kill "$pid" || true
rm -f "$PID_FILE"
echo "[agent-framework] orchestrator stopped (pid=$pid)"
;;
status)
if is_running; then
echo "[agent-framework] orchestrator running (pid=$(cat "$PID_FILE"), log=$LOG_FILE)"
else
echo "[agent-framework] orchestrator not running"
rm -f "$PID_FILE"
fi
;;
*)
usage
exit 1
;;
esac
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
set -euo pipefail
task_file="${1:-}"
if [[ -z "$task_file" || ! -f "$task_file" ]]; then
echo "[orchestrator-worker] missing task file argument" >&2
exit 1
fi
worker_exec="${MOSAIC_WORKER_EXEC:-}"
if [[ -z "$worker_exec" ]]; then
if command -v codex >/dev/null 2>&1; then
worker_exec="codex -p"
elif command -v opencode >/dev/null 2>&1; then
worker_exec="opencode -p"
else
echo "[orchestrator-worker] set MOSAIC_WORKER_EXEC to your worker command (example: 'codex -p' or 'opencode -p')" >&2
exit 1
fi
fi
prompt="$(python3 - "$task_file" <<'PY'
import json
import sys
from pathlib import Path
task = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
task_id = str(task.get("id", "TASK"))
title = str(task.get("title", ""))
description = str(task.get("description", ""))
meta = task.get("metadata", {}) or {}
issue = str(meta.get("issue", ""))
repo = str(meta.get("repo", ""))
branch = str(meta.get("branch", ""))
depends = task.get("depends_on", [])
if isinstance(depends, list):
depends_str = ", ".join(str(x) for x in depends)
else:
depends_str = str(depends)
print(
f"""Read ~/.config/mosaic/STANDARDS.md, then AGENTS.md and SOUL.md (if present).
Complete this queued task fully.
Task ID: {task_id}
Title: {title}
Description: {description}
Issue: {issue}
Repo hint: {repo}
Branch hint: {branch}
Depends on: {depends_str}
Requirements:
- Implement and verify the task end-to-end.
- Keep changes scoped to this task.
- Run project checks and tests relevant to touched code.
- Return with a concise summary of what changed and verification results.
"""
)
PY
)"
PROMPT="$prompt" bash -lc "$worker_exec \"\$PROMPT\""
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./common.sh
source "$SCRIPT_DIR/common.sh"
ensure_repo_root
load_repo_hooks
# ─── Mission session cleanup (ORCHESTRATOR-PROTOCOL) ────────────────────────
ORCH_DIR=".mosaic/orchestrator"
MISSION_JSON="$ORCH_DIR/mission.json"
SESSION_LOCK="$ORCH_DIR/session.lock"
COORD_LIB="$HOME/.config/mosaic/tools/orchestrator/_lib.sh"
if [[ -f "$SESSION_LOCK" ]] && [[ -f "$COORD_LIB" ]] && command -v jq &>/dev/null; then
# shellcheck source=/dev/null
source "$COORD_LIB"
sess_id="$(jq -r '.session_id // ""' "$SESSION_LOCK")"
if [[ -n "$sess_id" && -f "$MISSION_JSON" ]]; then
# Update mission.json: mark session ended
updated="$(jq \
--arg sid "$sess_id" \
--arg ts "$(iso_now)" \
--arg reason "completed" \
'(.sessions[] | select(.session_id == $sid)) |= . + {
ended_at: $ts,
ended_reason: $reason
}' "$MISSION_JSON")"
echo "$updated" > "$MISSION_JSON.tmp" && mv "$MISSION_JSON.tmp" "$MISSION_JSON"
echo "[agent-framework] Session $sess_id recorded in mission state"
fi
session_lock_clear "."
fi
if declare -F mosaic_hook_session_end >/dev/null 2>&1; then
run_step "Run repo end hook" mosaic_hook_session_end
else
echo "[agent-framework] No repo end hook configured (.mosaic/repo-hooks.sh)"
fi
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
run_step "Show status" git status --short
run_step "Show diff summary" git diff --stat
fi
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./common.sh
source "$SCRIPT_DIR/common.sh"
ensure_repo_root
load_repo_hooks
# ─── Update check (non-blocking) ────────────────────────────────────────────
if command -v mosaic &>/dev/null; then
if mosaic update --check 2>/dev/null; then
: # up to date
elif [[ $? -eq 2 ]]; then
echo ""
echo "[agent-framework] ⚠ A newer version of Mosaic CLI is available."
echo "[agent-framework] Run: mosaic update or bash tools/install.sh"
echo ""
fi
fi
if git rev-parse --is-inside-work-tree >/dev/null 2>&1 && has_remote; then
if git diff --quiet && git diff --cached --quiet; then
run_step "Pull latest changes" git pull --rebase
else
echo "[agent-framework] Skip pull: working tree has local changes"
fi
fi
# ─── Mission state detection (ORCHESTRATOR-PROTOCOL) ────────────────────────
ORCH_DIR=".mosaic/orchestrator"
MISSION_JSON="$ORCH_DIR/mission.json"
COORD_LIB="$HOME/.config/mosaic/tools/orchestrator/_lib.sh"
if [[ -f "$MISSION_JSON" ]] && command -v jq &>/dev/null; then
mission_status="$(jq -r '.status // "inactive"' "$MISSION_JSON")"
if [[ "$mission_status" == "active" || "$mission_status" == "paused" ]]; then
mission_name="$(jq -r '.name // "unnamed"' "$MISSION_JSON")"
echo ""
echo "========================================="
echo "ACTIVE MISSION DETECTED"
echo "========================================="
echo " Mission: $mission_name"
# Extract key fields from manifest if present
manifest="docs/MISSION-MANIFEST.md"
if [[ -f "$manifest" ]]; then
phase="$(grep -m1 '^\*\*Phase:\*\*' "$manifest" 2>/dev/null | sed 's/.*\*\*Phase:\*\* //' || true)"
milestone="$(grep -m1 '^\*\*Current Milestone:\*\*' "$manifest" 2>/dev/null | sed 's/.*\*\*Current Milestone:\*\* //' || true)"
progress="$(grep -m1 '^\*\*Progress:\*\*' "$manifest" 2>/dev/null | sed 's/.*\*\*Progress:\*\* //' || true)"
[[ -n "$phase" ]] && echo " Phase: $phase"
[[ -n "$milestone" ]] && echo " Milestone: $milestone"
[[ -n "$progress" ]] && echo " Progress: $progress"
fi
# Task counts
if [[ -f "docs/TASKS.md" ]]; then
total="$(grep -c '^|' "docs/TASKS.md" 2>/dev/null || true)"
total="${total:-0}"
done_count="$(grep -ci '| done \|| completed ' "docs/TASKS.md" 2>/dev/null || true)"
done_count="${done_count:-0}"
approx_total=$(( total > 2 ? total - 2 : 0 ))
echo " Tasks: ~${done_count} done of ~${approx_total} total"
fi
# Scratchpad
if [[ -d "docs/scratchpads" ]]; then
latest_sp="$(ls -t docs/scratchpads/*.md 2>/dev/null | head -1 || true)"
[[ -n "$latest_sp" ]] && echo " Scratchpad: $latest_sp"
fi
echo ""
echo " Resume: Read manifest + scratchpad before taking action."
echo " Protocol: ~/.config/mosaic/guides/ORCHESTRATOR-PROTOCOL.md"
echo "========================================="
echo ""
# Register session if coordinator lib is available
if [[ -f "$COORD_LIB" ]]; then
# shellcheck source=/dev/null
source "$COORD_LIB"
sess_id="$(next_session_id ".")"
runtime="${MOSAIC_RUNTIME:-unknown}"
session_lock_write "." "$sess_id" "$runtime" "$$"
# Append session to mission.json
updated="$(jq \
--arg sid "$sess_id" \
--arg rt "$runtime" \
--arg ts "$(iso_now)" \
'.sessions += [{"session_id":$sid,"runtime":$rt,"started_at":$ts,"ended_at":"","ended_reason":"","milestone_at_end":"","tasks_completed":[],"last_task_id":""}]' \
"$MISSION_JSON")"
echo "$updated" > "$MISSION_JSON.tmp" && mv "$MISSION_JSON.tmp" "$MISSION_JSON"
fi
fi
fi
if declare -F mosaic_hook_session_start >/dev/null 2>&1; then
run_step "Run repo start hook" mosaic_hook_session_start
else
echo "[agent-framework] No repo start hook configured (.mosaic/repo-hooks.sh)"
fi
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env bash
# reflect-board-history.sh — Phase-0 experiment P3 (outcome detectability)
#
# Question: for completed tasks, how often does a machine-detectable
# correct/wrong outcome signal appear within a follow-up window (default 30d)?
# If the base rate is too low, predicted-vs-actual calibration (design §7) has
# nothing to score against, so the kernel should capture caveat-notes only.
#
# Method: consume a board/task export (JSONL, one task object per line) OR fall
# back to scanning the git history of a `data/` task directory. For each task
# that reached a "done"-like state, decide whether a later signal marks it
# correct or wrong (reopen, revert, follow-up "fix"/"regression", explicit
# outcome field). Emit the detectable-outcome base rate. HARNESS + RUBRIC.
#
# Usage:
# scripts/analysis/reflect-board-history.sh --jsonl FILE [--window-days N] [--json|--md]
# scripts/analysis/reflect-board-history.sh --data-dir DIR [--window-days N] [--json|--md]
#
# JSONL fields used (best-effort): .id .status .completed_at .outcome
# .reopened_at .followups[] (free-form). Missing fields are tolerated.
#
# Requirements: jq (for --jsonl), git (for --data-dir), awk.
#
# PRE-REGISTERED KILL CONDITION:
# detectable-outcome base rate < 20% ⇒ do NOT build §7 calibration loop;
# capture caveat-notes only.
set -euo pipefail
JSONL=""
DATA_DIR=""
WINDOW_DAYS=30
FORMAT="json"
while [[ $# -gt 0 ]]; do
case "$1" in
--jsonl) JSONL="$2"; shift 2 ;;
--data-dir) DATA_DIR="$2"; shift 2 ;;
--window-days) WINDOW_DAYS="$2"; shift 2 ;;
--json) FORMAT="json"; shift ;;
--md) FORMAT="md"; shift ;;
-h|--help) sed -n '2,32p' "$0"; exit 0 ;;
*) echo "unknown arg: $1" >&2; exit 2 ;;
esac
done
KILL_CONDITION='detectable-outcome base rate < 20% ⇒ do NOT build §7 calibration loop'
echo "# pre-registered kill condition: ${KILL_CONDITION}" >&2
done_total=0
detectable=0
if [[ -n "$JSONL" ]]; then
command -v jq >/dev/null 2>&1 || { echo "jq required for --jsonl" >&2; exit 3; }
[[ -r "$JSONL" ]] || { echo "cannot read $JSONL" >&2; exit 3; }
# Count done tasks and those with a machine-detectable outcome signal.
done_total="$(jq -rs '[.[] | select((.status // "") | test("done|complete|closed"; "i"))] | length' "$JSONL" 2>/dev/null || echo 0)"
detectable="$(jq -rs '
[ .[]
| select((.status // "") | test("done|complete|closed"; "i"))
| select(
(.outcome // null) != null
or (.reopened_at // null) != null
or ((.followups // []) | length) > 0
)
] | length' "$JSONL" 2>/dev/null || echo 0)"
elif [[ -n "$DATA_DIR" ]]; then
command -v git >/dev/null 2>&1 || { echo "git required for --data-dir" >&2; exit 3; }
[[ -d "$DATA_DIR" ]] || { echo "no such dir: $DATA_DIR" >&2; exit 3; }
# Proxy: a task file later touched by a commit whose subject signals a
# correction is a "detectable outcome".
while IFS= read -r file; do
[[ -z "$file" ]] && continue
done_total=$((done_total + 1))
history_rc=0
history="$(git -C "$DATA_DIR" log --since="${WINDOW_DAYS} days ago" --pretty='%s' -- "$file" 2>/dev/null)" || history_rc=$?
if [[ "$history_rc" -eq 0 ]] && grep -qiE 'reopen|revert|fix|regression|wrong|incorrect|redo' <<<"$history"; then
detectable=$((detectable + 1))
fi
done < <(find "$DATA_DIR" -type f -name '*.json' 2>/dev/null)
else
echo "provide --jsonl FILE or --data-dir DIR" >&2
exit 2
fi
rate="$(awk "BEGIN{ if ($done_total==0) print \"0.0\"; else printf \"%.1f\", 100*$detectable/$done_total }")"
verdict="$(awk "BEGIN{print ($rate < 20.0) ? \"KILL §7 — caveat-notes only\" : \"signal present — proceed\"}")"
if [[ "$FORMAT" == "md" ]]; then
cat <<EOF
## P3 — outcome detectability
- done-like tasks: **${done_total}**
- with machine-detectable outcome (window ${WINDOW_DAYS}d): **${detectable}**
- base rate: **${rate}%**
- kill condition: ${KILL_CONDITION}
- verdict: **${verdict}**
EOF
else
awk -v dt="$done_total" -v d="$detectable" -v r="$rate" -v w="$WINDOW_DAYS" \
-v v="$verdict" -v kc="$KILL_CONDITION" 'BEGIN{
printf "{\n"
printf " \"experiment\": \"P3-board-history\",\n"
printf " \"window_days\": %d,\n", w
printf " \"done_tasks\": %d,\n", dt
printf " \"detectable_outcomes\": %d,\n", d
printf " \"base_rate_pct\": %s,\n", r
printf " \"kill_condition\": \"%s\",\n", kc
printf " \"verdict\": \"%s\"\n", v
printf "}\n"
}'
fi
+117
View File
@@ -0,0 +1,117 @@
#!/usr/bin/env bash
# reflect-calibration.sh — Phase-0 experiment P1 (confidence signal)
#
# Question: does an agent's self-reported confidence discriminate correct from
# incorrect work — especially on the self-rated-HIGH subset, where a closed
# loop would actually trust it? If confidence ≈ chance on the high subset, the
# signal is useless and design §7–§8 should not be built.
#
# Method: consume a labelled corpus — JSONL of {confidence: 0..1, correct:
# true|false}. Compute discrimination as ROC AUC over all rows, plus the
# correct-rate (lift) on the high-confidence subset (>= threshold), and compare
# to the pre-registered chance baseline (the overall correct-rate). HARNESS +
# RUBRIC; the labelled corpus is supplied later.
#
# Usage:
# scripts/analysis/reflect-calibration.sh --jsonl FILE [--high 0.8] [--json|--md]
#
# Requirements: jq, awk.
#
# PRE-REGISTERED KILL CONDITION:
# AUC <= 0.60 OR high-subset lift <= +5pp over base rate
# ⇒ confidence is not a usable routing signal; do NOT build §7–§8.
set -euo pipefail
JSONL=""
HIGH=0.8
FORMAT="json"
while [[ $# -gt 0 ]]; do
case "$1" in
--jsonl) JSONL="$2"; shift 2 ;;
--high) HIGH="$2"; shift 2 ;;
--json) FORMAT="json"; shift ;;
--md) FORMAT="md"; shift ;;
-h|--help) sed -n '2,27p' "$0"; exit 0 ;;
*) echo "unknown arg: $1" >&2; exit 2 ;;
esac
done
KILL_CONDITION='AUC <= 0.60 OR high-subset lift <= +5pp ⇒ do NOT build §7–§8'
echo "# pre-registered kill condition: ${KILL_CONDITION}" >&2
command -v jq >/dev/null 2>&1 || { echo "jq required" >&2; exit 3; }
[[ -r "$JSONL" ]] || { echo "provide a readable --jsonl FILE" >&2; exit 2; }
# Normalise to "<confidence> <0|1>" rows; tolerate bad lines.
ROWS="$(jq -rs '
[ .[] | select((.confidence|type)=="number") |
"\(.confidence) \((.correct==true) | if . then 1 else 0 end)" ]
| .[]' "$JSONL" 2>/dev/null || true)"
if [[ -z "$ROWS" ]]; then
echo '{ "experiment": "P1-calibration", "error": "no usable rows" }'
exit 0
fi
# AUC via the MannWhitney U relation (rank-based); base rate; high-subset lift.
read -r N POS BASE AUC HIGH_N HIGH_CORRECT HIGH_RATE LIFT <<EOF
$(printf '%s\n' "$ROWS" | awk -v high="$HIGH" '
{ c=$1; y=$2; conf[NR]=c; lab[NR]=y; n++;
if (y==1) pos++; else neg++;
if (c>=high) { hn++; if (y==1) hc++ } }
END{
base = (n>0)? pos/n : 0;
# Rank-sum AUC: average ranks (ties → average rank).
# sort indices by confidence
for (i=1;i<=n;i++) idx[i]=i;
for (i=1;i<=n;i++) for (j=i+1;j<=n;j++) if (conf[idx[i]]>conf[idx[j]]) { t=idx[i]; idx[i]=idx[j]; idx[j]=t }
i=1;
while (i<=n) {
j=i; while (j<n && conf[idx[j+1]]==conf[idx[i]]) j++;
avg=(i+j)/2.0;
for (k=i;k<=j;k++) rank[idx[k]]=avg;
i=j+1;
}
rsum=0; for (i=1;i<=n;i++) if (lab[i]==1) rsum+=rank[i];
if (pos>0 && neg>0) auc=(rsum - pos*(pos+1)/2.0)/(pos*neg); else auc=0.5;
hrate=(hn>0)? hc/hn : 0;
lift=hrate-base;
printf "%d %d %.4f %.4f %d %d %.4f %.4f", n, pos, base, auc, hn, hc, hrate, lift
}')
EOF
verdict="$(awk -v auc="$AUC" -v lift="$LIFT" 'BEGIN{
print (auc <= 0.60 || lift <= 0.05) ? "KILL §7–§8 — confidence not usable" : "signal present — proceed"
}')"
if [[ "$FORMAT" == "md" ]]; then
cat <<EOF
## P1 — confidence calibration
- rows: **${N}** (positives ${POS}) · base correct-rate **$(awk "BEGIN{printf \"%.1f\", 100*${BASE}}")%**
- ROC AUC: **${AUC}**
- high-confidence subset (>= ${HIGH}): n=${HIGH_N}, correct=${HIGH_CORRECT}, rate=$(awk "BEGIN{printf \"%.1f\", 100*${HIGH_RATE}}")%
- lift over base: **$(awk "BEGIN{printf \"%+.1f\", 100*${LIFT}}")pp**
- kill condition: ${KILL_CONDITION}
- verdict: **${verdict}**
EOF
else
awk -v n="$N" -v pos="$POS" -v base="$BASE" -v auc="$AUC" -v hn="$HIGH_N" \
-v hc="$HIGH_CORRECT" -v hr="$HIGH_RATE" -v lift="$LIFT" -v high="$HIGH" \
-v v="$verdict" -v kc="$KILL_CONDITION" 'BEGIN{
printf "{\n"
printf " \"experiment\": \"P1-calibration\",\n"
printf " \"rows\": %d,\n", n
printf " \"positives\": %d,\n", pos
printf " \"base_rate\": %.4f,\n", base
printf " \"auc\": %.4f,\n", auc
printf " \"high_threshold\": %s,\n", high
printf " \"high_subset\": { \"n\": %d, \"correct\": %d, \"rate\": %.4f },\n", hn, hc, hr
printf " \"lift_over_base\": %.4f,\n", lift
printf " \"kill_condition\": \"%s\",\n", kc
printf " \"verdict\": \"%s\"\n", v
printf "}\n"
}'
fi
+110
View File
@@ -0,0 +1,110 @@
#!/usr/bin/env bash
# reflect-git-history.sh — Phase-0 experiment P2 ("only-self-reflection" bucket)
#
# Question: of the failures visible in git history, what fraction would ONLY
# have been caught by end-of-run self-reflection — i.e. NOT by CI and NOT by
# independent human review? If that bucket is near-empty, the closed
# calibration / skill-synthesis loop (design §7–§8) is not worth building.
#
# Method: scan `git log` over a window for failure signals (reverts, and
# fix:/hotfix commits landing shortly after a feature merge). Classify each by
# the gate most likely to have caught it, using a pre-registered heuristic.
# This is a HARNESS + RUBRIC; the classifier is deliberately simple and the
# real corpus/labelling is wired later. It emits a structured tally.
#
# Usage:
# scripts/analysis/reflect-git-history.sh [--repo PATH] [--since SINCE] [--json|--md]
#
# Options:
# --repo PATH repo to analyse (default: current repo)
# --since SINCE git log --since value (default: "6 months ago")
# --json emit JSON (default)
# --md emit markdown
#
# Requirements: git, awk.
#
# PRE-REGISTERED KILL CONDITION:
# bucket "only_self_reflection" is near-empty (< 10% of classified failures)
# ⇒ do NOT build design §7–§8 (closed loop). Caveat-notes capture only.
set -euo pipefail
REPO="."
SINCE="6 months ago"
FORMAT="json"
while [[ $# -gt 0 ]]; do
case "$1" in
--repo) REPO="$2"; shift 2 ;;
--since) SINCE="$2"; shift 2 ;;
--json) FORMAT="json"; shift ;;
--md) FORMAT="md"; shift ;;
-h|--help) sed -n '2,30p' "$0"; exit 0 ;;
*) echo "unknown arg: $1" >&2; exit 2 ;;
esac
done
KILL_CONDITION='bucket only_self_reflection < 10% of classified failures ⇒ do NOT build §7–§8'
echo "# pre-registered kill condition: ${KILL_CONDITION}" >&2
command -v git >/dev/null 2>&1 || { echo "git required" >&2; exit 3; }
# Collect candidate failure commits: reverts + fix/hotfix subjects.
mapfile -t LINES < <(
git -C "$REPO" log --since="$SINCE" --pretty='%H%x09%s' 2>/dev/null \
| grep -iE 'revert|hotfix|hot-fix|regression|fix(\(|:|!| )' || true
)
total=0; ci=0; human=0; selfonly=0
for line in "${LINES[@]}"; do
[[ -z "$line" ]] && continue
subj="${line#*$'\t'}"
total=$((total + 1))
# Pre-registered classification heuristic (gate most likely to have caught it):
# - build/test/lint/type/ci signals → CI would have caught it
# - security/auth/permission/data/migration → human review would flag it
# - everything else (logic/UX/assumption/edge) → only-self-reflection bucket
if grep -qiE 'test|lint|type|build|ci|compile|typo' <<<"$subj"; then
ci=$((ci + 1))
elif grep -qiE 'security|auth|permission|rbac|secret|migration|data|sql|injection' <<<"$subj"; then
human=$((human + 1))
else
selfonly=$((selfonly + 1))
fi
done
pct() { awk "BEGIN{ if ($2==0) print \"0.0\"; else printf \"%.1f\", 100*$1/$2 }"; }
self_pct="$(pct "$selfonly" "$total")"
verdict="$(awk "BEGIN{print ($self_pct < 10.0) ? \"KILL §7–§8\" : \"signal present — proceed to deeper labelling\"}")"
if [[ "$FORMAT" == "md" ]]; then
cat <<EOF
## P2 — git-history failure-gate attribution
- window: \`${SINCE}\` · repo: \`${REPO}\`
- classified failures: **${total}**
| gate | count | share |
|---|---:|---:|
| CI would catch | ${ci} | $(pct "$ci" "$total")% |
| human review would catch | ${human} | $(pct "$human" "$total")% |
| only-self-reflection | ${selfonly} | ${self_pct}% |
- kill condition: ${KILL_CONDITION}
- verdict: **${verdict}**
EOF
else
awk -v t="$total" -v c="$ci" -v h="$human" -v s="$selfonly" -v sp="$self_pct" \
-v v="$verdict" -v since="$SINCE" -v repo="$REPO" -v kc="$KILL_CONDITION" 'BEGIN{
printf "{\n"
printf " \"experiment\": \"P2-git-history\",\n"
printf " \"repo\": \"%s\",\n", repo
printf " \"since\": \"%s\",\n", since
printf " \"classified_failures\": %d,\n", t
printf " \"buckets\": { \"ci\": %d, \"human_review\": %d, \"only_self_reflection\": %d },\n", c, h, s
printf " \"only_self_reflection_pct\": %s,\n", sp
printf " \"kill_condition\": \"%s\",\n", kc
printf " \"verdict\": \"%s\"\n", v
printf "}\n"
}'
fi
@@ -0,0 +1,28 @@
[
"tools/matrix-presence-harness/run.sh:TSX_CLI=\"$(ls -d \"${REPO}\"/node_modules/.pnpm/tsx@*/node_modules/tsx/dist/cli.mjs 2>/dev/null | head -1)\"",
"tools/e2e-install-test.sh:if ! mosaic gateway --help 2>&1 | grep -q 'verify'; then",
"tools/install.sh:EXTRACTED_DIR=\"$(find \"$WORK_DIR\" -maxdepth 1 -mindepth 1 -type d | head -1)\"",
"scripts/analysis/reflect-board-history.sh:if git -C \"$DATA_DIR\" log --since=\"${WINDOW_DAYS} days ago\" --pretty='%s' -- \"$file\" 2>/dev/null | grep -qiE 'reopen|revert|fix|regression|wrong|incorrect|redo'; then",
"scripts/analysis/reflect-git-history.sh:if printf '%s' \"$subj\" | grep -qiE 'test|lint|type|build|ci|compile|typo'; then",
"scripts/analysis/reflect-git-history.sh:elif printf '%s' \"$subj\" | grep -qiE 'security|auth|permission|rbac|secret|migration|data|sql|injection'; then",
"packages/mosaic/framework/tools/authentik/user-create.sh:group_pk=$(echo \"$group_response\" | jq -r \".results[] | select(.name == \\\"$GROUP\\\") | .pk\" | head -1)",
"packages/mosaic/framework/tools/git/mutate-push-guard.sh:PROSE_LO=\"$(grep -n '^usage() {' \"$BAK\" | head -1 | cut -d: -f1)\"",
"packages/mosaic/framework/tools/orchestrator/session-resume.sh:echo \"$dirty_files\" | head -20 | while IFS= read -r line; do",
"packages/mosaic/framework/tools/prdy/prdy-status.sh:if echo \"$PRD_CONTENT\" | grep -qiE \"$pattern\"; then",
"packages/mosaic/framework/tools/qa/reflect-stop-hook.sh:if printf '%s' \"$p\" | grep -qiE 'auth|login|session|token|permission|rbac|credential|secret'; then echo auth; return; fi",
"packages/mosaic/framework/tools/qa/reflect-stop-hook.sh:if printf '%s' \"$p\" | grep -qiE 'migration|prisma|schema|\\.sql|entity|repository|seed'; then echo data; return; fi",
"packages/mosaic/framework/tools/qa/reflect-stop-hook.sh:if printf '%s' \"$p\" | grep -qiE 'docker|\\.woodpecker|compose|traefik|deploy|helm|k8s|terraform'; then echo infra; return; fi",
"packages/mosaic/framework/tools/qa/reflect-stop-hook.sh:if printf '%s' \"$p\" | grep -qiE 'package\\.json|tsconfig|turbo\\.json|pnpm-|\\.config\\.|eslint|vite'; then echo build; return; fi",
"packages/mosaic/framework/tools/qa/reflect-stop-hook.sh:if printf '%s' \"$p\" | grep -qE '\\.tsx|\\.css|components/|apps/web/'; then echo ui; return; fi",
"packages/mosaic/framework/tools/qa/reflect-stop-hook.sh:if printf '%s' \"$p\" | grep -qE '\\.spec\\.|\\.test\\.|__tests__/'; then echo test; return; fi",
"packages/mosaic/framework/tools/qa/reflect-stop-hook.sh:if printf '%s' \"$p\" | grep -qE '\\.md$|docs/'; then echo docs; return; fi",
"packages/mosaic/framework/tools/qa/typecheck-hook.sh:FILE_PATH=$(echo \"$JSON_INPUT\" | grep -o '\"file_path\"[[:space:]]*:[[:space:]]*\"[^\"]*\"' | sed 's/.*\"\\([^\"]*\\)\"$/\\1/' | head -1)",
"packages/mosaic/framework/tools/qa/typecheck-hook.sh:RELEVANT=$(echo \"$OUTPUT\" | grep -A2 \"$BASENAME\" 2>/dev/null || echo \"$OUTPUT\" | head -20)",
"packages/mosaic/framework/tools/tmux/send-message.sh:if printf '%s' \"$pane\" | grep -qF \"$QUEUED_RE\"; then",
"packages/mosaic/framework/tools/tmux/send-message.sh:if [ -n \"$snippet\" ] && printf '%s' \"$promptline\" | grep -qF \"$snippet\"; then",
"packages/mosaic/framework/tools/wake/detector.sh:sed -n \"s/^${key}=//p\" \"$MANIFEST\" | head -n1 | tr -d '[:space:]'",
"packages/mosaic/framework/tools/wake/detector.sh:if [ -n \"$snap_sha\" ] && ! printf '%s' \"$snap_sha\" | grep -Eq '^[0-9a-f]{7,64}$'; then",
"packages/mosaic/framework/tools/wake/detector.sh:if [ -n \"$snap_ts\" ] && ! printf '%s' \"$snap_ts\" | grep -Eq '^[0-9]{1,12}$'; then",
"packages/mosaic/framework/tools/wake/digest.sh:olabel=\"$(_locator_line \"$oloc\" | head -n1)\"",
"packages/mosaic/framework/tools/wake/reconcile.sh:sed -n \"s/^${key}=//p\" \"$MANIFEST\" | head -n1 | tr -d '[:space:]'"
]
@@ -0,0 +1,24 @@
[
"packages/mosaic/framework/systemd/user/test-fleet-units.sh:if tmux -L \"$TEST_SOCKET\" show-environment -g LD_PRELOAD 2>/dev/null | grep -q '^LD_PRELOAD='; then",
"packages/mosaic/framework/tools/git/test-issue-comment-readback.sh:write_response \"$(printf '%s' \"$result\" | head -n1)\" \"$(printf '%s' \"$result\" | tail -n +2)\"",
"packages/mosaic/framework/tools/git/test-issue-comment-readback.sh:write_response \"$(printf '%s' \"$result\" | head -n1)\" \"$(printf '%s' \"$result\" | tail -n +2)\"",
"packages/mosaic/framework/tools/git/test-lane-brief-pr-linkage.sh:contains() { printf '%s\\n' \"$1\" | grep -qx \"$2\"; }",
"packages/mosaic/framework/tools/git/test-pr-review-gitea-comment.sh:write_response \"$(printf '%s' \"$result\" | head -n1)\" \"$(printf '%s' \"$result\" | tail -n +2)\"",
"packages/mosaic/framework/tools/git/test-pr-review-repo-host-override.sh:echo \"$HELP_TEXT\" | grep -q -- '-r, --repo'",
"packages/mosaic/framework/tools/git/test-pr-review-repo-host-override.sh:echo \"$HELP_TEXT\" | grep -q -- '-H, --host'",
"packages/mosaic/framework/tools/orchestrator/smoke-test.sh:if [[ \"$(printf '%s\\n' \"$codex_run_prompt\" | head -n1)\" == \"Now initiating Orchestrator mode...\" ]]; then pass_case \"codex run prompt first line is mode declaration\"; else fail_case \"codex run prompt first line is mode declaration\"; fi",
"packages/mosaic/framework/tools/orchestrator/smoke-test.sh:if [[ \"$(printf '%s\\n' \"$claude_run_prompt\" | head -n1)\" == \"## Continuation Mission\" ]]; then pass_case \"claude run prompt remains continuation prompt format\"; else fail_case \"claude run prompt remains continuation prompt format\"; fi",
"packages/mosaic/framework/tools/orchestrator/test-board-roll.sh:echo \"$out\" | grep -qi \"dry run\" || note \"dry-run did not announce itself\"",
"packages/mosaic/framework/tools/orchestrator/test-board-roll.sh:echo \"$out\" | grep -q \"would roll\" || note \"dry-run did not report a plan\"",
"packages/mosaic/framework/tools/quality/scripts/test-upgrade-durable-snapshot.sh:find \"$1/mosaic/backups\" -maxdepth 1 -type d -name 'pre-update-*' 2>/dev/null | LC_ALL=C sort -r | head -1",
"packages/mosaic/framework/tools/quality/scripts/test-upgrade-rollback.sh:SNAP_E=\"$(grep -o '/[^ ]*mosaic-snapshot[^ ]*' \"$OUTG\" | head -1)\"",
"packages/mosaic/framework/tools/quality/scripts/test-upgrade-rollback.sh:grep -o '/[^ ]*mosaic-snapshot[^ ]*' \"$OUTH\" 2>/dev/null | head -1 | while read -r s; do rm -rf \"$s\"; done",
"packages/mosaic/framework/tools/tmux/test-send-message-socket.sh:capture_named | grep -qF \"named socket hello\" || fail \"send-message.sh did not deliver to named socket\"",
"packages/mosaic/framework/tools/tmux/test-send-message-socket.sh:if capture_default | grep -qF \"named socket hello\"; then",
"packages/mosaic/framework/tools/tmux/test-send-message-socket.sh:capture_named | grep -qF \"[tester:source ->\" || fail \"agent-send.sh did not include preamble\"",
"packages/mosaic/framework/tools/tmux/test-send-message-socket.sh:capture_named | grep -qF \"agent socket hello\" || fail \"agent-send.sh did not deliver to named socket\"",
"packages/mosaic/framework/tools/tmux/test-send-message-socket.sh:if capture_default | grep -qF \"agent socket hello\"; then",
"packages/mosaic/framework/tools/tmux/test-send-message-socket.sh:printf '%s' \"$pane\" | grep -qF \"CONCPAYLOAD-${i}-END\" || fail \"concurrent send dropped payload for pane conc-$i\"",
"packages/mosaic/framework/tools/tmux/test-send-message-socket.sh:if printf '%s' \"$pane\" | grep -qF \"CONCPAYLOAD-${j}-END\"; then",
"packages/mosaic/framework/tools/tmux/test-send-message-verdict.sh:if [ \"$rc\" -eq 0 ] && printf '%s' \"$out\" | grep -qF \"✓ delivered\"; then"
]
@@ -0,0 +1,28 @@
[
"packages/mosaic/framework/tools/wake/test-wake-digest-quarantine.sh:fixture_line=\"$(has_match -F \"\\\"id\\\":\\\"$enum_id\\\"\" \"$self\" | has_match -F '\"observed_seq\":5' | head -n1)\"",
"packages/mosaic/framework/tools/wake/test-wake-preimage.sh:pre_seq=\"$(jq -r 'select(.locators.kind == \"preimage\") | .observed_seq' \"$sd/pending.jsonl\" | head -n1)\"",
"packages/mosaic/framework/tools/wake/test-wake-preimage.sh:src_seq=\"$(jq -r 'select(.locators.kind == \"repo\") | .observed_seq' \"$sd/pending.jsonl\" | head -n1)\"",
"packages/mosaic/framework/tools/wake/test-wake-preimage.sh:pre_seq=\"$(jq -r 'select(.locators.kind == \"preimage\") | .observed_seq' \"$sd/pending.jsonl\" | head -n1)\"",
"packages/mosaic/framework/tools/wake/test-wake-preimage.sh:enum_seq=\"$(jq -r 'select(.locators.reconciled == true) | .observed_seq' \"$sd/pending.jsonl\" | head -n1)\"",
"packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh:if ! sed -n \"${ln}p\" \"$f\" | grep -Eq 'has_match|count_lines'; then",
"packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh:printf '%s' \"$outA\" | grep -q 'mini-a: OK' &&",
"packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh:printf '%s' \"$outB\" | grep -q 'mini-b: OK' &&",
"packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh:check C1 1 \"rcA=$rcA rcB=$rcB expected($n_expected)/got diff: $(diff \"$TMP/expected-c1\" \"$TMP/got-c1\" 2>&1 | head -n 10 | tr '\\n' ' ')\"",
"packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh:! printf '%s' \"$out\" | grep -q 'mini-a: OK' &&",
"packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh:! printf '%s' \"$out\" | grep -q 'mini-a: FAILED' &&",
"packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh:printf '%s' \"$out\" | grep -q \"WAKE-ASSERT ARMED: forcing real grep error at $site\" &&",
"packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh:printf '%s' \"$out\" | grep -q \"WAKE-ASSERT ABORT\" &&",
"packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh:printf '%s' \"$out\" | grep -q \"$site\" &&",
"packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh:printf '%s' \"$out\" | grep -q \"grep exit 2\" &&",
"packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh:printf '%s' \"$out\" | grep -q 'mini-a: OK' &&",
"packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh:! printf '%s' \"$out\" | grep -q 'WAKE-ASSERT ARMED'; then",
"packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh:! printf '%s' \"$out\" | grep -q 'REACHED-PAST-INIT' &&",
"packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh:printf '%s' \"$out\" | grep -q 'WAKE-ASSERT INIT ABORT: BASH_LINENO convention violated'; then",
"packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh:printf '%s' \"$out\" | grep -q 'wake mini-c harness: FAILED (1 assertion(s))' &&",
"packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh:! printf '%s' \"$out\" | grep -q 'all invariants passed' &&",
"packages/mosaic/framework/tools/wake/validate-973/validate-973.sh:diff \"$TMP/expected.txt\" \"$TMP/static.txt\" | head -n 20 | sed 's/^/ /'",
"packages/mosaic/framework/tools/wake/validate-973/validate-973.sh:if printf '%s\\n' \"$out\" | grep -Eq \"$(sentinel_for \"$s\")\"; then",
"packages/mosaic/framework/tools/wake/validate-973/validate-973.sh:printf '%s\\n' \"$out\" | grep -q \"WAKE-ASSERT ARMED: forcing real grep error at $site\" ||",
"packages/mosaic/framework/tools/wake/validate-973/validate-973.sh:printf '%s\\n' \"$out\" | grep -q \"WAKE-ASSERT ABORT: ${helper} at ${site}: grep exit\" ||",
"packages/mosaic/framework/tools/wake/validate-973/validate-973.sh:printf '%s\\n' \"$out\" | grep -Eq \"$(sentinel_for \"$f\")\" || rc_sent=$?"
]
+156
View File
@@ -0,0 +1,156 @@
#!/usr/bin/env node
import { access, lstat, mkdir, readFile, readdir, rename, rm } from 'node:fs/promises';
import { execFile, spawn } from 'node:child_process';
import { promisify } from 'node:util';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
const execFileAsync = promisify(execFile);
function run(command, args, options) {
return new Promise((resolve, reject) => {
const child = spawn(command, args, options);
child.once('error', reject);
child.once('exit', (code, signal) => {
if (code === 0) resolve();
else reject(new Error(signal ? `husky terminated by ${signal}` : `husky exited ${code}`));
});
});
}
async function pathExists(target) {
try {
await access(target);
return true;
} catch (error) {
if (error.code === 'ENOENT') return false;
throw error;
}
}
async function directorySnapshot(root) {
const snapshot = [];
async function walk(current) {
const children = await readdir(current, { withFileTypes: true });
for (const child of children.sort((left, right) => left.name.localeCompare(right.name))) {
const target = path.join(current, child.name);
const relative = path.relative(root, target);
const stats = await lstat(target);
if (child.isDirectory()) {
snapshot.push([relative, 'directory', stats.mode & 0o777]);
await walk(target);
} else {
snapshot.push([
relative,
'file',
stats.mode & 0o777,
(await readFile(target)).toString('base64'),
]);
}
}
}
await walk(root);
return JSON.stringify(snapshot);
}
async function directoriesMatch(left, right) {
return (await directorySnapshot(left)) === (await directorySnapshot(right));
}
export async function installHooks({
root = process.cwd(),
disabled = process.env.HUSKY === '0',
quarantineRoot = path.join(root, '.mosaic-test-work', 'husky-quarantine'),
runHusky = async (_stagingHooks, stagingRepo) => {
await execFileAsync('git', ['init', '--quiet', stagingRepo]);
await run(path.join(root, 'node_modules', '.bin', 'husky'), ['.husky'], {
cwd: stagingRepo,
stdio: 'inherit',
});
},
activateHooks = async () => {
await run('git', ['config', 'core.hooksPath', '.husky/_'], { cwd: root, stdio: 'inherit' });
},
} = {}) {
if (disabled) return;
try {
await execFileAsync('git', ['--version']);
} catch (error) {
if (error.code === 'ENOENT') {
console.warn('git not found; skipping hook installation');
return;
}
throw error;
}
const huskyDir = path.join(root, '.husky');
const active = path.join(huskyDir, '_');
const nonce = `${Date.now()}-${process.pid}`;
const stagingRepo = path.join(root, '.mosaic-test-work', `husky-stage-${nonce}`);
const stagingHooks = path.join(stagingRepo, '.husky');
const quarantined = path.join(quarantineRoot, `${path.basename(root)}-${nonce}`);
await mkdir(huskyDir, { recursive: true });
await mkdir(quarantineRoot, { recursive: true });
const previousComplete = (await pathExists(active)) && (await pathExists(path.join(active, 'h')));
let previousQuarantined = false;
try {
if ((await pathExists(active)) && !previousComplete) {
await rename(active, quarantined);
previousQuarantined = true;
}
await mkdir(stagingRepo, { recursive: true });
await runHusky(stagingHooks, stagingRepo);
const staged = path.join(stagingHooks, '_');
if (!(await pathExists(path.join(staged, 'h')))) {
throw new Error('husky did not produce its required h shim');
}
if (previousComplete) {
if (!(await directoriesMatch(active, staged))) {
throw new Error('existing complete hook set differs from the installed Husky version');
}
await rm(stagingRepo, { recursive: true, force: true });
} else {
await rename(staged, active);
await rm(stagingRepo, { recursive: true, force: true });
}
await activateHooks();
if (previousQuarantined) {
try {
await rm(quarantined, { recursive: true, force: true });
} catch {
console.warn(
`Previous hook state was deactivated but remains quarantined at ${quarantined}`,
);
}
}
} catch (error) {
const cleanupFailures = [];
try {
if (await pathExists(stagingRepo)) {
await rename(stagingRepo, `${quarantined}-staging`);
}
} catch (cleanupError) {
cleanupFailures.push(`staging hooks: ${cleanupError.message}`);
}
const cleanup =
cleanupFailures.length === 0
? 'No partial hook set was activated.'
: `Automatic cleanup was incomplete (${cleanupFailures.join('; ')}).`;
throw new Error(
`Hook installation failed: ${error.message}. ${cleanup} Fix: rm -rf .husky/_ && git config core.hooksPath .husky/_ && pnpm install --frozen-lockfile`,
{ cause: error },
);
}
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
try {
await installHooks();
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
}
+208
View File
@@ -0,0 +1,208 @@
import assert from 'node:assert/strict';
import { access, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
import path from 'node:path';
import test from 'node:test';
import { installHooks } from './install-hooks.mjs';
const fixtureRoot = path.join(process.cwd(), '.mosaic-test-work', `hooks-${process.pid}`);
const quarantineRoot = path.join(fixtureRoot, 'quarantine');
async function fixture(name) {
const root = path.join(fixtureRoot, name);
await mkdir(path.join(root, '.husky'), { recursive: true });
return root;
}
async function exists(target) {
try {
await access(target);
return true;
} catch {
return false;
}
}
test.after(async () => {
await rm(fixtureRoot, { recursive: true, force: true });
});
test('an interrupted install quarantines the partial active hook set and fails loudly', async () => {
const root = await fixture('interrupted');
let restoredHooksPath = 'not-called';
await assert.rejects(
installHooks({
root,
quarantineRoot,
runHusky: async (stagingHooks) => {
await mkdir(path.join(stagingHooks, '_'), { recursive: true });
await writeFile(path.join(stagingHooks, '_', 'h'), 'partial');
throw new Error('simulated interruption');
},
activateHooks: async () => {},
readHooksPath: async () => null,
restoreHooksPath: async (value) => {
restoredHooksPath = value;
},
}),
(error) => {
assert.match(error.message, /Hook installation failed/);
assert.match(error.message, /pnpm install --frozen-lockfile/);
return true;
},
);
assert.equal(await exists(path.join(root, '.husky', '_')), false);
assert.equal(restoredHooksPath, 'not-called');
const quarantined = await readdir(quarantineRoot);
assert.equal(quarantined.length, 1);
});
test('a failed replacement restores a previously complete active hook set', async () => {
const root = await fixture('rollback');
const activeShim = path.join(root, '.husky', '_', 'h');
await mkdir(path.dirname(activeShim), { recursive: true });
await writeFile(activeShim, 'previous-complete');
let previousRemainedActiveDuringStaging = false;
await assert.rejects(
installHooks({
root,
quarantineRoot: path.join(fixtureRoot, 'rollback-quarantine'),
runHusky: async () => {
previousRemainedActiveDuringStaging =
(await readFile(activeShim, 'utf8')) === 'previous-complete';
throw new Error('simulated replacement failure');
},
activateHooks: async () => {},
readHooksPath: async () => '.husky/_',
restoreHooksPath: async () => {},
}),
/Hook installation failed/,
);
assert.equal(previousRemainedActiveDuringStaging, true);
assert.equal(await readFile(activeShim, 'utf8'), 'previous-complete');
});
test('a mismatched complete hook set fails loudly instead of reporting a stale install as current', async () => {
const root = await fixture('mismatch');
const activeShim = path.join(root, '.husky', '_', 'h');
await mkdir(path.dirname(activeShim), { recursive: true });
await writeFile(activeShim, 'old-complete');
await assert.rejects(
installHooks({
root,
quarantineRoot: path.join(fixtureRoot, 'mismatch-quarantine'),
runHusky: async (stagingHooks) => {
await mkdir(path.join(stagingHooks, '_'), { recursive: true });
await writeFile(path.join(stagingHooks, '_', 'h'), 'new-complete');
},
activateHooks: async () => {},
readHooksPath: async () => '.husky/_',
restoreHooksPath: async () => {},
}),
/Hook installation failed.*pnpm install --frozen-lockfile/,
);
assert.equal(await readFile(activeShim, 'utf8'), 'old-complete');
});
test('a competing successful installer is not removed by the losing process', async () => {
const root = await fixture('concurrent');
const activeShim = path.join(root, '.husky', '_', 'h');
let restored = false;
await assert.rejects(
installHooks({
root,
quarantineRoot: path.join(fixtureRoot, 'concurrent-quarantine'),
runHusky: async (stagingHooks) => {
await mkdir(path.join(stagingHooks, '_'), { recursive: true });
await writeFile(path.join(stagingHooks, '_', 'h'), 'ours');
await mkdir(path.dirname(activeShim), { recursive: true });
await writeFile(activeShim, 'peer');
},
activateHooks: async () => {},
readHooksPath: async () => null,
restoreHooksPath: async () => {
restored = true;
},
}),
/Hook installation failed/,
);
assert.equal(await readFile(activeShim, 'utf8'), 'peer');
assert.equal(restored, false);
});
test("a competing installer that replaces this installer's active set is preserved", async () => {
const root = await fixture('concurrent-after-rename');
const active = path.join(root, '.husky', '_');
const activeShim = path.join(active, 'h');
let restored = false;
await assert.rejects(
installHooks({
root,
quarantineRoot: path.join(fixtureRoot, 'concurrent-after-rename-quarantine'),
runHusky: async (stagingHooks) => {
await mkdir(path.join(stagingHooks, '_'), { recursive: true });
await writeFile(path.join(stagingHooks, '_', 'h'), 'ours');
},
activateHooks: async () => {
await rm(active, { recursive: true, force: true });
await mkdir(active, { recursive: true });
await writeFile(activeShim, 'peer');
throw new Error('our activation lost to peer');
},
readHooksPath: async () => null,
restoreHooksPath: async () => {
restored = true;
},
}),
/Hook installation failed/,
);
assert.equal(await readFile(activeShim, 'utf8'), 'peer');
assert.equal(restored, false);
});
test('an explicit interactive HUSKY=0 opt-out preserves existing hooks without running installer', async () => {
const root = await fixture('disabled');
const activeShim = path.join(root, '.husky', '_', 'h');
await mkdir(path.dirname(activeShim), { recursive: true });
await writeFile(activeShim, 'preserved');
let ran = false;
await installHooks({
root,
disabled: true,
runHusky: async () => {
ran = true;
},
});
assert.equal(ran, false);
assert.equal(await readFile(activeShim, 'utf8'), 'preserved');
});
test('a successful install leaves a complete active hook set', async () => {
const root = await fixture('success');
await installHooks({
root,
quarantineRoot,
runHusky: async (stagingHooks) => {
await mkdir(path.join(stagingHooks, '_'), { recursive: true });
await writeFile(path.join(stagingHooks, '_', 'h'), 'complete');
},
activateHooks: async () => {},
readHooksPath: async () => null,
restoreHooksPath: async () => {},
});
assert.equal(await exists(path.join(root, '.husky', '_', 'h')), true);
});
+207
View File
@@ -0,0 +1,207 @@
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { chmod, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import test from 'node:test';
const ROOT = new URL('../', import.meta.url);
const EXPECTED_BASELINE_SITES = 26;
const EXPECTED_TEST_BASELINE_SITES = 22;
const EXPECTED_WAKE_BASELINE_SITES = 26;
const TARGETS = [
'tools/matrix-presence-harness/run.sh',
'tools/e2e-install-test.sh',
'tools/install.sh',
'scripts/agent/session-start.sh',
'scripts/analysis/reflect-board-history.sh',
'scripts/analysis/reflect-git-history.sh',
'packages/mosaic/framework/templates/repo/scripts/agent/session-start.sh',
'packages/mosaic/framework/tools/authentik/user-create.sh',
'packages/mosaic/framework/tools/git/mutate-push-guard.sh',
'packages/mosaic/framework/tools/orchestrator/session-resume.sh',
'packages/mosaic/framework/tools/prdy/prdy-status.sh',
'packages/mosaic/framework/tools/qa/reflect-stop-hook.sh',
'packages/mosaic/framework/tools/qa/typecheck-hook.sh',
'packages/mosaic/framework/tools/tmux/send-message.sh',
'packages/mosaic/framework/tools/wake/detector.sh',
'packages/mosaic/framework/tools/wake/digest.sh',
'packages/mosaic/framework/tools/wake/reconcile.sh',
'packages/mosaic/framework/systemd/user/test-fleet-units.sh',
'packages/mosaic/framework/tools/git/test-issue-comment-readback.sh',
'packages/mosaic/framework/tools/git/test-lane-brief-pr-linkage.sh',
'packages/mosaic/framework/tools/git/test-pr-review-gitea-comment.sh',
'packages/mosaic/framework/tools/git/test-pr-review-repo-host-override.sh',
'packages/mosaic/framework/tools/orchestrator/smoke-test.sh',
'packages/mosaic/framework/tools/orchestrator/test-board-roll.sh',
'packages/mosaic/framework/tools/quality/scripts/test-upgrade-durable-snapshot.sh',
'packages/mosaic/framework/tools/quality/scripts/test-upgrade-rollback.sh',
'packages/mosaic/framework/tools/tmux/test-send-message-socket.sh',
'packages/mosaic/framework/tools/tmux/test-send-message-verdict.sh',
'packages/mosaic/framework/tools/wake/test-wake-digest-quarantine.sh',
'packages/mosaic/framework/tools/wake/test-wake-preimage.sh',
'packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh',
'packages/mosaic/framework/tools/wake/validate-973/validate-973.sh',
];
// These statuses are explicitly non-load-bearing or unreachable at designed input.
// They remain inventoried until the final #1099 tranche records every verdict.
const ACCEPTED = [
['tools/install.sh', 'mosaic-bak-', '|| true'],
['tools/install.sh', 'mosaicstack-mosaic-*.tgz', 'head -1'],
['tools/install.sh', 'mosaicstack-gateway-*.tgz', 'head -1'],
['scripts/agent/session-start.sh', 'docs/scratchpads/*.md', '|| true'],
[
'packages/mosaic/framework/templates/repo/scripts/agent/session-start.sh',
'docs/scratchpads/*.md',
'|| true',
],
];
const earlyExit =
/(?<!\|)\|(?!\|)[^;\n]*(?:grep\b[^;\n]*(?:-[A-Za-z]*q|--quiet|-m\s*1)|head\b(?:\s|$))/;
function scan(sources) {
const found = [];
for (const [file, rawSource] of sources) {
const source = rawSource.replace(/\\\n\s*/g, ' ');
for (const rawLine of source.split('\n')) {
const line = rawLine.trim();
if (!earlyExit.test(line)) continue;
const accepted = ACCEPTED.some(
([acceptedFile, ...fragments]) =>
acceptedFile === file && fragments.every((item) => line.includes(item)),
);
if (!accepted) found.push(`${file}:${line}`);
}
}
return found;
}
async function currentSources() {
return Promise.all(
TARGETS.map(async (file) => [file, await readFile(new URL(file, ROOT), 'utf8')]),
);
}
async function assertBaselineFixture(file, expectedCount, expectedUnique = expectedCount) {
const baseline = JSON.parse(await readFile(new URL(file, ROOT), 'utf8'));
assert.equal(baseline.length, expectedCount);
assert.equal(new Set(baseline).size, expectedUnique);
const fixtureSources = baseline.map((site) => {
const separator = site.indexOf(':');
assert.ok(separator > 0, `invalid baseline site: ${site}`);
return [site.slice(0, separator), site.slice(separator + 1)];
});
assert.deepEqual(scan(fixtureSources), baseline);
}
test('the registered runtime baseline denominator is exactly 26 unsafe sites', async () => {
await assertBaselineFixture(
'scripts/fixtures/pipefail-early-exit-baseline.json',
EXPECTED_BASELINE_SITES,
);
});
test('the registered test baseline denominator is exactly 22 unsafe sites', async () => {
await assertBaselineFixture(
'scripts/fixtures/pipefail-early-exit-test-baseline.json',
EXPECTED_TEST_BASELINE_SITES,
21,
);
});
test('the registered wake baseline denominator is exactly 26 unsafe sites', async () => {
await assertBaselineFixture(
'scripts/fixtures/pipefail-early-exit-wake-baseline.json',
EXPECTED_WAKE_BASELINE_SITES,
25,
);
});
test('load-bearing pipefail paths do not pipe into early-exiting consumers', async () => {
assert.deepEqual(scan(await currentSources()), []);
});
test('gateway verify capability preserves the complete help-probe truth table', async () => {
const directory = await mkdtemp(path.join(tmpdir(), 'gateway-help-probe-'));
const mosaic = path.join(directory, 'mosaic');
const probe = new URL('tools/e2e-gateway-verify-supported.sh', ROOT).pathname;
try {
await writeFile(
mosaic,
'#!/usr/bin/env bash\nprintf \'%s\\n\' "${MOCK_HELP_OUTPUT:-}"\nexit "${MOCK_HELP_RC:-0}"\n',
);
await chmod(mosaic, 0o755);
const run = (rc, output) =>
spawnSync('bash', [probe], {
env: {
...process.env,
PATH: `${directory}:${process.env.PATH}`,
MOCK_HELP_RC: String(rc),
MOCK_HELP_OUTPUT: output,
},
}).status;
assert.equal(run(0, 'commands: verify'), 0);
assert.equal(run(0, 'commands: install'), 1);
assert.equal(run(1, 'commands: verify'), 1);
} finally {
await rm(directory, { recursive: true, force: true });
}
});
test('wake JSONL selectors take the first match across the complete input stream', async () => {
const source = await readFile(
new URL('packages/mosaic/framework/tools/wake/test-wake-preimage.sh', ROOT),
'utf8',
);
assert.equal((source.match(/jq -nr 'first\(inputs \| select\(/g) ?? []).length, 4);
const directory = await mkdtemp(path.join(tmpdir(), 'wake-jsonl-first-'));
const input = path.join(directory, 'pending.jsonl');
const filter = 'first(inputs | select(.locators.kind == "preimage") | .observed_seq) // empty';
try {
await writeFile(
input,
'{"locators":{"kind":"repo"},"observed_seq":1}\n' +
'{"locators":{"kind":"preimage"},"observed_seq":4}\n' +
'{"locators":{"kind":"preimage"},"observed_seq":9}\n',
);
let result = spawnSync('jq', ['-nr', filter, input], { encoding: 'utf8' });
assert.equal(result.status, 0, result.stderr);
assert.equal(result.stdout, '4\n');
await writeFile(input, '{"locators":{"kind":"repo"},"observed_seq":1}\n');
result = spawnSync('jq', ['-nr', filter, input], { encoding: 'utf8' });
assert.equal(result.status, 0, result.stderr);
assert.equal(result.stdout, '');
await writeFile(input, '{invalid json}\n');
result = spawnSync('jq', ['-nr', filter, input], { encoding: 'utf8' });
assert.notEqual(result.status, 0);
} finally {
await rm(directory, { recursive: true, force: true });
}
});
test('board-history preserves non-git data-dir as a non-detectable result', async () => {
const directory = await mkdtemp(path.join(tmpdir(), 'reflect-board-non-git-'));
try {
await writeFile(path.join(directory, 'task.json'), '{}\n');
const result = spawnSync(
'bash',
[
new URL('scripts/analysis/reflect-board-history.sh', ROOT).pathname,
'--data-dir',
directory,
],
{ encoding: 'utf8' },
);
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /"done_tasks": 1/);
assert.match(result.stdout, /"detectable_outcomes": 0/);
} finally {
await rm(directory, { recursive: true, force: true });
}
});
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env node
import { constants } from 'node:fs';
import { access } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
export const MISSING_DEPS_EXIT = 42;
// The generated-state certification that used to live here (fingerprinting the
// web source tree and certifying apps/web/.next) retired with the Next.js build
// in Phase P5 (#1444): the Vite SPA has no generated tree that later gates
// consume, so there is no stale-output class left to defend against.
export async function runPreflight({ root = process.cwd() } = {}) {
const binDir = path.join(root, 'node_modules', '.bin');
const requiredBinaries = ['eslint', 'husky', 'prettier', 'tsc', 'turbo', 'vitest'];
const missingBinaries = [];
for (const binary of requiredBinaries) {
try {
await access(path.join(binDir, binary), constants.X_OK);
} catch {
missingBinaries.push(binary);
}
}
if (missingBinaries.length > 0) {
return {
code: MISSING_DEPS_EXIT,
message: `MOSAIC_PREFLIGHT_MISSING_DEPS: dependency installation is missing ${missingBinaries.join(', ')}; run pnpm install --frozen-lockfile`,
};
}
return { code: 0, message: 'checkout preflight passed' };
}
async function main() {
const result = await runPreflight();
const stream = result.code === 0 ? process.stdout : process.stderr;
stream.write(`${result.message}\n`);
process.exitCode = result.code;
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
await main();
}
+71
View File
@@ -0,0 +1,71 @@
import assert from 'node:assert/strict';
import { chmod, mkdir, rm, symlink, writeFile } from 'node:fs/promises';
import path from 'node:path';
import test from 'node:test';
import { runPreflight } from './preflight.mjs';
const fixtureRoot = path.join(process.cwd(), '.mosaic-test-work', `preflight-${process.pid}`);
const requiredBins = ['eslint', 'husky', 'prettier', 'tsc', 'turbo', 'vitest'];
async function fixture(name) {
const root = path.join(fixtureRoot, name);
await mkdir(path.join(root, 'apps', 'web', 'src'), { recursive: true });
await writeFile(path.join(root, 'apps', 'web', 'src', 'main.tsx'), 'export default 1;\n');
return root;
}
async function installRequiredBins(root) {
const binDir = path.join(root, 'node_modules', '.bin');
await mkdir(binDir, { recursive: true });
await Promise.all(
requiredBins.map(async (name) => {
const target = path.join(binDir, name);
await writeFile(target, '');
await chmod(target, 0o755);
}),
);
}
test.after(async () => {
await rm(fixtureRoot, { recursive: true, force: true });
});
test('missing dependencies have a dedicated exit code and install remediation', async () => {
const root = await fixture('missing-deps');
const result = await runPreflight({ root });
assert.equal(result.code, 42);
assert.match(result.message, /MOSAIC_PREFLIGHT_MISSING_DEPS/);
assert.match(result.message, /run pnpm install/i);
});
test('a partial dependency install keeps the dedicated missing-deps result', async () => {
const root = await fixture('partial-deps');
await mkdir(path.join(root, 'node_modules', '.bin'), { recursive: true });
await writeFile(path.join(root, 'node_modules', '.bin', 'tsc'), '', { mode: 0o755 });
const result = await runPreflight({ root });
assert.equal(result.code, 42);
assert.match(result.message, /turbo/);
});
test('a dangling required dependency shim keeps the dedicated missing-deps result', async () => {
const root = await fixture('dangling-deps');
await installRequiredBins(root);
const turbo = path.join(root, 'node_modules', '.bin', 'turbo');
await rm(turbo);
await symlink(path.join(root, 'node_modules', 'missing-turbo'), turbo);
const result = await runPreflight({ root });
assert.equal(result.code, 42);
assert.match(result.message, /turbo/);
});
test('installed dependencies pass', async () => {
const root = await fixture('clean');
await installRequiredBins(root);
assert.deepEqual(await runPreflight({ root }), { code: 0, message: 'checkout preflight passed' });
});
+309
View File
@@ -0,0 +1,309 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { createRequire } from 'node:module';
import path from 'node:path';
import test from 'node:test';
// RI-1-002 / RI-N1 publish-gate NEGATIVE CONTROLS (SDLC-D-034).
//
// scripts/verify-release.test.mjs pins the POSITIVE structure of the publish
// gate: every publish effect declares a direct `depends_on: verify` edge and
// the verify step asserts commit identity + runs the canonical command. This
// suite is the negative-control set: each test feeds a structural gate
// checker a pipeline in which the gate is bypassed by ONE specific shape and
// asserts the checker goes RED. The controls prove from the pipeline FILE —
// never by executing Woodpecker — that a verify step that FAILS (nonzero
// exit) blocks every publish effect.
//
// Woodpecker semantics these controls rely on:
// - A step that exits nonzero FAILS, and every step that transitively
// depends on a failed step is SKIPPED — never run. That skip is the only
// thing standing between a failed mandatory check and a publish effect.
// - `detach: true` removes the step from the wait graph: the pipeline does
// not wait for detached steps, so their failure can never block anything.
// - `failure: ignore` reports a failed step as success to the DAG.
// - `success: [codes...]` overrides which exit codes count as success;
// admitting any nonzero code launders a failed verification into green.
// - `when` on the verify step would skip verification entirely on some
// event/path classes while publish effects still run.
//
// Bypass shapes covered (one negative-control test each):
// S1 Missing edge — a publish effect whose dependency closure does not
// contain `verify` (a refactor drops the depends_on entry).
// S2 Hidden effect — a step whose NAME does not start with `publish` but
// whose COMMANDS publish npm packages or push images. Effects are
// classified by commands, so renaming a step cannot un-gate it.
// S3 Detached verify — `verify: { detach: true }`: publish steps no longer
// wait for verify, so the depends_on edge is decorative.
// S4 Always-pass verify — `failure: ignore`, or a `success` override
// admitting nonzero exit codes: verify fails, the DAG sees success.
// S5 Conditional verify — a `when`/path filter on verify itself.
// S6 Exact-commit drift — a HEAD-moving step (git checkout/switch/reset/
// clean/pull/clone/fetch) ordered between `verify` and a publish
// effect: the verified commit would not be the published commit. A
// LEGITIMATE re-checkout is allowed only when `verify` itself runs
// after it — positive control included.
// S7 Gate removal — the verify step deleted or renamed away entirely.
// Reuse the monorepo's existing YAML parser (@mosaicstack/mosaic's direct
// dependency) instead of adding a root dependency or vendoring a parser.
const mosaicRequire = createRequire(
path.resolve(process.cwd(), 'packages', 'mosaic', 'package.json'),
);
const { parse: parseYaml } = mosaicRequire('yaml');
const publishYmlPath = path.join(process.cwd(), '.woodpecker', 'publish.yml');
async function readPublishPipeline() {
return parseYaml(await readFile(publishYmlPath, 'utf8'));
}
// A command has a publish EFFECT when it publishes npm packages (`publish`
// anywhere after a package-manager token — `pnpm --filter "@x/*" publish`
// puts flags and quoted filters between the binary and the subcommand) or
// pushes an image (kaniko, docker push, or a registry --destination).
// Deliberately over-broad: a false positive forces justification, a false
// negative is the actual hazard.
function isPublishCommand(command) {
return (
/(^|\s)\/kaniko\/executor\b/.test(command) ||
/(^|\s)docker\s+push\b/.test(command) ||
/(^|\s)--destination(\s|=)/.test(command) ||
(/\bpublish\b/.test(command) && /(^|\s)(npm|pnpm|yarn)(\s|$)/.test(command))
);
}
function hasPublishEffect(step) {
return (step.commands ?? []).some(isPublishCommand);
}
// A step is a publish effect when its name says so OR (S2) when any of its
// commands does — classification must not depend on the name alone.
function publishEffectSteps(pipeline) {
return Object.entries(pipeline.steps ?? {})
.filter(([name, step]) => name.startsWith('publish') || hasPublishEffect(step))
.map(([name]) => name);
}
// Transitive closure of a step's depends_on graph.
function dependencyClosure(pipeline, stepName, seen = new Set()) {
const dependencies = pipeline.steps?.[stepName]?.depends_on ?? [];
for (const dependency of dependencies) {
if (seen.has(dependency)) continue;
seen.add(dependency);
dependencyClosure(pipeline, dependency, seen);
}
return seen;
}
// Deliberately over-broad: `git fetch` alone does not move HEAD, but the
// classic re-checkout pair is `git fetch && git reset --hard <remote>`; a
// fetch step sitting between verify and a publish effect deserves scrutiny,
// so the gate fails closed on it.
function movesHead(step) {
return (step.commands ?? []).some((command) =>
/(^|\s)git\s+(checkout|switch|reset|clean|pull|clone|fetch)\b/.test(command),
);
}
// The structural gate checker: green only when a failed (nonzero-exit)
// verify provably blocks every publish effect on the same commit.
function assertPublishGateBlocksOnVerify(pipeline) {
assert.ok(pipeline.steps, 'publish pipeline must define steps');
const verify = pipeline.steps.verify;
assert.ok(verify, 'publish pipeline must define a `verify` step (S7)');
// S5: a skipped verification authorizes publishes exactly as much as a
// failed one — verify must be unconditional.
assert.equal(verify.when, undefined, '`verify` must not carry a when/path filter (S5)');
// S3/S4: the depends_on edges are only meaningful if verify's own failure
// is both awaited and terminal for the DAG.
assert.equal(verify.detach, undefined, '`verify` must not be detached (S3)');
assert.equal(
verify.failure,
undefined,
'`verify` must not tolerate its own failure (S4: failure: ignore launders a failed gate into success)',
);
assert.equal(
verify.success,
undefined,
'`verify` must not override success exit codes (S4: nonzero codes would make failed verification pass)',
);
const effects = publishEffectSteps(pipeline);
assert.ok(effects.length > 0, 'publish pipeline must contain publish effect steps to guard');
const verifyClosure = dependencyClosure(pipeline, 'verify');
for (const stepName of effects) {
// S1: only the failure-skip semantics of the DAG stand between a failed
// verify and this effect — the verify edge in its closure is the proof.
const closure = dependencyClosure(pipeline, stepName);
assert.ok(
closure.has('verify'),
`publish effect '${stepName}' must transitively depend on verify (S1) — a failed verify must skip it`,
);
// S6: any step ordered after verify (outside its closure) but inside the
// effect's chain must not be able to move HEAD. If the pipeline
// legitimately re-checks-out, verify must run after the re-checkout.
for (const chainStep of closure) {
if (chainStep === 'verify' || verifyClosure.has(chainStep)) continue;
assert.ok(
!movesHead(pipeline.steps[chainStep]),
`step '${chainStep}' sits between verify and publish effect '${stepName}' and can move HEAD (S6)` +
' — verify must re-run after any re-checkout',
);
}
}
return effects;
}
// A minimal but healthy gate used as the base for every negative-control
// mutation: verify (identity + canonical command) → build → publish-npm,
// with the publish effect blocked by verify both directly and through build.
const HEALTHY_GATE_YAML = `
steps:
verify:
image: node:24-alpine
commands:
- |
if [ -z "$CI_COMMIT_SHA" ] || [ "$CI_COMMIT_SHA" != "$(git rev-parse HEAD)" ]; then
echo "identity mismatch" >&2
exit 1
fi
- pnpm verify:release
build:
image: node:24-alpine
commands:
- pnpm build
depends_on:
- verify
publish-npm:
image: node:24-alpine
commands:
- npm publish
depends_on:
- build
- verify
`;
// Fresh parse per call so every negative control mutates its own object.
function healthyPipeline() {
return parseYaml(HEALTHY_GATE_YAML);
}
test('the real publish pipeline: a failed verify provably blocks every publish effect', async () => {
const pipeline = await readPublishPipeline();
const effects = assertPublishGateBlocksOnVerify(pipeline);
assert.deepEqual(effects.sort(), [
'build-appservice',
'build-gateway',
'publish-next-npm',
'publish-npm',
]);
});
test('fixture sanity: the healthy gate base passes the checker unmutated', () => {
assertPublishGateBlocksOnVerify(healthyPipeline());
});
test('S1 negative control: a publish effect with no verify edge fails the checker', () => {
const pipeline = healthyPipeline();
pipeline.steps['publish-npm'].depends_on = ['build'];
pipeline.steps.build.depends_on = [];
assert.throws(
() => assertPublishGateBlocksOnVerify(pipeline),
/publish-npm.*must transitively depend on verify/s,
);
});
test('S2 negative control: an npm publish hidden behind a non-publish step name fails the checker', () => {
const pipeline = healthyPipeline();
delete pipeline.steps['publish-npm'];
pipeline.steps.build.depends_on = [];
pipeline.steps.deploy = {
image: 'node:24-alpine',
commands: ['npm publish'],
depends_on: ['build'],
};
// Detection must be by COMMAND: the name says "deploy", the commands say
// publish — an un-gated effect under either reading.
assert.throws(
() => assertPublishGateBlocksOnVerify(pipeline),
/deploy.*must transitively depend on verify/s,
);
});
test('S2 negative control: a kaniko image push under a build-* name fails the checker when ungated', () => {
const pipeline = healthyPipeline();
delete pipeline.steps['publish-npm'];
pipeline.steps.build.depends_on = [];
pipeline.steps['push-platform-image'] = {
image: 'gcr.io/kaniko-project/executor:debug',
commands: ['/kaniko/executor --context . --destination reg.example/img:latest'],
depends_on: ['build'],
};
assert.throws(
() => assertPublishGateBlocksOnVerify(pipeline),
/push-platform-image.*must transitively depend on verify/s,
);
});
test('S3 negative control: a detached verify fails the checker', () => {
const pipeline = healthyPipeline();
pipeline.steps.verify.detach = true;
assert.throws(() => assertPublishGateBlocksOnVerify(pipeline), /detached \(S3\)/);
});
test('S4 negative control: failure: ignore on verify fails the checker', () => {
const pipeline = healthyPipeline();
pipeline.steps.verify.failure = 'ignore';
assert.throws(() => assertPublishGateBlocksOnVerify(pipeline), /tolerate its own failure/);
});
test('S4 negative control: a success override admitting nonzero exit codes fails the checker', () => {
const pipeline = healthyPipeline();
pipeline.steps.verify.success = [0, 1];
assert.throws(() => assertPublishGateBlocksOnVerify(pipeline), /success exit codes/);
});
test('S5 negative control: a when filter on verify fails the checker', () => {
const pipeline = healthyPipeline();
pipeline.steps.verify.when = [{ event: 'push' }];
assert.throws(() => assertPublishGateBlocksOnVerify(pipeline), /when\/path filter \(S5\)/);
});
test('S6 negative control: a HEAD-moving step between verify and publish fails the checker', () => {
const pipeline = healthyPipeline();
pipeline.steps.resync = {
image: 'node:24-alpine',
commands: ['git fetch origin', 'git reset --hard origin/main'],
depends_on: [],
};
pipeline.steps.build.depends_on = ['verify', 'resync'];
// resync sits AFTER verify in the publish chain (verify does not depend on
// it), so the verified commit could be replaced before publishing.
assert.throws(() => assertPublishGateBlocksOnVerify(pipeline), /resync.*can move HEAD/s);
});
test('S6 positive control: a legitimate re-checkout passes when verify re-runs after it', () => {
const pipeline = healthyPipeline();
pipeline.steps.resync = {
image: 'node:24-alpine',
commands: ['git fetch origin', 'git reset --hard origin/main'],
depends_on: [],
};
pipeline.steps.verify.depends_on = ['resync'];
pipeline.steps.build.depends_on = ['verify'];
// resync precedes verify in the chain, so verification covers the
// re-checked-out HEAD — the exact-commit contract holds.
assertPublishGateBlocksOnVerify(pipeline);
});
test('S7 negative control: deleting the verify step entirely fails the checker', () => {
const pipeline = healthyPipeline();
delete pipeline.steps.verify;
pipeline.steps['publish-npm'].depends_on = ['build'];
assert.throws(() => assertPublishGateBlocksOnVerify(pipeline), /`verify` step/);
});
+165
View File
@@ -0,0 +1,165 @@
#!/usr/bin/env bash
# Publish @mosaicstack/* packages to npmjs.org as @mosaicstack/*
#
# This script patches each package.json to:
# 1. Rename @mosaicstack/X → @mosaicstack/X
# 2. Replace workspace:^ deps with resolved versions using @mosaicstack/* names
# 3. Run npm publish
# 4. Restore original package.json
#
# Usage:
# scripts/publish-npmjs.sh [--dry-run] [--filter <package-name>]
#
# Requirements:
# - NPM_TOKEN env var set (npmjs.org auth token)
# - jq installed
# - Run from monorepo root after `pnpm build`
set -euo pipefail
DRY_RUN=false
FILTER=""
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) DRY_RUN=true; shift ;;
--filter) FILTER="$2"; shift 2 ;;
*) echo "Unknown option: $1"; exit 1 ;;
esac
done
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
# Collect all publishable package directories (non-private, has publishConfig)
PACKAGE_DIRS=()
for pkg_json in "$REPO_ROOT"/packages/*/package.json "$REPO_ROOT"/plugins/*/package.json "$REPO_ROOT"/apps/gateway/package.json; do
[[ -f "$pkg_json" ]] || continue
is_private=$(jq -r '.private // false' "$pkg_json")
[[ "$is_private" == "true" ]] && continue
PACKAGE_DIRS+=("$(dirname "$pkg_json")")
done
echo "Found ${#PACKAGE_DIRS[@]} publishable packages"
# Build a version map: @mosaicstack/X → version
declare -A VERSION_MAP
for dir in "${PACKAGE_DIRS[@]}"; do
name=$(jq -r '.name' "$dir/package.json")
version=$(jq -r '.version' "$dir/package.json")
VERSION_MAP["$name"]="$version"
done
# Configure npmjs auth
if [[ -z "${NPM_TOKEN:-}" ]] && [[ "$DRY_RUN" == "false" ]]; then
echo "ERROR: NPM_TOKEN is required for publishing. Set it or use --dry-run."
exit 1
fi
publish_package() {
local dir="$1"
local orig_json="$dir/package.json"
local backup="$dir/package.json.bak"
local name
name=$(jq -r '.name' "$orig_json")
# Apply filter if set
if [[ -n "$FILTER" ]] && [[ "$name" != "$FILTER" ]]; then
return 0
fi
local new_name="${name/@mosaic\//@mosaicstack/}"
echo ""
echo "━━━ Publishing $name$new_name ━━━"
# Backup original
cp "$orig_json" "$backup"
# Patch: rename package
local patched
patched=$(jq --arg new_name "$new_name" '.name = $new_name' "$orig_json")
# Patch: publishConfig to npmjs
patched=$(echo "$patched" | jq '.publishConfig = {"registry": "https://registry.npmjs.org/", "access": "public"}')
# Patch: replace workspace:^ dependencies with @mosaicstack/* and resolved versions
for dep_field in dependencies devDependencies peerDependencies; do
if echo "$patched" | jq -e ".$dep_field" > /dev/null 2>&1; then
local deps
deps=$(echo "$patched" | jq -r ".$dep_field // {} | keys[]")
for dep in $deps; do
local dep_version
dep_version=$(echo "$patched" | jq -r ".$dep_field[\"$dep\"]")
# Only transform @mosaicstack/* workspace deps
if [[ "$dep" == @mosaicstack/* ]] && [[ "$dep_version" == workspace:* ]]; then
local new_dep="${dep/@mosaic\//@mosaicstack/}"
local resolved="${VERSION_MAP[$dep]:-}"
if [[ -z "$resolved" ]]; then
echo " WARNING: No version found for $dep — using '*'"
resolved="*"
else
# workspace:^ means ^version, workspace:* means *
if [[ "$dep_version" == "workspace:^" ]]; then
resolved="^$resolved"
fi
fi
# Rename the dep key and set the resolved version
patched=$(echo "$patched" | jq \
--arg field "$dep_field" \
--arg old_dep "$dep" \
--arg new_dep "$new_dep" \
--arg version "$resolved" \
'.[$field] |= (del(.[$old_dep]) | .[$new_dep] = $version)')
fi
done
fi
done
# Write patched package.json
echo "$patched" > "$orig_json"
echo " Patched: $new_name"
# Publish
if [[ "$DRY_RUN" == "true" ]]; then
echo " [DRY RUN] npm publish --dry-run"
(cd "$dir" && npm publish --dry-run 2>&1) || true
else
echo " Publishing to npmjs..."
(cd "$dir" && npm publish 2>&1) || echo " WARNING: Publish failed (may already exist at this version)"
fi
# Restore original
mv "$backup" "$orig_json"
echo " Restored original package.json"
}
# Set up npmrc for npmjs
if [[ -n "${NPM_TOKEN:-}" ]]; then
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc
fi
# Publish in dependency order: packages first, then plugins, then apps
echo ""
echo "=== Publishing packages ==="
for dir in "$REPO_ROOT"/packages/*/; do
[[ -f "$dir/package.json" ]] || continue
publish_package "$dir"
done
echo ""
echo "=== Publishing plugins ==="
for dir in "$REPO_ROOT"/plugins/*/; do
[[ -f "$dir/package.json" ]] || continue
publish_package "$dir"
done
echo ""
echo "=== Publishing apps ==="
publish_package "$REPO_ROOT/apps/gateway"
echo ""
echo "Done."
+176
View File
@@ -0,0 +1,176 @@
#!/usr/bin/env bash
# Hermetic structural check for the explicit dogfood Compose overlay.
set -euo pipefail
repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
mkdir -p "$tmp/worktree" "$tmp/common.git" "$tmp/seat/secrets"
base_config_json=$(
cd "$repo_root"
BETTER_AUTH_SECRET=test-only-not-a-credential \
docker compose --profile stack config --format json
)
BASE_CONFIG_JSON="$base_config_json" python3 <<'PY'
import json
import os
config = json.loads(os.environ["BASE_CONFIG_JSON"])
gateway = config["services"]["gateway"]
env = gateway["environment"]
for key in (
"MOSAIC_AGENT_NAME",
"MOSAIC_GIT_IDENTITY",
"MOSAIC_BRAIN_HOME",
"AGENT_FILE_SANDBOX_DIR",
"AGENT_USER_TOOLS",
"AGENT_SHELL_ENABLED",
"AGENT_DELIVERY_ENABLED",
"MOSAIC_GIT_TOOLS_DIR",
"MOSAIC_INTEGRATION_TRUNK",
):
assert key not in env, f"base compose unexpectedly sets dogfood variable {key}"
targets = {mount["target"] for mount in gateway["volumes"]}
assert "/workspace/stack" not in targets
assert not any(target.startswith("/opt/mosaic/brain/") for target in targets)
PY
config_json=$(
cd "$repo_root"
BETTER_AUTH_SECRET=test-only-not-a-credential \
MOSAIC_DOGFOOD_WORKTREE="$tmp/worktree" \
MOSAIC_DOGFOOD_COMMON_GIT_DIR="$tmp/common.git" \
MOSAIC_DOGFOOD_SEAT_HOME="$tmp/seat" \
docker compose \
-f docker-compose.yml \
-f docker-compose.dogfood.yml \
--profile stack \
config --format json
)
CONFIG_JSON="$config_json" EXPECT_WORKTREE="$tmp/worktree" EXPECT_COMMON_GIT="$tmp/common.git" EXPECT_SEAT="$tmp/seat" python3 <<'PY'
import json
import os
config = json.loads(os.environ["CONFIG_JSON"])
gateway = config["services"]["gateway"]
assert gateway.get("init") is True, "gateway must run below an init process for R4 lineage"
env = gateway["environment"]
expected_env = {
"MOSAIC_AGENT_NAME": "code-dogfood-01",
"MOSAIC_GIT_IDENTITY": "code-dogfood-01",
"MOSAIC_BRAIN_HOME": "/opt/mosaic/brain",
"AGENT_FILE_SANDBOX_DIR": "/workspace/stack",
"AGENT_SHELL_ENABLED": "false",
"AGENT_DELIVERY_ENABLED": "true",
"MOSAIC_GIT_TOOLS_DIR": "/opt/mosaic/tools/git",
"MOSAIC_INTEGRATION_TRUNK": "next",
}
for key, value in expected_env.items():
assert env.get(key) == value, f"{key}: expected {value!r}, got {env.get(key)!r}"
allowed = set(env["AGENT_USER_TOOLS"].split(","))
assert allowed == {
"fs_read_file",
"fs_write_file",
"fs_list_directory",
"fs_edit_file",
"git_status",
"git_log",
"git_diff",
"git_publish_branch",
"git_open_pull_request",
}, f"unexpected dogfood tool set: {sorted(allowed)}"
assert "shell_exec" not in allowed
mounts = {mount["target"]: mount for mount in gateway["volumes"]}
worktree = mounts["/workspace/stack"]
assert worktree["type"] == "bind"
assert worktree["source"] == os.environ["EXPECT_WORKTREE"]
assert not worktree.get("read_only", False), "dogfood worktree must be writable"
common_git = mounts[os.environ["EXPECT_COMMON_GIT"]]
assert common_git["type"] == "bind"
assert common_git["source"] == os.environ["EXPECT_COMMON_GIT"]
assert not common_git.get("read_only", False), "common Git directory must accept branch updates"
seat = mounts["/opt/mosaic/brain/fleet/agents/code-dogfood-01"]
assert seat["type"] == "bind"
assert seat["source"] == os.environ["EXPECT_SEAT"]
assert seat.get("read_only") is True, "seat credential slot must be read-only"
other_seat_mounts = [
target
for target in mounts
if target.startswith("/opt/mosaic/brain/fleet/agents/")
and target != "/opt/mosaic/brain/fleet/agents/code-dogfood-01"
]
assert other_seat_mounts == [], f"other seat mounts leaked: {other_seat_mounts}"
PY
# Each required path must fail closed rather than falling back to the current checkout.
expect_missing_path() {
local missing=$1 output rc
set +e
case "$missing" in
MOSAIC_DOGFOOD_WORKTREE)
output=$(
cd "$repo_root"
env -u MOSAIC_DOGFOOD_WORKTREE \
BETTER_AUTH_SECRET=test-only-not-a-credential \
MOSAIC_DOGFOOD_COMMON_GIT_DIR="$tmp/common.git" \
MOSAIC_DOGFOOD_SEAT_HOME="$tmp/seat" \
docker compose -f docker-compose.yml -f docker-compose.dogfood.yml \
--profile stack config 2>&1
)
rc=$?
;;
MOSAIC_DOGFOOD_COMMON_GIT_DIR)
output=$(
cd "$repo_root"
env -u MOSAIC_DOGFOOD_COMMON_GIT_DIR \
BETTER_AUTH_SECRET=test-only-not-a-credential \
MOSAIC_DOGFOOD_WORKTREE="$tmp/worktree" \
MOSAIC_DOGFOOD_SEAT_HOME="$tmp/seat" \
docker compose -f docker-compose.yml -f docker-compose.dogfood.yml \
--profile stack config 2>&1
)
rc=$?
;;
MOSAIC_DOGFOOD_SEAT_HOME)
output=$(
cd "$repo_root"
env -u MOSAIC_DOGFOOD_SEAT_HOME \
BETTER_AUTH_SECRET=test-only-not-a-credential \
MOSAIC_DOGFOOD_WORKTREE="$tmp/worktree" \
MOSAIC_DOGFOOD_COMMON_GIT_DIR="$tmp/common.git" \
docker compose -f docker-compose.yml -f docker-compose.dogfood.yml \
--profile stack config 2>&1
)
rc=$?
;;
*)
echo "FAIL: test requested unknown path variable $missing" >&2
exit 1
;;
esac
set -e
if [[ $rc -eq 0 ]]; then
echo "FAIL: dogfood compose accepted missing $missing" >&2
exit 1
fi
if [[ "$output" != *"$missing"* ]]; then
echo "FAIL: missing-path failure did not name $missing" >&2
exit 1
fi
}
expect_missing_path MOSAIC_DOGFOOD_WORKTREE
expect_missing_path MOSAIC_DOGFOOD_COMMON_GIT_DIR
expect_missing_path MOSAIC_DOGFOOD_SEAT_HOME
printf 'dogfood compose verification passed\n'
+188
View File
@@ -0,0 +1,188 @@
#!/usr/bin/env node
// verify-release.mjs — the ONE canonical terminal verification command
// (SDLC-D-034, `pnpm verify:release`).
//
// Publication (.woodpecker/publish.yml `verify` step) is bound to terminal
// verification of the exact commit through this command, which is composed
// from the SAME commands the PR CI pipeline (.woodpecker/ci.yml) runs — CI and
// publish share one semantic checklist:
//
// stage | mirrors ci.yml step | commands
// --------------|---------------------|------------------------------------------
// sanitization | sanitization | verify-sanitized.sh, check-resident-
// | | budget.sh (--self-test + run),
// | | check-test-enumeration.sh
// upgrade-guard | upgrade-guard | test-upgrade-manifest-guard.sh,
// | | test-upgrade-rollback.sh,
// | | test-upgrade-durable-snapshot.sh,
// | | test-install-migration.sh
// typecheck | typecheck | pnpm typecheck (runs the checkout
// | | preflight, then turbo typecheck)
// lint | lint | pnpm lint
// format | format | pnpm format:check
// test | test | pnpm test
// build | build (#1445, P6) | pnpm build (also publish.yml build)
// quality-rails | (canonical-only) | the TS quality-rails evaluator
// | | (RI-N4, QC-19 monorepo subject).
// | | The one stage with no ci.yml
// | | mirror; it is implemented by
// | | importing the evaluator CLI rather
// | | than duplicating its presence logic.
//
// Caller-provided prerequisites (kept at the pipeline level — see the comments
// in .woodpecker/ci.yml): `bash` + `rsync` for the guard stages, `openssl` and
// the pinned @earendil-works/pi-coding-agent for the test stage, and — on the
// postgres path only — the ci-postgres service plus
// `pnpm --filter @mosaicstack/db run db:migrate` before the test stage.
//
// This command works with DATABASE_URL set (CI postgres path) or unset (local
// PGlite path); it never sets, exports, or requires a database itself.
//
// scripts/verify-release.test.mjs enforces that this stage table keeps
// matching .woodpecker/ci.yml step-for-step, so the two surfaces cannot drift
// apart silently.
import { spawnSync } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
export const STAGES = [
{
name: 'sanitization',
// Mirror of the .woodpecker/ci.yml `sanitization` step (minus its
// `apk add` environment prep). Kept as direct command strings here: the
// #1017 test-enumeration guard audits these paths through the ci.yml
// surface, so indirection from ci.yml into this file is not possible.
commands: [
'bash packages/mosaic/framework/tools/quality/scripts/verify-sanitized.sh',
'bash packages/mosaic/framework/tools/quality/scripts/check-resident-budget.sh --self-test',
'bash packages/mosaic/framework/tools/quality/scripts/check-resident-budget.sh',
'bash packages/mosaic/framework/tools/quality/scripts/check-test-enumeration.sh',
'bash packages/mosaic/framework/tools/quality/scripts/check-tools-index.sh --self-test',
'bash packages/mosaic/framework/tools/quality/scripts/check-tools-index.sh',
'bash packages/mosaic/framework/tools/git/test-issue-close-fail-closed.sh',
'bash packages/mosaic/framework/tools/git/test-gitea-login-resolution.sh',
'bash packages/mosaic/framework/tools/git/test-issue-view-comments.sh',
'bash packages/mosaic/framework/tools/git/test-wrapper-guard.sh',
'bash packages/mosaic/framework/tools/git/test-mosaic-worktree-large-repo.sh',
],
},
{
name: 'upgrade-guard',
// Mirror of the .woodpecker/ci.yml `upgrade-guard` step (minus its
// `apk add` environment prep).
commands: [
'bash packages/mosaic/framework/tools/quality/scripts/test-upgrade-manifest-guard.sh',
'bash packages/mosaic/framework/tools/quality/scripts/test-upgrade-rollback.sh',
'bash packages/mosaic/framework/tools/quality/scripts/test-upgrade-durable-snapshot.sh',
'bash packages/mosaic/framework/tools/quality/scripts/test-install-migration.sh',
],
},
{
// `pnpm typecheck` is `pnpm preflight && turbo run typecheck`, so the
// checkout preflight (scripts/preflight.mjs) is part of this stage exactly
// as it is part of the ci.yml `typecheck` step.
name: 'typecheck',
commands: ['pnpm typecheck'],
},
{
name: 'lint',
commands: ['pnpm lint'],
},
{
name: 'format',
commands: ['pnpm format:check'],
},
{
// Requires `openssl` and the pinned `pi` binary on the pipeline path; see
// the caller-provided prerequisites above.
name: 'test',
commands: ['pnpm test'],
},
{
name: 'build',
commands: ['pnpm build'],
},
{
// RI-N4 (QC-19, card RI-3-002): the typed quality-rails evaluator, invoked
// as the implementation of the check it owns instead of a duplicated
// presence loop here. Canonical-only stage (no ci.yml mirror; `build`
// gained one in #1445); runs AFTER build so the evaluator's dist/ exists. Subject
// is this repository (`.` → monorepo subject kind, per-subject check set).
name: 'quality-rails',
commands: ['node packages/quality-rails/dist/cli.js quality-rails evaluate --project .'],
},
];
export function stageByName(name) {
return STAGES.find((stage) => stage.name === name);
}
function missingBinaries(bins) {
return bins.filter(
(bin) => spawnSync('sh', ['-c', `command -v ${bin} >/dev/null 2>&1`]).status !== 0,
);
}
function runCommand(command) {
const result = spawnSync(command, { shell: true, stdio: 'inherit' });
if (result.error) {
console.error(`[verify:release] failed to launch '${command}': ${result.error.message}`);
return false;
}
if (result.status !== 0) {
const reason = result.signal ? `terminated by ${result.signal}` : `exited ${result.status}`;
console.error(`[verify:release] command '${command}' ${reason}`);
return false;
}
return true;
}
// Runs the complete mandatory verification set (or, with --stage <name>, the
// single named stage — used for wiring/smoke-testing, not for gating: only a
// run of every stage is a terminal verification). Fails fast: the first
// failing command aborts with a non-zero exit code. Returns the exit code.
export function verifyRelease({ stages = STAGES } = {}) {
const missing = missingBinaries(['bash', 'rsync']);
if (missing.length > 0) {
console.error(
`[verify:release] FATAL: required binaries missing from PATH: ${missing.join(', ')}. ` +
'The caller provides them (ci-base bakes bash; pipelines apk add rsync).',
);
return 1;
}
for (const stage of stages) {
console.log(`\n[verify:release] === stage: ${stage.name} ===`);
for (const command of stage.commands) {
console.log(`[verify:release] $ ${command}`);
if (!runCommand(command)) {
console.error(
`[verify:release] FATAL: stage '${stage.name}' failed — verification inconclusive`,
);
return 1;
}
}
}
console.log(`\n[verify:release] all ${stages.length} stage(s) passed`);
return 0;
}
function main(argv) {
const stageFlagIndex = argv.indexOf('--stage');
if (stageFlagIndex !== -1) {
const name = argv[stageFlagIndex + 1];
const stage = stageByName(name);
if (!stage) {
console.error(
`[verify:release] unknown stage '${name ?? ''}' — expected one of: ${STAGES.map((entry) => entry.name).join(', ')}`,
);
process.exit(2);
}
process.exit(verifyRelease({ stages: [stage] }));
}
process.exit(verifyRelease());
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
main(process.argv.slice(2));
}
+349
View File
@@ -0,0 +1,349 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { createRequire } from 'node:module';
import path from 'node:path';
import test from 'node:test';
import { STAGES, stageByName } from './verify-release.mjs';
// SDLC-D-034 checkout invariant: publication in .woodpecker/publish.yml is
// bound to exact-commit terminal verification. This suite parses the real
// pipeline files and fails red when the gate is bypassed, weakened, or drifts
// out of sync with the canonical `pnpm verify:release` command. The negative
// controls for pipeline DAG/bypass shapes live in
// scripts/publish-gate-structure.test.mjs (RI-1-002); this file owns the
// canonical-command composition controls.
// Reuse the monorepo's existing YAML parser (@mosaicstack/mosaic's direct
// dependency) instead of adding a root dependency or vendoring a parser.
const mosaicRequire = createRequire(
path.resolve(process.cwd(), 'packages', 'mosaic', 'package.json'),
);
const { parse: parseYaml } = mosaicRequire('yaml');
const publishYmlPath = path.join(process.cwd(), '.woodpecker', 'publish.yml');
const ciYmlPath = path.join(process.cwd(), '.woodpecker', 'ci.yml');
async function readPublishPipeline() {
return parseYaml(await readFile(publishYmlPath, 'utf8'));
}
// A step has an external publication effect when its name starts with
// `publish` or when any command pushes an image to a registry.
function pushesImage(step) {
return (step.commands ?? []).some((command) =>
/(^|\s)(\/kaniko\/executor|docker push)\b|--destination/.test(command),
);
}
function publishEffectSteps(pipeline) {
return Object.entries(pipeline.steps ?? {})
.filter(([name, step]) => name.startsWith('publish') || pushesImage(step))
.map(([name]) => name);
}
// Transitive closure of a step's depends_on graph.
function dependencyClosure(pipeline, stepName, seen = new Set()) {
const dependencies = pipeline.steps?.[stepName]?.depends_on ?? [];
for (const dependency of dependencies) {
if (seen.has(dependency)) continue;
seen.add(dependency);
dependencyClosure(pipeline, dependency, seen);
}
return seen;
}
function verifyCommands(pipeline) {
const verify = pipeline.steps?.verify;
assert.ok(verify, 'publish pipeline must define a `verify` step');
assert.ok(Array.isArray(verify.commands), '`verify` step must have commands');
return verify.commands;
}
function assertCommitIdentityAssertion(commands) {
const text = commands.join('\n');
assert.match(
text,
/CI_COMMIT_SHA/,
'`verify` must compare the provider commit identity (CI_COMMIT_SHA)',
);
assert.match(text, /git rev-parse HEAD/, '`verify` must compare against git rev-parse HEAD');
assert.match(
text,
/exit 1/,
'`verify` must fail closed (exit 1) on identity mismatch or emptiness',
);
}
function assertCanonicalCommand(commands) {
assert.ok(
commands.some((command) => /^pnpm verify:release\b/.test(command.trim())),
'`verify` must run the canonical terminal verification command `pnpm verify:release`',
);
}
function assertPublishGate(pipeline) {
assert.ok(pipeline.steps, 'publish pipeline must define steps');
const commands = verifyCommands(pipeline);
assertCommitIdentityAssertion(commands);
assertCanonicalCommand(commands);
const effects = publishEffectSteps(pipeline);
assert.ok(effects.length > 0, 'publish pipeline must contain publish effect steps to guard');
for (const stepName of effects) {
const step = pipeline.steps[stepName];
assert.ok(
Array.isArray(step.depends_on) && step.depends_on.includes('verify'),
`publish effect '${stepName}' must depend DIRECTLY on the verify step (SDLC-D-034: transitively through build is not enough)`,
);
assert.ok(
dependencyClosure(pipeline, stepName).has('verify'),
`publish effect '${stepName}' must depend on a chain that includes verify`,
);
}
return effects;
}
test('the publish pipeline gates every publish effect behind exact-commit verification', async () => {
const pipeline = await readPublishPipeline();
const effects = assertPublishGate(pipeline);
assert.deepEqual(effects.sort(), [
'build-appservice',
'build-gateway',
'publish-next-npm',
'publish-npm',
]);
});
test('the verify step carries no path/event short-circuit of its own', async () => {
const pipeline = await readPublishPipeline();
// A `when` filter on `verify` would let a publish effect fire on an event
// class that skipped verification — the gate must be unconditional.
assert.equal(pipeline.steps.verify.when, undefined);
});
test('a publish step that bypasses verify fails the gate checker', () => {
// Negative fixture: a plausible publish pipeline where `publish-npm` hangs
// off `build` only and `build` never chains to `verify` — the exact bypass
// class SDLC-D-034 closes. The checker must go red on it.
const bypassingPipeline = `
steps:
install:
image: node:24-alpine
commands:
- pnpm install --frozen-lockfile
verify:
image: node:24-alpine
commands:
- |
if [ -z "$CI_COMMIT_SHA" ] || [ "$CI_COMMIT_SHA" != "$(git rev-parse HEAD)" ]; then
echo "identity mismatch" >&2
exit 1
fi
- pnpm verify:release
depends_on:
- install
build:
image: node:24-alpine
commands:
- pnpm build
depends_on:
- install
publish-npm:
image: node:24-alpine
commands:
- pnpm publish
depends_on:
- build
`;
assert.throws(
() => assertPublishGate(parseYaml(bypassingPipeline)),
/publish-npm.*DIRECTLY.*verify/s,
);
});
test('a publish step chained to verify only transitively fails the gate checker', () => {
// Negative fixture: `build` depends on verify but `publish-npm` does not
// carry the direct edge — weaker than SDLC-D-034 requires of the real DAG.
const transitiveOnlyPipeline = `
steps:
install:
image: node:24-alpine
commands:
- pnpm install --frozen-lockfile
verify:
image: node:24-alpine
commands:
- |
if [ -z "$CI_COMMIT_SHA" ] || [ "$CI_COMMIT_SHA" != "$(git rev-parse HEAD)" ]; then
echo "identity mismatch" >&2
exit 1
fi
- pnpm verify:release
depends_on:
- install
build:
image: node:24-alpine
commands:
- pnpm build
depends_on:
- install
- verify
publish-npm:
image: node:24-alpine
commands:
- pnpm publish
depends_on:
- build
`;
assert.throws(
() => assertPublishGate(parseYaml(transitiveOnlyPipeline)),
/publish-npm.*DIRECTLY.*verify/s,
);
});
test('a verify step without the commit-identity assertion fails the gate checker', () => {
const noIdentityPipeline = `
steps:
verify:
image: node:24-alpine
commands:
- pnpm verify:release
publish-npm:
image: node:24-alpine
commands:
- pnpm publish
depends_on:
- verify
`;
assert.throws(() => assertPublishGate(parseYaml(noIdentityPipeline)), /CI_COMMIT_SHA/);
});
// The composition check: the canonical stage table must mirror the PR CI
// pipeline's complete mandatory set. Parameterized by the stage list so the
// subset negative control below can prove a dropped stage goes red (RI-1-002:
// the canonical command cannot silently lose a check).
function assertStagesMirrorCi(stages, ci) {
const canonical = Object.fromEntries(stages.map((stage) => [stage.name, stage.commands]));
// The complete mandatory set, in gate order. `quality-rails` is the one
// canonical-only stage (RI-N4, QC-19) with no ci.yml mirror to match — its
// contract is asserted separately below. `build` gained a ci.yml mirror in
// #1445 (P6) and is enforced with the other pnpm stages.
assert.deepEqual(
stages.map((stage) => stage.name),
[
'sanitization',
'upgrade-guard',
'typecheck',
'lint',
'format',
'test',
'build',
'quality-rails',
],
);
// Guard stages: ci.yml commands minus its `apk add` environment prep must be
// exactly the canonical stage commands (order included).
for (const stageName of ['sanitization', 'upgrade-guard']) {
assert.deepEqual(
ci.steps[stageName].commands.filter((command) => !command.startsWith('apk add')),
canonical[stageName],
`canonical '${stageName}' stage must match the ci.yml step`,
);
}
// pnpm stages: ci.yml commands minus `corepack enable` must be exactly the
// canonical stage commands.
for (const stepName of ['typecheck', 'lint', 'format', 'build']) {
assert.deepEqual(
ci.steps[stepName].commands.filter((command) => command !== 'corepack enable'),
canonical[stepName],
`canonical '${stepName}' stage must match the ci.yml step`,
);
}
// The test stage is shared, but ci.yml wraps it in pipeline-level
// prerequisites the canonical command expects its caller to provide
// (SDLC-D-034): the postgres service + readiness wait + db:migrate, openssl,
// and the pinned pi runtime. None of those may be dropped silently.
for (const command of canonical.test) {
assert.ok(
ci.steps.test.commands.includes(command),
`ci.yml test step must run the canonical test stage command '${command}'`,
);
}
for (const fragment of [
'pg_isready -h ci-postgres',
'pnpm --filter @mosaicstack/db run db:migrate',
'npm install -g @earendil-works/[email protected]',
]) {
assert.ok(
ci.steps.test.commands.some((command) => command.includes(fragment)),
`ci.yml test step must keep its pipeline-level prerequisite '${fragment}'`,
);
}
}
test('the canonical verify:release stages mirror the PR CI pipeline one-for-one', async () => {
const ci = parseYaml(await readFile(ciYmlPath, 'utf8'));
assertStagesMirrorCi(STAGES, ci);
});
test('a subset stage list fails the composition check — a dropped stage cannot pass silently', async () => {
const ci = parseYaml(await readFile(ciYmlPath, 'utf8'));
// Drop each stage one at a time: every stage is load-bearing, so every drop
// must go red. If any drop went green, a refactor could silently delete a
// mandatory check from the canonical command.
for (const stage of STAGES) {
const subset = STAGES.filter((entry) => entry.name !== stage.name);
assert.throws(
() => assertStagesMirrorCi(subset, ci),
Error,
`composition check must fail when the '${stage.name}' stage is dropped from the table`,
);
}
});
test('the root package.json exposes verify:release as the canonical command', async () => {
const packageJson = JSON.parse(await readFile(path.join(process.cwd(), 'package.json'), 'utf8'));
assert.match(packageJson.scripts['verify:release'], /scripts\/verify-release\.mjs/);
});
// RI-N4 (card RI-3-002): the `quality-rails` stage must route through the TS
// evaluator instead of duplicating its presence logic inline. The evaluator
// owns QC-19; this file keeps that delegation honest.
function assertEvaluatorStage(stage) {
assert.ok(stage, 'canonical stages must include a quality-rails stage');
assert.ok(Array.isArray(stage.commands) && stage.commands.length > 0);
for (const command of stage.commands) {
assert.match(
command,
/packages\/quality-rails\/dist\/cli\.js.*quality-rails evaluate/,
`quality-rails stage command must invoke the evaluator CLI, got: '${command}'`,
);
}
}
test('the quality-rails stage invokes the evaluator rather than duplicating its logic', () => {
assertEvaluatorStage(stageByName('quality-rails'));
});
test('a quality-rails stage that re-implements presence logic inline fails the checker', () => {
// Negative control: replacing the evaluator invocation with an inline
// `test -f` presence loop is exactly the duplication RI-N4 forbids — the
// checker must go red on it.
const duplicated = {
name: 'quality-rails',
commands: ['test -f .husky/pre-commit && test -f .husky/pre-push'],
};
assert.throws(() => assertEvaluatorStage(duplicated), /must invoke the evaluator CLI/);
});
test('a quality-rails stage that silently drops the evaluator command fails the checker', () => {
const empty = { name: 'quality-rails', commands: [] };
assert.throws(() => assertEvaluatorStage(empty), /commands/);
});