@cratylus/canon 0.8.0 → 0.9.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/README.md +6 -4
- package/dist/agents/architect.js +2 -2
- package/dist/agents/architect.js.map +1 -1
- package/dist/agents/assayer.js +10 -10
- package/dist/agents/assayer.js.map +1 -1
- package/dist/agents/implementer.js +3 -3
- package/dist/agents/implementer.js.map +1 -1
- package/dist/agents/integrator.d.ts +2 -0
- package/dist/agents/integrator.js +18 -0
- package/dist/agents/integrator.js.map +1 -0
- package/dist/agents/kino.js +5 -4
- package/dist/agents/kino.js.map +1 -1
- package/dist/agents/mav.js +19 -37
- package/dist/agents/mav.js.map +1 -1
- package/dist/agents/nico.js +13 -31
- package/dist/agents/nico.js.map +1 -1
- package/dist/agents/planner.js +5 -5
- package/dist/agents/planner.js.map +1 -1
- package/dist/dimensions/actions/delegation.js +6 -1
- package/dist/dimensions/actions/delegation.js.map +1 -1
- package/dist/dimensions/engineering-principles/cold-decode-oracle.js +1 -1
- package/dist/dimensions/engineering-principles/cold-decode-oracle.js.map +1 -1
- package/dist/dimensions/engineering-principles/trust-but-verify.js +1 -1
- package/dist/dimensions/engineering-principles/trust-but-verify.js.map +1 -1
- package/dist/dimensions/objective/conceptual-integrity.d.ts +2 -0
- package/dist/dimensions/objective/conceptual-integrity.js +2 -0
- package/dist/dimensions/objective/conceptual-integrity.js.map +1 -0
- package/dist/dimensions/role/architect.js +5 -3
- package/dist/dimensions/role/architect.js.map +1 -1
- package/dist/dimensions/role/assayer.js +24 -13
- package/dist/dimensions/role/assayer.js.map +1 -1
- package/dist/dimensions/role/implementer.js +21 -4
- package/dist/dimensions/role/implementer.js.map +1 -1
- package/dist/dimensions/role/integrator.d.ts +2 -0
- package/dist/dimensions/role/integrator.js +27 -0
- package/dist/dimensions/role/integrator.js.map +1 -0
- package/dist/dimensions/role/planner.js +19 -7
- package/dist/dimensions/role/planner.js.map +1 -1
- package/dist/genus/prime-principle.d.ts +10 -0
- package/dist/genus/prime-principle.js +16 -0
- package/dist/genus/prime-principle.js.map +1 -0
- package/dist/hooks/deploy-drift-notice.js +9 -9
- package/dist/hooks/deploy-drift-notice.js.map +1 -1
- package/dist/hooks/purview-guardrail.js +134 -57
- package/dist/hooks/purview-guardrail.js.map +1 -1
- package/dist/hooks/stance-guardrail-pre.js +105 -42
- package/dist/hooks/stance-guardrail-pre.js.map +1 -1
- package/dist/hooks/stance-guardrail.js +161 -57
- package/dist/hooks/stance-guardrail.js.map +1 -1
- package/dist/index.js +1 -4
- package/dist/index.js.map +1 -1
- package/dist/manifest.d.ts +3 -2
- package/dist/manifest.js +6 -6
- package/dist/manifest.js.map +1 -1
- package/dist/roles/architect.js +27 -22
- package/dist/roles/architect.js.map +1 -1
- package/dist/roles/assayer.js +9 -7
- package/dist/roles/assayer.js.map +1 -1
- package/dist/roles/hold.js +1 -1
- package/dist/roles/hold.js.map +1 -1
- package/dist/roles/implementer.js +15 -5
- package/dist/roles/implementer.js.map +1 -1
- package/dist/roles/integrator.d.ts +2 -0
- package/dist/roles/integrator.js +51 -0
- package/dist/roles/integrator.js.map +1 -0
- package/dist/roles/planner.js +6 -5
- package/dist/roles/planner.js.map +1 -1
- package/dist/skills/create-skill/skill.js +2 -0
- package/dist/skills/create-skill/skill.js.map +1 -1
- package/dist/skills/deliver/skill.js +59 -130
- package/dist/skills/deliver/skill.js.map +1 -1
- package/dist/skills/design/skill.js +20 -10
- package/dist/skills/design/skill.js.map +1 -1
- package/dist/skills/exemplify/skill.js +2 -0
- package/dist/skills/exemplify/skill.js.map +1 -1
- package/dist/skills/formalize/skill.js +2 -0
- package/dist/skills/formalize/skill.js.map +1 -1
- package/dist/skills/introspect/skill.js +2 -0
- package/dist/skills/introspect/skill.js.map +1 -1
- package/dist/skills/materialize/skill.js +2 -0
- package/dist/skills/materialize/skill.js.map +1 -1
- package/dist/skills/plan/skill.js +30 -32
- package/dist/skills/plan/skill.js.map +1 -1
- package/dist/skills/probe/skill.js +2 -0
- package/dist/skills/probe/skill.js.map +1 -1
- package/dist/skills/signify/skill.js +2 -0
- package/dist/skills/signify/skill.js.map +1 -1
- package/package.json +4 -4
- package/dist/genus/founding-doctrine.d.ts +0 -7
- package/dist/genus/founding-doctrine.js +0 -13
- package/dist/genus/founding-doctrine.js.map +0 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { anchorOf } from '@cratylus/schema';
|
|
2
|
+
import { handoff } from '../dimensions/autonomy/handoff.js';
|
|
1
3
|
// stance-guardrail-pre — the BEFORE-THE-CALL twin of `stance-guardrail` (the turn-end
|
|
2
4
|
// cell). It STRUCTURALLY REFUSES, BEFORE the call fires, a mid-turn tool call that
|
|
3
5
|
// collapses out of the intent-driven-expert stance. Two acts, and the acts are what it
|
|
@@ -10,19 +12,23 @@
|
|
|
10
12
|
//
|
|
11
13
|
// IT BINDS TWO ACTS, NOT ONE EVENT PLUS A REGEX. This cell used to read
|
|
12
14
|
// `events: ['tool.use.pre']` + `matcher: 'AskUserQuestion|Agent|SendMessage'` — three
|
|
13
|
-
// claude tool names on a harness-agnostic shape, which
|
|
14
|
-
// without a word, so the hook there fired on EVERY tool
|
|
15
|
-
// own `*) allow ;;` branch kept it correct. The two acts
|
|
16
|
-
// the residue (`permission-menu` · `dispatch-echo`);
|
|
17
|
-
// to the adapter, which alone knows which of its
|
|
18
|
-
// computes its own ⟨native event, native
|
|
19
|
-
// but not narrow it now SAYS SO through
|
|
15
|
+
// claude tool names on a harness-agnostic shape, which an adapter that could not
|
|
16
|
+
// narrow by tool name dropped without a word, so the hook there fired on EVERY tool
|
|
17
|
+
// call and only the worker's own `*) allow ;;` branch kept it correct. The two acts
|
|
18
|
+
// below were already latent in the residue (`permission-menu` · `dispatch-echo`);
|
|
19
|
+
// naming them moves the narrowing to the adapter, which alone knows which of its
|
|
20
|
+
// tools performs the act. Each adapter computes its own ⟨native event, native
|
|
21
|
+
// selector⟩ pair, and one that can fire an act but not narrow it now SAYS SO through
|
|
22
|
+
// the projection's warnings.
|
|
20
23
|
// `test/hook-rule-boundary.test.ts` byte-locks the worker;
|
|
21
24
|
// `test/hook-act-selector.test.ts` holds the seam.
|
|
22
25
|
export const stanceGuardrailPre = {
|
|
23
26
|
id: 'stance-guardrail-pre',
|
|
24
27
|
residue: 'structural-refusal ↾ mid-turn tool-call · deny-before-fire ⟨intent-driven-expert-collapse⟩ ⟨permission-menu · dispatch-echo ⟨literal-transcription ∄ extracted-intent⟩⟩ · pass ⟨reserved · irreversible-outward-consent · substantive-dispatch · intent-ambiguity ↦ elicit⟩ · shared judge-backend ⟨sibling⟩ · loop-safe ⟨re-entry-cap : ¬deny identical twice⟩',
|
|
25
28
|
substrate: 'harness',
|
|
29
|
+
// Bound by the same composition as its turn-end twin: the stance it enforces is the
|
|
30
|
+
// one `handoff` declares.
|
|
31
|
+
binds: { dimension: 'autonomy', value: anchorOf(handoff) },
|
|
26
32
|
order: 1,
|
|
27
33
|
events: ['operator.consult.pre', 'subagent.dispatch.pre'],
|
|
28
34
|
entry: 'stance-guardrail-pre.sh',
|
|
@@ -49,22 +55,85 @@ export const stanceGuardrailPre = {
|
|
|
49
55
|
# - SCOPE-ENROLLED, exactly as its twin: a scope carrying a stance manifest is judged and
|
|
50
56
|
# every other scope is silent. No repo opt-in, no allowlist — the projection that places a
|
|
51
57
|
# persona also enrolls it, and declining the guard means declining the persona.
|
|
52
|
-
# - FAILS OPEN. Any error (no jq, judge failure,
|
|
58
|
+
# - FAILS OPEN, NEVER SILENTLY. Any error (no jq, no input, judge failure, unparseable verdict,
|
|
59
|
+
# unexpected error) -> exit 0 (allow the call), and the call goes through WITH A NOTICE the
|
|
60
|
+
# operator reads (\`say\`: Claude Code's JSON \`systemMessage\`, omp's relayed line) naming this
|
|
61
|
+
# guard and why it could not judge. Silence is reserved for "judged, pass" and "not enrolled".
|
|
53
62
|
# - LOOP-SAFE. A re-entry cap: never deny an identical tool_input twice (there is no
|
|
54
|
-
# stop_hook_active analog for PreToolUse), so it can never wedge a call.
|
|
55
|
-
#
|
|
63
|
+
# stop_hook_active analog for PreToolUse), so it can never wedge a call. The second,
|
|
64
|
+
# identical call goes through unjudged, and says so.
|
|
65
|
+
# - OBSERVABLE. Every notice is also a line in the miss log.
|
|
56
66
|
#
|
|
57
67
|
# INPUT : Claude Code PreToolUse hook JSON on stdin (tool_name, tool_input, agent_type
|
|
58
68
|
# [subagents], session_id, cwd, ...).
|
|
59
69
|
# OUTPUT : on collapse -> stdout
|
|
60
70
|
# {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny",
|
|
61
71
|
# "permissionDecisionReason":"..."}} + exit 0.
|
|
62
|
-
#
|
|
72
|
+
# judged, pass -> no stdout + exit 0 (allow the call).
|
|
73
|
+
# let through with no verdict -> the notice \`say\` prints + exit 0 (allow the call).
|
|
63
74
|
#
|
|
64
|
-
# POSIX sh. Depends on: jq. Missing jq ->
|
|
75
|
+
# POSIX sh. Depends on: jq. Missing jq -> the guard says so and allows the call.
|
|
65
76
|
|
|
66
77
|
set -eu
|
|
67
78
|
|
|
79
|
+
# WHAT AN OPERATOR READS WHEN THE GUARD CANNOT JUDGE, defined before anything that needs a
|
|
80
|
+
# command: the first thing that can be missing is \`jq\`, and a worker run with a PATH holding
|
|
81
|
+
# little beyond \`sh\` and \`cat\` must still say it did not judge, so \`esc\`, \`say\` and \`open\`
|
|
82
|
+
# are builtins only and nothing external runs above the jq check.
|
|
83
|
+
#
|
|
84
|
+
# Claude Code shows the operator a hook's JSON \`systemMessage\` and NOT its plain stdout on
|
|
85
|
+
# exit 0. omp's extension bridge reads the worker's plain stdout and relays a line naming this
|
|
86
|
+
# guard and DARK to the session, and would relay a JSON object as raw JSON — hence one form per
|
|
87
|
+
# harness, chosen at projection by the harness's own name.
|
|
88
|
+
esc() {
|
|
89
|
+
_in="$1"
|
|
90
|
+
_out=""
|
|
91
|
+
_nl="$(printf '\\nx')"
|
|
92
|
+
_nl="\${_nl%x}"
|
|
93
|
+
_tab="$(printf '\\t')"
|
|
94
|
+
_cr="$(printf '\\r')"
|
|
95
|
+
while [ -n "$_in" ]; do
|
|
96
|
+
_rest="\${_in#?}"
|
|
97
|
+
_c="\${_in%"$_rest"}"
|
|
98
|
+
case "$_c" in
|
|
99
|
+
'\\') _c='\\\\' ;;
|
|
100
|
+
'"') _c='\\"' ;;
|
|
101
|
+
"$_nl") _c='\\n' ;;
|
|
102
|
+
"$_tab") _c='\\t' ;;
|
|
103
|
+
"$_cr") _c='\\r' ;;
|
|
104
|
+
esac
|
|
105
|
+
_out="$_out$_c"
|
|
106
|
+
_in="$_rest"
|
|
107
|
+
done
|
|
108
|
+
printf '%s' "$_out"
|
|
109
|
+
}
|
|
110
|
+
say() {
|
|
111
|
+
case "{{fact:harness-name}}" in
|
|
112
|
+
claude) printf '{"systemMessage":"%s"}\\n' "$(esc "$1")" ;;
|
|
113
|
+
*) printf '%s\\n' "$1" ;;
|
|
114
|
+
esac
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
allow() { exit 0; } # emit nothing; the tool call proceeds.
|
|
118
|
+
|
|
119
|
+
# A verdict and a failure are different facts and silence carries only one of them: \`allow\`
|
|
120
|
+
# means "judged, pass" or "not this guard's call", \`open\` means "could not judge" — the call
|
|
121
|
+
# still proceeds, and the operator is told.
|
|
122
|
+
open() {
|
|
123
|
+
[ -z "\${LOG:-}" ] || printf '%s\\n' "open: $1 tool=\${tool_name:-?} agent=\${agent_type:-?}" >> "$LOG" 2>/dev/null || true
|
|
124
|
+
say "STANCE GUARDRAIL (pre) — DARK: $1. This call was NOT judged; the absence of a deny is an absence of a verdict, not a clean one."
|
|
125
|
+
exit 0
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# A PreToolUse hook must never break a session. An unexpected error (a nonzero status reaching
|
|
129
|
+
# the trap) is let through AND SAID; every deliberate \`exit 0\` arrives with status 0.
|
|
130
|
+
trap 'rc=$?; [ "$rc" -eq 0 ] || open "an unexpected error stopped it (exit status $rc)"; exit 0' EXIT
|
|
131
|
+
|
|
132
|
+
# --- read hook input ------------------------------------------------------------------------
|
|
133
|
+
input="$(cat)"
|
|
134
|
+
[ -n "$input" ] || open "the hook received no input"
|
|
135
|
+
command -v jq >/dev/null 2>&1 || open "jq is not installed, so the hook payload cannot be read"
|
|
136
|
+
|
|
68
137
|
# The sibling Stop hook's deployed dir owns the SHARED judge + rubric (deployed
|
|
69
138
|
# together, as siblings under the SAME hooks root).
|
|
70
139
|
#
|
|
@@ -87,17 +156,6 @@ RUBRIC="\${STANCE_RUBRIC:-$NEUTRAL_ROOT/stance-guardrail/stance-judge-prompt.md}
|
|
|
87
156
|
JUDGE_CMD="\${STANCE_JUDGE_CMD:-sh $JUDGE_DIR/stance-judge.sh}"
|
|
88
157
|
LOG="\${STANCE_GUARD_LOG:-$SELF_DIR/misses.log}"
|
|
89
158
|
|
|
90
|
-
# A PreToolUse hook must never break a session. Trap any unexpected error -> allow.
|
|
91
|
-
trap 'exit 0' EXIT
|
|
92
|
-
|
|
93
|
-
allow() { exit 0; } # emit nothing; the tool call proceeds.
|
|
94
|
-
note() { printf '%s\\n' "$1" >> "$LOG" 2>/dev/null || true; }
|
|
95
|
-
|
|
96
|
-
# --- read hook input ------------------------------------------------------------------------
|
|
97
|
-
input="$(cat)"
|
|
98
|
-
[ -n "$input" ] || allow
|
|
99
|
-
command -v jq >/dev/null 2>&1 || allow # no jq -> cannot parse -> fail open
|
|
100
|
-
|
|
101
159
|
# The per-repo opt-in is GONE, for the reason its twin states: it asked whether a guard may run in
|
|
102
160
|
# a DIRECTORY, and a stance belongs to the agent, not the checkout. \`cwd\` survives it — the worker
|
|
103
161
|
# still runs where the session runs.
|
|
@@ -108,16 +166,23 @@ cwd="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null || true)"
|
|
|
108
166
|
# The twin of the turn-end guard's gate, and inverted for the same reason: an allowlist here was
|
|
109
167
|
# a runtime self-filter over an enrollment the corpus already derives, and it had drifted the way
|
|
110
168
|
# 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.
|
|
169
|
+
# Absent → silence, never an error, so an unenrolled launch is untouched. The scope is the one
|
|
170
|
+
# the harness hands over: \`stance_scope\` where a dispatcher sits in the persona's own scope (omp),
|
|
171
|
+
# else the persona directory of the agent the payload NAMES (\`agent_type\`, Claude Code — none on a
|
|
172
|
+
# bare session) under this harness's home, one hop above the hooks root. No agent list here.
|
|
112
173
|
stance_scope="$(printf '%s' "$input" | jq -r '.stance_scope // empty' 2>/dev/null || true)"
|
|
113
|
-
[ -
|
|
114
|
-
|
|
174
|
+
if [ -z "$stance_scope" ]; then
|
|
175
|
+
named="$(printf '%s' "$input" | jq -r '.agent_type // empty' 2>/dev/null || true)"
|
|
176
|
+
case "$named" in '' | */* | . | ..) allow ;; esac
|
|
177
|
+
stance_scope="$(dirname -- "$HOOKS_ROOT")/{{fact:harness-persona-root}}/$named"
|
|
178
|
+
fi
|
|
179
|
+
manifest="$stance_scope/{{fact:stance-manifest}}"
|
|
115
180
|
[ -f "$manifest" ] || allow
|
|
116
181
|
agent_type="$(jq -r '.agent // empty' "$manifest" 2>/dev/null || true)"
|
|
117
182
|
|
|
118
183
|
# --- extract the judged payload, branched by tool -------------------------------------------
|
|
119
184
|
tool_name="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null || true)"
|
|
120
|
-
[ -n "$tool_name" ] ||
|
|
185
|
+
[ -n "$tool_name" ] || open "the payload names no tool, so there is no call to judge"
|
|
121
186
|
|
|
122
187
|
case "$tool_name" in
|
|
123
188
|
AskUserQuestion)
|
|
@@ -134,10 +199,10 @@ case "$tool_name" in
|
|
|
134
199
|
body="$(printf '%s' "$input" | jq -r '.tool_input.prompt // .tool_input.message // .tool_input.description // ""' 2>/dev/null || true)"
|
|
135
200
|
payload="$tool_name dispatch (the delegate prompt/message): $body" ;;
|
|
136
201
|
*)
|
|
137
|
-
allow ;; # DEFENCE IN DEPTH
|
|
138
|
-
#
|
|
139
|
-
#
|
|
140
|
-
#
|
|
202
|
+
allow ;; # DEFENCE IN DEPTH: every supported adapter computes a selector from the
|
|
203
|
+
# act, so this never fires there. A harness that could fire the act but
|
|
204
|
+
# not narrow it would WARN in projection, and this branch is what would
|
|
205
|
+
# keep the guard off every other tool call.
|
|
141
206
|
esac
|
|
142
207
|
|
|
143
208
|
[ -n "\${body:-}" ] || allow # nothing judgeable -> allow
|
|
@@ -147,7 +212,7 @@ esac
|
|
|
147
212
|
session_id="$(printf '%s' "$input" | jq -r '.session_id // "nosession"' 2>/dev/null || echo nosession)"
|
|
148
213
|
sig="$(printf '%s' "$input" | jq -c '.tool_input' 2>/dev/null | cksum | cut -d' ' -f1 2>/dev/null || echo 0)"
|
|
149
214
|
seen="\${TMPDIR:-/tmp}/.stance-pre-$session_id-$sig"
|
|
150
|
-
[ -f "$seen" ] &&
|
|
215
|
+
[ -f "$seen" ] && open "this exact call was already denied once, so the re-entry cap lets it through unjudged"
|
|
151
216
|
|
|
152
217
|
# --- judge (SHARED backend + rubric) --------------------------------------------------------
|
|
153
218
|
# The same out-of-process seam the Stop worker carries, and for the same reason: a
|
|
@@ -159,19 +224,17 @@ if [ -n "\${STANCE_EMIT_PAYLOAD:-}" ]; then
|
|
|
159
224
|
fi
|
|
160
225
|
if [ -n "\${STANCE_VERDICT_FILE:-}" ]; then
|
|
161
226
|
verdict="$(cat "\${STANCE_VERDICT_FILE}" 2>/dev/null || true)"
|
|
162
|
-
[ -n "$verdict" ] ||
|
|
163
|
-
note "$(date -u +%Y-%m-%dT%H:%M:%SZ) judge-empty tool=$tool_name agent=\${agent_type:-?}"
|
|
164
|
-
allow
|
|
165
|
-
}
|
|
227
|
+
[ -n "$verdict" ] || open "the judge did not answer"
|
|
166
228
|
else
|
|
167
|
-
verdict="$(printf '%s' "$payload" | $JUDGE_CMD "$RUBRIC" 2>/dev/null)" ||
|
|
168
|
-
note "$(date -u +%Y-%m-%dT%H:%M:%SZ) judge-fail tool=$tool_name agent=\${agent_type:-?}"
|
|
169
|
-
allow
|
|
170
|
-
}
|
|
229
|
+
verdict="$(printf '%s' "$payload" | $JUDGE_CMD "$RUBRIC" 2>/dev/null)" || open "the judge did not answer"
|
|
171
230
|
fi
|
|
172
231
|
|
|
173
|
-
decision="$(printf '%s\\n' "$verdict" | sed -n 's/^VERDICT:[[:space:]]*//p' | head -1)"
|
|
174
|
-
|
|
232
|
+
decision="$(printf '%s\\n' "$verdict" | sed -n 's/^VERDICT:[[:space:]]*//p' | head -1 | sed 's/[[:space:]]*$//')"
|
|
233
|
+
case "$decision" in
|
|
234
|
+
PASS) allow ;; # judged, pass: the one silent verdict
|
|
235
|
+
BLOCK) ;;
|
|
236
|
+
*) open "the judge's verdict was unparseable (no VERDICT: PASS or VERDICT: BLOCK line)" ;;
|
|
237
|
+
esac
|
|
175
238
|
|
|
176
239
|
reason="$(printf '%s\\n' "$verdict" | sed -n 's/^REASON:[[:space:]]*//p' | head -1)"
|
|
177
240
|
[ -n "$reason" ] || reason="This tool call collapsed out of the intent-driven-expert stance."
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stance-guardrail-pre.js","sourceRoot":"","sources":["../../src/hooks/stance-guardrail-pre.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"stance-guardrail-pre.js","sourceRoot":"","sources":["../../src/hooks/stance-guardrail-pre.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,mCAAmC,CAAC;AAG5D,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,oFAAoF;AACpF,oFAAoF;AACpF,kFAAkF;AAClF,iFAAiF;AACjF,8EAA8E;AAC9E,qFAAqF;AACrF,6BAA6B;AAC7B,2DAA2D;AAC3D,mDAAmD;AAEnD,MAAM,CAAC,MAAM,kBAAkB,GAAa;IAC1C,EAAE,EAAE,sBAAsB;IAC1B,OAAO,EACL,iWAAiW;IACnW,SAAS,EAAE,SAAS;IACpB,oFAAoF;IACpF,0BAA0B;IAC1B,KAAK,EAAE,EAAE,SAAS,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,EAAE;IAC1D,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqNd;SACI;KACF;CACF,CAAC"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { anchorOf } from '@cratylus/schema';
|
|
1
2
|
import { handoff } from '../dimensions/autonomy/handoff.js';
|
|
2
3
|
// stance-guardrail — the harness-half of the principal stance. A source
|
|
3
4
|
// `hook` cell (activation=event): its canonical DEFINIENS is the σ*-signified
|
|
@@ -9,6 +10,9 @@ export const stanceGuardrail = {
|
|
|
9
10
|
id: 'stance-guardrail',
|
|
10
11
|
residue: 'structural-refusal ↾ turn-end · block ⟨intent-driven-expert-collapse⟩ ⟨permission-seeking · own-judgment-deferral · order-taking⟩ · pass ⟨reserved · irreversible-outward ↦ consent · intent-ambiguity ↦ elicit · ∄ mandate ↦ surface ⟨electing the objective ∉ in-remit⟩⟩ · harness-invariant ⟨prompt-identity erodes ↾ RLHF-corrigibility⟩',
|
|
11
12
|
substrate: 'harness',
|
|
13
|
+
// The composition that binds an agent: it carries `handoff`, whose laws the rubric
|
|
14
|
+
// judges. Composing it enrolls the persona; nothing else does.
|
|
15
|
+
binds: { dimension: 'autonomy', value: anchorOf(handoff) },
|
|
12
16
|
order: 0,
|
|
13
17
|
events: ['turn.end', 'subagent.end'],
|
|
14
18
|
entry: 'stance-guardrail.sh',
|
|
@@ -57,12 +61,16 @@ export const stanceGuardrail = {
|
|
|
57
61
|
# whether a guard may run in a DIRECTORY. The off switch is launching \`omp\` rather than a
|
|
58
62
|
# persona — declining to BE the agent, instead of being it unjudged.
|
|
59
63
|
# - FAILS OPEN, BUT NEVER SILENTLY-CLEAN. Any error → exit 0 (allow stop): a guardrail that
|
|
60
|
-
# wedges work on its own flakiness is worse than a missed block. But
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
64
|
+
# wedges work on its own flakiness is worse than a missed block. But a turn let through
|
|
65
|
+
# WITHOUT A VERDICT says so where the operator reads it (\`say\`: Claude Code's JSON
|
|
66
|
+
# \`systemMessage\`, omp's relayed line), naming this guard and why it could not judge —
|
|
67
|
+
# no jq, no input, an unreadable transcript or rubric, a judge that does not answer or
|
|
68
|
+
# answers unparseably, a block it could not verify, state it cannot write, an unexpected
|
|
69
|
+
# error, and the two re-entry caps (no-progress, spent bypass). Silence is reserved for
|
|
70
|
+
# "judged, no collapse" and for "not enrolled" (no persona manifest in scope, or nothing
|
|
71
|
+
# judgeable in the turn): there, not-checking is the correct answer, not a failure to report.
|
|
72
|
+
# - LOOP-SAFE. Judges every turn and bounds the BLOCKS (a no-progress detector and a
|
|
73
|
+
# one-shot, self-resetting bypass), so it can never wedge a turn.
|
|
66
74
|
# - POSITION-SOUND. Every rubric rule that can fire is a claim about the turn's CLOSE. So the
|
|
67
75
|
# L1 window and the EVIDENCE check both run against \`asst_close\` — the text AFTER the last
|
|
68
76
|
# tool call — never the whole-turn blob. A span from a mid-turn preamble is out of scope for
|
|
@@ -71,32 +79,56 @@ export const stanceGuardrail = {
|
|
|
71
79
|
# INPUT : Claude Code Stop/SubagentStop hook JSON on stdin (transcript_path, stop_hook_active,
|
|
72
80
|
# agent_type [SubagentStop], session_id, cwd, …).
|
|
73
81
|
# OUTPUT : on collapse → stdout \`{"decision":"block","reason":"…"}\` + exit 0 (Stop-hook block).
|
|
74
|
-
#
|
|
82
|
+
# judged, no collapse → no stdout + exit 0 (allow stop).
|
|
83
|
+
# let through with no verdict → the notice \`say\` prints + exit 0 (allow stop).
|
|
75
84
|
#
|
|
76
|
-
# POSIX sh. Depends on: jq (transcript parse). Missing jq →
|
|
85
|
+
# POSIX sh. Depends on: jq (transcript parse). Missing jq → the guard says so and allows the stop.
|
|
77
86
|
|
|
78
87
|
set -eu
|
|
79
88
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
#
|
|
84
|
-
# follows whatever home this copy was deployed under (a real \$HOME, or a test's
|
|
85
|
-
# \`--home\`). This worker sits at \`<harness-home>/hooks/<id>/\`, and \`.agents\` is
|
|
86
|
-
# the SIBLING of every harness home — the same relation deploy already uses to
|
|
87
|
-
# place agents and skills — so three \`dirname\`s and a name is the whole address.
|
|
89
|
+
# WHAT AN OPERATOR READS WHEN THE GUARD CANNOT JUDGE, defined before anything that needs a
|
|
90
|
+
# command. The first thing that can be missing is \`jq\`, and a worker run with a PATH holding
|
|
91
|
+
# little beyond \`sh\` and \`cat\` must still say it did not judge — so \`esc\`, \`say\` and \`dark\`
|
|
92
|
+
# are builtins only, and nothing external runs above the jq check.
|
|
88
93
|
#
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
|
|
93
|
-
#
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
# Claude Code shows the operator a hook's JSON \`systemMessage\` and NOT its plain stdout on
|
|
95
|
+
# exit 0, so a notice printed as a bare line reaches nobody there. omp's extension bridge
|
|
96
|
+
# reads the worker's plain stdout and relays a line naming this guard and DARK to the
|
|
97
|
+
# session, and would relay a JSON object as raw JSON — hence one form per harness, chosen at
|
|
98
|
+
# projection by the harness's own name.
|
|
99
|
+
esc() {
|
|
100
|
+
_in="$1"
|
|
101
|
+
_out=""
|
|
102
|
+
_nl="$(printf '\\nx')"
|
|
103
|
+
_nl="\${_nl%x}"
|
|
104
|
+
_tab="$(printf '\\t')"
|
|
105
|
+
_cr="$(printf '\\r')"
|
|
106
|
+
while [ -n "$_in" ]; do
|
|
107
|
+
_rest="\${_in#?}"
|
|
108
|
+
_c="\${_in%"$_rest"}"
|
|
109
|
+
case "$_c" in
|
|
110
|
+
'\\') _c='\\\\' ;;
|
|
111
|
+
'"') _c='\\"' ;;
|
|
112
|
+
"$_nl") _c='\\n' ;;
|
|
113
|
+
"$_tab") _c='\\t' ;;
|
|
114
|
+
"$_cr") _c='\\r' ;;
|
|
115
|
+
esac
|
|
116
|
+
_out="$_out$_c"
|
|
117
|
+
_in="$_rest"
|
|
118
|
+
done
|
|
119
|
+
printf '%s' "$_out"
|
|
120
|
+
}
|
|
121
|
+
say() {
|
|
122
|
+
case "{{fact:harness-name}}" in
|
|
123
|
+
claude) printf '{"systemMessage":"%s"}\\n' "$(esc "$1")" ;;
|
|
124
|
+
*) printf '%s\\n' "$1" ;;
|
|
125
|
+
esac
|
|
126
|
+
}
|
|
97
127
|
|
|
98
|
-
# A Stop hook must never break a session.
|
|
99
|
-
trap
|
|
128
|
+
# A Stop hook must never break a session. An unexpected error (a nonzero status reaching the
|
|
129
|
+
# trap) is let through AND SAID; every deliberate \`exit 0\` arrives here with status 0 and stays
|
|
130
|
+
# as quiet as it chose to be.
|
|
131
|
+
trap 'rc=$?; [ -z "\${view:-}" ] || rm -f "$view" 2>/dev/null; [ "$rc" -eq 0 ] || dark "an unexpected error stopped it (exit status $rc)"; exit 0' EXIT
|
|
100
132
|
|
|
101
133
|
allow_stop() { exit 0; } # emit nothing; the agent is permitted to stop.
|
|
102
134
|
|
|
@@ -107,8 +139,8 @@ allow_stop() { exit 0; } # emit nothing; the agent is permitted to stop.
|
|
|
107
139
|
# defect fixed one file over in the memory nudge ("a broken runtime read as a clean bill
|
|
108
140
|
# of health, silently and forever"); the inversion is the same — \`if signal absent then
|
|
109
141
|
# pass\` becomes \`if signal absent then SAY SO\` — and, as there, it still never wedges the
|
|
110
|
-
# turn. Reached only
|
|
111
|
-
#
|
|
142
|
+
# turn. Reached only for a guard that is IN SCOPE and could not judge: a scope carrying no
|
|
143
|
+
# persona manifest stays silent.
|
|
112
144
|
dark() {
|
|
113
145
|
# THE HEARTBEAT, and the reason the log is worth keeping. A VERDICT row and a
|
|
114
146
|
# DARK row are both evidence the guard RAN; only an EMPTY log means it never
|
|
@@ -117,17 +149,35 @@ dark() {
|
|
|
117
149
|
# accepted connections and never answered, and five hours of one session were
|
|
118
150
|
# unjudged while leaving no trace whatsoever to notice it by.
|
|
119
151
|
if [ -n "\${verdict_log:-}" ]; then
|
|
120
|
-
printf 'DARK\\t\\t%s\\n' "
|
|
152
|
+
printf 'DARK\\t\\t%s\\n' "$1" >> "$verdict_log" 2>/dev/null || true
|
|
121
153
|
fi
|
|
122
|
-
|
|
154
|
+
say "STANCE GUARDRAIL — DARK: $1. This turn was NOT judged; the absence of a block is an absence of a verdict, not a clean one."
|
|
123
155
|
exit 0
|
|
124
156
|
}
|
|
125
157
|
|
|
126
158
|
# --- read hook input ------------------------------------------------------------------------
|
|
127
159
|
input="$(cat)"
|
|
128
|
-
[ -n "$input" ] ||
|
|
160
|
+
[ -n "$input" ] || dark "the hook received no input"
|
|
161
|
+
|
|
162
|
+
command -v jq >/dev/null 2>&1 || dark "jq is not installed, so the hook payload cannot be read"
|
|
163
|
+
|
|
164
|
+
SELF_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
|
165
|
+
JUDGE_CMD="\${STANCE_JUDGE_CMD:-sh $SELF_DIR/stance-judge.sh}"
|
|
129
166
|
|
|
130
|
-
|
|
167
|
+
# THE RUBRIC LIVES AT THE VENDOR-NEUTRAL ROOT, and is reached by DERIVATION so it
|
|
168
|
+
# follows whatever home this copy was deployed under (a real \$HOME, or a test's
|
|
169
|
+
# \`--home\`). This worker sits at \`<harness-home>/hooks/<id>/\`, and \`.agents\` is
|
|
170
|
+
# the SIBLING of every harness home — the same relation deploy already uses to
|
|
171
|
+
# place agents and skills — so three \`dirname\`s and a name is the whole address.
|
|
172
|
+
#
|
|
173
|
+
# Derived rather than baked: a literal \`\$HOME/.agents\` would expand correctly on
|
|
174
|
+
# a host and point outside a sandboxed deploy, which is the class of bug that put
|
|
175
|
+
# \`\$HOME/.claude\` in the sibling worker and made it read another harness's tree.
|
|
176
|
+
NEUTRAL_ROOT="$(dirname -- "$(dirname -- "$(dirname -- "$SELF_DIR")")")/.agents"
|
|
177
|
+
# The DEFAULT rubric. A persona's own manifest may name another — that is the
|
|
178
|
+
# strategy seam: one opinionated implementation, each persona free to bind a
|
|
179
|
+
# different contract without the dispatcher learning anything about either.
|
|
180
|
+
RUBRIC="\${STANCE_RUBRIC:-$NEUTRAL_ROOT/stance-guardrail/stance-judge-prompt.md}"
|
|
131
181
|
|
|
132
182
|
# --- loop safety: budget the BLOCKS, never the JUDGING ---------------------------------------
|
|
133
183
|
# SUPERSEDES the original \`stop_hook_active=true → allow_stop\` short-circuit, which was not a
|
|
@@ -144,7 +194,8 @@ command -v jq >/dev/null 2>&1 || allow_stop # no jq → cannot parse → fail o
|
|
|
144
194
|
# - consecutive-block cap — after N blocks on one task, stop blocking and fail LOUD+OPEN.
|
|
145
195
|
# - no-progress detector — if the judged turn is byte-identical to the one already blocked,
|
|
146
196
|
# the agent changed nothing and won't on the next attempt either.
|
|
147
|
-
# State is per-session, in a tmp file keyed by session id;
|
|
197
|
+
# State is per-session, in a tmp file keyed by session id; a state dir it cannot write goes dark
|
|
198
|
+
# before any block is issued, since the caps below could not hold.
|
|
148
199
|
session="$(printf '%s' "$input" | jq -r '.session_id // "nosession"' 2>/dev/null || echo nosession)"
|
|
149
200
|
state_dir="\${TMPDIR:-/tmp}/stance-guardrail"
|
|
150
201
|
mkdir -p "$state_dir" 2>/dev/null || true
|
|
@@ -177,15 +228,24 @@ cwd="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null || true)"
|
|
|
177
228
|
# written in different languages in different files.
|
|
178
229
|
#
|
|
179
230
|
# Enrollment is now PRESENCE. Each bound persona's projection lands a manifest in that persona's
|
|
180
|
-
# own scope, and the
|
|
181
|
-
#
|
|
182
|
-
#
|
|
183
|
-
#
|
|
184
|
-
#
|
|
185
|
-
#
|
|
231
|
+
# own scope, and the scope reaches this worker in the one form its harness offers. omp's dispatcher
|
|
232
|
+
# lives in that same scope and therefore already knows it — it passes it in the envelope as
|
|
233
|
+
# \`stance_scope\`. Claude Code places no dispatcher; its hook payload NAMES the running agent as
|
|
234
|
+
# \`agent_type\` (on the main thread of a \`--agent\` session and inside a subagent, and on neither for a
|
|
235
|
+
# bare session), so the scope is the persona's directory under this harness's own home, one hop
|
|
236
|
+
# above the hooks root this worker was deployed into. Either way the scope is then read the same:
|
|
237
|
+
# A manifest means enrolled, and carries THIS agent's contract: its rubric, its moments, its
|
|
238
|
+
# handoff laws. No manifest means not enrolled, which is silence rather than an error, so a plain
|
|
239
|
+
# session, a built-in agent or a host's own agent — none of which declared itself anything — stays
|
|
240
|
+
# untouched exactly as it was. Adding a persona enrolls it; adding a gated dimension edits one
|
|
241
|
+
# persona's manifest; neither is a line in this file, and this file carries no agent list.
|
|
186
242
|
stance_scope="$(printf '%s' "$input" | jq -r '.stance_scope // empty' 2>/dev/null || true)"
|
|
187
|
-
[ -
|
|
188
|
-
|
|
243
|
+
if [ -z "$stance_scope" ]; then
|
|
244
|
+
named="$(printf '%s' "$input" | jq -r '.agent_type // empty' 2>/dev/null || true)"
|
|
245
|
+
case "$named" in '' | */* | . | ..) allow_stop ;; esac
|
|
246
|
+
stance_scope="$(dirname -- "$(dirname -- "$SELF_DIR")")/{{fact:harness-persona-root}}/$named"
|
|
247
|
+
fi
|
|
248
|
+
manifest="$stance_scope/{{fact:stance-manifest}}"
|
|
189
249
|
[ -f "$manifest" ] || allow_stop
|
|
190
250
|
|
|
191
251
|
# The persona's own contract, each field falling back to the shipped default. A manifest that
|
|
@@ -200,14 +260,46 @@ manifest_rubric="$(jq -r '.rubric // empty' "$manifest" 2>/dev/null || true)"
|
|
|
200
260
|
case "$manifest_rubric" in
|
|
201
261
|
'') ;;
|
|
202
262
|
/*) RUBRIC="$manifest_rubric" ;;
|
|
203
|
-
*) RUBRIC="$
|
|
263
|
+
*) RUBRIC="$(dirname -- "$manifest")/$manifest_rubric" ;;
|
|
204
264
|
esac
|
|
205
265
|
[ -z "$manifest_rubric" ] || [ -f "$RUBRIC" ] || \\
|
|
206
266
|
dark "the rubric named by $manifest is not readable at '\$RUBRIC'"
|
|
207
267
|
|
|
208
268
|
# --- extract the last assistant turn from the transcript ------------------------------------
|
|
209
|
-
|
|
269
|
+
# WHICH TRANSCRIPT, AND WHOSE TURN. A SubagentStop payload names TWO: \`transcript_path\` is the
|
|
270
|
+
# PARENT session's, \`agent_transcript_path\` is the subagent's own. The scope above is the subagent's
|
|
271
|
+
# (\`agent_type\`), so the text it is judged on must be the subagent's too. This read
|
|
272
|
+
# \`transcript_path\` alone, so a subagent's enrollment judged the PARENT's words — observed live: a
|
|
273
|
+
# bare session's own text was judged under a subagent's scope. A Stop payload carries no
|
|
274
|
+
# \`agent_transcript_path\`, so the main thread's transcript is the only one there is. The operator
|
|
275
|
+
# session is still the parent's, and the standing directive below reads it from there.
|
|
276
|
+
session_transcript="$(printf '%s' "$input" | jq -r '.transcript_path // empty' 2>/dev/null || true)"
|
|
277
|
+
transcript="$(printf '%s' "$input" | jq -r '.agent_transcript_path // empty' 2>/dev/null || true)"
|
|
278
|
+
[ -n "$transcript" ] || transcript="$session_transcript"
|
|
210
279
|
[ -n "$transcript" ] && [ -f "$transcript" ] || dark "no readable transcript at '\$transcript'"
|
|
280
|
+
[ -n "$session_transcript" ] && [ -f "$session_transcript" ] || session_transcript="$transcript"
|
|
281
|
+
|
|
282
|
+
# THE CLOSE IS IN THE PAYLOAD, NOT YET IN THE TRANSCRIPT. On Claude Code a Stop hook fires before the
|
|
283
|
+
# final assistant message is written, so the transcript ends one message short: a turn that is only
|
|
284
|
+
# text had NO assistant text to judge (fired, and passed in silence), and a tool turn was judged
|
|
285
|
+
# without its close. The payload carries that message as \`last_assistant_message\`. It is appended
|
|
286
|
+
# to a private copy as the last assistant record — unless the transcript already ends with exactly
|
|
287
|
+
# that text — so every extraction below sees the whole turn and the original file is never touched.
|
|
288
|
+
last="$(printf '%s' "$input" | jq -r '.last_assistant_message // empty' 2>/dev/null || true)"
|
|
289
|
+
if [ -n "$last" ]; then
|
|
290
|
+
present="$(jq -rs --arg t "$last" '
|
|
291
|
+
[ .[] | select(.type == "assistant") ] | last
|
|
292
|
+
| ((.message.content // []) | map(select(.type == "text") | .text) | join("\\n")) == $t
|
|
293
|
+
' "$transcript" 2>/dev/null || echo false)"
|
|
294
|
+
if [ "$present" != true ]; then
|
|
295
|
+
view="$state_dir/$session.turn.$$"
|
|
296
|
+
{
|
|
297
|
+
cat "$transcript"
|
|
298
|
+
printf '\\n'
|
|
299
|
+
jq -cn --arg t "$last" '{type:"assistant", isSidechain:false, message:{role:"assistant", content:[{type:"text", text:$t}]}}'
|
|
300
|
+
} > "$view" 2>/dev/null && transcript="$view"
|
|
301
|
+
fi
|
|
302
|
+
fi
|
|
211
303
|
|
|
212
304
|
# The transcript is JSONL: each line has top-level .type ("assistant"/"user"), .isSidechain
|
|
213
305
|
# (true for subagent lines), and .message.content as an array of blocks (thinking/text/tool_use)
|
|
@@ -309,9 +401,12 @@ asst_close="$(jq -rs '
|
|
|
309
401
|
# context: it is confidently wrong context, and the rubric leans on this slot to decide
|
|
310
402
|
# whether an irreversible act was authorized.
|
|
311
403
|
#
|
|
312
|
-
# Skill bodies are recognizable
|
|
313
|
-
#
|
|
314
|
-
# message
|
|
404
|
+
# Skill bodies are recognizable by the wrapper the HARNESS puts on them, never by their prose:
|
|
405
|
+
# claude injects one as a meta user message opening "Base directory for this skill: <dir>", after
|
|
406
|
+
# a <command-name>/<command-message> invocation message; omp's \`skill-prompt\` message closes with
|
|
407
|
+
# a "---" rule and "Skill: <path>/SKILL.md". An operator message that merely looks like a skill
|
|
408
|
+
# (an H1 over a fenced block) carries neither, and stays. Fall back to the most recent message
|
|
409
|
+
# that survives the filter.
|
|
315
410
|
operator="$(jq -rs '
|
|
316
411
|
[ .[]
|
|
317
412
|
| select(.type == "user")
|
|
@@ -324,8 +419,8 @@ operator="$(jq -rs '
|
|
|
324
419
|
| map(select(
|
|
325
420
|
(test("<command-name>") | not)
|
|
326
421
|
and (test("<command-message>") | not)
|
|
327
|
-
and (test("Base directory for this skill:") | not)
|
|
328
|
-
and (test("
|
|
422
|
+
and (test("^\\\\s*Base directory for this skill:") | not)
|
|
423
|
+
and (test("\\\\n---\\\\n\\\\nSkill: [^\\\\n]*SKILL\\\\.md\\\\s*$") | not)
|
|
329
424
|
and (test("^\\\\s*<system-reminder>") | not)
|
|
330
425
|
and (test("\\\\[SYSTEM NOTIFICATION - NOT USER INPUT\\\\]") | not)
|
|
331
426
|
and (test("<task-notification>") | not)
|
|
@@ -385,7 +480,7 @@ standing="$(jq -rs '
|
|
|
385
480
|
else . end
|
|
386
481
|
| gsub("\\\\s+"; " ")) as $said
|
|
387
482
|
| "\\($n - 1 - $hit.key)\\n\\($said)" end
|
|
388
|
-
' "
|
|
483
|
+
' "\${session_transcript:-$transcript}" 2>/dev/null || true)"
|
|
389
484
|
|
|
390
485
|
if [ -n "$standing" ]; then
|
|
391
486
|
since="$(printf '%s\\n' "$standing" | head -1)"
|
|
@@ -460,7 +555,7 @@ fi
|
|
|
460
555
|
|
|
461
556
|
# --- LAYER 2: the judge (semantic residue only) ----------------------------------------------
|
|
462
557
|
# The judge contract: turn on stdin, rubric path as argv[1]; emits VERDICT: PASS|BLOCK [+ REASON].
|
|
463
|
-
# Non-zero judge exit →
|
|
558
|
+
# Non-zero judge exit, an empty answer or a verdict that is neither PASS nor BLOCK → dark.
|
|
464
559
|
judged="$turn"
|
|
465
560
|
[ -n "$l1_evidence" ] && judged="$turn
|
|
466
561
|
|
|
@@ -497,8 +592,12 @@ else
|
|
|
497
592
|
verdict="$(printf '%s' "$judged" | $JUDGE_CMD "$RUBRIC" 2>/dev/null)" || dark "the judge did not answer"
|
|
498
593
|
fi
|
|
499
594
|
|
|
500
|
-
decision="$(printf '%s\\n' "$verdict" | sed -n 's/^VERDICT:[[:space:]]*//p' | head -1)"
|
|
501
|
-
|
|
595
|
+
decision="$(printf '%s\\n' "$verdict" | sed -n 's/^VERDICT:[[:space:]]*//p' | head -1 | sed 's/[[:space:]]*$//')"
|
|
596
|
+
case "$decision" in
|
|
597
|
+
PASS) allow_stop ;; # judged, no collapse: the one silent verdict
|
|
598
|
+
BLOCK) ;;
|
|
599
|
+
*) dark "the judge's verdict was unparseable (no VERDICT: PASS or VERDICT: BLOCK line)" ;;
|
|
600
|
+
esac
|
|
502
601
|
|
|
503
602
|
reason="$(printf '%s\\n' "$verdict" | sed -n 's/^REASON:[[:space:]]*//p' | head -1)"
|
|
504
603
|
[ -n "$reason" ] || reason="This turn collapsed out of the intent-driven-expert stance."
|
|
@@ -524,7 +623,8 @@ reason="$(printf '%s\\n' "$verdict" | sed -n 's/^REASON:[[:space:]]*//p' | head
|
|
|
524
623
|
evidence="$(printf '%s\\n' "$verdict" | sed -n 's/^EVIDENCE:[[:space:]]*//p' | head -1 \\
|
|
525
624
|
| sed 's/^["“]//;s/["”]$//;s/^[[:space:]]*//;s/[[:space:]]*$//')"
|
|
526
625
|
if [ -z "$evidence" ] && [ -z "$l1_evidence" ]; then
|
|
527
|
-
printf '
|
|
626
|
+
printf 'DARK\\t\\t%s\\n' "$reason" >> "$verdict_log" 2>/dev/null || true
|
|
627
|
+
say "STANCE GUARDRAIL — BLOCK DISCARDED: the judge blocked this turn but quoted no EVIDENCE line, and an unevidenced block cannot be told from a fabricated one. No verdict stands; this turn was NOT judged clean. The judge's reason was: $reason"
|
|
528
628
|
allow_stop
|
|
529
629
|
fi
|
|
530
630
|
if [ -n "$evidence" ] && [ "\${#evidence}" -ge 12 ]; then
|
|
@@ -558,11 +658,15 @@ printf '%s\\t%s\\t%s\\n' "$decision" "$evidence" "$reason" >> "$verdict_log" 2>/
|
|
|
558
658
|
ev_norm="$(printf '%s' "$evidence" | tr '\\n' ' ' | sed -e 's/[[:space:]][[:space:]]*/ /g' -e 's/^[-*][[:space:]]//' -e 's/ [-*] / /g')"
|
|
559
659
|
close_norm="$(printf '%s' "$asst_close" | tr '\\n' ' ' | sed -e 's/[[:space:]][[:space:]]*/ /g' -e 's/^[-*][[:space:]]//' -e 's/ [-*] / /g')"
|
|
560
660
|
if ! printf '%s' "$close_norm" | grep -qF -- "$ev_norm"; then
|
|
561
|
-
printf '
|
|
661
|
+
printf 'DARK\\t\\t%s\\n' "$evidence" >> "$verdict_log" 2>/dev/null || true
|
|
662
|
+
say "STANCE GUARDRAIL — BLOCK DISCARDED: the judge blocked this turn but quoted a span that is not in the turn's CLOSE (mid-turn preamble, or confabulated): $evidence. No verdict stands; this turn was NOT judged clean."
|
|
562
663
|
allow_stop
|
|
563
664
|
fi
|
|
564
665
|
fi
|
|
565
666
|
|
|
667
|
+
# A block is only safe to issue when the caps below can hold, and they live in the state dir.
|
|
668
|
+
[ -w "$state_dir" ] || dark "its state directory '$state_dir' is not writable, so the caps that keep a block from wedging the turn cannot hold, and a block was not issued"
|
|
669
|
+
|
|
566
670
|
# --- loop safety: bound EFFORT, never PERMISSION ----------------------------------------------
|
|
567
671
|
# A safety check must never be a function of how many times it has fired. Ethernet caps attempts
|
|
568
672
|
# then aborts and REPORTS; TCP caps retransmits then closes and SIGNALS; systemd caps starts then
|
|
@@ -589,7 +693,7 @@ BLOCK_CAP="\${STANCE_BLOCK_CAP:-3}"
|
|
|
589
693
|
turn_hash="$(printf '%s' "$asst_text" | cksum | cut -d' ' -f1)"
|
|
590
694
|
last_hash="$(cat "$hash_file" 2>/dev/null || echo none)"
|
|
591
695
|
if [ "$turn_hash" = "$last_hash" ]; then
|
|
592
|
-
|
|
696
|
+
say "STANCE GUARDRAIL — NO PROGRESS: this turn is byte-identical to the one already blocked, so blocking again cannot help. Allowing the stop UNRESOLVED: $reason — the finding STANDS and is unaddressed."
|
|
593
697
|
allow_stop
|
|
594
698
|
fi
|
|
595
699
|
|
|
@@ -611,7 +715,7 @@ fi
|
|
|
611
715
|
# whether the policy still applies.
|
|
612
716
|
if [ "$block_count" -ge "$BLOCK_CAP" ]; then
|
|
613
717
|
printf '0' > "$count_file" 2>/dev/null || true
|
|
614
|
-
|
|
718
|
+
say "STANCE GUARDRAIL — BYPASS SPENT after $BLOCK_CAP consecutive blocks. This turn was judged COLLAPSED and is allowed through UNRESOLVED: $reason — the gate is RE-ARMED as of now; the next collapsed turn blocks again."
|
|
615
719
|
allow_stop
|
|
616
720
|
fi
|
|
617
721
|
printf '%s' "$turn_hash" > "$hash_file" 2>/dev/null || true
|
|
@@ -690,7 +794,7 @@ turn="$(cat)"
|
|
|
690
794
|
# Resolve the judge model CLI — THIS HARNESS'S OWN, carried as a projection fact.
|
|
691
795
|
#
|
|
692
796
|
# It read \`\${STANCE_JUDGE_BIN:-claude}\`, and that one literal made every harness
|
|
693
|
-
# depend on one vendor:
|
|
797
|
+
# depend on one vendor: omp's stance guard needed \`claude\`
|
|
694
798
|
# installed and separately authenticated, and when that OAuth lapsed every verdict
|
|
695
799
|
# on every harness failed open in silence. A harness answers with its own model.
|
|
696
800
|
#
|
|
@@ -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":"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"}
|