@cratylus/canon 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/architect.js +14 -108
- package/dist/agents/architect.js.map +1 -1
- package/dist/agents/assayer.d.ts +2 -0
- package/dist/agents/assayer.js +20 -0
- package/dist/agents/assayer.js.map +1 -0
- package/dist/agents/implementer.js +9 -69
- package/dist/agents/implementer.js.map +1 -1
- package/dist/agents/kino.js +24 -110
- package/dist/agents/kino.js.map +1 -1
- package/dist/agents/mav.js +43 -79
- package/dist/agents/mav.js.map +1 -1
- package/dist/agents/nico.js +33 -61
- package/dist/agents/nico.js.map +1 -1
- package/dist/agents/planner.js +8 -80
- package/dist/agents/planner.js.map +1 -1
- package/dist/dimensions/autonomy/human-on-the-loop.js +1 -1
- package/dist/dimensions/autonomy/human-on-the-loop.js.map +1 -1
- package/dist/dimensions/role/architect.js +19 -1
- package/dist/dimensions/role/architect.js.map +1 -1
- package/dist/dimensions/role/{build.d.ts → assayer.d.ts} +1 -1
- package/dist/dimensions/role/assayer.js +25 -0
- package/dist/dimensions/role/assayer.js.map +1 -0
- package/dist/dimensions/role/{diagnose.d.ts → implementer.d.ts} +1 -1
- package/dist/dimensions/role/implementer.js +12 -0
- package/dist/dimensions/role/implementer.js.map +1 -0
- package/dist/dimensions/role/{curate.d.ts → planner.d.ts} +1 -1
- package/dist/dimensions/role/planner.js +14 -0
- package/dist/dimensions/role/planner.js.map +1 -0
- package/dist/hook-cells.js +5 -2
- package/dist/hook-cells.js.map +1 -1
- package/dist/hooks/deploy-drift-notice.js +1 -1
- package/dist/hooks/deploy-drift-notice.js.map +1 -1
- package/dist/hooks/purview-guardrail.d.ts +2 -0
- package/dist/hooks/purview-guardrail.js +302 -0
- package/dist/hooks/purview-guardrail.js.map +1 -0
- package/dist/hooks/stance-guardrail-pre.js +17 -22
- package/dist/hooks/stance-guardrail-pre.js.map +1 -1
- package/dist/hooks/stance-guardrail.js +84 -27
- package/dist/hooks/stance-guardrail.js.map +1 -1
- package/dist/plan-states.d.ts +3 -3
- package/dist/plan-states.js +3 -3
- package/dist/roles/architect.d.ts +2 -0
- package/dist/roles/architect.js +101 -0
- package/dist/roles/architect.js.map +1 -0
- package/dist/roles/assayer.d.ts +2 -0
- package/dist/roles/assayer.js +66 -0
- package/dist/roles/assayer.js.map +1 -0
- package/dist/roles/hold.d.ts +57 -0
- package/dist/roles/hold.js +105 -0
- package/dist/roles/hold.js.map +1 -0
- package/dist/roles/implementer.d.ts +2 -0
- package/dist/roles/implementer.js +60 -0
- package/dist/roles/implementer.js.map +1 -0
- package/dist/roles/planner.d.ts +2 -0
- package/dist/roles/planner.js +70 -0
- package/dist/roles/planner.js.map +1 -0
- package/dist/skills/carry-on/skill.js +2 -2
- package/dist/skills/carry-on/skill.js.map +1 -1
- package/dist/skills/deliver/skill.js +41 -3
- package/dist/skills/deliver/skill.js.map +1 -1
- package/package.json +3 -6
- package/dist/dimensions/role/build.js +0 -2
- package/dist/dimensions/role/build.js.map +0 -1
- package/dist/dimensions/role/converse.d.ts +0 -2
- package/dist/dimensions/role/converse.js +0 -2
- package/dist/dimensions/role/converse.js.map +0 -1
- package/dist/dimensions/role/curate.js +0 -2
- package/dist/dimensions/role/curate.js.map +0 -1
- package/dist/dimensions/role/diagnose.js +0 -2
- package/dist/dimensions/role/diagnose.js.map +0 -1
- package/dist/dimensions/role/document.d.ts +0 -2
- package/dist/dimensions/role/document.js +0 -2
- package/dist/dimensions/role/document.js.map +0 -1
- package/dist/dimensions/role/implement.d.ts +0 -2
- package/dist/dimensions/role/implement.js +0 -2
- package/dist/dimensions/role/implement.js.map +0 -1
- package/dist/dimensions/role/operate.d.ts +0 -2
- package/dist/dimensions/role/operate.js +0 -2
- package/dist/dimensions/role/operate.js.map +0 -1
- package/dist/dimensions/role/orchestrate.d.ts +0 -2
- package/dist/dimensions/role/orchestrate.js +0 -2
- package/dist/dimensions/role/orchestrate.js.map +0 -1
- package/dist/dimensions/role/plan.d.ts +0 -2
- package/dist/dimensions/role/plan.js +0 -2
- package/dist/dimensions/role/plan.js.map +0 -1
- package/dist/dimensions/role/research.d.ts +0 -2
- package/dist/dimensions/role/research.js +0 -2
- package/dist/dimensions/role/research.js.map +0 -1
- package/dist/dimensions/role/review.d.ts +0 -2
- package/dist/dimensions/role/review.js +0 -2
- package/dist/dimensions/role/review.js.map +0 -1
- package/dist/dimensions/role/test.d.ts +0 -2
- package/dist/dimensions/role/test.js +0 -2
- package/dist/dimensions/role/test.js.map +0 -1
- package/dist/hooks/praxis-continuity.d.ts +0 -2
- package/dist/hooks/praxis-continuity.js +0 -132
- package/dist/hooks/praxis-continuity.js.map +0 -1
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
// purview-guardrail — the gate that makes a ROLE CONTRACT scoreable.
|
|
2
|
+
//
|
|
3
|
+
// A role value now states the arrow the position is over the ladder
|
|
4
|
+
// `intent ≺ C ≺ spec ≺ artifact`, and an act whose domain or codomain falls outside
|
|
5
|
+
// that arrow is delegated NECESSARILY. Declared, that is a steer. This cell is the
|
|
6
|
+
// bound: it refuses, before the call fires, an act the holder's own contract puts
|
|
7
|
+
// outside its arrow.
|
|
8
|
+
//
|
|
9
|
+
// WHY A SECOND CELL RATHER THAN A SECOND RUBRIC LEG. One gate, one contract. The
|
|
10
|
+
// stance guard judges `autonomy` — whether the agent collapsed into order-taking — and
|
|
11
|
+
// this one judges `role`. They score different declarations, at different moments, and
|
|
12
|
+
// `gates` in the stance manifest is keyed by OWNING CELL precisely so a second guarded
|
|
13
|
+
// dimension is a second entry rather than an append to somebody else's prompt. The
|
|
14
|
+
// judge BACKEND is shared, exactly as the pre-hook shares it: one judge, two contracts.
|
|
15
|
+
//
|
|
16
|
+
// IT QUOTES THE AGENT'S OWN PROJECTED CONTRACT, and that is the design decision worth
|
|
17
|
+
// stating. The rubric carries no role text and names no agent. The worker reads the
|
|
18
|
+
// `## Role` section out of the persona's own deployed Target and hands it to the judge
|
|
19
|
+
// as the law to score against, so the gate scores WHAT THE CORPUS DECLARES rather than
|
|
20
|
+
// an author's opinion about good behaviour — and a corpus that mints a sixth role gets
|
|
21
|
+
// it judged with no edit here. This is the lesson the stance guard learned the
|
|
22
|
+
// expensive way: its allowlist drifted and left `architect` and `kino` unjudged while
|
|
23
|
+
// every agent in the corpus composed the fragment it claimed to enforce.
|
|
24
|
+
//
|
|
25
|
+
// WHAT IT BLOCKS is what the arrow excludes, and nothing else:
|
|
26
|
+
// · a dispatch to a builder for a piece no planner decomposed, from a holder whose
|
|
27
|
+
// arrow does not write spec — the architect skipping the middle rung;
|
|
28
|
+
// · a write to the substrate by a holder whose arrow does not write artifact;
|
|
29
|
+
// · a dispatch whose prompt is the operator's literal words rather than a cut piece
|
|
30
|
+
// — the same dispatch-echo the stance guard catches at turn end, refused here
|
|
31
|
+
// because at this moment the contract that convicts it is the ROLE's.
|
|
32
|
+
// WHAT IT PASSES: a dispatch to a planner, a dispatch to an assayer, READING anything,
|
|
33
|
+
// and writing whatever the arrow reserves. Reading is never a block — the architect's
|
|
34
|
+
// contract makes an artifact read a descent, but a read is cheap to undo and a gate
|
|
35
|
+
// that fires on one would wedge every legitimate orientation.
|
|
36
|
+
//
|
|
37
|
+
// FAIL-OPEN, EVIDENCE-CHECKED, RE-ENTRY-CAPPED — all three inherited from the sibling
|
|
38
|
+
// pre-hook, which is also the structural model for the worker. A block whose cited span
|
|
39
|
+
// is not literally in the payload is DISCARDED: a fabricated block is not a lesser
|
|
40
|
+
// error than a missed one.
|
|
41
|
+
export const purviewGuardrail = {
|
|
42
|
+
id: 'purview-guardrail',
|
|
43
|
+
residue: 'structural-refusal ↾ mid-turn act ∉ role-arrow · deny-before-fire ⟨descent ⟨write(artifact) ∉ writes⟩ · skipped-rung ⟨dispatch(build) ∄ decomposition⟩ · dispatch-echo ⟨literal-words ≠ cut-piece⟩⟩ · pass ⟨read · act ∈ reserves · dispatch ↦ plan ∨ assay⟩ · law = the HOLDER-projected role-section ⟨quoted verbatim · ¬ authored-opinion⟩ · shared judge-backend ⟨sibling⟩ · fail-open ∧ evidence-checked ∧ re-entry-capped',
|
|
44
|
+
substrate: 'harness',
|
|
45
|
+
order: 1,
|
|
46
|
+
events: ['subagent.dispatch.pre', 'tool.use.pre'],
|
|
47
|
+
entry: 'purview-guardrail.sh',
|
|
48
|
+
timeout: 60,
|
|
49
|
+
refs: [],
|
|
50
|
+
workers: [
|
|
51
|
+
{
|
|
52
|
+
filename: 'purview-guardrail.sh',
|
|
53
|
+
targetPath: 'packages/canon/targets/guardrail/purview-guardrail.sh',
|
|
54
|
+
executable: true,
|
|
55
|
+
content: `#!/usr/bin/env sh
|
|
56
|
+
# purview-guardrail — a PreToolUse hook that STRUCTURALLY REFUSES, before the call
|
|
57
|
+
# fires, a mid-turn act that falls outside the arrow the agent's OWN role contract
|
|
58
|
+
# declares.
|
|
59
|
+
#
|
|
60
|
+
# THE LAW IS NOT IN THIS FILE AND NOT IN THE RUBRIC. It is the \`## Role\` section of
|
|
61
|
+
# the persona's deployed Target, read at run time and handed to the judge as the
|
|
62
|
+
# contract to score against. No role text, no agent name and no behavioural opinion is
|
|
63
|
+
# hard-coded anywhere in this gate.
|
|
64
|
+
#
|
|
65
|
+
# SAFETY MODEL (mirrors both siblings):
|
|
66
|
+
# - SCOPE-ENROLLED by PRESENCE. A persona carrying a stance manifest is judged; every
|
|
67
|
+
# other scope is silent. No allowlist — the projection that places a persona enrolls
|
|
68
|
+
# it, and nothing central lists anybody.
|
|
69
|
+
# - CONTRACT-ENROLLED, one step further. A persona whose Target carries no \`## Role\`
|
|
70
|
+
# section has no arrow to be outside of, so it is allowed. A gate cannot convict
|
|
71
|
+
# against a law that was never declared.
|
|
72
|
+
# - FAILS OPEN. Any error (no jq, no Target, judge failure) -> exit 0.
|
|
73
|
+
# - EVIDENCE-CHECKED. A BLOCK must quote a span that is literally present in the
|
|
74
|
+
# payload; a citation that is not there is a fabricated block and is DISCARDED.
|
|
75
|
+
# - LOOP-SAFE. A re-entry cap: never deny an identical tool_input twice.
|
|
76
|
+
#
|
|
77
|
+
# INPUT : Claude Code PreToolUse hook JSON on stdin.
|
|
78
|
+
# OUTPUT : on a purview breach -> a deny decision on stdout + exit 0.
|
|
79
|
+
# otherwise -> no stdout + exit 0.
|
|
80
|
+
#
|
|
81
|
+
# POSIX sh. Depends on: jq. Missing jq -> fail open.
|
|
82
|
+
|
|
83
|
+
set -eu
|
|
84
|
+
|
|
85
|
+
# The sibling Stop hook's deployed dir owns the SHARED judge backend, resolved from
|
|
86
|
+
# this script's own location exactly as the pre-hook resolves it — every hooks root
|
|
87
|
+
# holds the hook dirs as siblings, so this derivation is true at every site this file
|
|
88
|
+
# can land.
|
|
89
|
+
SELF_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
|
90
|
+
HOOKS_ROOT="$(dirname -- "$SELF_DIR")"
|
|
91
|
+
JUDGE_DIR="\${STANCE_GUARD_DIR:-$HOOKS_ROOT/stance-guardrail}"
|
|
92
|
+
NEUTRAL_ROOT="$(dirname -- "$(dirname -- "$HOOKS_ROOT")")/.agents"
|
|
93
|
+
RUBRIC="\${PURVIEW_RUBRIC:-$NEUTRAL_ROOT/purview-guardrail/purview-judge-prompt.md}"
|
|
94
|
+
JUDGE_CMD="\${STANCE_JUDGE_CMD:-sh $JUDGE_DIR/stance-judge.sh}"
|
|
95
|
+
LOG="\${PURVIEW_GUARD_LOG:-$SELF_DIR/misses.log}"
|
|
96
|
+
|
|
97
|
+
trap 'exit 0' EXIT
|
|
98
|
+
|
|
99
|
+
allow() { exit 0; }
|
|
100
|
+
note() { printf '%s\\n' "$1" >> "$LOG" 2>/dev/null || true; }
|
|
101
|
+
|
|
102
|
+
input="$(cat)"
|
|
103
|
+
[ -n "$input" ] || allow
|
|
104
|
+
command -v jq >/dev/null 2>&1 || allow
|
|
105
|
+
|
|
106
|
+
cwd="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null || true)"
|
|
107
|
+
[ -n "$cwd" ] && cd "$cwd" 2>/dev/null || true
|
|
108
|
+
|
|
109
|
+
# --- scope gate: the persona's OWN stance manifest -------------------------------------------
|
|
110
|
+
stance_scope="$(printf '%s' "$input" | jq -r '.stance_scope // empty' 2>/dev/null || true)"
|
|
111
|
+
[ -n "$stance_scope" ] || allow
|
|
112
|
+
manifest="$stance_scope/stance/manifest.json"
|
|
113
|
+
[ -f "$manifest" ] || allow
|
|
114
|
+
agent_type="$(jq -r '.agent // empty' "$manifest" 2>/dev/null || true)"
|
|
115
|
+
[ -n "$agent_type" ] || allow
|
|
116
|
+
|
|
117
|
+
# --- the LAW: this persona's own projected role contract -------------------------------------
|
|
118
|
+
# A persona scope is \`<root>/agent/personas/<name>\` and the Target it was projected
|
|
119
|
+
# from is \`<root>/agent/agents/<name>.md\` — two levels out and back down, the same
|
|
120
|
+
# shape of derivation the judge path above uses. Overridable for test rigs.
|
|
121
|
+
AGENT_ROOT="$(dirname -- "$(dirname -- "$stance_scope")")"
|
|
122
|
+
AGENT_MD="\${PURVIEW_AGENT_MD:-$AGENT_ROOT/agents/$agent_type.md}"
|
|
123
|
+
[ -f "$AGENT_MD" ] || allow
|
|
124
|
+
contract="$(awk '/^## Role$/{f=1;next} /^## /{f=0} f' "$AGENT_MD" 2>/dev/null || true)"
|
|
125
|
+
# A contract that is a BARE TOKEN states no arrow, so there is nothing to be outside
|
|
126
|
+
# of. Only a multi-line contract is scoreable, and saying so here is what keeps the
|
|
127
|
+
# gate from inventing a law for a corpus that has not declared one.
|
|
128
|
+
[ "$(printf '%s\\n' "$contract" | grep -c '[^[:space:]]')" -ge 2 ] || allow
|
|
129
|
+
|
|
130
|
+
# --- extract the judged payload, branched by act ---------------------------------------------
|
|
131
|
+
tool_name="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null || true)"
|
|
132
|
+
[ -n "$tool_name" ] || allow
|
|
133
|
+
|
|
134
|
+
case "$tool_name" in
|
|
135
|
+
Agent|SendMessage|Task)
|
|
136
|
+
body="$(printf '%s' "$input" | jq -r '.tool_input.prompt // .tool_input.message // .tool_input.description // ""' 2>/dev/null || true)"
|
|
137
|
+
target="$(printf '%s' "$input" | jq -r '.tool_input.subagent_type // .tool_input.agent // .tool_input.name // ""' 2>/dev/null || true)"
|
|
138
|
+
act="DISPATCH to \\\`\${target:-unnamed}\\\` (codomain: the spec this delegate will build from)"
|
|
139
|
+
;;
|
|
140
|
+
Write|Edit|MultiEdit|NotebookEdit)
|
|
141
|
+
body="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.path // .tool_input.notebook_path // ""' 2>/dev/null || true)"
|
|
142
|
+
act="WRITE to the substrate (codomain: artifact)"
|
|
143
|
+
;;
|
|
144
|
+
*)
|
|
145
|
+
# DEFENCE IN DEPTH. \`tool.use.pre\` fires on every tool on a harness with no
|
|
146
|
+
# subject selector; these two classes are the only acts this contract can
|
|
147
|
+
# convict, and a read is deliberately not one of them.
|
|
148
|
+
allow ;;
|
|
149
|
+
esac
|
|
150
|
+
|
|
151
|
+
[ -n "\${body:-}" ] || allow
|
|
152
|
+
|
|
153
|
+
payload="=== THE HOLDER'S DECLARED CONTRACT (agent: $agent_type) ===
|
|
154
|
+
$contract
|
|
155
|
+
=== THE ACT ABOUT TO FIRE ===
|
|
156
|
+
$act
|
|
157
|
+
$body"
|
|
158
|
+
|
|
159
|
+
# --- loop-safety: never deny an identical tool_input twice ----------------------------------
|
|
160
|
+
session_id="$(printf '%s' "$input" | jq -r '.session_id // "nosession"' 2>/dev/null || echo nosession)"
|
|
161
|
+
sig="$(printf '%s' "$input" | jq -c '.tool_input' 2>/dev/null | cksum | cut -d' ' -f1 2>/dev/null || echo 0)"
|
|
162
|
+
seen="\${TMPDIR:-/tmp}/.purview-pre-$session_id-$sig"
|
|
163
|
+
[ -f "$seen" ] && allow
|
|
164
|
+
|
|
165
|
+
# --- judge (SHARED backend, OWN rubric) -----------------------------------------------------
|
|
166
|
+
if [ -n "\${PURVIEW_EMIT_PAYLOAD:-}" ]; then
|
|
167
|
+
jq -cn --arg r "$RUBRIC" --arg p "$payload" '{rubric:$r, payload:$p}'
|
|
168
|
+
exit 0
|
|
169
|
+
fi
|
|
170
|
+
if [ -n "\${PURVIEW_VERDICT_FILE:-}" ]; then
|
|
171
|
+
verdict="$(cat "\${PURVIEW_VERDICT_FILE}" 2>/dev/null || true)"
|
|
172
|
+
[ -n "$verdict" ] || {
|
|
173
|
+
note "$(date -u +%Y-%m-%dT%H:%M:%SZ) judge-empty tool=$tool_name agent=$agent_type"
|
|
174
|
+
allow
|
|
175
|
+
}
|
|
176
|
+
else
|
|
177
|
+
verdict="$(printf '%s' "$payload" | $JUDGE_CMD "$RUBRIC" 2>/dev/null)" || {
|
|
178
|
+
note "$(date -u +%Y-%m-%dT%H:%M:%SZ) judge-fail tool=$tool_name agent=$agent_type"
|
|
179
|
+
allow
|
|
180
|
+
}
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
decision="$(printf '%s\\n' "$verdict" | sed -n 's/^VERDICT:[[:space:]]*//p' | head -1)"
|
|
184
|
+
[ "$decision" = "BLOCK" ] || allow
|
|
185
|
+
|
|
186
|
+
reason="$(printf '%s\\n' "$verdict" | sed -n 's/^REASON:[[:space:]]*//p' | head -1)"
|
|
187
|
+
[ -n "$reason" ] || reason="This act falls outside the arrow your role contract declares."
|
|
188
|
+
|
|
189
|
+
# --- EVIDENCE CHECK: the cited span must be literally in the payload -------------------------
|
|
190
|
+
# A block naming a span the payload does not contain is a hallucinated block, and a
|
|
191
|
+
# fabricated refusal is not a lesser error than a missed one. Same rule, same reason as
|
|
192
|
+
# the turn-end sibling.
|
|
193
|
+
cited="$(printf '%s\\n' "$verdict" | sed -n 's/^SPAN:[[:space:]]*//p' | head -1)"
|
|
194
|
+
if [ -n "$cited" ]; then
|
|
195
|
+
printf '%s' "$payload" | grep -qF -- "$cited" || {
|
|
196
|
+
note "$(date -u +%Y-%m-%dT%H:%M:%SZ) evidence-fail tool=$tool_name agent=$agent_type span=$cited"
|
|
197
|
+
allow
|
|
198
|
+
}
|
|
199
|
+
fi
|
|
200
|
+
|
|
201
|
+
: > "$seen" 2>/dev/null || true
|
|
202
|
+
|
|
203
|
+
feedback="PURVIEW GUARDRAIL — denied this $tool_name call: it falls outside the arrow your own \\
|
|
204
|
+
role contract declares. $reason Delegation is a THEOREM of that arrow, not an option: an act whose \\
|
|
205
|
+
domain or codomain lies outside it goes to the role that owns it — a decomposition to a planner, a \\
|
|
206
|
+
build to an implementer, an artifact reading to an assayer. Hand it over and carry on with what the \\
|
|
207
|
+
contract reserves. (Legitimate and NOT blocked here: reading anything, dispatching to a planner or an \\
|
|
208
|
+
assayer, and performing any act the contract's own \\\`reserves\\\` clause names.)"
|
|
209
|
+
|
|
210
|
+
jq -cn --arg r "$feedback" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}'
|
|
211
|
+
exit 0
|
|
212
|
+
`,
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
filename: 'purview-judge-prompt.md',
|
|
216
|
+
targetPath: 'packages/canon/targets/guardrail/purview-judge-prompt.md',
|
|
217
|
+
executable: false,
|
|
218
|
+
// NEUTRAL, exactly as the stance rubric is, and for a stronger reason here: this
|
|
219
|
+
// rubric carries no corpus content at all. It is a procedure for scoring a
|
|
220
|
+
// contract supplied at run time, so it is the same bytes for any corpus that
|
|
221
|
+
// declares a role arrow — which is what makes the neutral root its right home
|
|
222
|
+
// rather than a convenience.
|
|
223
|
+
shared: true,
|
|
224
|
+
content: `# Purview judge — does this act fall outside the agent's declared arrow?
|
|
225
|
+
|
|
226
|
+
You are a PURVIEW JUDGE. You are given two things:
|
|
227
|
+
|
|
228
|
+
1. **THE HOLDER'S DECLARED CONTRACT** — the \`## Role\` section of the agent's own definition,
|
|
229
|
+
verbatim. It states an arrow of the form \`reads⟨…⟩ → writes⟨…⟩\` over a ladder of
|
|
230
|
+
representational layers, the acts the arrow **reserves**, and often a named defect.
|
|
231
|
+
2. **THE ACT ABOUT TO FIRE** — either a DISPATCH (the delegate prompt) or a WRITE (the path).
|
|
232
|
+
|
|
233
|
+
Your one job: decide whether the act falls **outside** that arrow. You judge conformance to the
|
|
234
|
+
contract you were handed. You do **not** judge whether the contract is wise, whether the act is a
|
|
235
|
+
good idea, or whether the code is any good.
|
|
236
|
+
|
|
237
|
+
## The law is the contract, not your opinion
|
|
238
|
+
|
|
239
|
+
Every rule you apply must be readable **in the supplied contract**. If the contract does not exclude
|
|
240
|
+
the act, the act passes — however unwise it looks to you. An agent is entitled to the whole of what
|
|
241
|
+
its arrow permits, and a gate that convicts on an unwritten rule is worse than no gate, because it
|
|
242
|
+
teaches the agent to distrust a law it can read.
|
|
243
|
+
|
|
244
|
+
Read the arrow first, then the \`reserves\` clause, then the rest.
|
|
245
|
+
|
|
246
|
+
## How to read an arrow
|
|
247
|
+
|
|
248
|
+
\`reads⟨A · B⟩ → writes⟨C⟩\` means: the agent may consume layers A and B, and may produce layer C.
|
|
249
|
+
The ladder runs \`intent ≺ C ≺ spec ≺ artifact\` from most abstract to least, where \`C\` is the
|
|
250
|
+
durative concept lattice — the design.
|
|
251
|
+
|
|
252
|
+
An act has a **domain** (the layer it consumes) and a **codomain** (the layer it produces):
|
|
253
|
+
|
|
254
|
+
- a DISPATCH produces a **spec** — the delegate's instructions are the layer being written;
|
|
255
|
+
- a WRITE to a source or config file produces an **artifact**;
|
|
256
|
+
- reading anything at all produces **nothing**, and is never a breach.
|
|
257
|
+
|
|
258
|
+
An act is **outside the arrow** when its codomain is not in the contract's \`writes\` set. That is the
|
|
259
|
+
whole test, and it is mechanical.
|
|
260
|
+
|
|
261
|
+
## What to BLOCK
|
|
262
|
+
|
|
263
|
+
- **A write whose codomain is \`artifact\` by a holder whose \`writes\` set excludes \`artifact\`.**
|
|
264
|
+
The contract usually names this: \`descent\`. The path in the payload is the evidence.
|
|
265
|
+
- **A dispatch whose codomain is \`spec\` by a holder whose \`writes\` set excludes \`spec\`.** This is
|
|
266
|
+
the skipped rung: a holder that writes only the design has handed a delegate a spec that no
|
|
267
|
+
spec-writing role produced. The prompt is the evidence.
|
|
268
|
+
- **A dispatch whose prompt is the operator's literal words rather than a piece the agent cut.** The
|
|
269
|
+
contract that convicts this is the same clause: a dispatch is a spec, and transcription is not
|
|
270
|
+
authorship. Evidence is the prompt reading as relayed instructions rather than a bounded piece
|
|
271
|
+
with its own acceptance.
|
|
272
|
+
|
|
273
|
+
## What to PASS — and these are the majority
|
|
274
|
+
|
|
275
|
+
- **Any read.** Reading is outside the test by construction. Even where a contract calls
|
|
276
|
+
artifact-reading a descent, that is a standing discipline, not a mid-turn refusal.
|
|
277
|
+
- **A dispatch to a role the contract explicitly routes to.** Contracts name their delegations
|
|
278
|
+
(\`⟨C → spec⟩ ↦ plan\`, \`⟨artifact → C⟩ ↦ assay\`). Handing work to a named delegate is the arrow
|
|
279
|
+
WORKING.
|
|
280
|
+
- **Any act the \`reserves\` clause names**, whatever it is.
|
|
281
|
+
- **A write by a holder whose \`writes\` set includes \`artifact\`.** Most agents in most corpora
|
|
282
|
+
build. Do not read a contract's other clauses as narrowing an arrow that plainly permits the act.
|
|
283
|
+
- **Anything you are unsure about.** Fail toward PASS. A missed breach costs one wrong act; a
|
|
284
|
+
fabricated block costs the agent's trust in a law it can read for itself, and it wedges work.
|
|
285
|
+
|
|
286
|
+
## Output
|
|
287
|
+
|
|
288
|
+
Emit **only** this block, nothing before or after:
|
|
289
|
+
|
|
290
|
+
\`\`\`
|
|
291
|
+
VERDICT: BLOCK|PASS
|
|
292
|
+
REASON: <one sentence naming the clause of the contract the act falls outside, and the codomain that put it there>
|
|
293
|
+
SPAN: <a short literal substring copied from the payload that shows the act — omit this line entirely when the verdict is PASS>
|
|
294
|
+
\`\`\`
|
|
295
|
+
|
|
296
|
+
\`SPAN\` is checked against the payload character for character. If you cannot copy a literal span out
|
|
297
|
+
of the payload, you do not have the evidence for a BLOCK, and the verdict is PASS.
|
|
298
|
+
`,
|
|
299
|
+
},
|
|
300
|
+
],
|
|
301
|
+
};
|
|
302
|
+
//# sourceMappingURL=purview-guardrail.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"purview-guardrail.js","sourceRoot":"","sources":["../../src/hooks/purview-guardrail.ts"],"names":[],"mappings":"AAEA,qEAAqE;AACrE,EAAE;AACF,oEAAoE;AACpE,oFAAoF;AACpF,mFAAmF;AACnF,kFAAkF;AAClF,qBAAqB;AACrB,EAAE;AACF,iFAAiF;AACjF,uFAAuF;AACvF,uFAAuF;AACvF,uFAAuF;AACvF,mFAAmF;AACnF,wFAAwF;AACxF,EAAE;AACF,sFAAsF;AACtF,oFAAoF;AACpF,uFAAuF;AACvF,uFAAuF;AACvF,uFAAuF;AACvF,+EAA+E;AAC/E,sFAAsF;AACtF,yEAAyE;AACzE,EAAE;AACF,+DAA+D;AAC/D,qFAAqF;AACrF,0EAA0E;AAC1E,gFAAgF;AAChF,sFAAsF;AACtF,kFAAkF;AAClF,0EAA0E;AAC1E,uFAAuF;AACvF,sFAAsF;AACtF,oFAAoF;AACpF,8DAA8D;AAC9D,EAAE;AACF,sFAAsF;AACtF,wFAAwF;AACxF,mFAAmF;AACnF,2BAA2B;AAE3B,MAAM,CAAC,MAAM,gBAAgB,GAAa;IACxC,EAAE,EAAE,mBAAmB;IACvB,OAAO,EACL,iaAAia;IACna,SAAS,EAAE,SAAS;IACpB,KAAK,EAAE,CAAC;IACR,MAAM,EAAE,CAAC,uBAAuB,EAAE,cAAc,CAAC;IACjD,KAAK,EAAE,sBAAsB;IAC7B,OAAO,EAAE,EAAE;IACX,IAAI,EAAE,EAAE;IACR,OAAO,EAAE;QACP;YACE,QAAQ,EAAE,sBAAsB;YAChC,UAAU,EAAE,uDAAuD;YACnE,UAAU,EAAE,IAAI;YAChB,OAAO,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6Jd;SACI;QACD;YACE,QAAQ,EAAE,yBAAyB;YACnC,UAAU,EAAE,0DAA0D;YACtE,UAAU,EAAE,KAAK;YACjB,iFAAiF;YACjF,2EAA2E;YAC3E,6EAA6E;YAC7E,8EAA8E;YAC9E,6BAA6B;YAC7B,MAAM,EAAE,IAAI;YACZ,OAAO,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0Ed;SACI;KACF;CACF,CAAC"}
|
|
@@ -46,9 +46,9 @@ export const stanceGuardrailPre = {
|
|
|
46
46
|
# ambiguity, a substantive intent-extracted dispatch.
|
|
47
47
|
#
|
|
48
48
|
# SAFETY MODEL (mirrors stance-guardrail):
|
|
49
|
-
# -
|
|
50
|
-
#
|
|
51
|
-
#
|
|
49
|
+
# - SCOPE-ENROLLED, exactly as its twin: a scope carrying a stance manifest is judged and
|
|
50
|
+
# every other scope is silent. No repo opt-in, no allowlist — the projection that places a
|
|
51
|
+
# persona also enrolls it, and declining the guard means declining the persona.
|
|
52
52
|
# - FAILS OPEN. Any error (no jq, judge failure, bad input) -> exit 0 (allow the call).
|
|
53
53
|
# - LOOP-SAFE. A re-entry cap: never deny an identical tool_input twice (there is no
|
|
54
54
|
# stop_hook_active analog for PreToolUse), so it can never wedge a call.
|
|
@@ -98,27 +98,22 @@ input="$(cat)"
|
|
|
98
98
|
[ -n "$input" ] || allow
|
|
99
99
|
command -v jq >/dev/null 2>&1 || allow # no jq -> cannot parse -> fail open
|
|
100
100
|
|
|
101
|
-
#
|
|
102
|
-
#
|
|
101
|
+
# The per-repo opt-in is GONE, for the reason its twin states: it asked whether a guard may run in
|
|
102
|
+
# a DIRECTORY, and a stance belongs to the agent, not the checkout. \`cwd\` survives it — the worker
|
|
103
|
+
# still runs where the session runs.
|
|
103
104
|
cwd="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null || true)"
|
|
104
105
|
[ -n "$cwd" ] && cd "$cwd" 2>/dev/null || true
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
#
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
in_scope=false
|
|
117
|
-
for a in $allowlist; do
|
|
118
|
-
[ "$a" = "$agent_type" ] && in_scope=true && break
|
|
119
|
-
done
|
|
120
|
-
[ "$in_scope" = true ] || allow
|
|
121
|
-
fi
|
|
106
|
+
|
|
107
|
+
# --- scope gate: the persona's OWN stance manifest -------------------------------------------
|
|
108
|
+
# The twin of the turn-end guard's gate, and inverted for the same reason: an allowlist here was
|
|
109
|
+
# a runtime self-filter over an enrollment the corpus already derives, and it had drifted the way
|
|
110
|
+
# such a list always drifts. Presence of a manifest in this persona's own scope IS enrollment.
|
|
111
|
+
# Absent → silence, never an error, so an unenrolled launch is untouched.
|
|
112
|
+
stance_scope="$(printf '%s' "$input" | jq -r '.stance_scope // empty' 2>/dev/null || true)"
|
|
113
|
+
[ -n "$stance_scope" ] || allow
|
|
114
|
+
manifest="$stance_scope/stance/manifest.json"
|
|
115
|
+
[ -f "$manifest" ] || allow
|
|
116
|
+
agent_type="$(jq -r '.agent // empty' "$manifest" 2>/dev/null || true)"
|
|
122
117
|
|
|
123
118
|
# --- extract the judged payload, branched by tool -------------------------------------------
|
|
124
119
|
tool_name="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null || true)"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stance-guardrail-pre.js","sourceRoot":"","sources":["../../src/hooks/stance-guardrail-pre.ts"],"names":[],"mappings":"AAEA,sFAAsF;AACtF,mFAAmF;AACnF,uFAAuF;AACvF,SAAS;AACT,wFAAwF;AACxF,6FAA6F;AAC7F,0FAA0F;AAC1F,uFAAuF;AACvF,qEAAqE;AACrE,EAAE;AACF,wEAAwE;AACxE,sFAAsF;AACtF,iFAAiF;AACjF,mFAAmF;AACnF,sFAAsF;AACtF,qFAAqF;AACrF,sFAAsF;AACtF,sFAAsF;AACtF,mEAAmE;AACnE,2DAA2D;AAC3D,mDAAmD;AAEnD,MAAM,CAAC,MAAM,kBAAkB,GAAa;IAC1C,EAAE,EAAE,sBAAsB;IAC1B,OAAO,EACL,iWAAiW;IACnW,SAAS,EAAE,SAAS;IACpB,KAAK,EAAE,CAAC;IACR,MAAM,EAAE,CAAC,sBAAsB,EAAE,uBAAuB,CAAC;IACzD,KAAK,EAAE,yBAAyB;IAChC,OAAO,EAAE,EAAE;IACX,IAAI,EAAE,EAAE;IACR,OAAO,EAAE;QACP;YACE,QAAQ,EAAE,yBAAyB;YACnC,UAAU,EAAE,0DAA0D;YACtE,UAAU,EAAE,IAAI;YAChB,OAAO,EAAE
|
|
1
|
+
{"version":3,"file":"stance-guardrail-pre.js","sourceRoot":"","sources":["../../src/hooks/stance-guardrail-pre.ts"],"names":[],"mappings":"AAEA,sFAAsF;AACtF,mFAAmF;AACnF,uFAAuF;AACvF,SAAS;AACT,wFAAwF;AACxF,6FAA6F;AAC7F,0FAA0F;AAC1F,uFAAuF;AACvF,qEAAqE;AACrE,EAAE;AACF,wEAAwE;AACxE,sFAAsF;AACtF,iFAAiF;AACjF,mFAAmF;AACnF,sFAAsF;AACtF,qFAAqF;AACrF,sFAAsF;AACtF,sFAAsF;AACtF,mEAAmE;AACnE,2DAA2D;AAC3D,mDAAmD;AAEnD,MAAM,CAAC,MAAM,kBAAkB,GAAa;IAC1C,EAAE,EAAE,sBAAsB;IAC1B,OAAO,EACL,iWAAiW;IACnW,SAAS,EAAE,SAAS;IACpB,KAAK,EAAE,CAAC;IACR,MAAM,EAAE,CAAC,sBAAsB,EAAE,uBAAuB,CAAC;IACzD,KAAK,EAAE,yBAAyB;IAChC,OAAO,EAAE,EAAE;IACX,IAAI,EAAE,EAAE;IACR,OAAO,EAAE;QACP;YACE,QAAQ,EAAE,yBAAyB;YACnC,UAAU,EAAE,0DAA0D;YACtE,UAAU,EAAE,IAAI;YAChB,OAAO,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4Jd;SACI;KACF;CACF,CAAC"}
|
|
@@ -49,8 +49,13 @@ export const stanceGuardrail = {
|
|
|
49
49
|
# electing the session's objective is supplying intent, not sequencing — the operator's to own
|
|
50
50
|
#
|
|
51
51
|
# SAFETY MODEL:
|
|
52
|
-
# -
|
|
53
|
-
#
|
|
52
|
+
# - SCOPE-ENROLLED. Fires for a scope carrying a stance manifest, and for no other. Enrollment
|
|
53
|
+
# is PRESENCE: the projection places a manifest in each persona's own scope, so composing the
|
|
54
|
+
# cell enrolls the persona and nothing central lists anybody. A bare launch carries the
|
|
55
|
+
# dispatcher and no manifest, and is therefore silent by placement.
|
|
56
|
+
# - NO REPO OPT-IN. There was one, per-repo in .git/config, and it asked the wrong question:
|
|
57
|
+
# whether a guard may run in a DIRECTORY. The off switch is launching \`omp\` rather than a
|
|
58
|
+
# persona — declining to BE the agent, instead of being it unjudged.
|
|
54
59
|
# - FAILS OPEN, BUT NEVER SILENTLY-CLEAN. Any error → exit 0 (allow stop): a guardrail that
|
|
55
60
|
# wedges work on its own flakiness is worse than a missed block. But once the guard is
|
|
56
61
|
# ENABLED and in scope, a failure that prevents judging (no transcript, judge unreachable)
|
|
@@ -85,6 +90,9 @@ JUDGE_CMD="\${STANCE_JUDGE_CMD:-sh $SELF_DIR/stance-judge.sh}"
|
|
|
85
90
|
# a host and point outside a sandboxed deploy, which is the class of bug that put
|
|
86
91
|
# \`\$HOME/.claude\` in the sibling worker and made it read another harness's tree.
|
|
87
92
|
NEUTRAL_ROOT="$(dirname -- "$(dirname -- "$(dirname -- "$SELF_DIR")")")/.agents"
|
|
93
|
+
# The DEFAULT rubric. A persona's own manifest may name another — that is the
|
|
94
|
+
# strategy seam: one opinionated implementation, each persona free to bind a
|
|
95
|
+
# different contract without the dispatcher learning anything about either.
|
|
88
96
|
RUBRIC="\${STANCE_RUBRIC:-$NEUTRAL_ROOT/stance-guardrail/stance-judge-prompt.md}"
|
|
89
97
|
|
|
90
98
|
# A Stop hook must never break a session. Trap any unexpected error → allow the stop.
|
|
@@ -102,6 +110,15 @@ allow_stop() { exit 0; } # emit nothing; the agent is permitted to stop.
|
|
|
102
110
|
# turn. Reached only AFTER the opt-in and allowlist checks, so a repo that never enabled
|
|
103
111
|
# the guard stays silent.
|
|
104
112
|
dark() {
|
|
113
|
+
# THE HEARTBEAT, and the reason the log is worth keeping. A VERDICT row and a
|
|
114
|
+
# DARK row are both evidence the guard RAN; only an EMPTY log means it never
|
|
115
|
+
# did. Recording judged turns alone made "not judged" and "no session" read
|
|
116
|
+
# identically — measured on a live host: an endpoint behind the advisor role
|
|
117
|
+
# accepted connections and never answered, and five hours of one session were
|
|
118
|
+
# unjudged while leaving no trace whatsoever to notice it by.
|
|
119
|
+
if [ -n "\${verdict_log:-}" ]; then
|
|
120
|
+
printf 'DARK\\t\\t%s\\n' "\$1" >> "\$verdict_log" 2>/dev/null || true
|
|
121
|
+
fi
|
|
105
122
|
printf 'STANCE GUARDRAIL — DARK: %s. This turn was NOT judged; the absence of a block is an absence of a verdict, not a clean one.\\n' "\$1"
|
|
106
123
|
exit 0
|
|
107
124
|
}
|
|
@@ -138,32 +155,55 @@ verdict_log="$state_dir/$session.verdicts"
|
|
|
138
155
|
block_count="$(cat "$count_file" 2>/dev/null || echo 0)"
|
|
139
156
|
case "$block_count" in *[!0-9]*) block_count=0 ;; esac
|
|
140
157
|
|
|
141
|
-
#
|
|
142
|
-
#
|
|
143
|
-
#
|
|
158
|
+
# The per-repo opt-in is GONE, and its absence is the point. It answered "may a guard run in this
|
|
159
|
+
# DIRECTORY", which is a category error: a stance is a property of the agent, not of the checkout
|
|
160
|
+
# it happens to be standing in. Worse, it made the stance OPTIONAL AT RUNTIME for an agent already
|
|
161
|
+
# launched as itself — the ambient form, when the whole reason this harness half exists is that
|
|
162
|
+
# identity alone erodes. The off switch is launching \`omp\` instead of \`mav\`: declining to be the
|
|
163
|
+
# persona, rather than being it unjudged.
|
|
144
164
|
cwd="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null || true)"
|
|
145
165
|
[ -n "$cwd" ] && cd "$cwd" 2>/dev/null || true
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
#
|
|
150
|
-
#
|
|
151
|
-
#
|
|
152
|
-
#
|
|
153
|
-
#
|
|
154
|
-
#
|
|
155
|
-
#
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
166
|
+
|
|
167
|
+
# --- scope gate: the persona's OWN stance manifest -------------------------------------------
|
|
168
|
+
# COMPOSITION IS THE SCOPE, NOT A RUNTIME SELF-FILTER — MODEL.md's ENFORCED clause, and this
|
|
169
|
+
# gate used to violate it outright. It read an allowlist, \`nico mav\` by default and overridable
|
|
170
|
+
# by \`STANCE_GUARD_AGENTS\`, which is precisely the runtime self-filter that clause forbids: a
|
|
171
|
+
# module asking WHO IS RUNNING rather than being placed where only the right launch reaches it.
|
|
172
|
+
#
|
|
173
|
+
# It had already drifted, in the direction such a list always drifts. Every agent in the corpus
|
|
174
|
+
# composes \`handoff\`, so \`Binding.agents\` DERIVES all six as bound, and six persona scopes are
|
|
175
|
+
# deployed to prove it — while the list enforced two. \`architect\` and \`kino\` held principal
|
|
176
|
+
# authority and were never once judged, and nothing surfaced the gap because the two facts were
|
|
177
|
+
# written in different languages in different files.
|
|
178
|
+
#
|
|
179
|
+
# Enrollment is now PRESENCE. Each bound persona's projection lands a manifest in that persona's
|
|
180
|
+
# own scope, and the dispatcher — which lives in that same scope and therefore already knows it
|
|
181
|
+
# — passes it in the envelope. A manifest means enrolled, and carries THIS agent's contract: its
|
|
182
|
+
# rubric, its moments, its handoff laws. No manifest means not enrolled, which is silence rather
|
|
183
|
+
# than an error, so a plain session that never declared itself anything stays untouched exactly
|
|
184
|
+
# as it was. Adding a persona enrolls it; adding a gated dimension edits one persona's manifest;
|
|
185
|
+
# neither is a line in this file.
|
|
186
|
+
stance_scope="$(printf '%s' "$input" | jq -r '.stance_scope // empty' 2>/dev/null || true)"
|
|
187
|
+
[ -n "$stance_scope" ] || allow_stop
|
|
188
|
+
manifest="$stance_scope/stance/manifest.json"
|
|
189
|
+
[ -f "$manifest" ] || allow_stop
|
|
190
|
+
|
|
191
|
+
# The persona's own contract, each field falling back to the shipped default. A manifest that
|
|
192
|
+
# names nothing still enrolls: presence is the assertion, the fields are the refinement.
|
|
193
|
+
agent_type="$(jq -r '.agent // empty' "$manifest" 2>/dev/null || true)"
|
|
194
|
+
manifest_rubric="$(jq -r '.rubric // empty' "$manifest" 2>/dev/null || true)"
|
|
195
|
+
# An OVERRIDE that does not resolve is a defect and goes DARK — a persona that asked to be judged
|
|
196
|
+
# against its own contract and is silently judged against someone else's has been mis-scored, not
|
|
197
|
+
# spared. The DEFAULT is not checked here: a judge backend need not read the file at all (the
|
|
198
|
+
# fixture judge does not), and refusing to run because a path the manifest never named is absent
|
|
199
|
+
# would fail closed on a case the manifest made no claim about.
|
|
200
|
+
case "$manifest_rubric" in
|
|
201
|
+
'') ;;
|
|
202
|
+
/*) RUBRIC="$manifest_rubric" ;;
|
|
203
|
+
*) RUBRIC="$stance_scope/stance/$manifest_rubric" ;;
|
|
204
|
+
esac
|
|
205
|
+
[ -z "$manifest_rubric" ] || [ -f "$RUBRIC" ] || \\
|
|
206
|
+
dark "the rubric named by $manifest is not readable at '\$RUBRIC'"
|
|
167
207
|
|
|
168
208
|
# --- extract the last assistant turn from the transcript ------------------------------------
|
|
169
209
|
transcript="$(printf '%s' "$input" | jq -r '.transcript_path // empty' 2>/dev/null || true)"
|
|
@@ -500,7 +540,24 @@ if [ -n "$evidence" ] && [ "\${#evidence}" -ge 12 ]; then
|
|
|
500
540
|
# 3/5 on identical payloads — i.e. the record of WHY a turn was blocked was reconstructible only
|
|
501
541
|
# by a non-deterministic process. Appended, never rotated by this hook; the state dir is tmp.
|
|
502
542
|
printf '%s\\t%s\\t%s\\n' "$decision" "$evidence" "$reason" >> "$verdict_log" 2>/dev/null || true
|
|
503
|
-
|
|
543
|
+
# NORMALIZED ON BOTH SIDES, and with \`--\`, because this check had two ways to
|
|
544
|
+
# throw away a correct block — and both of them fired on the SAME shape, which is
|
|
545
|
+
# the one shape this guard most exists to catch.
|
|
546
|
+
#
|
|
547
|
+
# 1. \`grep -qF "$evidence"\` with no \`--\`: an evidence span opening with a markdown
|
|
548
|
+
# bullet is read as an OPTION. \`grep: invalid option -- ' '\`, non-zero, block
|
|
549
|
+
# discarded. A tail-enumeration collapse IS a bullet list, so its evidence always
|
|
550
|
+
# begins \`- \`, and the rule could never convict the shape it was written for.
|
|
551
|
+
# 2. A judge quoting several bullets joins them with spaces and drops the markers,
|
|
552
|
+
# so a verbatim-substring test fails on punctuation while every word is present.
|
|
553
|
+
#
|
|
554
|
+
# The proposition worth authenticating is that the judge quoted THIS TURN'S WORDS,
|
|
555
|
+
# not that it reproduced its list syntax. So both sides are flattened the same way:
|
|
556
|
+
# newlines to spaces, runs of whitespace to one, list markers dropped. Symmetric, so
|
|
557
|
+
# nothing is accepted on one side that would be rejected on the other.
|
|
558
|
+
ev_norm="$(printf '%s' "$evidence" | tr '\\n' ' ' | sed -e 's/[[:space:]][[:space:]]*/ /g' -e 's/^[-*][[:space:]]//' -e 's/ [-*] / /g')"
|
|
559
|
+
close_norm="$(printf '%s' "$asst_close" | tr '\\n' ' ' | sed -e 's/[[:space:]][[:space:]]*/ /g' -e 's/^[-*][[:space:]]//' -e 's/ [-*] / /g')"
|
|
560
|
+
if ! printf '%s' "$close_norm" | grep -qF -- "$ev_norm"; then
|
|
504
561
|
printf 'stance-guardrail: DISCARDING block — judge quoted a span that is not in the turn'"'"'s CLOSE (mid-turn preamble, or confabulated): %s\\n' "$evidence" >&2
|
|
505
562
|
allow_stop
|
|
506
563
|
fi
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stance-guardrail.js","sourceRoot":"","sources":["../../src/hooks/stance-guardrail.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,mCAAmC,CAAC;AAG5D,wEAAwE;AACxE,8EAA8E;AAC9E,2EAA2E;AAC3E,2EAA2E;AAC3E,8EAA8E;AAC9E,gFAAgF;AAEhF,MAAM,CAAC,MAAM,eAAe,GAAa;IACvC,EAAE,EAAE,kBAAkB;IACtB,OAAO,EACL,8UAA8U;IAChV,SAAS,EAAE,SAAS;IACpB,KAAK,EAAE,CAAC;IACR,MAAM,EAAE,CAAC,UAAU,EAAE,cAAc,CAAC;IACpC,KAAK,EAAE,qBAAqB;IAC5B,OAAO,EAAE,EAAE;IACX,IAAI,EAAE,EAAE;IACR,OAAO,EAAE;QACP;YACE,QAAQ,EAAE,qBAAqB;YAC/B,UAAU,EAAE,sDAAsD;YAClE,UAAU,EAAE,IAAI;YAChB,OAAO,EAAE
|
|
1
|
+
{"version":3,"file":"stance-guardrail.js","sourceRoot":"","sources":["../../src/hooks/stance-guardrail.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,mCAAmC,CAAC;AAG5D,wEAAwE;AACxE,8EAA8E;AAC9E,2EAA2E;AAC3E,2EAA2E;AAC3E,8EAA8E;AAC9E,gFAAgF;AAEhF,MAAM,CAAC,MAAM,eAAe,GAAa;IACvC,EAAE,EAAE,kBAAkB;IACtB,OAAO,EACL,8UAA8U;IAChV,SAAS,EAAE,SAAS;IACpB,KAAK,EAAE,CAAC;IACR,MAAM,EAAE,CAAC,UAAU,EAAE,cAAc,CAAC;IACpC,KAAK,EAAE,qBAAqB;IAC5B,OAAO,EAAE,EAAE;IACX,IAAI,EAAE,EAAE;IACR,OAAO,EAAE;QACP;YACE,QAAQ,EAAE,qBAAqB;YAC/B,UAAU,EAAE,sDAAsD;YAClE,UAAU,EAAE,IAAI;YAChB,OAAO,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwnBd;SACI;QACD;YACE,QAAQ,EAAE,iBAAiB;YAC3B,UAAU,EAAE,kDAAkD;YAC9D,UAAU,EAAE,IAAI;YAChB,OAAO,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmFd;SACI;QACD;YACE,QAAQ,EAAE,wBAAwB;YAClC,UAAU,EAAE,yDAAyD;YACrE,UAAU,EAAE,KAAK;YACjB,yEAAyE;YACzE,sEAAsE;YACtE,0EAA0E;YAC1E,2EAA2E;YAC3E,yEAAyE;YACzE,yEAAyE;YACzE,MAAM,EAAE,IAAI;YACZ,OAAO,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAsNX,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyLV;SACI;KACF;CACF,CAAC"}
|
package/dist/plan-states.d.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/** The plan-layout state folders, in task-file lifecycle order. */
|
|
2
2
|
export declare const PLAN_STATES: readonly ["pending", "ready", "active", "completed"];
|
|
3
3
|
/** `frontier(P)` — the states a shard sits in when the plan is workable AT it
|
|
4
|
-
* (
|
|
4
|
+
* (`{ t | state(t) ∈ { ready, active } }`, where the plan IS, not merely
|
|
5
5
|
* what is dispatchable). A subset of {@link PLAN_STATES} and not derivable from
|
|
6
6
|
* their ORDER: `pending` is open too, and is not frontier. */
|
|
7
7
|
export declare const PLAN_FRONTIER: readonly ["ready", "active"];
|
|
8
8
|
/**
|
|
9
9
|
* The plan-tier dotfile MARKERS whose only home is here.
|
|
10
10
|
*
|
|
11
|
-
* A relation with no on-disk carrier is not readable — the reading `
|
|
11
|
+
* A relation with no on-disk carrier is not readable — the reading `plan-set.sh`
|
|
12
12
|
* states in its own words when it explains why `.landed` exists ("exactly parallel
|
|
13
13
|
* to `.superseded-by`"). These are the two markers a reader of the plan set needs
|
|
14
14
|
* that have no other TypeScript home: `.superseded-by` already has one
|
|
@@ -16,7 +16,7 @@ export declare const PLAN_FRONTIER: readonly ["ready", "active"];
|
|
|
16
16
|
*
|
|
17
17
|
* `.ruling-owed` is the third carrier this corpus has minted on that reading, and
|
|
18
18
|
* it carries the concept the corpus ALREADY signifies as a ruling owed — "a
|
|
19
|
-
* decision nobody has taken", which the
|
|
19
|
+
* decision nobody has taken", which the retired praxis cell distinguished from a
|
|
20
20
|
* dependency ("a dep waits itself out, a ruling needs a human"). At the SHARD tier
|
|
21
21
|
* that concept is `ruling-owed(t)`, and it gates `ready`; at the
|
|
22
22
|
* PLAN tier it had no carrier at all, so a fork the principal cannot resolve —
|
package/dist/plan-states.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
/** The plan-layout state folders, in task-file lifecycle order. */
|
|
13
13
|
export const PLAN_STATES = ['pending', 'ready', 'active', 'completed'];
|
|
14
14
|
/** `frontier(P)` — the states a shard sits in when the plan is workable AT it
|
|
15
|
-
* (
|
|
15
|
+
* (`{ t | state(t) ∈ { ready, active } }`, where the plan IS, not merely
|
|
16
16
|
* what is dispatchable). A subset of {@link PLAN_STATES} and not derivable from
|
|
17
17
|
* their ORDER: `pending` is open too, and is not frontier. */
|
|
18
18
|
export const PLAN_FRONTIER = [
|
|
@@ -22,7 +22,7 @@ export const PLAN_FRONTIER = [
|
|
|
22
22
|
/**
|
|
23
23
|
* The plan-tier dotfile MARKERS whose only home is here.
|
|
24
24
|
*
|
|
25
|
-
* A relation with no on-disk carrier is not readable — the reading `
|
|
25
|
+
* A relation with no on-disk carrier is not readable — the reading `plan-set.sh`
|
|
26
26
|
* states in its own words when it explains why `.landed` exists ("exactly parallel
|
|
27
27
|
* to `.superseded-by`"). These are the two markers a reader of the plan set needs
|
|
28
28
|
* that have no other TypeScript home: `.superseded-by` already has one
|
|
@@ -30,7 +30,7 @@ export const PLAN_FRONTIER = [
|
|
|
30
30
|
*
|
|
31
31
|
* `.ruling-owed` is the third carrier this corpus has minted on that reading, and
|
|
32
32
|
* it carries the concept the corpus ALREADY signifies as a ruling owed — "a
|
|
33
|
-
* decision nobody has taken", which the
|
|
33
|
+
* decision nobody has taken", which the retired praxis cell distinguished from a
|
|
34
34
|
* dependency ("a dep waits itself out, a ruling needs a human"). At the SHARD tier
|
|
35
35
|
* that concept is `ruling-owed(t)`, and it gates `ready`; at the
|
|
36
36
|
* PLAN tier it had no carrier at all, so a fork the principal cannot resolve —
|