-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathpre-commit
More file actions
executable file
·295 lines (283 loc) · 14.4 KB
/
Copy pathpre-commit
File metadata and controls
executable file
·295 lines (283 loc) · 14.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
#!/usr/bin/env bash
# pre-commit hook — the staged-surface half of the commit gate (rules.md §3),
# enforced rather than remembered. Any failing check exits non-zero and BLOCKS
# the commit.
#
# The gate composes TWO layers with DIFFERENT activation (rules.md §3):
#
# STRUCTURAL (always; artifact-local, campaign-INDEPENDENT) — checks 1-3.
# They ask only "does this artifact satisfy its own contract?" and need no
# campaign to be meaningful, so they run on every commit:
# 1. ORPHANS no staged authoritative file may reference a removed/demoted
# workflow as if it were live. The orphan gate is a repo-wide
# referential-truth scan; here it is SCOPED to the staged set so
# a commit is blocked for orphans IT introduces or touches, not
# for pre-existing debt elsewhere (which would force routine
# --no-verify and defeat the gate). The full repo-wide audit
# stays available as: gates/check_orphans.sh <root> <names>...
# 2. DOC LINKS staged markdown has valid local links (check_docs.py).
# 3. LEDGER a staged Nickel ledger artifact satisfies its contract
# (ledger-validate.sh structure) — a malformed .ncl cannot land.
#
# AUTHORITY (iff a campaign is in flight; per-commit, campaign-DEPENDENT) —
# check 4. "Not in the IBC -> not authorized": do the staged paths fall
# under some node's file_surface in the ACTIVE campaign DAG? This is
# meaningless without a campaign plan, so it runs ONLY when one is declared
# via the active-DAG pointer (see check 4). An ordinary commit (no pointer)
# runs the structural layer alone — nothing in the authority layer blocks it.
#
# A check whose inputs aren't staged is skipped (no-op), so unrelated commits
# pass cleanly.
set -u
# Two distinct locations, deliberately kept apart (the packaging invariant):
# $plugin = where predicate's MACHINERY lives. Resolved from THIS hook's own
# real path. The installed hook is a SYMLINK in the consuming repo's
# .git/hooks/ pointing back to <plugin>/hooks/pre-commit, so we must
# resolve symlinks (realpath) to land in the real plugin tree. All
# sibling machinery (gate scripts, per-skill checkers) is located
# relative to $plugin — never relative to the repo being gated.
# $root = the repo being GATED (the consuming repo). Used ONLY for the FILES
# under inspection (staged paths, the target tree) — never to locate
# machinery. In the self-host case ($plugin == $root) both coincide.
plugin="$(cd "$(dirname "$(realpath "${BASH_SOURCE[0]}")")/.." && pwd)"
root="$(git rev-parse --show-toplevel)"
# Load project config if present; a downstream repo can override REMOVED (bash
# array of demoted workflows) and DOC_AUDIT_CHECK (path to the doc-link checker).
# Config belongs to the GATED repo ($root), not the plugin. Absent → defaults.
# shellcheck source=/dev/null
[ -f "$root/.ledger/config.sh" ] && source "$root/.ledger/config.sh"
# The removed/demoted workflows the orphan gate polices. Kept in sync with the
# cohesion campaign's cut set; a name here must have no live skills/<name>/ dir.
if [ -z "${REMOVED+set}" ]; then
REMOVED=(plan charter plan-review continue personalization sketch dialectic planning)
fi
# Path to the doc-link checker — predicate MACHINERY, so it lives under $plugin.
# Downstream repos can still redirect to their own script via config.
: "${DOC_AUDIT_CHECK:=$plugin/skills/doc-audit/scripts/check_docs.py}"
rc=0
# Staged files (added/copied/modified), repo-root-relative paths — the same
# coordinate system check_orphans.sh emits, so prefix matching is exact.
mapfile -t staged < <(git diff --cached --name-only --diff-filter=ACMR)
# --- 1. ORPHANS (scoped to staged files) ----------------------------------
# Run the full repo-wide gate for its pattern truth, then fail the commit only
# if a flagged "file:line:match" names a file in the staged set.
orphan_out="$(bash "$plugin/gates/check_orphans.sh" "$root" "${REMOVED[@]}" 2>/dev/null || true)"
staged_orphans=""
while IFS= read -r line; do
# gate output rows are "path:line:matched-text"; take the path column.
file="${line%%:*}"
for s in "${staged[@]}"; do
if [ "$file" = "$s" ]; then
staged_orphans+="$line"$'\n'
break
fi
done
done <<< "$orphan_out"
if [ -n "${staged_orphans//[$'\n']/}" ]; then
echo "pre-commit: ORPHAN WORKFLOW REFERENCES in staged files:" >&2
printf '%s' "$staged_orphans" >&2
echo " A staged file names a removed/demoted workflow as if it were live." >&2
rc=1
fi
# --- 1b. SCRATCH CONTAINMENT (staged paths) --------------------------------
# .scratch/ is the ephemeral working set — git-ignored by the three-store law
# and never part of the deliverable. A staged path under it means a force-add
# bypassed the ignore; block the commit.
scratch_staged=""
for s in "${staged[@]}"; do
case "$s" in
.scratch/*) scratch_staged+=" $s"$'\n' ;;
esac
done
if [ -n "$scratch_staged" ]; then
echo "pre-commit: STAGED FILES UNDER .scratch/ (never committed):" >&2
printf '%s' "$scratch_staged" >&2
echo " Unstage with: git restore --staged .scratch" >&2
rc=1
fi
# --- 2. DOC LINKS (staged markdown) ----------------------------------------
for f in "${staged[@]}"; do
case "$f" in
*.md)
if [ -f "$root/$f" ]; then
( cd "$root" && python3 "$DOC_AUDIT_CHECK" "$f" )
doc_rc=$?
if [ "$doc_rc" -ne 0 ]; then rc=1; fi
fi
;;
esac
done
# --- 3. LEDGER STRUCTURE (staged .ncl) -------------------------------------
# SCOPED to predicate-owned paths: ledger/, .ledger/, conditioning/.
# A downstream repo's own .ncl files (e.g. a function library at the repo
# root, or any path outside predicate's artifact directories) are NOT
# predicate artifacts — structure-validating them with ledger-validate.sh
# would produce a misleading "ledger artifact failed structure validation"
# error for code that is perfectly valid Nickel but non-serializable. Only
# predicate's own directories carry ledger artifacts and procedure instances;
# everything else is the consuming repo's concern, not the ledger gate's.
for f in "${staged[@]}"; do
case "$f" in
*.ncl)
# Only validate .ncl files under predicate-owned directories.
case "$f" in
ledger/*|.ledger/*|conditioning/*) : ;; # predicate artifact → validate
*) continue ;; # user's own .ncl → skip silently
esac
if [ -f "$root/$f" ]; then
bash "$plugin/ledger/gate/ledger-validate.sh" structure "$root/$f"
led_rc=$?
if [ "$led_rc" -ne 0 ]; then
echo "pre-commit: ledger artifact failed structure validation: $f" >&2
rc=1
fi
fi
;;
esac
done
# --- 4. AUTHORITY (iff an active campaign DAG is declared) -----------------
# The authority overlay is campaign-DEPENDENT: it is meaningless without a plan,
# so it runs ONLY when a campaign is in flight. The active-DAG pointer is a file
# in the main tree's .ledger/active-dag, written by the orchestration driver
# when a campaign is in flight and absent otherwise. Its content is the path to
# the active campaign DAG artifact (main-tree-relative, or absolute).
#
# CRITICAL: the pointer is resolved from the COMMON git dir, not the worktree
# $root. A linked worktree has no .ledger of its own — it lives only in the main
# tree — so keying the pointer off $root makes a worker committing from a
# worktree never find it, and the authority overlay silently never fires (the
# very commits a campaign produces would go un-authorized).
#
# Pointer ABSENT -> ordinary commit: structural layer alone, authority skipped.
# Pointer PRESENT -> authority enforced: every staged path must fall under some
# DAG node's file_surface (ledger-validate.sh commit-gate).
ledger_root="$(cd "$(dirname "$(git rev-parse --git-common-dir)")" 2>/dev/null && pwd || echo "$root")"
pointer="$ledger_root/.ledger/active-dag"
if [ -f "$pointer" ]; then
# Resolve the named DAG against the MAIN tree (the pointer and the DAG both
# live there, not in the worktree); the machinery that reads it stays
# $plugin-relative.
dag="$(head -n1 "$pointer" | tr -d '[:space:]')"
case "$dag" in
/*) dag_path="$dag" ;; # absolute, used as-is
*) dag_path="$ledger_root/$dag" ;; # relative to the main tree
esac
if [ -z "$dag" ] || [ ! -r "$dag_path" ]; then
echo "pre-commit: active-dag pointer names an unreadable DAG: ${dag:-<empty>}" >&2
echo " ($pointer -> $dag_path)" >&2
rc=1
else
# The authority half: authorize the staged set against the active DAG.
# We call `authorize`, NOT `commit-gate`: for NCL DAGs, the DagNoConflict
# STRUCTURE proof is an ORCHESTRATOR-level check, run once when the DAG is
# amended — not re-proved per worker commit. A staged .ncl edit is still
# structure-checked by the STRUCTURAL layer (check 3) above.
# NOTE: YAML DAGs' authorize now runs --apply-contract dag_apply.ncl
# (Dag ∘ DagNoConflict) on every call after the cache removal; dag-perf
# keeps this fast (~0.1 s), so it is not a performance concern. The
# NCL/YAML distinction is in ledger-validate.sh cmd_authorize.
# ledger-validate.sh resolves its own machinery ($plugin).
bash "$plugin/ledger/gate/ledger-validate.sh" authorize "$dag_path"
auth_rc=$?
if [ "$auth_rc" -ne 0 ]; then
echo "pre-commit: AUTHORITY check failed against active DAG: $dag" >&2
echo " A staged path falls under no node's file_surface" \
"(not in the IBC -> not authorized)." >&2
rc=1
fi
fi
fi
# --- 5. PROCESS (iff an active walk is declared) ---------------------------
# The process overlay is WALK-ACTIVATED: it fires only when an agent walk has
# declared its run-state via the active-walk pointer. The pointer lives in the
# MAIN tree's .ledger/active-walk (the same placement as active-dag), written
# by the walk at startup via `process-gate.sh register` and removed by its
# teardown trap on exit. A human commit never writes this pointer; humans meet
# checks 1-4 only (K7).
#
# Pointer ABSENT -> ordinary commit (human or walk with no procedure deposits):
# checks 1-4 only, PROCESS layer skipped.
# Pointer PRESENT -> PROCESS enforced: the declared deposit-path (line 2 of the
# pointer, pinned at register time) is validated against the
# class contract (line 1) IFF it is in the staged set.
# Non-deposit .ncl files (contracts, DAGs, fixtures) are never
# validated by this gate — only the declared deposit is.
# A malformed/legacy pointer (no line 2) passes silently:
# a walk that declared no deposit has nothing to validate.
walk_pointer="$ledger_root/.ledger/active-walk"
if [ -f "$walk_pointer" ]; then
walk_class="$(sed -n '1p' "$walk_pointer" | tr -d '[:space:]')"
walk_deposit="$(sed -n '2p' "$walk_pointer" | tr -d '[:space:]')"
if [ -z "$walk_class" ]; then
echo "pre-commit: active-walk pointer is empty; cannot run process gate" >&2
rc=1
elif [ -z "$walk_deposit" ]; then
# Legacy or malformed pointer (no deposit-path line): pass silently.
# A walk that declared no deposit has nothing to process-validate.
: # no-op
else
# B3 fix: normalize the deposit path before comparing to the staged set.
# git diff --cached --name-only emits repo-root-relative paths without
# leading ./ or doubled //. Normalize the pointer value to match:
# 1. strip leading ./
# 2. collapse // sequences to / (covers ledger//fixtures/... forms)
# 3. strip absolute $root/ prefix (pointer may use an absolute path)
walk_deposit="${walk_deposit#./}"
walk_deposit="$(printf '%s' "$walk_deposit" | sed 's|//|/|g')"
if [[ "$walk_deposit" == /* ]]; then
walk_deposit="${walk_deposit#"$root/"}"
fi
# Validate ONLY the declared deposit-path, and only if it is staged.
# B2 fix: if the pointer declares a deposit path but the deposit is NOT
# staged, fail closed — a "declared-but-never-delivered" walk must not
# silently pass. The declared deposit MUST be staged in this commit.
deposit_staged=0
for f in "${staged[@]}"; do
[ "$f" = "$walk_deposit" ] && deposit_staged=1 && break
done
if [ "$deposit_staged" -eq 1 ]; then
if [ -f "$root/$walk_deposit" ]; then
bash "$plugin/ledger/gate/process-gate.sh" validate "$root/$walk_deposit" "$walk_class"
proc_rc=$?
if [ "$proc_rc" -ne 0 ]; then
echo "pre-commit: PROCESS gate failed for $walk_deposit (class: $walk_class)" >&2
echo " The procedure deposit omits a required step or has bad shape." >&2
rc=1
fi
else
echo "pre-commit: declared deposit path not found in working tree: $walk_deposit" >&2
rc=1
fi
else
# B2: declared deposit not staged → fail closed. A walk that registers a
# deposit path but commits without staging that exact path has not
# delivered its declared deposit. Non-deposit .ncl files (contracts, DAGs,
# fixtures) are never process-validated; but the declared deposit must be.
echo "pre-commit: declared deposit '$walk_deposit' not staged in this commit" >&2
echo " Active-walk pointer requires the deposit to be staged and pass." >&2
rc=1
fi
fi
fi
# --- 6. PROJECT-LOCAL GATES -------------------------------------------------
# A consuming project can declare its own idiosyncratic gates in its
# .ledger/gates/ directory — project data, parallel to the flight log and
# state, owned entirely by the consuming repo. The runner discovers every
# executable file therein, runs them in sorted name order (passing $root as
# $1), and exits non-zero iff any fail.
#
# A project with no .ledger/gates/ is a clean no-op. Unlike tiers 4 and 5,
# no activation pointer is needed: the runner's own no-op-when-absent
# property IS the activation guard. Predicate's own shipped machinery is
# zero-knowledge of what any project-local gate checks.
bash "$plugin/ledger/gate/project-gates.sh" "$root"
proj_rc=$?
if [ "$proj_rc" -ne 0 ]; then
echo "pre-commit: PROJECT-LOCAL gate(s) failed — see diagnostics above." >&2
rc=1
fi
if [ "$rc" -ne 0 ]; then
echo "pre-commit: staged-surface gate failed — commit blocked." >&2
echo " (Hooks enforce the commit gate; bypassing with --no-verify defeats it.)" >&2
fi
exit "$rc"