@chrono-meta/fh-gate 1.4.93 → 1.4.95
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/.claude-plugin/marketplace.json +2 -2
- package/CLAUDE.md +1 -1
- package/knowledge/shared/harness-core/capability_composition_contract.md +25 -4
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +33 -0
- package/package.json +2 -1
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-commons/skills/ko-tech-writer/SKILL.md +37 -5
- package/plugins/fh-commons/skills/mcp-circuit-breaker/SKILL.md +30 -13
- package/plugins/fh-commons/skills/token-budget-gate/SKILL.md +31 -12
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/skills/field-harvest/SKILL.md +34 -11
- package/plugins/fh-meta/skills/marketplace-gate/SKILL.md +59 -31
- package/plugins/fh-meta/skills/pipeline-conductor/SKILL.md +13 -3
- package/plugins/fh-meta/skills/public-surface-audit/SKILL.md +56 -22
- package/plugins/fh-meta/skills/public-surface-audit/SKILL_detail.md +50 -11
- package/plugins/fh-meta/skills/return-path-gate/SKILL.md +36 -7
- package/scripts/capability_registry_check.sh +280 -0
- package/scripts/psa_scan_lib.sh +53 -1
- package/scripts/relay_channel.sh +6 -1
|
@@ -94,7 +94,11 @@ The four constituent skills use heterogeneous scope models. Translate the pipeli
|
|
|
94
94
|
|
|
95
95
|
## Step 0.5. return-path-gate — Pre-flight Chain Audit
|
|
96
96
|
|
|
97
|
-
> Skip
|
|
97
|
+
> Skip only when scope is a single non-pipeline skill. **return-path-gate not installed is a recorded
|
|
98
|
+
> degrade, never a free skip**: proceed, but record `degraded: return-path-gate (not installed)` in the
|
|
99
|
+
> final report — and, symmetric with the user-override row below, the sweep **cannot reach
|
|
100
|
+
> `CLEAN (--full)`**. A missing tool must not pass more cheaply than an explicit refusal (that
|
|
101
|
+
> asymmetry trains uninstalling the gate).
|
|
98
102
|
|
|
99
103
|
Run `/return-path-gate --skill [scope]`.
|
|
100
104
|
|
|
@@ -197,7 +201,8 @@ After all steps complete (or after chain halt), output the aggregated report (te
|
|
|
197
201
|
|
|
198
202
|
| Condition | Overall |
|
|
199
203
|
|---|---|
|
|
200
|
-
| All steps PASS | `CLEAN ({mode})` |
|
|
204
|
+
| All steps PASS, zero `degraded:` entries | `CLEAN ({mode})` |
|
|
205
|
+
| All steps PASS but 1+ `degraded:` entries (skip/not-installed/override) | `CLEAN ({mode}, degraded: [list])` — and the mode may not render `--full` (the Step 0.5 "cannot reach CLEAN (--full)" rule binds HERE, in the aggregate, not only in prose) |
|
|
201
206
|
| Any step CONDITIONAL_PASS or accepted ESCALATE; none FAIL | `PENDING` |
|
|
202
207
|
| Any step FAIL or unresolved ESCALATE (option c) | `BLOCKED` |
|
|
203
208
|
|
|
@@ -263,7 +268,7 @@ complexity_routing:
|
|
|
263
268
|
|
|
264
269
|
```
|
|
265
270
|
Step 0 scope confirmed and scope translation table applied
|
|
266
|
-
+ Step 0.5 return-path-gate pre-flight: PASS / CONDITIONAL_PASS / FAIL (halts sweep) / explicitly skipped / degraded (user override)
|
|
271
|
+
+ Step 0.5 return-path-gate pre-flight: PASS / CONDITIONAL_PASS / FAIL (halts sweep) / explicitly skipped / degraded (user override | not installed)
|
|
267
272
|
+ All in-scope steps executed and verdicts emitted
|
|
268
273
|
+ Aggregated report output (Step 5 format)
|
|
269
274
|
+ Report saved to tracks/_meta/ (or skip warning issued)
|
|
@@ -273,6 +278,11 @@ Step 0 scope confirmed and scope translation table applied
|
|
|
273
278
|
+ If ESCALATE occurred: user presented three options; choice recorded in report
|
|
274
279
|
```
|
|
275
280
|
|
|
281
|
+
**Check classes**: every fence row above is *mandatory-pass*, except the completion-claim discipline
|
|
282
|
+
below, which is *measured* — its reconciliation (failed+skipped list length vs. in-scope − passed) is
|
|
283
|
+
the mechanical anchor. No Done When row here is judged: verdict aggregation is per-step-typed, not
|
|
284
|
+
discretionary.
|
|
285
|
+
|
|
276
286
|
Verdict: PASS (all conditions met, sweep complete) | CONDITIONAL_PASS (sweep complete, pending items captured) | FAIL (chain halted, blocking items remain) | ESCALATE (chain paused, human decision required)
|
|
277
287
|
|
|
278
288
|
A sweep is not done until the Step 5 report is output. Emitting per-step verdicts without the aggregated report is incomplete.
|
|
@@ -38,7 +38,9 @@ deliberately excluded — `CLAUDE.local.md`, the companion store, and local sess
|
|
|
38
38
|
*correct* home for operator-private tokens, so finding them there is not a leak.
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
|
-
|
|
41
|
+
ARGS="${ARGUMENTS//--json/}" # strip flags first — "--json" alone must not become the path
|
|
42
|
+
REPO_PATH="${ARGS#--target }"
|
|
43
|
+
REPO_PATH="$(printf '%s' "$REPO_PATH" | xargs)"
|
|
42
44
|
REPO_PATH="${REPO_PATH:-$(pwd)}"
|
|
43
45
|
git -C "$REPO_PATH" rev-parse --is-inside-work-tree >/dev/null 2>&1 \
|
|
44
46
|
|| { echo "Not a git repo — public-surface-audit scans git-tracked files only. Aborting."; exit 1; }
|
|
@@ -50,12 +52,18 @@ git -C "$REPO_PATH" ls-files | wc -l | xargs echo "Tracked files:"
|
|
|
50
52
|
|
|
51
53
|
## Step 1. Pattern List (configurable)
|
|
52
54
|
|
|
53
|
-
The
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
The pattern source is **two layers, matching the mechanical scan layer** (`scripts/psa_scan_lib.sh`,
|
|
56
|
+
shared with the pre-commit/pre-push/pre-publish hooks — this skill must render the same verdict the
|
|
57
|
+
hooks would, or the same repo state gets two answers):
|
|
58
|
+
|
|
59
|
+
1. **`.claude/rules/.public-surface-patterns.defaults`** — committed, universal placeholder-grade
|
|
60
|
+
patterns (safe to publish; carries no operator literal).
|
|
61
|
+
2. **`.claude/rules/.public-surface-patterns`** — **gitignored, operator literals** (or `PSA_PATTERNS`
|
|
62
|
+
env override). Your real username and employer name live *only* here — hardcoding them in a tracked
|
|
63
|
+
file would leak exactly what this skill hunts. One `severity<TAB>regex` per line.
|
|
64
|
+
|
|
65
|
+
The scan reads the **union of both layers**. This SKILL.md carries only placeholders; the skill
|
|
66
|
+
dogfoods its own rule.
|
|
59
67
|
|
|
60
68
|
| # | Token class | Severity | Placeholder (real value goes in the gitignored source) | Why private |
|
|
61
69
|
|:-:|---|:-:|---|---|
|
|
@@ -71,12 +79,16 @@ its own rule.
|
|
|
71
79
|
- **LOW** — companion-store / private-wiring name. Methodology should be model-agnostic; naming a private
|
|
72
80
|
store is drift, not a confidentiality breach.
|
|
73
81
|
|
|
74
|
-
> **Setup**: put your real values in the gitignored
|
|
75
|
-
>
|
|
76
|
-
>
|
|
77
|
-
>
|
|
78
|
-
>
|
|
79
|
-
>
|
|
82
|
+
> **Setup**: put your real values in the gitignored override (one `severity<TAB>regex` per line).
|
|
83
|
+
> Degrade states, by layer — never collapse them:
|
|
84
|
+
> - **Neither layer present** → **NOT CONFIGURED** — *not* CLEAN. A missing pattern source must never
|
|
85
|
+
> masquerade as a clean bill of health ("nothing scanned" misread as "nothing leaked").
|
|
86
|
+
> - **Defaults only** (override absent) → scan runs, but the verdict carries the label
|
|
87
|
+
> `operator literals NOT CONFIGURED — universal patterns only; identity/company classes UNSCANNED`.
|
|
88
|
+
> This mirrors what the hooks scan in the same state, instead of reporting NOT CONFIGURED while the
|
|
89
|
+
> hook path quietly scans defaults (two verdicts for one repo state — the divergence this line closes).
|
|
90
|
+
> - To declare "I genuinely have no private tokens", create the override **empty** — an empty file is
|
|
91
|
+
> an explicit declaration, an absent file is unconfigured.
|
|
80
92
|
|
|
81
93
|
---
|
|
82
94
|
|
|
@@ -86,6 +98,11 @@ Some tracked files legitimately reference otherwise-private tokens — the scan
|
|
|
86
98
|
leaks. Maintain an allowlist of `file path :: token` pairs. A match is suppressed only when **both**
|
|
87
99
|
the file and the token are on the allowlist row.
|
|
88
100
|
|
|
101
|
+
**Where the filled allowlist lives**: a populated allowlist row *names private tokens*, so it belongs
|
|
102
|
+
in a **gitignored** source beside the pattern override (`.claude/rules/.public-surface-allowlist`, or a
|
|
103
|
+
section of `CLAUDE.local.md`) — never in a tracked file. The table below shows generic shapes only; a
|
|
104
|
+
tracked copy of your real allowlist would itself be the leak this skill scans for.
|
|
105
|
+
|
|
89
106
|
| Tracked file | Allowed tokens | Reason |
|
|
90
107
|
|---|---|---|
|
|
91
108
|
| `.gitignore` | companion-store name, sync-script name | Must name what it ignores |
|
|
@@ -172,11 +189,17 @@ class of leak). Diagnostic-only: this step never writes — it reports, the oper
|
|
|
172
189
|
Report per-hit `file:line → matched token [class]` grouped by severity, then the overall verdict.
|
|
173
190
|
**Verdict thresholds** (behavioral — these define the gate):
|
|
174
191
|
|
|
175
|
-
- ⚪ **NOT CONFIGURED** — pattern
|
|
176
|
-
- 🟢 **CLEAN** — pattern source present (incl. empty), 0 HIGH + 0 MED + 0 LOW (after allowlist)
|
|
192
|
+
- ⚪ **NOT CONFIGURED** — no pattern layer present (nothing scanned — NOT a clean result; set up first)
|
|
193
|
+
- 🟢 **CLEAN** — pattern source present (incl. empty override), 0 HIGH + 0 MED + 0 LOW (after allowlist)
|
|
177
194
|
- 🟡 **REVIEW** — 0 HIGH + 0 MED, LOW-only (drift, not a breach)
|
|
178
195
|
- 🔴 **LEAK** — 1+ HIGH or 1+ MED (block publish / fix before commit)
|
|
179
196
|
|
|
197
|
+
**Every verdict line carries a `coverage:` qualifier** — `full` (both layers loaded) or
|
|
198
|
+
`defaults-only` (override absent: operator identity/company classes UNSCANNED). The qualifier exists
|
|
199
|
+
because the enum alone cannot preserve the defaults-only state: `CLEAN (coverage: defaults-only)` on
|
|
200
|
+
an operator-configured checkout is missing evidence, not a pass — publish-gating callers there must
|
|
201
|
+
require `coverage: full`.
|
|
202
|
+
|
|
180
203
|
> **Detail**: See `SKILL_detail.md §Report-Template` — the full human-report ASCII layout (severity
|
|
181
204
|
> buckets, allowlist-suppressed line) — read when formatting the Step 4 report.
|
|
182
205
|
|
|
@@ -207,7 +230,9 @@ enforceable (FH's "enforcement is a hook, not a prompt" principle). Imported fro
|
|
|
207
230
|
authoritative — never gate on `counts` alone**: a counts-only check (`HIGH==0 && MED==0`) misreads
|
|
208
231
|
`NOT_CONFIGURED` (which also has zero counts) as a pass. A caller blocks when `verdict` is `LEAK` **or**
|
|
209
232
|
`NOT_CONFIGURED` — an unconfigured scan is not a pass (the same silent-failure guard as the human path:
|
|
210
|
-
absence ≠ CLEAN).
|
|
233
|
+
absence ≠ CLEAN). The JSON carries the same `coverage` field as Step 4; on an operator-configured
|
|
234
|
+
checkout a publish-gating caller additionally blocks on `coverage: "defaults-only"` (identity/company
|
|
235
|
+
classes unscanned there — zero counts prove nothing about the classes that never ran).
|
|
211
236
|
|
|
212
237
|
---
|
|
213
238
|
|
|
@@ -236,15 +261,24 @@ Usable standalone — no hub clone required.
|
|
|
236
261
|
## Done When
|
|
237
262
|
|
|
238
263
|
```
|
|
239
|
-
Step 1 pattern
|
|
240
|
-
|
|
241
|
-
+ Step
|
|
242
|
-
+ Step
|
|
243
|
-
|
|
264
|
+
Step 1 pattern source resolved (both layers checked; absent/partial
|
|
265
|
+
states rendered as their labeled degrade state, never as CLEAN) — mandatory-pass
|
|
266
|
+
+ Step 2 allowlist applied (from a gitignored source) — mandatory-pass
|
|
267
|
+
+ Step 3 scan run for every pattern over git ls-files (tracked only —
|
|
268
|
+
gitignored excluded), malformed pattern rows counted and reported,
|
|
269
|
+
never silently dropped — measured (hit + malformed counts)
|
|
270
|
+
+ Step 3b/3c run (FP-hygiene on matched tokens; ignore-verification
|
|
271
|
+
for expected-private paths) or explicitly skipped with reason — mandatory-pass
|
|
272
|
+
+ Step 4 report output: per-hit file:line + token + severity, plus
|
|
273
|
+
overall verdict — mandatory-pass
|
|
274
|
+
+ "public-surface-audit Complete" declaration output — mandatory-pass
|
|
244
275
|
```
|
|
245
276
|
|
|
246
277
|
Verdict: **CLEAN** (0 tokens after allowlist) | **REVIEW** (LOW-only — drift, prescriptions noted) |
|
|
247
|
-
**LEAK** (1+ HIGH or 1+ MED — block publish, prescriptions attached)
|
|
278
|
+
**LEAK** (1+ HIGH or 1+ MED — block publish, prescriptions attached) | **NOT_CONFIGURED** (no pattern
|
|
279
|
+
source — nothing scanned, not a pass; same 4-value enum as Step 5 `--json`. An earlier version of this
|
|
280
|
+
enum had 3 values here, so a session following only Done When would force absent-config into CLEAN/
|
|
281
|
+
REVIEW/LEAK — re-opening the exact silent-pass the body closes).
|
|
248
282
|
|
|
249
283
|
---
|
|
250
284
|
|
|
@@ -11,19 +11,58 @@ cd "$REPO_PATH" || exit 1
|
|
|
11
11
|
# Build the tracked-file list once.
|
|
12
12
|
git ls-files > /tmp/_psa_tracked.txt
|
|
13
13
|
|
|
14
|
-
#
|
|
15
|
-
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
14
|
+
# ── Single-source preference: when the shared library exists, use it and write NO second loop ──
|
|
15
|
+
# scripts/psa_scan_lib.sh owns loading + row validation + exemptions for the hook layer; a hand-rolled
|
|
16
|
+
# copy here is a second normalizer with its own leniency (the divergence class this rewrite removed).
|
|
17
|
+
if [ -r "scripts/psa_scan_lib.sh" ]; then
|
|
18
|
+
. scripts/psa_scan_lib.sh
|
|
19
|
+
psa_load ".claude/rules/.public-surface-patterns.defaults" \
|
|
20
|
+
"${PSA_PATTERNS:-.claude/rules/.public-surface-patterns}"
|
|
21
|
+
{ [ "$PSA_DEFAULTS_OK" -eq 1 ] || [ "$PSA_OVERRIDE_PRESENT" -eq 1 ]; } \
|
|
22
|
+
|| { echo "⚪ NOT CONFIGURED: neither pattern layer present. Not scanning."; exit 2; }
|
|
23
|
+
[ "$PSA_BAD_ROWS" -gt 0 ] \
|
|
24
|
+
&& { echo "HARNESS_ERROR: $PSA_BAD_ROWS unusable pattern row(s) — verdict cannot be CLEAN"; exit 10; }
|
|
25
|
+
# Feed every tracked file as path<TAB>line, the stream psa_scan_tagged consumes. Sourcing the lib
|
|
26
|
+
# without these calls is a no-op scan — measured on this repo (PSA_STREAM stayed unset), so the
|
|
27
|
+
# calls are spelled out here rather than pointed at.
|
|
28
|
+
while IFS= read -r f; do
|
|
29
|
+
awk -v p="$f" '{printf "%s\t%s\n", p, $0}' "$f" 2>/dev/null
|
|
30
|
+
done < /tmp/_psa_tracked.txt | psa_scan_tagged
|
|
31
|
+
[ "$PSA_OVERRIDE_PRESENT" -eq 1 ] \
|
|
32
|
+
|| echo "coverage: defaults-only — operator literals NOT CONFIGURED (identity/company classes UNSCANNED)"
|
|
33
|
+
else
|
|
34
|
+
# ── Standalone fallback (no hub scripts in this repo) — validated loop, malformed rows COUNTED ──
|
|
35
|
+
PSA_DEFAULTS=".claude/rules/.public-surface-patterns.defaults"
|
|
36
|
+
PATTERN_SRC="${PSA_PATTERNS:-.claude/rules/.public-surface-patterns}"
|
|
37
|
+
SRC_LIST=""
|
|
38
|
+
[ -e "$PSA_DEFAULTS" ] && SRC_LIST="$PSA_DEFAULTS"
|
|
39
|
+
[ -e "$PATTERN_SRC" ] && SRC_LIST="$SRC_LIST $PATTERN_SRC"
|
|
40
|
+
[ -n "$SRC_LIST" ] || { echo "⚪ NOT CONFIGURED: no pattern source (neither defaults nor override). Not scanning."; exit 2; }
|
|
41
|
+
|
|
42
|
+
MALFORMED=0
|
|
43
|
+
cat $SRC_LIST > /tmp/_psa_rows.txt # no pipe into the loop — a piped while runs in a subshell and loses MALFORMED
|
|
44
|
+
# `|| [ -n "$severity" ]` keeps a final row that lacks a trailing newline — `read` alone drops it silently.
|
|
45
|
+
while IFS=$'\t' read -r severity regex || [ -n "$severity" ]; do
|
|
46
|
+
case "$severity" in ''|'#'*) continue ;; esac # blank / comment rows
|
|
47
|
+
if [ -z "$regex" ]; then # no tab separator → malformed, VISIBLE
|
|
48
|
+
MALFORMED=$((MALFORMED+1)); echo "MALFORMED ROW (no <TAB>): $severity" >&2; continue
|
|
49
|
+
fi
|
|
50
|
+
printf 'x\n' | grep -qE "$regex" 2>/dev/null
|
|
51
|
+
rc=$? # plain rc capture — `if !` would negate $?
|
|
52
|
+
if [ "$rc" -ge 2 ]; then # grep rc≥2 = invalid regex, not "no match"
|
|
53
|
+
MALFORMED=$((MALFORMED+1)); echo "MALFORMED ROW (bad regex): $regex" >&2; continue
|
|
54
|
+
fi
|
|
55
|
+
grep -nIE "$regex" $(cat /tmp/_psa_tracked.txt) | sed "s/^/[$severity] /"
|
|
56
|
+
done < /tmp/_psa_rows.txt
|
|
57
|
+
# Malformed rows poison the verdict: part of the pattern file never scanned → CLEAN is unprovable.
|
|
58
|
+
[ "$MALFORMED" -gt 0 ] && { echo "HARNESS_ERROR: $MALFORMED malformed pattern row(s) — verdict cannot be CLEAN"; exit 10; }
|
|
59
|
+
fi
|
|
25
60
|
```
|
|
26
61
|
|
|
62
|
+
**Why the fallback validates instead of skipping**: the previous loop dropped a malformed row with a
|
|
63
|
+
bare `continue` and discarded grep's stderr — a broken pattern file scanned "clean" by silently not
|
|
64
|
+
scanning. `not found ≠ 0`: a row that never ran is not a row with zero hits.
|
|
65
|
+
|
|
27
66
|
For each pattern, run `grep -nIE "<regex>" $(git ls-files)`:
|
|
28
67
|
- `-n` → line numbers (required for `file:line` output)
|
|
29
68
|
- `-I` → skip binary files
|
|
@@ -66,7 +66,27 @@ find plugins/ -name "SKILL.md" | sort
|
|
|
66
66
|
find plugins/ -path "*[name]/SKILL.md"
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
If 0 SKILL.md files are in scope,
|
|
69
|
+
If 0 SKILL.md files are in scope, **distinguish the two causes before stopping** — a failed scope
|
|
70
|
+
command and a genuinely empty scope both render as zero lines, and only one of them is skippable:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
git rev-parse --git-dir >/dev/null 2>&1 \
|
|
74
|
+
|| { echo "HARNESS_ERROR: git unavailable — scope could NOT be measured (not an empty scope)"; exit 10; }
|
|
75
|
+
|
|
76
|
+
# Repo-liveness alone is NOT enough: a broken ref (e.g. missing origin/main in --pr mode) fails the
|
|
77
|
+
# scope command with rc=1 and empty output while rev-parse stays green. Capture the scope command's
|
|
78
|
+
# OWN exit status — before the grep, which legitimately returns 1 on no-match:
|
|
79
|
+
scope_out=$(git diff --name-only origin/main...HEAD 2>&1); scope_rc=$?
|
|
80
|
+
[ "$scope_rc" -ne 0 ] \
|
|
81
|
+
&& { echo "HARNESS_ERROR: scope command failed (rc=$scope_rc): $scope_out"; exit 10; }
|
|
82
|
+
printf '%s\n' "$scope_out" | grep "SKILL\.md"
|
|
83
|
+
# (same rc-capture shape applies to the default and --all scope commands)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
With both controls green and genuinely 0 files in scope, output:
|
|
87
|
+
"No SKILL.md files in scope — audit skipped (control: git rev-parse OK)." and stop.
|
|
88
|
+
A HARNESS_ERROR is a distinct non-pass — it must never be reported as "audit skipped", because this
|
|
89
|
+
skill is pipeline-conductor's Step 0.5 pre-flight and a tooling-down skip would render as a pass there.
|
|
70
90
|
|
|
71
91
|
---
|
|
72
92
|
|
|
@@ -221,11 +241,19 @@ Next actions:
|
|
|
221
241
|
## Done When
|
|
222
242
|
|
|
223
243
|
```
|
|
224
|
-
Step 0 scope determined;
|
|
225
|
-
|
|
226
|
-
+ Step
|
|
227
|
-
|
|
228
|
-
+ Step
|
|
244
|
+
Step 0 scope determined; a failed scope command reported as
|
|
245
|
+
HARNESS_ERROR, never as empty scope — mandatory-pass
|
|
246
|
+
+ Step 1 two-pass extraction complete (Pass A caller-wait +
|
|
247
|
+
Pass B callee-output evaluated separately) — mandatory-pass
|
|
248
|
+
+ Step 2 every (caller → callee) pair classified CLOSED or
|
|
249
|
+
OPEN with two-dimension severity — judged (adversarial pairing:
|
|
250
|
+
before ANY chain is reported OPEN, the ground-truth CLOSED
|
|
251
|
+
pair from §Operating Notes must classify CLOSED in the same
|
|
252
|
+
run — an instrument that cannot separate the known pair
|
|
253
|
+
reports nothing)
|
|
254
|
+
+ Step 3 fix prescriptions output for each OPEN chain
|
|
255
|
+
(core FH skills: proposal-only) — mandatory-pass
|
|
256
|
+
+ Step 4 summary report with CLOSED/OPEN counts output — measured (counts)
|
|
229
257
|
```
|
|
230
258
|
|
|
231
259
|
> Fix prescriptions are text output only (Write not in allowed-tools). Prescription application is manual and out of scope for this skill. Verification of applied fixes requires re-running `/return-path-gate --skill [name]`.
|
|
@@ -254,4 +282,5 @@ Verdict: PASS (0 HIGH severity OPEN chains) | CONDITIONAL_PASS (MEDIUM/LOW sever
|
|
|
254
282
|
- **CONDITIONAL_PASS gate is the highest-risk gap**: A chain where CONDITIONAL_PASS conditions are listed by the callee but the caller has no enforcement path is OPEN even if other verdict paths fold correctly.
|
|
255
283
|
- **Scope default is narrow by design**: Default captures modified + newly added files (not just staged changes). Use `--pr` for PR-relative mode in worktrees, `--all` for periodic sweeps.
|
|
256
284
|
- **Core FH skill prescriptions are proposals only**: Edits to harvest-loop, steel-quench, sim-conductor, and other core skills require deliberate review — output proposal notes, not standard prescriptions.
|
|
257
|
-
- **Reference pattern**: `knowledge/shared/harness-core/return_path_gate.md` defines the canonical closed-loop structure and verified instances (apex-review → sim-conductor, agent-composer ↔ deliberation). These are the ground-truth CLOSED examples for calibrating classification.
|
|
285
|
+
- **Reference pattern**: `knowledge/shared/harness-core/return_path_gate.md` defines the canonical closed-loop structure and verified instances (apex-review → sim-conductor, agent-composer ↔ deliberation). These are the ground-truth CLOSED examples for calibrating classification — and that calibration is a Done When condition (Step 2's adversarial pairing), not an optional note.
|
|
286
|
+
- **The Pass A signal list is a closed English keyword list — know its degrade direction**: synonyms ("blocks until", "halts on") and non-English documentation will read as false OPEN. That direction is by design (over-report, never silent-close), but treat an OPEN verdict on unconventional wording as a candidate for human reading before prescribing a fix.
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# capability_registry_check.sh — 정체성 ① 의 «등록 시점» 검사기.
|
|
3
|
+
#
|
|
4
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
# 왜 이게 지어졌나 — 스펙이 이름까지 붙여놓고 «없다» 고 적어둔 파일
|
|
6
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
# `capability_composition_contract.md §Salience` 는 이렇게 적는다:
|
|
8
|
+
#
|
|
9
|
+
# "Named residual, not built: a scripts/capability_registry_check.sh that validates
|
|
10
|
+
# the schema and runs each declared M4 pair would make registration *measured*
|
|
11
|
+
# rather than reviewed. It does not exist."
|
|
12
|
+
#
|
|
13
|
+
# `relay_channel.sh` 헤더도 같은 말을 한다 — 자기는 **call moment** 만 보고, 선언이
|
|
14
|
+
# **진실인지**는 검사하지 않는다고. 이 파일이 그 나머지 절반이다.
|
|
15
|
+
#
|
|
16
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
17
|
+
# 이 검사기가 증명하지 않는 것 (과잉주장 금지 — 명명된 잔여)
|
|
18
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
19
|
+
# · **등록을 강제하지 않는다.** "지금 capability 를 등록하려 한다"를 관측하는 훅은 없다.
|
|
20
|
+
# 돌리면 판정하지만 돌리도록 강제하지는 못한다(relay_channel.sh 의 같은 잔여와 동형).
|
|
21
|
+
# · **M3(모델 독립성)은 선언 검사다.** reps 로 재지 않는다 — `judge:` 축 선언과, mechanical
|
|
22
|
+
# 선언인데 entry 가 LLM CLI 를 부르는 명백한 모순만 잡는다. M4 를 reps=2 로 돌려 부분 방어.
|
|
23
|
+
# · 🟥 **`writes:` 축은 검증 불가 — 그리고 그게 이 파일에서 실제로 터졌다.**
|
|
24
|
+
# 2026-08-11, 이 검사기를 통과한 capability(`writes: read-only` 선언)의 진입점이
|
|
25
|
+
# 정리 트랩 결함으로 **레포의 `scripts/` 를 rm -rf 했다.** M1(실행 가능)·M2(닫힌 enum)·
|
|
26
|
+
# M3(mechanical)·M4(known-pair 통과)·M5(cwd) 를 **전부 통과한 채로** 그랬다.
|
|
27
|
+
# 등록 바는 «선언이 형식에 맞나 · 답 아는 쌍을 가르나» 를 보지, **«선언이 사실인가»**
|
|
28
|
+
# 를 보지 않는다. read-only 선언의 진위는 여기서 닫히지 않는다.
|
|
29
|
+
# 부분 처방(오늘 적용): 진입점 쪽 트랩 규율(정리 대상 변수 재대입 금지 + 임시경로 검문).
|
|
30
|
+
# 구조 처방(미구축): 샌드박스/읽기전용 마운트에서 M4 를 돌려 쓰기 시도를 관측하는 것.
|
|
31
|
+
# 그 전까지 `writes: read-only` 는 **등록자 주장**이지 이 검사기의 판정이 아니다.
|
|
32
|
+
#
|
|
33
|
+
# 사용법
|
|
34
|
+
# capability_registry_check.sh <capfile> [<capfile> ...]
|
|
35
|
+
# capability_registry_check.sh --self-test
|
|
36
|
+
#
|
|
37
|
+
# exit code
|
|
38
|
+
# 0 REGISTRABLE 전 capfile 이 M1–M5 + 추가조항 통과
|
|
39
|
+
# 1 REJECTED 하나 이상 기준 미달 (등록 불가 — 그 표면은 dispatch 로 남는다)
|
|
40
|
+
# 10 HARNESS_ERROR capfile 도달 불가·파손·검사기 자신의 전제 파손
|
|
41
|
+
#
|
|
42
|
+
set -o pipefail
|
|
43
|
+
set -f # noglob — 선언 파일의 값은 데이터지 파일 패턴이 아니다 (relay_channel.sh 와 동일)
|
|
44
|
+
|
|
45
|
+
RC_OK=0; RC_REJECT=1; RC_HARNESS=10
|
|
46
|
+
|
|
47
|
+
CLOSED_KEYS="id entry requires_cwd verdict_channel verdict_enum verdict_stdout_key upstream_argv echoes_upstream approval reversibility residency degrade tier_floor writes judge verdict_binding calibration_positive_args calibration_positive_expect calibration_negative_args calibration_negative_expect"
|
|
48
|
+
|
|
49
|
+
# 「안 돌았다」를 뜻하는 이름들 — 추가조항(§ⓑ.4 B1)이 요구하는 구분항
|
|
50
|
+
DIDNOTRUN_NAMES="DID_NOT_RUN DIDNOTRUN NOT_RUN NO_TARGET SKIPPED UNMEASURED NOT_CONFIGURED HARNESS_ERROR"
|
|
51
|
+
|
|
52
|
+
FAILED=0 # 전 파일 누적 (종료코드용)
|
|
53
|
+
FILE_FAILED=0 # 현재 capfile 한정 — 파일마다 초기화한다. 이게 없으면 앞 파일의 실패가
|
|
54
|
+
# 뒤 파일의 M4 를 SKIPPED 로 만들어, 뒤 파일의 실제 결함이 안 보인다(보고 결함).
|
|
55
|
+
_fail() { printf ' ❌ %s — %s\n' "$1" "$2"; FAILED=1; FILE_FAILED=1; }
|
|
56
|
+
_ok() { printf ' ✅ %s — %s\n' "$1" "$2"; }
|
|
57
|
+
_die() { printf '❌ HARNESS_ERROR: %s\n' "$*" >&2; exit "$RC_HARNESS"; }
|
|
58
|
+
|
|
59
|
+
_parse() {
|
|
60
|
+
CAP_id=""; CAP_entry=""; CAP_requires_cwd=""; CAP_verdict_channel=""
|
|
61
|
+
CAP_verdict_enum=""; CAP_verdict_stdout_key=""; CAP_judge=""; CAP_writes=""
|
|
62
|
+
CAP_cal_pos_args=""; CAP_cal_pos_expect=""; CAP_cal_neg_args=""; CAP_cal_neg_expect=""
|
|
63
|
+
UNKNOWN_KEYS=""
|
|
64
|
+
local line key val
|
|
65
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
66
|
+
case "$line" in ''|'#'*) continue ;; esac
|
|
67
|
+
case "$line" in *:*) ;; *) UNKNOWN_KEYS="$UNKNOWN_KEYS malformed-line"; continue ;; esac
|
|
68
|
+
key="${line%%:*}"; val="${line#*:}"
|
|
69
|
+
# POSIX 문자클래스만 쓴다. BSD sed 에서 `[ \t]` 는 탭이 아니라 «공백·역슬래시·문자 t»
|
|
70
|
+
# 집합이라 `exit` 의 끝 t 가 잘려 `exi` 가 된다 — 이 검사기의 known-positive 레인이
|
|
71
|
+
# 실제로 그걸 잡았다(2026-08-11, 자기 계기가 자기 버그를 적발한 사례).
|
|
72
|
+
key="$(printf '%s' "$key" | tr -d '[:space:]')"
|
|
73
|
+
val="$(printf '%s' "$val" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')"
|
|
74
|
+
case " $CLOSED_KEYS " in *" $key "*) ;; *) UNKNOWN_KEYS="$UNKNOWN_KEYS $key"; continue ;; esac
|
|
75
|
+
case "$key" in
|
|
76
|
+
id) CAP_id="$val" ;; entry) CAP_entry="$val" ;;
|
|
77
|
+
requires_cwd) CAP_requires_cwd="$val" ;; verdict_channel) CAP_verdict_channel="$val" ;;
|
|
78
|
+
verdict_enum) CAP_verdict_enum="$val" ;; verdict_stdout_key) CAP_verdict_stdout_key="$val" ;;
|
|
79
|
+
judge) CAP_judge="$val" ;; writes) CAP_writes="$val" ;;
|
|
80
|
+
calibration_positive_args) CAP_cal_pos_args="$val" ;;
|
|
81
|
+
calibration_positive_expect) CAP_cal_pos_expect="$val" ;;
|
|
82
|
+
calibration_negative_args) CAP_cal_neg_args="$val" ;;
|
|
83
|
+
calibration_negative_expect) CAP_cal_neg_expect="$val" ;;
|
|
84
|
+
esac
|
|
85
|
+
done < "$1"
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
_enum_name_of() { # $1=exit code, $2=enum string → 이름 or ""
|
|
89
|
+
local pair
|
|
90
|
+
for pair in $2; do [ "${pair%%=*}" = "$1" ] && { printf '%s' "${pair#*=}"; return 0; }; done
|
|
91
|
+
printf ''
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# 🟥 capfile 은 **실행 신뢰경계**다 — M4 는 선언된 arm 을 «실제로 실행» 하는 것이 요점이므로,
|
|
95
|
+
# 검사기에 넘긴 capfile 은 그 자체로 "이 명령을 돌려도 된다"는 선언이다. 신뢰하지 않는
|
|
96
|
+
# capfile 을 이 검사기에 넘기지 마라. 배포 전 보안 패스가 실증한 것: `entry: /usr/bin/touch`
|
|
97
|
+
# + cal args 로 **REJECTED 판정이 나는 와중에도 부작용이 이미 발생**했다(판정 전에 arm 이 돈다).
|
|
98
|
+
# 아래 검문은 그 경계를 없애지 못한다 — 우발적 형태만 막는다.
|
|
99
|
+
_validate_arm_args() { # $1=args → 셸 메타문자/상위경로 탈출을 거부
|
|
100
|
+
case "$1" in
|
|
101
|
+
*'|'*|*';'*|*'&'*|*'>'*|*'<'*|*'`'*|*'$('*|*$'\n'*)
|
|
102
|
+
_fail "M4" "캘리브레이션 args 에 셸 메타문자가 있다(entry 와 같은 인젝션 표면): $1"; return 1 ;;
|
|
103
|
+
*'../'*)
|
|
104
|
+
_fail "M4" "캘리브레이션 args 가 상위 경로로 탈출한다: $1"; return 1 ;;
|
|
105
|
+
esac
|
|
106
|
+
return 0
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
_run_arm() { # $1=extra args → ARM_RC / ARM_NAME. 파이프로 읽지 않는다(PIPE-VERDICT).
|
|
110
|
+
( cd "$CAP_requires_cwd" 2>/dev/null || exit 127
|
|
111
|
+
# shellcheck disable=SC2086 # argv 토큰 분리는 의도 (noglob 로 확장은 막혀 있다)
|
|
112
|
+
set -- $CAP_entry $1; "$@" ) > /dev/null 2>&1
|
|
113
|
+
ARM_RC=$?
|
|
114
|
+
ARM_NAME="$(_enum_name_of "$ARM_RC" "$CAP_verdict_enum")"
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
_check_one() {
|
|
118
|
+
local f="$1"
|
|
119
|
+
[ -r "$f" ] || _die "capfile 도달 불가: $f"
|
|
120
|
+
_parse "$f"
|
|
121
|
+
FILE_FAILED=0
|
|
122
|
+
printf '\n── %s (%s)\n' "${CAP_id:-<id 미선언>}" "$f"
|
|
123
|
+
|
|
124
|
+
[ -n "$UNKNOWN_KEYS" ] && _fail "SCHEMA" "닫힌 키 목록 밖:$UNKNOWN_KEYS (오타 축 무음드롭 방지 — 무시하지 않는다)"
|
|
125
|
+
|
|
126
|
+
# ── M1 실행 가능한 진입점 ──────────────────────────────────────────────────
|
|
127
|
+
local first second
|
|
128
|
+
first="$(printf '%s' "$CAP_entry" | awk '{print $1}')"
|
|
129
|
+
second="$(printf '%s' "$CAP_entry" | awk '{print $2}')"
|
|
130
|
+
case "$CAP_entry" in
|
|
131
|
+
*'|'*|*';'*|*'&&'*|*'>'*|*'`'*|*'$('*)
|
|
132
|
+
_fail "M1" "entry 가 argv 가 아니라 셸 문자열이다(파이프/리다이렉트/치환 포함) — 인젝션 표면" ;;
|
|
133
|
+
'') _fail "M1" "entry 미선언" ;;
|
|
134
|
+
*)
|
|
135
|
+
if [ -x "$first" ] 2>/dev/null; then _ok "M1" "실행 가능: $first"
|
|
136
|
+
elif command -v "$first" >/dev/null 2>&1 && [ -r "$second" ]; then
|
|
137
|
+
_ok "M1" "선언된 인터프리터($first) + 읽을 수 있는 스크립트: $second"
|
|
138
|
+
else _fail "M1" "entry 를 셸이 모델 없이 실행할 수 없다: $CAP_entry"; fi ;;
|
|
139
|
+
esac
|
|
140
|
+
|
|
141
|
+
# ── M2 닫힌 채널의 typed verdict + 추가조항(ran ≠ did-not-run) ─────────────
|
|
142
|
+
case "$CAP_verdict_channel" in
|
|
143
|
+
exit|stdout-key|both) ;;
|
|
144
|
+
*) _fail "M2" "verdict_channel 이 {exit|stdout-key|both} 밖: '${CAP_verdict_channel}'" ;;
|
|
145
|
+
esac
|
|
146
|
+
case "$CAP_verdict_channel" in
|
|
147
|
+
stdout-key|both) [ -n "$CAP_verdict_stdout_key" ] || _fail "M2" "channel 이 stdout-key 를 포함하는데 verdict_stdout_key 미선언" ;;
|
|
148
|
+
esac
|
|
149
|
+
if [ -z "$CAP_verdict_enum" ]; then
|
|
150
|
+
_fail "M2" "verdict_enum 미선언 — 열린 채널은 등록 불가"
|
|
151
|
+
else
|
|
152
|
+
local pair bad=0 has_dnr=0 nm
|
|
153
|
+
for pair in $CAP_verdict_enum; do
|
|
154
|
+
case "$pair" in *=*) ;; *) bad=1 ;; esac
|
|
155
|
+
nm="${pair#*=}"
|
|
156
|
+
case " $DIDNOTRUN_NAMES " in *" $nm "*) has_dnr=1 ;; esac
|
|
157
|
+
done
|
|
158
|
+
[ "$bad" -eq 1 ] && _fail "M2" "verdict_enum 항목이 N=NAME 형식이 아니다: $CAP_verdict_enum"
|
|
159
|
+
if [ "$has_dnr" -eq 1 ]; then _ok "M2" "typed enum + 「안 돌았다」 구분항: $CAP_verdict_enum"
|
|
160
|
+
else _fail "M2+" "enum 에 「안 돌았다」를 뜻하는 값이 없다 ($CAP_verdict_enum) — PASS 가 no-op 과 구분 불가(§ⓑ.4 B1). PASS 는 적극 증거지 실패의 부재가 아니다"; fi
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
# ── M3 모델 독립성 (선언 검사 + 명백한 모순만) ─────────────────────────────
|
|
164
|
+
case "$CAP_judge" in
|
|
165
|
+
mechanical)
|
|
166
|
+
case "$CAP_entry" in
|
|
167
|
+
*claude*|*codex*|*gemini*|*copilot*|*ollama*|*llm*)
|
|
168
|
+
_fail "M3" "judge: mechanical 선언인데 entry 가 모델 CLI 를 부른다: $CAP_entry" ;;
|
|
169
|
+
*) _ok "M3" "judge: mechanical (모델 미개입 선언)" ;;
|
|
170
|
+
esac ;;
|
|
171
|
+
model) _ok "M3" "judge: model — 선언됨(합법). 조합에서 이 PASS 는 NON_CLEARING 이다" ;;
|
|
172
|
+
'') _fail "M3" "judge 축 미선언 — 모델 개입 여부가 불명이면 조합이 계산될 수 없다" ;;
|
|
173
|
+
*) _fail "M3" "judge 값이 {mechanical|model} 밖: '$CAP_judge'" ;;
|
|
174
|
+
esac
|
|
175
|
+
|
|
176
|
+
# ── M5 선언된 cwd (M4 를 그 자리에서 돌리므로 먼저) ────────────────────────
|
|
177
|
+
case "$CAP_requires_cwd" in
|
|
178
|
+
/*) [ -d "$CAP_requires_cwd" ] && _ok "M5" "requires_cwd 실재: $CAP_requires_cwd" \
|
|
179
|
+
|| _fail "M5" "requires_cwd 가 절대경로지만 존재하지 않는다: $CAP_requires_cwd" ;;
|
|
180
|
+
'') _fail "M5" "requires_cwd 미선언 — 콕핏이 어디서 부를지 알 수 없다" ;;
|
|
181
|
+
*) _fail "M5" "requires_cwd 가 절대경로가 아니다: $CAP_requires_cwd" ;;
|
|
182
|
+
esac
|
|
183
|
+
|
|
184
|
+
# ── M4 캘리브레이션 쌍 — 선언 + **실행** ──────────────────────────────────
|
|
185
|
+
if [ -z "$CAP_cal_pos_expect" ] || [ -z "$CAP_cal_neg_expect" ]; then
|
|
186
|
+
_fail "M4" "캘리브레이션 쌍 미선언(양성·음성 expect 둘 다 필요) — 답을 아는 케이스를 못 가르는 계기는 재는 게 아니다"
|
|
187
|
+
elif [ "$FILE_FAILED" -eq 1 ] && [ -z "${CRC_FORCE_M4:-}" ]; then
|
|
188
|
+
printf ' ⏭ M4 — 앞선 축이 실패해 실행 생략(SKIPPED, PASS 아님)\n'
|
|
189
|
+
elif ! _validate_arm_args "$CAP_cal_pos_args" || ! _validate_arm_args "$CAP_cal_neg_args"; then
|
|
190
|
+
printf ' ⏭ M4 — args 검문 실패로 arm 을 실행하지 않았다(SKIPPED, PASS 아님)\n'
|
|
191
|
+
else
|
|
192
|
+
local pos_name neg_name pos_rc neg_rc pos_name2
|
|
193
|
+
_run_arm "$CAP_cal_pos_args"; pos_rc="$ARM_RC"; pos_name="$ARM_NAME"
|
|
194
|
+
[ "$pos_rc" = "127" ] && _fail "M4" "requires_cwd 로 진입 실패 — arm 을 돌릴 수 없다"
|
|
195
|
+
_run_arm "$CAP_cal_pos_args"; pos_name2="$ARM_NAME" # reps=2 (M3 부분 방어)
|
|
196
|
+
_run_arm "$CAP_cal_neg_args"; neg_rc="$ARM_RC"; neg_name="$ARM_NAME"
|
|
197
|
+
|
|
198
|
+
if [ -z "$pos_name" ]; then
|
|
199
|
+
_fail "M4" "양성 arm 의 exit $pos_rc 가 선언된 enum 밖 — enum 밖 값은 HARNESS_ERROR 이지 PASS 가 아니다"
|
|
200
|
+
elif [ "$pos_name" != "$CAP_cal_pos_expect" ]; then
|
|
201
|
+
_fail "M4" "양성 arm 이 선언과 다르다: expect=$CAP_cal_pos_expect actual=$pos_name (exit $pos_rc)"
|
|
202
|
+
elif [ "$pos_name" != "$pos_name2" ]; then
|
|
203
|
+
_fail "M4/M3" "같은 양성 arm 2회에 verdict 가 갈렸다: $pos_name vs $pos_name2 — 모델 독립성 미충족"
|
|
204
|
+
elif [ -z "$neg_name" ]; then
|
|
205
|
+
_fail "M4" "음성 arm 의 exit $neg_rc 가 선언된 enum 밖"
|
|
206
|
+
elif [ "$neg_name" != "$CAP_cal_neg_expect" ]; then
|
|
207
|
+
_fail "M4" "음성 arm 이 선언과 다르다: expect=$CAP_cal_neg_expect actual=$neg_name (exit $neg_rc)"
|
|
208
|
+
elif [ "$pos_name" = "$neg_name" ]; then
|
|
209
|
+
_fail "M4" "양성·음성이 같은 verdict($pos_name) — 답을 아는 두 케이스를 못 가른다"
|
|
210
|
+
else
|
|
211
|
+
_ok "M4" "known-pair 실행 통과: 양성→$pos_name(x2 일치) · 음성→$neg_name"
|
|
212
|
+
fi
|
|
213
|
+
fi
|
|
214
|
+
|
|
215
|
+
# ── 검증 불가 축의 정직한 표기 (판정 아님) ────────────────────────────────
|
|
216
|
+
[ "$CAP_writes" = "read-only" ] && \
|
|
217
|
+
printf ' ⚠️ writes: read-only 는 **등록자 주장**이다 — 이 검사기는 그 진위를 못 잰다(헤더 §잔여 참조)\n'
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
# ── self-test (known-pair: 통과해야 할 선언 1 · 막혀야 할 선언 6) ─────────────
|
|
221
|
+
_self_test() {
|
|
222
|
+
local T; T="$(mktemp -d)"; local pass=0 fail=0
|
|
223
|
+
_t() {
|
|
224
|
+
local o; o="$(bash "$0" "$3" 2>&1)"; local r=$?
|
|
225
|
+
if [ "$r" = "$2" ]; then pass=$((pass+1)); printf ' ✅ %-34s rc=%s\n' "$1" "$r"
|
|
226
|
+
else fail=$((fail+1)); printf ' ❌ %-34s rc=%s (기대 %s)\n%s\n' "$1" "$r" "$2" "$o"; fi
|
|
227
|
+
}
|
|
228
|
+
printf 'capability_registry_check --self-test\n'
|
|
229
|
+
cat > "$T/good.cap" <<EOF
|
|
230
|
+
id: selftest:true-false
|
|
231
|
+
entry: bash $T/probe.sh
|
|
232
|
+
requires_cwd: $T
|
|
233
|
+
verdict_channel: exit
|
|
234
|
+
verdict_enum: 0=PASS 1=FAIL 3=DID_NOT_RUN
|
|
235
|
+
approval: auto
|
|
236
|
+
reversibility: reversible
|
|
237
|
+
residency: public
|
|
238
|
+
degrade: fail-closed
|
|
239
|
+
tier_floor: none
|
|
240
|
+
writes: read-only
|
|
241
|
+
judge: mechanical
|
|
242
|
+
verdict_binding: FAIL
|
|
243
|
+
calibration_positive_args: ok
|
|
244
|
+
calibration_positive_expect: PASS
|
|
245
|
+
calibration_negative_args: no
|
|
246
|
+
calibration_negative_expect: FAIL
|
|
247
|
+
EOF
|
|
248
|
+
printf '#!/bin/sh\n[ "$1" = ok ] && exit 0\nexit 1\n' > "$T/probe.sh"
|
|
249
|
+
_t "known-positive: 온전한 선언" 0 "$T/good.cap"
|
|
250
|
+
sed 's/^verdict_enum: .*/verdict_enum: 0=PASS 1=FAIL/' "$T/good.cap" > "$T/no_dnr.cap"
|
|
251
|
+
_t "추가조항: 「안 돌았다」 없음" 1 "$T/no_dnr.cap"
|
|
252
|
+
sed 's/^calibration_negative_expect: .*/calibration_negative_expect: PASS/' "$T/good.cap" > "$T/badpair.cap"
|
|
253
|
+
_t "M4: 음성이 선언과 불일치" 1 "$T/badpair.cap"
|
|
254
|
+
grep -v '^calibration_' "$T/good.cap" > "$T/nocal.cap"
|
|
255
|
+
_t "M4: 캘리브레이션 쌍 미선언" 1 "$T/nocal.cap"
|
|
256
|
+
sed 's/^judge: .*/degrad: fail-closed/' "$T/good.cap" > "$T/typo.cap"
|
|
257
|
+
_t "D3: 오타 축은 무시 아닌 실패" 1 "$T/typo.cap"
|
|
258
|
+
sed "s|^entry: .*|entry: bash $T/probe.sh \| grep x|" "$T/good.cap" > "$T/shellstr.cap"
|
|
259
|
+
_t "M1: entry 가 셸 문자열" 1 "$T/shellstr.cap"
|
|
260
|
+
sed 's|^requires_cwd: .*|requires_cwd: relative/path|' "$T/good.cap" > "$T/relcwd.cap"
|
|
261
|
+
_t "M5: requires_cwd 상대경로" 1 "$T/relcwd.cap"
|
|
262
|
+
printf '\n 통과 %d · 실패 %d\n' "$pass" "$fail"
|
|
263
|
+
rm -rf "$T"
|
|
264
|
+
[ "$fail" -eq 0 ] || return 1
|
|
265
|
+
return 0
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
[ "${1:-}" = "--self-test" ] && { _self_test; exit $?; }
|
|
269
|
+
[ $# -ge 1 ] || _die "capfile 인자가 없다. 사용법: $0 <capfile> [<capfile>...]"
|
|
270
|
+
|
|
271
|
+
printf 'capability_registry_check — M1–M5 + 추가조항 (등록 시점)\n'
|
|
272
|
+
for f in "$@"; do _check_one "$f"; done
|
|
273
|
+
|
|
274
|
+
printf '\n'
|
|
275
|
+
if [ "$FAILED" -eq 0 ]; then
|
|
276
|
+
printf '✅ REGISTRABLE — 전 capfile 이 M1–M5 + 추가조항 통과\n'; exit "$RC_OK"
|
|
277
|
+
else
|
|
278
|
+
printf '❌ REJECTED — 등록 불가. 그 표면은 오늘 있던 자리(dispatch 엔트리)에 그대로 남는다 — 손실이 아니다\n'
|
|
279
|
+
exit "$RC_REJECT"
|
|
280
|
+
fi
|