@cairnkeep/cli 1.0.2
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/AGENTS.md +45 -0
- package/CONTRIBUTING.md +49 -0
- package/LICENSE +201 -0
- package/README.md +149 -0
- package/SECURITY.md +37 -0
- package/bin/cairn +73 -0
- package/claude/agents/code-reviewer.md +174 -0
- package/claude/agents/security-investigator.md +112 -0
- package/claude/agents/security-target-selector.md +90 -0
- package/claude/agents/security-validator.md +97 -0
- package/claude/agents/wiki-ingester.md +108 -0
- package/claude/agents/wiki-lint-auditor.md +94 -0
- package/claude/agents/wiki-query-analyst.md +84 -0
- package/claude/commands/context-explore.md +40 -0
- package/claude/commands/graphify.md +150 -0
- package/claude/commands/memory-review.md +85 -0
- package/claude/commands/memory-sync.md +71 -0
- package/claude/commands/recall.md +32 -0
- package/claude/commands/remember.md +43 -0
- package/claude/commands/repo-review.md +191 -0
- package/claude/commands/security-audit.md +174 -0
- package/claude/commands/wiki-ingest.md +112 -0
- package/claude/commands/wiki-lint.md +86 -0
- package/claude/commands/wiki-query.md +92 -0
- package/claude/hooks/context-explore-pretask.sh +93 -0
- package/claude/hooks/memory-capture.sh +59 -0
- package/claude/hooks/memory-recall.sh +84 -0
- package/claude/hooks/memory-wakeup.sh +45 -0
- package/docs/building-an-overlay.md +117 -0
- package/docs/domain-knowledge.md +86 -0
- package/docs/git-providers.md +36 -0
- package/docs/operating.md +366 -0
- package/mcp-memory-server/dist/embeddings.js +122 -0
- package/mcp-memory-server/dist/explore-cache.js +97 -0
- package/mcp-memory-server/dist/index.js +1256 -0
- package/mcp-memory-server/package.json +36 -0
- package/opencode/agents/code-reviewer.md +173 -0
- package/opencode/agents/security-investigator.md +111 -0
- package/opencode/agents/security-target-selector.md +89 -0
- package/opencode/agents/security-validator.md +96 -0
- package/opencode/agents/wiki-ingester.md +107 -0
- package/opencode/agents/wiki-lint-auditor.md +93 -0
- package/opencode/agents/wiki-query-analyst.md +83 -0
- package/opencode/command/code-review.md +199 -0
- package/opencode/command/context-explore.md +42 -0
- package/opencode/command/graphify.md +153 -0
- package/opencode/command/memory-review.md +44 -0
- package/opencode/command/memory-sync.md +94 -0
- package/opencode/command/recall.md +38 -0
- package/opencode/command/remember.md +46 -0
- package/opencode/command/security-audit.md +80 -0
- package/opencode/command/wiki-ingest.md +55 -0
- package/opencode/command/wiki-lint.md +51 -0
- package/opencode/command/wiki-query.md +50 -0
- package/opencode/plugins/memory-capture.ts +188 -0
- package/opencode/plugins/memory-recall.ts +132 -0
- package/opencode/plugins/memory-wakeup.ts +102 -0
- package/opencode/templates/security-finding-register.yaml.template +51 -0
- package/opencode/templates/security-issue.md.template +43 -0
- package/opencode/templates/security-policy.md.template +29 -0
- package/opencode/templates/security-report.md.template +43 -0
- package/opencode/templates/wiki-contradictions.md.template +31 -0
- package/opencode/templates/wiki-index.md.template +40 -0
- package/opencode/templates/wiki-lint-report.md.template +38 -0
- package/opencode/templates/wiki-log.md.template +11 -0
- package/opencode/templates/wiki-policy.md.template +47 -0
- package/opencode/templates/wiki-query-answer.md.template +26 -0
- package/opencode/templates/wiki-source-summary.md.template +33 -0
- package/opencode/workflows/memory-review-workflow.md +66 -0
- package/opencode/workflows/security-audit-workflow.md +197 -0
- package/opencode/workflows/wiki-ingest-workflow.md +149 -0
- package/opencode/workflows/wiki-lint-workflow.md +128 -0
- package/opencode/workflows/wiki-query-workflow.md +122 -0
- package/package.json +56 -0
- package/scripts/bootstrap.sh +99 -0
- package/scripts/doctor.sh +91 -0
- package/scripts/install-audit-timer.sh +68 -0
- package/scripts/lib/assert-tool-event.mjs +50 -0
- package/scripts/memory-store.sh +71 -0
- package/scripts/memory-wiki-audit.sh +156 -0
- package/scripts/sync-claude-assets.sh +182 -0
- package/scripts/sync-opencode-explore-assets.sh +187 -0
- package/scripts/sync-opencode-graphify-assets.sh +152 -0
- package/scripts/sync-opencode-memory-assets.sh +160 -0
- package/scripts/sync-opencode-plugin-assets.sh +163 -0
- package/scripts/sync-opencode-security-assets.sh +195 -0
- package/scripts/sync-opencode-wiki-assets.sh +250 -0
- package/scripts/test-audit-timer.sh +29 -0
- package/scripts/test-bootstrap-untracked.sh +41 -0
- package/scripts/test-cli-dispatch.sh +21 -0
- package/scripts/test-doctor.sh +32 -0
- package/scripts/test-launcher-hooks.sh +66 -0
- package/scripts/test-memory-store.sh +44 -0
- package/scripts/test-remember-recall-assertions.sh +70 -0
- package/scripts/transcript-to-text.mjs +53 -0
- package/scripts/verify-docs-parity.sh +112 -0
- package/scripts/verify-explore-maturation.sh +288 -0
- package/scripts/verify-fastcontext-reliability.sh +624 -0
- package/scripts/verify-no-private-references.sh +145 -0
- package/scripts/verify-opencode-live-parity.sh +864 -0
- package/scripts/verify-routing-seam.sh +169 -0
- package/scripts/verify-token-savings-ab.sh +557 -0
- package/templates/PROJECT-INSTRUCTIONS.md.template +42 -0
- package/templates/alignment-gap-register.yaml.template +56 -0
- package/templates/alignment-policy.md.template +28 -0
- package/templates/alignment-pr-template.md.template +18 -0
- package/templates/alignment-report.md.template +56 -0
- package/templates/env.example.template +25 -0
- package/templates/graph-policy.md.template +37 -0
- package/templates/graphs-gitignore.template +3 -0
- package/templates/memory-wiki-audit.service.template +6 -0
- package/templates/memory-wiki-audit.timer.template +9 -0
- package/templates/memory.json.template +4 -0
- package/templates/planning-config.json.template +6 -0
- package/templates/project-brief.md.template +40 -0
- package/templates/security-finding-register.yaml.template +51 -0
- package/templates/security-issue.md.template +43 -0
- package/templates/security-policy.md.template +29 -0
- package/templates/security-report.md.template +43 -0
- package/templates/start-claude.sh.template +46 -0
- package/templates/start-opencode.sh.template +41 -0
- package/templates/structured-change-canvas.md.template +63 -0
- package/templates/wiki-contradictions.md.template +31 -0
- package/templates/wiki-index.md.template +40 -0
- package/templates/wiki-lint-report.md.template +38 -0
- package/templates/wiki-log.md.template +11 -0
- package/templates/wiki-policy.md.template +47 -0
- package/templates/wiki-query-answer.md.template +26 -0
- package/templates/wiki-source-summary.md.template +33 -0
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# FastContext raw-endpoint reliability probe (CTX-06 / Phase 6 D-01/D-02/D-03).
|
|
5
|
+
#
|
|
6
|
+
# Mirrors scripts/verify-opencode-live-parity.sh's shape (staged, env-driven,
|
|
7
|
+
# loopback-safe, generous timeouts, single exit-code pass/fail). Unlike that
|
|
8
|
+
# harness, this probe never touches opencode or the cairn-memory MCP at all —
|
|
9
|
+
# it drives the raw llama-server `/v1/chat/completions` endpoint directly with
|
|
10
|
+
# FastContext's own read/glob/grep tool schemas, isolating the variable under
|
|
11
|
+
# test (model + chat-template + --jinja + quant) from any execution loop.
|
|
12
|
+
#
|
|
13
|
+
# Every value that could identify the operator's real infra (the endpoint URL,
|
|
14
|
+
# any Authorization header / API key) is read from the ambient shell at
|
|
15
|
+
# runtime and is never hardcoded, echoed, or written to the evidence log
|
|
16
|
+
# (DEC-no-private-references). Only a loopback default is committed.
|
|
17
|
+
#
|
|
18
|
+
# --self-test is the offline, no-network backstop: it exercises the record-
|
|
19
|
+
# and-check logic (gate #1 /props recording, gate #2 per-turn assertion, and
|
|
20
|
+
# the refined-D-05 verdict scoring) against canned fixture JSON so the gate
|
|
21
|
+
# discriminates PASS from FAIL before any live endpoint is ever reached.
|
|
22
|
+
|
|
23
|
+
usage() {
|
|
24
|
+
cat <<'EOF'
|
|
25
|
+
Usage: verify-fastcontext-reliability.sh --self-test
|
|
26
|
+
verify-fastcontext-reliability.sh --props-only
|
|
27
|
+
verify-fastcontext-reliability.sh --full
|
|
28
|
+
verify-fastcontext-reliability.sh -h|--help
|
|
29
|
+
|
|
30
|
+
Probes a llama-server-hosted FastContext GGUF for tool-call reliability
|
|
31
|
+
(CTX-06). The go/no-go verdict is anchored to the empirical per-turn gate
|
|
32
|
+
(finish_reason == "tool_calls" on every turn of a multi-prompt, multi-turn
|
|
33
|
+
matrix); the /props chat_template_tool_use field is recorded as evidence
|
|
34
|
+
alongside the verdict, not used as an automatic blocker (refined D-05).
|
|
35
|
+
|
|
36
|
+
Options:
|
|
37
|
+
--self-test
|
|
38
|
+
Offline: exercises the /props recording, the strict per-turn tool-call
|
|
39
|
+
assertion, and the refined-D-05 verdict logic against canned fixture
|
|
40
|
+
JSON. No network access, no live model required. This is the automated
|
|
41
|
+
Nyquist backstop for the operator-gated live probe below.
|
|
42
|
+
--props-only
|
|
43
|
+
Live: fetches GET /props from FASTCONTEXT_PROBE_URL and records
|
|
44
|
+
chat_template_tool_use / chat_template_caps / build_info / the raw
|
|
45
|
+
chat_template verbatim to the evidence log (gate #1 only, no chat
|
|
46
|
+
round-trips).
|
|
47
|
+
--full
|
|
48
|
+
Live: gate #1 (/props) plus the full >=5-prompt x >=3-turn tool-call
|
|
49
|
+
matrix (gate #2), then computes and records the go/no-go verdict.
|
|
50
|
+
Exits 0 only on a GO verdict.
|
|
51
|
+
-h, --help
|
|
52
|
+
Show this help text.
|
|
53
|
+
|
|
54
|
+
Environment:
|
|
55
|
+
FASTCONTEXT_PROBE_URL OpenAI-compatible base URL for the deployed
|
|
56
|
+
llama-server. Defaults to a loopback address;
|
|
57
|
+
the operator supplies the real endpoint at
|
|
58
|
+
runtime. Never echoed to stdout or the evidence
|
|
59
|
+
log — only a presence indicator is logged.
|
|
60
|
+
FASTCONTEXT_MODEL_ALIAS Model alias sent in each chat-completion request.
|
|
61
|
+
Defaults to the FastContext RL alias documented
|
|
62
|
+
in 06-RESEARCH.md; override for a different quant.
|
|
63
|
+
FASTCONTEXT_EVIDENCE_LOG Path to the raw evidence log. Defaults to a
|
|
64
|
+
*.log file under this phase's directory, which
|
|
65
|
+
the repo .gitignore already excludes from commits.
|
|
66
|
+
EOF
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
70
|
+
|
|
71
|
+
FASTCONTEXT_PROBE_URL_DEFAULT="http://127.0.0.1:8081/v1"
|
|
72
|
+
FASTCONTEXT_PROBE_URL="${FASTCONTEXT_PROBE_URL:-$FASTCONTEXT_PROBE_URL_DEFAULT}"
|
|
73
|
+
FASTCONTEXT_MODEL_ALIAS="${FASTCONTEXT_MODEL_ALIAS:-fastcontext-4b-rl}"
|
|
74
|
+
FASTCONTEXT_EVIDENCE_LOG="${FASTCONTEXT_EVIDENCE_LOG:-$ROOT_DIR/.planning/phases/06-fastcontext-reliability-spike/06-EVIDENCE.log}"
|
|
75
|
+
|
|
76
|
+
# System prompt copied verbatim from 06-RESEARCH.md finding #2's curl example
|
|
77
|
+
# (itself copied from token-miser/src/explore/client.rs) - inventing an
|
|
78
|
+
# approximate prompt would confound a no-go with a schema/prompt-mismatch
|
|
79
|
+
# artifact (RESEARCH #3 / Pitfall 5).
|
|
80
|
+
SYSTEM_PROMPT="You are a repository exploration agent. Locate the code relevant to the user's task using the read, glob, and grep tools. Do not attempt to solve the task."
|
|
81
|
+
|
|
82
|
+
# Tool schemas copied verbatim from 06-RESEARCH.md finding #2/#3 (the
|
|
83
|
+
# token-miser client.rs read/glob/grep shape) - field names, descriptions,
|
|
84
|
+
# and required lists are not to be re-derived approximately.
|
|
85
|
+
TOOL_SCHEMAS_JSON=$(cat <<'EOF'
|
|
86
|
+
[
|
|
87
|
+
{
|
|
88
|
+
"type": "function",
|
|
89
|
+
"function": {
|
|
90
|
+
"name": "read",
|
|
91
|
+
"description": "Read a file's contents with line numbers. Use offset/limit to read a span.",
|
|
92
|
+
"parameters": {
|
|
93
|
+
"type": "object",
|
|
94
|
+
"properties": {
|
|
95
|
+
"path": {"type": "string", "description": "Path relative to the repo root."},
|
|
96
|
+
"offset": {"type": "integer", "description": "1-based first line to read."},
|
|
97
|
+
"limit": {"type": "integer", "description": "Number of lines to read."}
|
|
98
|
+
},
|
|
99
|
+
"required": ["path"]
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"type": "function",
|
|
105
|
+
"function": {
|
|
106
|
+
"name": "glob",
|
|
107
|
+
"description": "List files matching a glob pattern (gitignore-aware).",
|
|
108
|
+
"parameters": {
|
|
109
|
+
"type": "object",
|
|
110
|
+
"properties": {
|
|
111
|
+
"pattern": {"type": "string"},
|
|
112
|
+
"base": {"type": "string", "description": "Optional subdirectory to search under."}
|
|
113
|
+
},
|
|
114
|
+
"required": ["pattern"]
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"type": "function",
|
|
120
|
+
"function": {
|
|
121
|
+
"name": "grep",
|
|
122
|
+
"description": "Regex search across the repo (gitignore-aware). Returns path:line:content.",
|
|
123
|
+
"parameters": {
|
|
124
|
+
"type": "object",
|
|
125
|
+
"properties": {
|
|
126
|
+
"regex": {"type": "string"},
|
|
127
|
+
"path": {"type": "string", "description": "Optional single file to search."},
|
|
128
|
+
"glob": {"type": "string", "description": "Optional glob to limit which files are searched."}
|
|
129
|
+
},
|
|
130
|
+
"required": ["regex"]
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
]
|
|
135
|
+
EOF
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# >=5 distinct exploration prompts targeting cairnkeep's own repo (the same
|
|
139
|
+
# corpus Phase 9's A/B will measure), per CONTEXT.md Claude's Discretion.
|
|
140
|
+
# Plausible exploration asks only - the probe never executes them; every
|
|
141
|
+
# turn is answered with a static stubbed tool result regardless of what the
|
|
142
|
+
# model actually asked for (Pattern 1).
|
|
143
|
+
EXPLORATION_PROMPTS=(
|
|
144
|
+
"Where is scope path containment implemented in this repo?"
|
|
145
|
+
"Where is the AgentFS project scope resolved in this repo?"
|
|
146
|
+
"Where is the git-provider abstraction configured in this repo?"
|
|
147
|
+
"Where does the OpenCode memory-wakeup plugin live in this repo?"
|
|
148
|
+
"Where do the asset-sync scripts render the infra-root placeholder value?"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Globals populated by inspect_props()/record_props_evidence() and by
|
|
152
|
+
# run_turn_matrix()/compute_verdict() (Tasks 2-3). Declared here with safe
|
|
153
|
+
# defaults because `set -u` treats an unset reference as an error.
|
|
154
|
+
GATE1_STATUS=""
|
|
155
|
+
PROPS_RAW=""
|
|
156
|
+
MATRIX_TOTAL=0
|
|
157
|
+
MATRIX_PASS=0
|
|
158
|
+
MATRIX_RESULTS=()
|
|
159
|
+
VERDICT=""
|
|
160
|
+
|
|
161
|
+
# validate_probe_url(url): refuses to run against a value that doesn't look
|
|
162
|
+
# like an http(s) URL (T-06-03 / V5 input validation) before any curl use.
|
|
163
|
+
# Never includes the URL value itself in the error message (T-06-02).
|
|
164
|
+
validate_probe_url() {
|
|
165
|
+
local url="$1"
|
|
166
|
+
if [[ ! "$url" =~ ^https?:// ]]; then
|
|
167
|
+
echo "FATAL: FASTCONTEXT_PROBE_URL does not look like an http(s) URL (value withheld)" >&2
|
|
168
|
+
exit 1
|
|
169
|
+
fi
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
# log_endpoint_presence(): echoes only whether FASTCONTEXT_PROBE_URL was
|
|
173
|
+
# overridden from the loopback default — never the value itself (T-06-02,
|
|
174
|
+
# mirrors log_env_presence() in verify-opencode-live-parity.sh).
|
|
175
|
+
log_endpoint_presence() {
|
|
176
|
+
local overridden="no"
|
|
177
|
+
[[ "$FASTCONTEXT_PROBE_URL" != "$FASTCONTEXT_PROBE_URL_DEFAULT" ]] && overridden="yes"
|
|
178
|
+
echo "[env] FASTCONTEXT_PROBE_URL overridden from loopback default: $overridden" >&2
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
# append_evidence(line): appends one line to the evidence log, creating the
|
|
182
|
+
# parent directory if needed. Callers are responsible for never passing the
|
|
183
|
+
# endpoint URL or a secret through this helper.
|
|
184
|
+
append_evidence() {
|
|
185
|
+
local line="$1"
|
|
186
|
+
mkdir -p "$(dirname "$FASTCONTEXT_EVIDENCE_LOG")"
|
|
187
|
+
echo "$line" >> "$FASTCONTEXT_EVIDENCE_LOG"
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
# record_props_evidence(props_json, log_target): records chat_template_tool_use
|
|
191
|
+
# (present-or-absent), chat_template_caps, build_info, and the raw
|
|
192
|
+
# chat_template verbatim to log_target (if non-empty), sets the GATE1_STATUS
|
|
193
|
+
# global, and echoes the gate-1 line to stdout (used by self-test to assert
|
|
194
|
+
# on the recorded outcome without a live network call).
|
|
195
|
+
record_props_evidence() {
|
|
196
|
+
local props="$1"
|
|
197
|
+
local log_target="${2:-}"
|
|
198
|
+
local gate1_line
|
|
199
|
+
|
|
200
|
+
if echo "$props" | jq -e 'has("chat_template_tool_use")' >/dev/null 2>&1; then
|
|
201
|
+
GATE1_STATUS="PRESENT"
|
|
202
|
+
gate1_line="[gate-1] chat_template_tool_use PRESENT"
|
|
203
|
+
else
|
|
204
|
+
GATE1_STATUS="ABSENT"
|
|
205
|
+
gate1_line="[gate-1] chat_template_tool_use ABSENT (expected for a single-unified-template Qwen3-family GGUF - see 06-RESEARCH.md finding #1; recorded as evidence, NOT a script bug and NOT an auto-no-go per refined D-05)"
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
if [[ -n "$log_target" ]]; then
|
|
209
|
+
mkdir -p "$(dirname "$log_target")"
|
|
210
|
+
{
|
|
211
|
+
echo "=== /props snapshot ($(date -u +%FT%TZ)) ==="
|
|
212
|
+
echo "$props" | jq '{chat_template_tool_use, chat_template_caps, build_info}'
|
|
213
|
+
echo "raw chat_template:"
|
|
214
|
+
echo "$props" | jq -r '.chat_template // "MISSING"'
|
|
215
|
+
echo "$gate1_line"
|
|
216
|
+
} >> "$log_target"
|
|
217
|
+
fi
|
|
218
|
+
|
|
219
|
+
echo "$gate1_line"
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
# inspect_props(url): GET {url}/props (falling back to {url%/v1}/props),
|
|
223
|
+
# records the raw payload verbatim via record_props_evidence(), and sets
|
|
224
|
+
# PROPS_RAW for later use (verdict finalize, Task 3). An absent
|
|
225
|
+
# chat_template_tool_use field never triggers a retry or a non-zero return
|
|
226
|
+
# on its own.
|
|
227
|
+
inspect_props() {
|
|
228
|
+
local url="$1"
|
|
229
|
+
local props
|
|
230
|
+
props=$(curl -sf --max-time 15 "${url}/props" 2>/dev/null) || \
|
|
231
|
+
props=$(curl -sf --max-time 15 "${url%/v1}/props" 2>/dev/null) || {
|
|
232
|
+
echo "[inspect_props] FAIL: unable to reach a /props endpoint" >&2
|
|
233
|
+
return 1
|
|
234
|
+
}
|
|
235
|
+
PROPS_RAW="$props"
|
|
236
|
+
record_props_evidence "$props" "$FASTCONTEXT_EVIDENCE_LOG"
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
# assert_tool_call_turn(response_json): PASS only when finish_reason ==
|
|
240
|
+
# "tool_calls" AND message.tool_calls is a non-empty array - gated strictly
|
|
241
|
+
# on jq JSON structure, never a substring match on content. The stale-doc
|
|
242
|
+
# value "tool" (singular, docs/function-calling.md's example - confirmed
|
|
243
|
+
# stale against the current llama.cpp source in 06-RESEARCH.md finding #2)
|
|
244
|
+
# is deliberately NOT accepted as a pass; do not "fix" this to match the
|
|
245
|
+
# stale doc if a future build is observed to emit it.
|
|
246
|
+
assert_tool_call_turn() {
|
|
247
|
+
local response_json="$1"
|
|
248
|
+
local finish_reason n_calls
|
|
249
|
+
|
|
250
|
+
finish_reason=$(echo "$response_json" | jq -r '.choices[0].finish_reason // "missing"')
|
|
251
|
+
n_calls=$(echo "$response_json" | jq '.choices[0].message.tool_calls | length? // 0')
|
|
252
|
+
|
|
253
|
+
if [[ "$finish_reason" == "tool_calls" && "$n_calls" -gt 0 ]]; then
|
|
254
|
+
return 0
|
|
255
|
+
fi
|
|
256
|
+
return 1
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
# tool_result_message(tool_call_id): the stubbed role:"tool" reply appended
|
|
260
|
+
# after every observed tool_calls turn (Pattern 1 - the probe never executes
|
|
261
|
+
# real read/glob/grep against the filesystem; it only observes whether the
|
|
262
|
+
# model reliably emits well-formed tool calls turn after turn).
|
|
263
|
+
tool_result_message() {
|
|
264
|
+
local tool_call_id="$1"
|
|
265
|
+
jq -n --arg id "$tool_call_id" '{
|
|
266
|
+
role: "tool",
|
|
267
|
+
tool_call_id: $id,
|
|
268
|
+
content: "1: fn placeholder() {}\n2: // stubbed tool result for reliability probing\n3: }"
|
|
269
|
+
}'
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
# normalize_tool_call_ids(message_json, id_prefix): guarantees every tool_call
|
|
273
|
+
# in an assistant message carries a non-empty id, synthesizing "<prefix>_<idx>"
|
|
274
|
+
# for any that lack one. Fixes CR-01: the replay previously answered only
|
|
275
|
+
# tool_calls[0].id, so a turn with PARALLEL tool_calls (or a missing .id)
|
|
276
|
+
# desynced the re-sent OpenAI transcript and biased later turns toward
|
|
277
|
+
# narration - a false NO-GO. With ids normalized, exactly one role:"tool"
|
|
278
|
+
# reply is matched to each call.
|
|
279
|
+
normalize_tool_call_ids() {
|
|
280
|
+
local message_json="$1" prefix="$2"
|
|
281
|
+
echo "$message_json" | jq --arg pfx "$prefix" '
|
|
282
|
+
if (.tool_calls | type) == "array" then
|
|
283
|
+
.tool_calls |= (to_entries | map(
|
|
284
|
+
.value + (if ((.value.id // "") == "")
|
|
285
|
+
then {id: ($pfx + "_" + (.key | tostring))}
|
|
286
|
+
else {} end)))
|
|
287
|
+
else . end'
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
# run_turn_matrix(url, turns_per_prompt): the gate #2 matrix. For each
|
|
291
|
+
# EXPLORATION_PROMPTS entry, runs a genuine multi-turn loop over an
|
|
292
|
+
# accumulating messages array (system + user, then each assistant turn and
|
|
293
|
+
# stubbed tool result appended in place - never rebuilt as a fresh length-2
|
|
294
|
+
# array between turns, Pitfall 3). Populates MATRIX_TOTAL/MATRIX_PASS/
|
|
295
|
+
# MATRIX_RESULTS for compute_verdict() and finalize_evidence_log() (Task 3).
|
|
296
|
+
run_turn_matrix() {
|
|
297
|
+
local url="$1"
|
|
298
|
+
local turns_per_prompt="${2:-3}"
|
|
299
|
+
local prompt prompt_idx=0 turn_idx
|
|
300
|
+
local messages request response n_calls tool_call_id assistant_msg tool_msg result
|
|
301
|
+
|
|
302
|
+
MATRIX_TOTAL=0
|
|
303
|
+
MATRIX_PASS=0
|
|
304
|
+
MATRIX_RESULTS=()
|
|
305
|
+
|
|
306
|
+
for prompt in "${EXPLORATION_PROMPTS[@]}"; do
|
|
307
|
+
prompt_idx=$((prompt_idx + 1))
|
|
308
|
+
messages=$(jq -n --arg sys "$SYSTEM_PROMPT" --arg user "$prompt" \
|
|
309
|
+
'[{role: "system", content: $sys}, {role: "user", content: $user}]')
|
|
310
|
+
|
|
311
|
+
for ((turn_idx = 1; turn_idx <= turns_per_prompt; turn_idx++)); do
|
|
312
|
+
MATRIX_TOTAL=$((MATRIX_TOTAL + 1))
|
|
313
|
+
|
|
314
|
+
request=$(jq -n --argjson messages "$messages" --argjson tools "$TOOL_SCHEMAS_JSON" \
|
|
315
|
+
--arg model "$FASTCONTEXT_MODEL_ALIAS" \
|
|
316
|
+
'{model: $model, temperature: 0, stream: false, tool_choice: "auto", tools: $tools, messages: $messages}')
|
|
317
|
+
|
|
318
|
+
if ! response=$(curl -sf --max-time 60 "${url}/chat/completions" \
|
|
319
|
+
-H "Content-Type: application/json" -d "$request" 2>/dev/null); then
|
|
320
|
+
echo "[matrix] FAIL: request error at prompt $prompt_idx turn $turn_idx" >&2
|
|
321
|
+
MATRIX_RESULTS+=("$prompt_idx|$turn_idx|FAIL|request-error|0")
|
|
322
|
+
append_evidence "[matrix] prompt=$prompt_idx turn=$turn_idx result=FAIL reason=request-error"
|
|
323
|
+
continue
|
|
324
|
+
fi
|
|
325
|
+
|
|
326
|
+
# WR-01: a 2xx body that is not valid JSON must be recorded as a FAIL,
|
|
327
|
+
# not abort the whole probe. Under `set -euo pipefail` a bare jq parse
|
|
328
|
+
# error downstream would exit the script mid-matrix, so validate up front
|
|
329
|
+
# and record-then-continue instead of silently dying.
|
|
330
|
+
if ! echo "$response" | jq -e . >/dev/null 2>&1; then
|
|
331
|
+
echo "[matrix] FAIL: malformed JSON body at prompt $prompt_idx turn $turn_idx" >&2
|
|
332
|
+
MATRIX_RESULTS+=("$prompt_idx|$turn_idx|FAIL|malformed-json|0")
|
|
333
|
+
append_evidence "[matrix] prompt=$prompt_idx turn=$turn_idx result=FAIL reason=malformed-json"
|
|
334
|
+
continue
|
|
335
|
+
fi
|
|
336
|
+
|
|
337
|
+
# Log the raw tool-call arguments even though the result is stubbed -
|
|
338
|
+
# a free early signal for Phase 7 planning (Pitfall 4).
|
|
339
|
+
echo "$response" | jq -c '.choices[0].message.tool_calls[]?.function.arguments // empty' 2>/dev/null \
|
|
340
|
+
| while IFS= read -r args; do
|
|
341
|
+
append_evidence "[tool-call-args] prompt=$prompt_idx turn=$turn_idx args=$args"
|
|
342
|
+
done
|
|
343
|
+
|
|
344
|
+
n_calls=$(echo "$response" | jq '.choices[0].message.tool_calls | length? // 0')
|
|
345
|
+
if assert_tool_call_turn "$response"; then
|
|
346
|
+
MATRIX_PASS=$((MATRIX_PASS + 1))
|
|
347
|
+
result="PASS"
|
|
348
|
+
else
|
|
349
|
+
result="FAIL"
|
|
350
|
+
fi
|
|
351
|
+
local finish_reason
|
|
352
|
+
finish_reason=$(echo "$response" | jq -r '.choices[0].finish_reason // "missing"')
|
|
353
|
+
MATRIX_RESULTS+=("$prompt_idx|$turn_idx|$result|$finish_reason|$n_calls")
|
|
354
|
+
append_evidence "[matrix] prompt=$prompt_idx turn=$turn_idx result=$result finish_reason=$finish_reason tool_calls=$n_calls"
|
|
355
|
+
|
|
356
|
+
# Append this turn to the accumulating conversation and continue -
|
|
357
|
+
# never reset to a fresh length-2 messages array (Pitfall 3). Normalize
|
|
358
|
+
# tool_call ids first (CR-01) so the appended assistant message and its
|
|
359
|
+
# replies agree on every id, including any the model omitted.
|
|
360
|
+
assistant_msg=$(echo "$response" | jq '.choices[0].message')
|
|
361
|
+
assistant_msg=$(normalize_tool_call_ids "$assistant_msg" "call_p${prompt_idx}_t${turn_idx}")
|
|
362
|
+
messages=$(echo "$messages" | jq --argjson m "$assistant_msg" '. + [$m]')
|
|
363
|
+
|
|
364
|
+
# CR-01: reply to EVERY tool_call with its own role:"tool" message, not
|
|
365
|
+
# just tool_calls[0]. Parallel calls (common for Qwen3-family exploration
|
|
366
|
+
# models) otherwise leave dangling calls and desync the next turn.
|
|
367
|
+
while IFS= read -r tool_call_id; do
|
|
368
|
+
[[ -z "$tool_call_id" ]] && continue
|
|
369
|
+
tool_msg=$(tool_result_message "$tool_call_id")
|
|
370
|
+
messages=$(echo "$messages" | jq --argjson t "$tool_msg" '. + [$t]')
|
|
371
|
+
done < <(echo "$assistant_msg" | jq -r '.tool_calls[]?.id // empty')
|
|
372
|
+
done
|
|
373
|
+
done
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
# compute_verdict(): the refined-D-05 scoring ("Evidence, not hard gate",
|
|
377
|
+
# this plan's <d05_refinement>). GO only when every turn in the matrix
|
|
378
|
+
# passed assert_tool_call_turn (gate #2, D-06 hard blocker on any single
|
|
379
|
+
# narration/malformed turn). Gate #1 (chat_template_tool_use, from
|
|
380
|
+
# GATE1_STATUS) is recorded as evidence alongside the verdict and MUST NOT
|
|
381
|
+
# by itself force a NO-GO - its absence is expected and architecturally
|
|
382
|
+
# explainable for a single-unified-template Qwen3-family GGUF.
|
|
383
|
+
compute_verdict() {
|
|
384
|
+
if [[ "$MATRIX_TOTAL" -gt 0 && "$MATRIX_PASS" -eq "$MATRIX_TOTAL" ]]; then
|
|
385
|
+
VERDICT="GO"
|
|
386
|
+
else
|
|
387
|
+
VERDICT="NO-GO"
|
|
388
|
+
fi
|
|
389
|
+
|
|
390
|
+
echo "[verdict] gate-1 chat_template_tool_use: ${GATE1_STATUS:-UNKNOWN} (absence alone never forces NO-GO - refined D-05)"
|
|
391
|
+
echo "[verdict] gate-2 tool-call matrix: ${MATRIX_PASS}/${MATRIX_TOTAL} turns passed"
|
|
392
|
+
echo "[verdict] VERDICT: $VERDICT"
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
# run_token_miser_corroboration(): optional stage 3 (D-04). Corroboration
|
|
396
|
+
# only - never the verdict basis, and its absence is never a failure.
|
|
397
|
+
run_token_miser_corroboration() {
|
|
398
|
+
if command -v token_miser >/dev/null 2>&1; then
|
|
399
|
+
local out
|
|
400
|
+
echo "[stage-3] token_miser found on PATH - running corroboration explore against this repo"
|
|
401
|
+
out=$(cd "$ROOT_DIR" && timeout 60 token_miser explore --repo-root . 2>&1) || true
|
|
402
|
+
# WR-02: token_miser output can echo endpoint/env config; keep the raw text
|
|
403
|
+
# out of the evidence log (which advertises URL/secret-free) - surface it on
|
|
404
|
+
# stderr for the operator and record only a redacted status line.
|
|
405
|
+
echo "$out" >&2
|
|
406
|
+
append_evidence "[stage-3] token_miser corroboration ran ($(printf '%s' "$out" | wc -l) lines); raw output withheld from evidence log to preserve the URL/secret-free invariant"
|
|
407
|
+
else
|
|
408
|
+
local msg="[stage-3] token_miser absent from PATH - optional corroboration skipped (D-04); verdict remains anchored to the raw endpoint (D-03)"
|
|
409
|
+
echo "$msg"
|
|
410
|
+
append_evidence "$msg"
|
|
411
|
+
fi
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
# finalize_evidence_log(): appends the per-turn results table and the D-08
|
|
415
|
+
# pinned-combination block (build_info, chat_template excerpt, gate-1
|
|
416
|
+
# status from /props) to the evidence log, then re-affirms the URL/secret
|
|
417
|
+
# scrub - only a presence indicator is ever written, never the endpoint
|
|
418
|
+
# value or a bearer token.
|
|
419
|
+
finalize_evidence_log() {
|
|
420
|
+
{
|
|
421
|
+
echo "=== per-turn results ($(date -u +%FT%TZ)) ==="
|
|
422
|
+
echo "prompt|turn|result|finish_reason|tool_calls_count"
|
|
423
|
+
local r
|
|
424
|
+
for r in "${MATRIX_RESULTS[@]:-}"; do
|
|
425
|
+
[[ -n "$r" ]] && echo "$r"
|
|
426
|
+
done
|
|
427
|
+
|
|
428
|
+
echo "=== pinned combination (D-08) ==="
|
|
429
|
+
if [[ -n "$PROPS_RAW" ]]; then
|
|
430
|
+
echo "build_info: $(echo "$PROPS_RAW" | jq -r '.build_info // "unknown"')"
|
|
431
|
+
echo "chat_template_tool_use: ${GATE1_STATUS:-UNKNOWN}"
|
|
432
|
+
echo "--jinja: assumed on (server-side flag, not queryable via /props)"
|
|
433
|
+
echo "chat_template excerpt (first 400 chars):"
|
|
434
|
+
echo "$PROPS_RAW" | jq -r '.chat_template // "unknown"' | head -c 400
|
|
435
|
+
echo
|
|
436
|
+
else
|
|
437
|
+
echo "PROPS_RAW unavailable - /props was not fetched this run"
|
|
438
|
+
fi
|
|
439
|
+
|
|
440
|
+
echo "=== verdict ==="
|
|
441
|
+
echo "gate-1 chat_template_tool_use: ${GATE1_STATUS:-UNKNOWN}"
|
|
442
|
+
echo "gate-2 matrix: ${MATRIX_PASS}/${MATRIX_TOTAL}"
|
|
443
|
+
echo "VERDICT: ${VERDICT:-UNKNOWN}"
|
|
444
|
+
echo "[scrub-check] this log never contains FASTCONTEXT_PROBE_URL's value or an Authorization header/API key"
|
|
445
|
+
} >> "$FASTCONTEXT_EVIDENCE_LOG"
|
|
446
|
+
|
|
447
|
+
echo "[finalize] evidence log updated at $FASTCONTEXT_EVIDENCE_LOG (URL/secrets never written)"
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
# self_test_verdict(): proves compute_verdict()'s refined-D-05 scoring
|
|
451
|
+
# offline - (a) an all-PASS matrix yields GO, (b) a single narration turn
|
|
452
|
+
# yields NO-GO (D-06 hard blocker), and (c) a chat_template_tool_use-ABSENT
|
|
453
|
+
# fixture with an all-PASS matrix still yields GO (the refined-D-05 guard:
|
|
454
|
+
# field absence alone must never block).
|
|
455
|
+
self_test_verdict() {
|
|
456
|
+
local failures=0
|
|
457
|
+
|
|
458
|
+
MATRIX_TOTAL=15
|
|
459
|
+
MATRIX_PASS=15
|
|
460
|
+
GATE1_STATUS="ABSENT"
|
|
461
|
+
compute_verdict >/dev/null
|
|
462
|
+
if [[ "$VERDICT" != "GO" ]]; then
|
|
463
|
+
echo "[self-test:verdict] FAIL: all-PASS matrix + gate1 ABSENT expected GO, got $VERDICT" >&2
|
|
464
|
+
failures=1
|
|
465
|
+
fi
|
|
466
|
+
|
|
467
|
+
MATRIX_TOTAL=15
|
|
468
|
+
MATRIX_PASS=14
|
|
469
|
+
GATE1_STATUS="ABSENT"
|
|
470
|
+
compute_verdict >/dev/null
|
|
471
|
+
if [[ "$VERDICT" != "NO-GO" ]]; then
|
|
472
|
+
echo "[self-test:verdict] FAIL: one-narration-turn matrix expected NO-GO, got $VERDICT" >&2
|
|
473
|
+
failures=1
|
|
474
|
+
fi
|
|
475
|
+
|
|
476
|
+
MATRIX_TOTAL=15
|
|
477
|
+
MATRIX_PASS=15
|
|
478
|
+
GATE1_STATUS="PRESENT"
|
|
479
|
+
compute_verdict >/dev/null
|
|
480
|
+
if [[ "$VERDICT" != "GO" ]]; then
|
|
481
|
+
echo "[self-test:verdict] FAIL: all-PASS matrix + gate1 PRESENT expected GO, got $VERDICT" >&2
|
|
482
|
+
failures=1
|
|
483
|
+
fi
|
|
484
|
+
|
|
485
|
+
if [[ "$failures" -eq 0 ]]; then
|
|
486
|
+
echo "[self-test:verdict] OK: GO/NO-GO logic verified, including the refined-D-05 absent-field guard"
|
|
487
|
+
return 0
|
|
488
|
+
fi
|
|
489
|
+
return 1
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
# self_test_matrix_assertion(): proves assert_tool_call_turn() discriminates
|
|
493
|
+
# a PASS fixture (finish_reason == "tool_calls" AND a non-empty tool_calls
|
|
494
|
+
# array) from a narration-FAIL fixture (finish_reason == "stop", content-only)
|
|
495
|
+
# - offline, no network call.
|
|
496
|
+
self_test_matrix_assertion() {
|
|
497
|
+
local fixture_pass fixture_fail
|
|
498
|
+
|
|
499
|
+
fixture_pass='{"choices":[{"finish_reason":"tool_calls","message":{"role":"assistant","content":null,"tool_calls":[{"id":"call_1","type":"function","function":{"name":"grep","arguments":"{\"regex\":\"selftest\"}"}}]}}]}'
|
|
500
|
+
fixture_fail='{"choices":[{"finish_reason":"stop","message":{"role":"assistant","content":"I will grep for selftest next..."}}]}'
|
|
501
|
+
|
|
502
|
+
if ! assert_tool_call_turn "$fixture_pass"; then
|
|
503
|
+
echo "[self-test:matrix] FAIL: PASS fixture was rejected by assert_tool_call_turn" >&2
|
|
504
|
+
return 1
|
|
505
|
+
fi
|
|
506
|
+
|
|
507
|
+
if assert_tool_call_turn "$fixture_fail"; then
|
|
508
|
+
echo "[self-test:matrix] FAIL: narration-FAIL fixture was incorrectly accepted by assert_tool_call_turn" >&2
|
|
509
|
+
return 1
|
|
510
|
+
fi
|
|
511
|
+
|
|
512
|
+
echo "[self-test:matrix] OK: PASS fixture accepted, narration-FAIL fixture rejected"
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
# self_test_props(): exercises record_props_evidence() against a
|
|
516
|
+
# chat_template_tool_use-ABSENT fixture and a -PRESENT fixture, entirely
|
|
517
|
+
# offline (no curl, no FASTCONTEXT_PROBE_URL reachability required), and
|
|
518
|
+
# asserts the absent case is recorded as evidence rather than erroring.
|
|
519
|
+
self_test_props() {
|
|
520
|
+
local tmp_log fixture_absent fixture_present out
|
|
521
|
+
|
|
522
|
+
tmp_log=$(mktemp)
|
|
523
|
+
|
|
524
|
+
fixture_absent='{"chat_template":"{% raw fixture, single unified template %}","chat_template_caps":{"tools":true},"build_info":"b0000-selftest"}'
|
|
525
|
+
fixture_present='{"chat_template":"{% raw fixture, default variant %}","chat_template_tool_use":"{% raw fixture, tool_use variant %}","chat_template_caps":{"tools":true},"build_info":"b0000-selftest"}'
|
|
526
|
+
|
|
527
|
+
out=$(record_props_evidence "$fixture_absent" "$tmp_log")
|
|
528
|
+
if ! echo "$out" | grep -q "ABSENT"; then
|
|
529
|
+
echo "[self-test:props] FAIL: absent-field fixture was not recorded as ABSENT" >&2
|
|
530
|
+
rm -f "$tmp_log"
|
|
531
|
+
return 1
|
|
532
|
+
fi
|
|
533
|
+
|
|
534
|
+
out=$(record_props_evidence "$fixture_present" "$tmp_log")
|
|
535
|
+
if ! echo "$out" | grep -q "PRESENT"; then
|
|
536
|
+
echo "[self-test:props] FAIL: present-field fixture was not recorded as PRESENT" >&2
|
|
537
|
+
rm -f "$tmp_log"
|
|
538
|
+
return 1
|
|
539
|
+
fi
|
|
540
|
+
|
|
541
|
+
rm -f "$tmp_log"
|
|
542
|
+
echo "[self-test:props] OK: absent-field and present-field /props fixtures both recorded without error"
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
# self_test_parallel_tool_replies(): proves the CR-01 fix offline - a turn with
|
|
546
|
+
# two tool_calls (one of them missing an id) is normalized so that every call
|
|
547
|
+
# gets a distinct, non-empty id, i.e. one role:"tool" reply per call rather than
|
|
548
|
+
# only the first. Guards against a regression back to the single-reply desync.
|
|
549
|
+
self_test_parallel_tool_replies() {
|
|
550
|
+
local fixture normalized ids id_count distinct_count
|
|
551
|
+
# Two parallel calls; the first deliberately omits "id" to exercise synthesis.
|
|
552
|
+
fixture='{"role":"assistant","content":null,"tool_calls":[{"type":"function","function":{"name":"grep","arguments":"{}"}},{"id":"call_b","type":"function","function":{"name":"read","arguments":"{}"}}]}'
|
|
553
|
+
normalized=$(normalize_tool_call_ids "$fixture" "call_pX_tY")
|
|
554
|
+
ids=$(echo "$normalized" | jq -r '.tool_calls[].id // ""')
|
|
555
|
+
id_count=$(printf '%s\n' "$ids" | grep -c .)
|
|
556
|
+
distinct_count=$(printf '%s\n' "$ids" | sort -u | grep -c .)
|
|
557
|
+
|
|
558
|
+
if printf '%s\n' "$ids" | grep -q '^$'; then
|
|
559
|
+
echo "[self-test:parallel] FAIL: a tool_call was left without an id: [$ids]" >&2
|
|
560
|
+
return 1
|
|
561
|
+
fi
|
|
562
|
+
if [[ "$id_count" -ne 2 || "$distinct_count" -ne 2 ]]; then
|
|
563
|
+
echo "[self-test:parallel] FAIL: expected 2 distinct tool_call ids, got count=$id_count distinct=$distinct_count" >&2
|
|
564
|
+
return 1
|
|
565
|
+
fi
|
|
566
|
+
echo "[self-test:parallel] OK: every tool_call (incl. missing-id) gets a distinct reply id (CR-01 guard)"
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
# run_self_test(): the offline self-test entry point. Task 1 covers only the
|
|
570
|
+
# /props recording path; Tasks 2-3 extend this with the tool-call-matrix
|
|
571
|
+
# assertion and verdict-scoring fixtures.
|
|
572
|
+
run_self_test() {
|
|
573
|
+
local failures=0
|
|
574
|
+
|
|
575
|
+
self_test_props || failures=1
|
|
576
|
+
self_test_matrix_assertion || failures=1
|
|
577
|
+
self_test_verdict || failures=1
|
|
578
|
+
self_test_parallel_tool_replies || failures=1
|
|
579
|
+
|
|
580
|
+
if [[ "$failures" -ne 0 ]]; then
|
|
581
|
+
echo "[self-test] FAILED" >&2
|
|
582
|
+
return 1
|
|
583
|
+
fi
|
|
584
|
+
echo "[self-test] PASSED"
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
main() {
|
|
588
|
+
case "${1:-}" in
|
|
589
|
+
--self-test)
|
|
590
|
+
run_self_test
|
|
591
|
+
;;
|
|
592
|
+
--props-only)
|
|
593
|
+
validate_probe_url "$FASTCONTEXT_PROBE_URL"
|
|
594
|
+
log_endpoint_presence
|
|
595
|
+
inspect_props "$FASTCONTEXT_PROBE_URL"
|
|
596
|
+
;;
|
|
597
|
+
--full)
|
|
598
|
+
validate_probe_url "$FASTCONTEXT_PROBE_URL"
|
|
599
|
+
log_endpoint_presence
|
|
600
|
+
if ! inspect_props "$FASTCONTEXT_PROBE_URL"; then
|
|
601
|
+
echo "[main:--full] FATAL: /props unreachable - server not up per D-07, recording as a no-go blocker" >&2
|
|
602
|
+
append_evidence "[main:--full] FATAL: /props unreachable at run time; treated as a documented no-go blocker (D-08 - never a silent skip)"
|
|
603
|
+
exit 1
|
|
604
|
+
fi
|
|
605
|
+
run_turn_matrix "$FASTCONTEXT_PROBE_URL" 3
|
|
606
|
+
compute_verdict
|
|
607
|
+
run_token_miser_corroboration
|
|
608
|
+
finalize_evidence_log
|
|
609
|
+
if [[ "$VERDICT" == "GO" ]]; then
|
|
610
|
+
exit 0
|
|
611
|
+
fi
|
|
612
|
+
exit 1
|
|
613
|
+
;;
|
|
614
|
+
-h|--help)
|
|
615
|
+
usage
|
|
616
|
+
;;
|
|
617
|
+
*)
|
|
618
|
+
usage >&2
|
|
619
|
+
exit 2
|
|
620
|
+
;;
|
|
621
|
+
esac
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
main "$@"
|