@kontourai/flow-agents 0.1.1 → 0.1.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/.github/workflows/publish-npm.yml +1 -1
- package/CHANGELOG.md +15 -0
- package/README.md +6 -10
- package/build/src/cli/utterance-check.js +172 -0
- package/build/src/cli.js +3 -0
- package/build/src/tools/validate-source-tree.js +1 -0
- package/docs/agent-system-guidebook.md +4 -5
- package/docs/index.md +1 -2
- package/docs/north-star.md +1 -1
- package/docs/repository-structure.md +1 -1
- package/docs/skills-map.md +10 -4
- package/docs/survey-utterance-check.md +191 -0
- package/docs/workflow-usage-guide.md +1 -1
- package/evals/integration/test_utterance_check.sh +271 -0
- package/package.json +1 -1
- package/scripts/README.md +1 -0
- package/scripts/hooks/utterance-check.js +225 -0
- package/skills/idea-to-backlog/SKILL.md +1 -1
- package/src/cli/utterance-check.ts +254 -0
- package/src/cli.ts +3 -0
- package/src/tools/validate-source-tree.ts +1 -0
- package/build/src/cli/docs-preview.js +0 -39
- package/build/src/cli/export-bookmarks.js +0 -38
- package/build/src/cli/import-bookmarks.js +0 -50
- package/build/src/cli/instinct-cli.js +0 -93
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_utterance_check.sh — Survey utterance check hook and CLI adapter coverage
|
|
3
|
+
set -uo pipefail
|
|
4
|
+
|
|
5
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
6
|
+
source "$ROOT/evals/lib/node.sh"
|
|
7
|
+
|
|
8
|
+
TMPDIR_EVAL="$(mktemp -d)"
|
|
9
|
+
errors=0
|
|
10
|
+
|
|
11
|
+
cleanup() {
|
|
12
|
+
rm -rf "$TMPDIR_EVAL"
|
|
13
|
+
}
|
|
14
|
+
trap cleanup EXIT
|
|
15
|
+
|
|
16
|
+
_pass() { echo " ✓ $1"; }
|
|
17
|
+
_fail() { echo " ✗ $1"; errors=$((errors + 1)); }
|
|
18
|
+
|
|
19
|
+
echo "=== Utterance Check Hook and CLI Adapter ==="
|
|
20
|
+
|
|
21
|
+
HOOK="$ROOT/scripts/hooks/utterance-check.js"
|
|
22
|
+
RUN_HOOK="$ROOT/scripts/hooks/run-hook.js"
|
|
23
|
+
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
# Hook: pass-through when disabled (default)
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
echo ""
|
|
29
|
+
echo "--- hook: disabled by default ---"
|
|
30
|
+
|
|
31
|
+
INPUT_JSON='{"hook_event_name":"PostToolUse","tool_response":"The coverage is 92% and all tests pass."}'
|
|
32
|
+
|
|
33
|
+
if node "$HOOK" >"$TMPDIR_EVAL/disabled.out" 2>"$TMPDIR_EVAL/disabled.err" <<< "$INPUT_JSON"; then
|
|
34
|
+
if grep -qF '"hook_event_name"' "$TMPDIR_EVAL/disabled.out"; then
|
|
35
|
+
_pass "utterance check hook passes through when FLOW_AGENTS_UTTERANCE_CHECK_ENABLED is unset"
|
|
36
|
+
else
|
|
37
|
+
_fail "utterance check hook pass-through output was not the raw input"
|
|
38
|
+
fi
|
|
39
|
+
else
|
|
40
|
+
_fail "utterance check hook should exit 0 when disabled"
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
# Hook: pass-through with empty input
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
echo ""
|
|
48
|
+
echo "--- hook: empty input ---"
|
|
49
|
+
|
|
50
|
+
if FLOW_AGENTS_UTTERANCE_CHECK_ENABLED=true node "$HOOK" >"$TMPDIR_EVAL/empty.out" 2>"$TMPDIR_EVAL/empty.err" <<< '{}'; then
|
|
51
|
+
_pass "utterance check hook passes through when no utterance text is present"
|
|
52
|
+
else
|
|
53
|
+
_fail "utterance check hook should exit 0 on empty input"
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
# Hook: pass-through when CLI is not built yet
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
echo ""
|
|
61
|
+
echo "--- hook: missing CLI gracefully fails open ---"
|
|
62
|
+
|
|
63
|
+
if FLOW_AGENTS_UTTERANCE_CHECK_ENABLED=true \
|
|
64
|
+
node "$HOOK" >"$TMPDIR_EVAL/nocli.out" 2>"$TMPDIR_EVAL/nocli.err" <<JSON
|
|
65
|
+
{"hook_event_name":"PostToolUse","tool_response":"Some agent text."}
|
|
66
|
+
JSON
|
|
67
|
+
then
|
|
68
|
+
# Either built CLI path worked, or hook failed open (exit 0)
|
|
69
|
+
_pass "utterance check hook fails open when CLI or survey is not available"
|
|
70
|
+
else
|
|
71
|
+
_fail "utterance check hook should not block when CLI is unavailable"
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
# Hook: respects SA_DISABLED_HOOKS through run-hook.js
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
echo ""
|
|
79
|
+
echo "--- hook: run-hook.js respects SA_DISABLED_HOOKS ---"
|
|
80
|
+
|
|
81
|
+
HOOK_INPUT='{"hook_event_name":"PostToolUse","tool_response":"text"}'
|
|
82
|
+
|
|
83
|
+
if SA_DISABLED_HOOKS=post:utterance-check \
|
|
84
|
+
node "$RUN_HOOK" post:utterance-check utterance-check.js standard,strict \
|
|
85
|
+
>"$TMPDIR_EVAL/disabled-runner.out" 2>"$TMPDIR_EVAL/disabled-runner.err" <<< "$HOOK_INPUT"
|
|
86
|
+
then
|
|
87
|
+
if cmp -s "$TMPDIR_EVAL/disabled-runner.out" <(printf '%s
|
|
88
|
+
' "$HOOK_INPUT"); then
|
|
89
|
+
_pass "run-hook.js passes input through when hook id is in SA_DISABLED_HOOKS"
|
|
90
|
+
else
|
|
91
|
+
_fail "run-hook.js disabled hook output did not match raw input"
|
|
92
|
+
fi
|
|
93
|
+
else
|
|
94
|
+
_fail "run-hook.js with disabled hook should exit 0"
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
# CLI: build and test --not-configured
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
echo ""
|
|
102
|
+
echo "--- cli: not-configured output ---"
|
|
103
|
+
|
|
104
|
+
# Build the TypeScript source if needed
|
|
105
|
+
if [[ ! -f "$ROOT/build/src/cli.js" ]]; then
|
|
106
|
+
echo " (building TypeScript source...)"
|
|
107
|
+
if ! (cd "$ROOT" && npm run build --silent 2>"$TMPDIR_EVAL/build.err"); then
|
|
108
|
+
_fail "TypeScript build failed: $(cat "$TMPDIR_EVAL/build.err" | head -5)"
|
|
109
|
+
errors=$((errors + 1))
|
|
110
|
+
echo ""
|
|
111
|
+
echo "Utterance check integration tests failed: $errors issue(s)."
|
|
112
|
+
exit 1
|
|
113
|
+
fi
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
if node "$ROOT/build/src/cli.js" utterance-check check --not-configured \
|
|
117
|
+
>"$TMPDIR_EVAL/not-configured.out" 2>"$TMPDIR_EVAL/not-configured.err"
|
|
118
|
+
then
|
|
119
|
+
if node -e '
|
|
120
|
+
const r = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
|
|
121
|
+
if (r.status !== "not_configured") process.exit(1);
|
|
122
|
+
if (!Array.isArray(r.statements)) process.exit(2);
|
|
123
|
+
if (typeof r.summary !== "string") process.exit(3);
|
|
124
|
+
' "$TMPDIR_EVAL/not-configured.out"; then
|
|
125
|
+
_pass "CLI outputs not_configured JSON when --not-configured is set"
|
|
126
|
+
else
|
|
127
|
+
_fail "CLI not-configured output did not match expected shape"
|
|
128
|
+
fi
|
|
129
|
+
else
|
|
130
|
+
_fail "CLI should exit 0 with --not-configured"
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
# CLI: --help exits 0 and prints usage
|
|
135
|
+
# ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
echo ""
|
|
138
|
+
echo "--- cli: help output ---"
|
|
139
|
+
|
|
140
|
+
if node "$ROOT/build/src/cli.js" utterance-check --help \
|
|
141
|
+
>"$TMPDIR_EVAL/help.out" 2>"$TMPDIR_EVAL/help.err"; then
|
|
142
|
+
if grep -q 'utterance-check check' "$TMPDIR_EVAL/help.err"; then
|
|
143
|
+
_pass "CLI --help prints usage"
|
|
144
|
+
else
|
|
145
|
+
_fail "CLI --help did not print expected usage text"
|
|
146
|
+
fi
|
|
147
|
+
else
|
|
148
|
+
_fail "CLI --help should exit 0"
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
# ---------------------------------------------------------------------------
|
|
152
|
+
# CLI: missing --utterance exits non-zero
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
echo ""
|
|
156
|
+
echo "--- cli: missing utterance flag ---"
|
|
157
|
+
|
|
158
|
+
if node "$ROOT/build/src/cli.js" utterance-check check \
|
|
159
|
+
>"$TMPDIR_EVAL/no-utterance.out" 2>"$TMPDIR_EVAL/no-utterance.err"
|
|
160
|
+
then
|
|
161
|
+
_fail "CLI check without --utterance should exit non-zero"
|
|
162
|
+
else
|
|
163
|
+
_pass "CLI check without --utterance exits non-zero (usage error)"
|
|
164
|
+
fi
|
|
165
|
+
|
|
166
|
+
# ---------------------------------------------------------------------------
|
|
167
|
+
# CLI: survey not installed → not_configured output, exits 1
|
|
168
|
+
# ---------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
echo ""
|
|
171
|
+
echo "--- cli: @kontourai/survey not installed ---"
|
|
172
|
+
|
|
173
|
+
# Run with a NODE_PATH that does not include any survey package, so the
|
|
174
|
+
# dynamic import fails. node's module resolution will not find @kontourai/survey
|
|
175
|
+
# from this test since it is not installed in flow-agents/node_modules.
|
|
176
|
+
if node "$ROOT/build/src/cli.js" utterance-check check \
|
|
177
|
+
--utterance "The test coverage is 92%." \
|
|
178
|
+
>"$TMPDIR_EVAL/no-survey.out" 2>"$TMPDIR_EVAL/no-survey.err"
|
|
179
|
+
then
|
|
180
|
+
# survey might be installed; check for not_configured or ok status
|
|
181
|
+
status_val=$(node -e 'console.log(JSON.parse(require("fs").readFileSync(process.argv[1],"utf8")).status)' \
|
|
182
|
+
"$TMPDIR_EVAL/no-survey.out" 2>/dev/null || echo "parse-error")
|
|
183
|
+
if [[ "$status_val" == "ok" || "$status_val" == "not_configured" ]]; then
|
|
184
|
+
_pass "CLI utterance check produces valid report (status: $status_val)"
|
|
185
|
+
else
|
|
186
|
+
_fail "CLI utterance check output had unexpected status: $status_val"
|
|
187
|
+
fi
|
|
188
|
+
else
|
|
189
|
+
exit_code=$?
|
|
190
|
+
# Exit 1 means not_configured (survey not installed) — expected in CI
|
|
191
|
+
if [[ "$exit_code" -eq 1 ]]; then
|
|
192
|
+
if node -e '
|
|
193
|
+
const r = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
|
|
194
|
+
if (r.status !== "not_configured") process.exit(1);
|
|
195
|
+
' "$TMPDIR_EVAL/no-survey.out" 2>/dev/null; then
|
|
196
|
+
_pass "CLI outputs not_configured when @kontourai/survey is not installed"
|
|
197
|
+
else
|
|
198
|
+
_fail "CLI exit 1 but output was not not_configured JSON"
|
|
199
|
+
fi
|
|
200
|
+
else
|
|
201
|
+
_fail "CLI should exit 0 or 1, got exit code: $exit_code"
|
|
202
|
+
fi
|
|
203
|
+
fi
|
|
204
|
+
|
|
205
|
+
# ---------------------------------------------------------------------------
|
|
206
|
+
# CLI: utterance check registers as a valid flow-agents command
|
|
207
|
+
# ---------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
echo ""
|
|
210
|
+
echo "--- cli: command registration ---"
|
|
211
|
+
|
|
212
|
+
if node "$ROOT/build/src/cli.js" commands 2>/dev/null | grep -q 'utterance-check'; then
|
|
213
|
+
_pass "utterance-check is registered as a flow-agents CLI command"
|
|
214
|
+
else
|
|
215
|
+
_fail "utterance-check is not registered in flow-agents CLI commands"
|
|
216
|
+
fi
|
|
217
|
+
|
|
218
|
+
# ---------------------------------------------------------------------------
|
|
219
|
+
# Hook: module.exports shape
|
|
220
|
+
# ---------------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
echo ""
|
|
223
|
+
echo "--- hook: module.exports contract ---"
|
|
224
|
+
|
|
225
|
+
if node -e '
|
|
226
|
+
const h = require(process.argv[1]);
|
|
227
|
+
if (typeof h.run !== "function") { console.error("run missing"); process.exit(1); }
|
|
228
|
+
if (typeof h.extractUtteranceText !== "function") { console.error("extractUtteranceText missing"); process.exit(2); }
|
|
229
|
+
if (typeof h.findPackageRoot !== "function") { console.error("findPackageRoot missing"); process.exit(3); }
|
|
230
|
+
' "$HOOK"; then
|
|
231
|
+
_pass "utterance-check hook exports run, extractUtteranceText, findPackageRoot"
|
|
232
|
+
else
|
|
233
|
+
_fail "utterance-check hook module.exports is missing expected functions"
|
|
234
|
+
fi
|
|
235
|
+
|
|
236
|
+
# ---------------------------------------------------------------------------
|
|
237
|
+
# Hook: extractUtteranceText extracts from PostToolUse and Stop events
|
|
238
|
+
# ---------------------------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
echo ""
|
|
241
|
+
echo "--- hook: extractUtteranceText ---"
|
|
242
|
+
|
|
243
|
+
if node -e '
|
|
244
|
+
const { extractUtteranceText } = require(process.argv[1]);
|
|
245
|
+
const postToolUse = { hook_event_name: "PostToolUse", tool_response: "The answer is 42." };
|
|
246
|
+
const text = extractUtteranceText(postToolUse);
|
|
247
|
+
if (text !== "The answer is 42.") { console.error("PostToolUse extract failed:", text); process.exit(1); }
|
|
248
|
+
const stopWithContent = { hook_event_name: "Stop", content: [{ type: "text", text: "Done!" }] };
|
|
249
|
+
const text2 = extractUtteranceText(stopWithContent);
|
|
250
|
+
if (text2 !== "Done!") { console.error("Stop content extract failed:", text2); process.exit(2); }
|
|
251
|
+
const emptyEvent = { hook_event_name: "PostToolUse" };
|
|
252
|
+
const text3 = extractUtteranceText(emptyEvent);
|
|
253
|
+
if (text3 !== null) { console.error("Empty event should return null, got:", text3); process.exit(3); }
|
|
254
|
+
' "$HOOK"; then
|
|
255
|
+
_pass "extractUtteranceText handles PostToolUse, Stop content, and empty events"
|
|
256
|
+
else
|
|
257
|
+
_fail "extractUtteranceText behavior was unexpected"
|
|
258
|
+
fi
|
|
259
|
+
|
|
260
|
+
# ---------------------------------------------------------------------------
|
|
261
|
+
# Summary
|
|
262
|
+
# ---------------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
echo ""
|
|
265
|
+
if [[ "$errors" -eq 0 ]]; then
|
|
266
|
+
echo "Utterance check integration tests passed."
|
|
267
|
+
exit 0
|
|
268
|
+
fi
|
|
269
|
+
|
|
270
|
+
echo "Utterance check integration tests failed: $errors issue(s)."
|
|
271
|
+
exit 1
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kontourai/flow-agents",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Flow Agents — a Kontour product that applies Flow and Veritas discipline inside the agent tools you already use: Claude Code, Codex, Kiro, and GitHub Actions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
package/scripts/README.md
CHANGED
|
@@ -59,6 +59,7 @@ renamed, or changes category, update the table and the validator together.
|
|
|
59
59
|
| `report-only-guard.js` | policy hook | `evals/integration/test_hook_category_behaviors.sh` | Protects report-only specialist roles from production edits. |
|
|
60
60
|
| `stop-format-typecheck.js` | policy hook | `evals/integration/test_hook_category_behaviors.sh` | Runs stop-time format/typecheck feedback. |
|
|
61
61
|
| `stop-goal-fit.js` | policy hook | `evals/integration/test_goal_fit_hook.sh` | Warns when a workflow is about to stop short of Goal Fit. |
|
|
62
|
+
| `utterance-check.js` | policy hook | `evals/integration/test_utterance_check.sh` | Optionally checks agent utterances for evidence coverage using @kontourai/survey (disabled by default; opt-in via FLOW_AGENTS_UTTERANCE_CHECK_ENABLED). |
|
|
62
63
|
| `workflow-steering.js` | policy hook | `evals/integration/test_workflow_steering_hook.sh` | Provides workflow guidance from current artifact state. |
|
|
63
64
|
| `pre-commit-quality.js` | repo guardrail hook | `evals/integration/test_hook_category_behaviors.sh` | Supports repository Git hook checks, not installed runtime hooks. |
|
|
64
65
|
| `desktop-notify.sh` | local notification helper | `evals/integration/test_hook_category_behaviors.sh` | Optional local desktop notification helper. |
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Utterance Check Hook — ADR 0003 §9 / survey integration.
|
|
4
|
+
*
|
|
5
|
+
* Optionally inspects agent output text for evidence coverage using
|
|
6
|
+
* @kontourai/survey's surveyAgentUtterance. Injects badge guidance into the
|
|
7
|
+
* agent context when concerning statements (unsupported/disputed/rejected) are
|
|
8
|
+
* found.
|
|
9
|
+
*
|
|
10
|
+
* Disabled by default. Enable with:
|
|
11
|
+
* FLOW_AGENTS_UTTERANCE_CHECK_ENABLED=true
|
|
12
|
+
*
|
|
13
|
+
* Hook category: PostToolUse / Stop (non-blocking, always exits 0).
|
|
14
|
+
*
|
|
15
|
+
* Strict mode (blocks Stop when concerning badges present):
|
|
16
|
+
* FLOW_AGENTS_UTTERANCE_CHECK_STRICT=true
|
|
17
|
+
*
|
|
18
|
+
* Bundle path (optional trust bundle for claim resolution):
|
|
19
|
+
* FLOW_AGENTS_UTTERANCE_CHECK_BUNDLE_PATH=/path/to/bundle.json
|
|
20
|
+
*
|
|
21
|
+
* Agent ID (for provenance):
|
|
22
|
+
* FLOW_AGENTS_UTTERANCE_CHECK_AGENT_ID=my-agent
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const path = require('path');
|
|
29
|
+
const { spawnSync } = require('child_process');
|
|
30
|
+
|
|
31
|
+
const MAX_STDIN = 1024 * 1024;
|
|
32
|
+
const CLI_TIMEOUT_MS = 30000;
|
|
33
|
+
// Maximum utterance text to pass to the CLI to keep stdin under MAX_STDIN.
|
|
34
|
+
const MAX_UTTERANCE_CHARS = 8192;
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Helpers
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
function parseJson(raw) {
|
|
41
|
+
try { return JSON.parse(raw || '{}'); } catch { return {}; }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Walk up from startDir to find the flow-agents package root.
|
|
46
|
+
* Identified by having both package.json and build/src/cli.js present.
|
|
47
|
+
*/
|
|
48
|
+
function findPackageRoot(startDir) {
|
|
49
|
+
let dir = path.resolve(startDir || __dirname);
|
|
50
|
+
for (let depth = 0; depth < 10; depth++) {
|
|
51
|
+
if (
|
|
52
|
+
fs.existsSync(path.join(dir, 'package.json')) &&
|
|
53
|
+
fs.existsSync(path.join(dir, 'build', 'src', 'cli.js'))
|
|
54
|
+
) {
|
|
55
|
+
return dir;
|
|
56
|
+
}
|
|
57
|
+
const parent = path.dirname(dir);
|
|
58
|
+
if (parent === dir) break;
|
|
59
|
+
dir = parent;
|
|
60
|
+
}
|
|
61
|
+
// Fallback: assume hooks dir is scripts/hooks/, so root is two levels up.
|
|
62
|
+
return path.resolve(__dirname, '..', '..');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Extract agent utterance text from the hook event input.
|
|
67
|
+
* - PostToolUse: tool_response or tool_output field
|
|
68
|
+
* - Stop: stop_reason or agent_message field (varies by harness)
|
|
69
|
+
* - Fallback: returns null (hook passes through)
|
|
70
|
+
*/
|
|
71
|
+
function extractUtteranceText(input) {
|
|
72
|
+
if (!input || typeof input !== 'object') return null;
|
|
73
|
+
|
|
74
|
+
// PostToolUse: agent output is in tool_response or tool_output
|
|
75
|
+
const resp = input.tool_response || input.tool_output;
|
|
76
|
+
if (typeof resp === 'string' && resp.trim()) return resp.trim();
|
|
77
|
+
|
|
78
|
+
// Stop: some harnesses expose agent message content
|
|
79
|
+
const agentMsg = input.agent_message || input.message;
|
|
80
|
+
if (typeof agentMsg === 'string' && agentMsg.trim()) return agentMsg.trim();
|
|
81
|
+
|
|
82
|
+
// Stop with content array (Claude Code format)
|
|
83
|
+
if (Array.isArray(input.content)) {
|
|
84
|
+
const texts = input.content
|
|
85
|
+
.filter(b => b && b.type === 'text' && typeof b.text === 'string')
|
|
86
|
+
.map(b => String(b.text).trim())
|
|
87
|
+
.filter(Boolean);
|
|
88
|
+
if (texts.length > 0) return texts.join('\n\n');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function safeOneLineExcerpt(text, max = 120) {
|
|
95
|
+
const s = String(text || '').replace(/\s+/g, ' ').trim();
|
|
96
|
+
return s.length > max ? `${s.slice(0, max - 3)}...` : s;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Hook runner
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
function run(rawInput) {
|
|
104
|
+
const enabled = String(process.env.FLOW_AGENTS_UTTERANCE_CHECK_ENABLED || '').toLowerCase() === 'true';
|
|
105
|
+
if (!enabled) return rawInput;
|
|
106
|
+
|
|
107
|
+
let input;
|
|
108
|
+
try { input = JSON.parse(rawInput || '{}'); } catch { return rawInput; }
|
|
109
|
+
|
|
110
|
+
const utteranceText = extractUtteranceText(input);
|
|
111
|
+
if (!utteranceText) return rawInput;
|
|
112
|
+
|
|
113
|
+
// Truncate very long utterances before passing to CLI.
|
|
114
|
+
const utterance = utteranceText.length > MAX_UTTERANCE_CHARS
|
|
115
|
+
? utteranceText.slice(0, MAX_UTTERANCE_CHARS)
|
|
116
|
+
: utteranceText;
|
|
117
|
+
|
|
118
|
+
const packageRoot = findPackageRoot(__dirname);
|
|
119
|
+
const cliPath = path.join(packageRoot, 'build', 'src', 'cli.js');
|
|
120
|
+
|
|
121
|
+
if (!fs.existsSync(cliPath)) {
|
|
122
|
+
process.stderr.write(
|
|
123
|
+
`[UtteranceCheck] CLI not found at ${cliPath}. Run npm run build in the flow-agents checkout.\n`
|
|
124
|
+
);
|
|
125
|
+
return rawInput;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const cliArgs = ['utterance-check', 'check', '--utterance', utterance];
|
|
129
|
+
|
|
130
|
+
const bundlePath = process.env.FLOW_AGENTS_UTTERANCE_CHECK_BUNDLE_PATH;
|
|
131
|
+
if (bundlePath) cliArgs.push('--bundle-path', bundlePath);
|
|
132
|
+
|
|
133
|
+
const agentId = process.env.FLOW_AGENTS_UTTERANCE_CHECK_AGENT_ID || 'flow-agents-hook';
|
|
134
|
+
cliArgs.push('--agent-id', agentId);
|
|
135
|
+
|
|
136
|
+
const strict = String(process.env.FLOW_AGENTS_UTTERANCE_CHECK_STRICT || '').toLowerCase() === 'true';
|
|
137
|
+
if (strict) cliArgs.push('--strict');
|
|
138
|
+
|
|
139
|
+
const result = spawnSync(process.execPath, [cliPath, ...cliArgs], {
|
|
140
|
+
encoding: 'utf8',
|
|
141
|
+
timeout: CLI_TIMEOUT_MS,
|
|
142
|
+
cwd: packageRoot,
|
|
143
|
+
env: { ...process.env },
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (result.error || result.signal || result.status === null) {
|
|
147
|
+
const detail = result.error
|
|
148
|
+
? result.error.message
|
|
149
|
+
: result.signal
|
|
150
|
+
? `signal ${result.signal}`
|
|
151
|
+
: 'missing exit status';
|
|
152
|
+
process.stderr.write(`[UtteranceCheck] CLI execution failed (failing open): ${detail}\n`);
|
|
153
|
+
return rawInput;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Parse the JSON report from stdout.
|
|
157
|
+
let report = null;
|
|
158
|
+
try { report = JSON.parse(String(result.stdout || '').trim()); } catch { /* pass through */ }
|
|
159
|
+
|
|
160
|
+
if (result.stderr) {
|
|
161
|
+
process.stderr.write(String(result.stderr));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!report) return rawInput;
|
|
165
|
+
|
|
166
|
+
// If survey is not configured, pass through silently.
|
|
167
|
+
if (report.status === 'not_configured') return rawInput;
|
|
168
|
+
|
|
169
|
+
// Build guidance text from the badge report.
|
|
170
|
+
const statements = Array.isArray(report.statements) ? report.statements : [];
|
|
171
|
+
const concerning = statements.filter(s =>
|
|
172
|
+
s && (s.badge === 'unsupported' || s.badge === 'disputed' || s.badge === 'rejected')
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
if (concerning.length === 0) return rawInput;
|
|
176
|
+
|
|
177
|
+
const lines = [
|
|
178
|
+
`UTTERANCE CHECK: ${concerning.length} statement(s) in this response lack evidence coverage.`,
|
|
179
|
+
`Summary: ${report.summary || 'unknown'}`,
|
|
180
|
+
...concerning.slice(0, 4).map(s =>
|
|
181
|
+
` - [${s.badge}] "${safeOneLineExcerpt(s.excerpt)}"`
|
|
182
|
+
),
|
|
183
|
+
'Evidence note: unsupported = no matching claim in the trust bundle; disputed = conflicting evidence; rejected = claim was rejected.',
|
|
184
|
+
'Cite sources, note gaps, or run survey-utterance-check to record coverage.',
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
// For PostToolUse: append guidance to the raw input as additional context.
|
|
188
|
+
// For Stop: report to stderr (non-blocking warning) unless strict mode.
|
|
189
|
+
const event = input.hook_event_name || '';
|
|
190
|
+
const guidance = '\n\n---\n' + lines.join('\n') + '\n---';
|
|
191
|
+
|
|
192
|
+
if (strict && result.status === 2) {
|
|
193
|
+
return {
|
|
194
|
+
stdout: rawInput,
|
|
195
|
+
stderr: lines.join('\n'),
|
|
196
|
+
exitCode: 2,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (event === 'PostToolUse') {
|
|
201
|
+
return rawInput + guidance;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
process.stderr.write(`[UtteranceCheck] ${lines.join('\n')}\n`);
|
|
205
|
+
return rawInput;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (require.main === module) {
|
|
209
|
+
let data = '';
|
|
210
|
+
process.stdin.setEncoding('utf8');
|
|
211
|
+
process.stdin.on('data', chunk => {
|
|
212
|
+
if (data.length < MAX_STDIN) data += chunk.substring(0, MAX_STDIN - data.length);
|
|
213
|
+
});
|
|
214
|
+
process.stdin.on('end', () => {
|
|
215
|
+
const output = run(data);
|
|
216
|
+
if (output && typeof output === 'object') {
|
|
217
|
+
if (output.stderr) process.stderr.write(output.stderr.endsWith('\n') ? output.stderr : `${output.stderr}\n`);
|
|
218
|
+
process.stdout.write(String(output.stdout ?? data));
|
|
219
|
+
process.exit(Number.isInteger(output.exitCode) ? output.exitCode : 0);
|
|
220
|
+
}
|
|
221
|
+
process.stdout.write(String(output));
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
module.exports = { run, extractUtteranceText, findPackageRoot };
|
|
@@ -102,7 +102,7 @@ Gate: the opportunity is worth shaping, or it is parked/rejected.
|
|
|
102
102
|
|
|
103
103
|
### 4. Explore Options
|
|
104
104
|
|
|
105
|
-
Use `
|
|
105
|
+
Use `search-first` or `explore` when context is missing.
|
|
106
106
|
|
|
107
107
|
Decide the path:
|
|
108
108
|
|