@kontourai/flow-agents 3.3.0 → 3.4.1

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.
Files changed (256) hide show
  1. package/.github/workflows/add-to-project.yml +15 -0
  2. package/.github/workflows/ci.yml +161 -0
  3. package/CHANGELOG.md +48 -0
  4. package/CONTEXT.md +5 -1
  5. package/README.md +19 -8
  6. package/build/src/builder-flow-run-adapter.d.ts +80 -0
  7. package/build/src/builder-flow-run-adapter.js +241 -0
  8. package/build/src/builder-flow-runtime.d.ts +16 -0
  9. package/build/src/builder-flow-runtime.js +290 -0
  10. package/build/src/cli/builder-run.d.ts +1 -0
  11. package/build/src/cli/builder-run.js +27 -0
  12. package/build/src/cli/effective-backlog-settings.js +70 -2
  13. package/build/src/cli/init.d.ts +34 -0
  14. package/build/src/cli/init.js +341 -61
  15. package/build/src/cli/kit.js +55 -12
  16. package/build/src/cli/pull-work-provider.js +346 -5
  17. package/build/src/cli/skill-drift-check.d.ts +1 -0
  18. package/build/src/cli/skill-drift-check.js +165 -0
  19. package/build/src/cli/telemetry-doctor.d.ts +37 -0
  20. package/build/src/cli/telemetry-doctor.js +53 -6
  21. package/build/src/cli/validate-hook-influence.js +37 -7
  22. package/build/src/cli/workflow-sidecar.d.ts +93 -8
  23. package/build/src/cli/workflow-sidecar.js +1175 -158
  24. package/build/src/cli.js +5 -0
  25. package/build/src/flow-kit/validate.d.ts +54 -34
  26. package/build/src/flow-kit/validate.js +237 -26
  27. package/build/src/index.d.ts +2 -0
  28. package/build/src/index.js +1 -0
  29. package/build/src/lib/console-connect-options.d.ts +97 -0
  30. package/build/src/lib/console-connect-options.js +199 -0
  31. package/build/src/lib/console-telemetry-validate.d.ts +49 -0
  32. package/build/src/lib/console-telemetry-validate.js +91 -0
  33. package/build/src/lib/flow-resolver.d.ts +56 -3
  34. package/build/src/lib/flow-resolver.js +151 -11
  35. package/build/src/lib/fs.d.ts +17 -0
  36. package/build/src/lib/fs.js +172 -0
  37. package/build/src/lib/local-artifact-root.d.ts +44 -1
  38. package/build/src/lib/local-artifact-root.js +131 -3
  39. package/build/src/runtime-adapters.d.ts +39 -3
  40. package/build/src/runtime-adapters.js +77 -31
  41. package/build/src/tools/build-universal-bundles.js +40 -2
  42. package/build/src/tools/codex-agent-routing.d.ts +2 -0
  43. package/build/src/tools/codex-agent-routing.js +49 -0
  44. package/build/src/tools/generate-context-map.js +1 -0
  45. package/build/src/tools/validate-source-tree.js +27 -1
  46. package/context/scripts/hooks/lib/kit-catalog.js +235 -0
  47. package/context/scripts/hooks/lib/runnable-command.js +177 -0
  48. package/context/scripts/hooks/stop-goal-fit.js +278 -48
  49. package/context/scripts/hooks/workflow-steering.js +121 -21
  50. package/context/scripts/package.json +3 -0
  51. package/context/scripts/telemetry/install-console-config.sh +25 -4
  52. package/context/scripts/telemetry/lib/config.sh +102 -12
  53. package/context/scripts/telemetry/lib/pricing.sh +50 -0
  54. package/context/scripts/telemetry/lib/session.sh +3 -0
  55. package/context/scripts/telemetry/lib/transport.sh +87 -0
  56. package/context/scripts/telemetry/lib/usage.sh +205 -4
  57. package/context/scripts/telemetry/telemetry.conf +6 -0
  58. package/context/scripts/telemetry/telemetry.sh +48 -0
  59. package/context/settings/workspace-backlog-provider-settings.example.json +48 -0
  60. package/docs/agent-usage-feedback-loop.md +35 -0
  61. package/docs/architecture-engine-and-kits.md +110 -0
  62. package/docs/context-map.md +2 -0
  63. package/docs/decisions/embeddable-engine.md +152 -0
  64. package/docs/decisions/index.md +3 -1
  65. package/docs/decisions/trust-ledger-retention.md +88 -0
  66. package/docs/decisions/workflow-enforcement.md +31 -9
  67. package/docs/fixture-ownership.md +3 -0
  68. package/docs/implementing-trust-reconciliation.md +129 -0
  69. package/docs/index.md +19 -9
  70. package/docs/integrations/flow-agents-console.md +167 -0
  71. package/docs/kit-authoring-guide.md +52 -21
  72. package/docs/spec/builder-flow-runtime.md +80 -0
  73. package/docs/spec/runtime-hook-surface.md +45 -1
  74. package/docs/specs/economics-record-contract.md +270 -0
  75. package/docs/specs/harness-capability-matrix.md +74 -0
  76. package/docs/specs/learning-review-proposals-contract.md +340 -0
  77. package/docs/specs/routing-efficiency-review.md +59 -0
  78. package/docs/verifiable-trust.md +74 -25
  79. package/docs/workflow-usage-guide.md +10 -0
  80. package/evals/acceptance/prove-capture-teeth.sh +132 -0
  81. package/evals/ci/antigaming-suite.sh +1 -0
  82. package/evals/ci/run-baseline.sh +72 -4
  83. package/evals/fixtures/economics/acceptance.json +12 -0
  84. package/evals/fixtures/economics/agents/tool-worker-1/events.jsonl +2 -0
  85. package/evals/fixtures/economics/agents/tool-worker-2/events.jsonl +2 -0
  86. package/evals/fixtures/economics/agents/tool-worker-3/events.jsonl +2 -0
  87. package/evals/fixtures/economics/agents/tool-worker-4/events.jsonl +1 -0
  88. package/evals/fixtures/economics/agents/tool-worker-5/events.jsonl +2 -0
  89. package/evals/fixtures/economics/critique.json +22 -0
  90. package/evals/fixtures/economics/expected-record.json +71 -0
  91. package/evals/fixtures/economics/session-usage-event.json +1 -0
  92. package/evals/fixtures/economics/state.json +11 -0
  93. package/evals/fixtures/economics/transcript.jsonl +3 -0
  94. package/evals/fixtures/hook-influence/cases.json +7 -7
  95. package/evals/fixtures/learning-review-proposals/balanced/economics.jsonl +6 -0
  96. package/evals/fixtures/learning-review-proposals/effect-follow-up/economics.jsonl +5 -0
  97. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-1/trust.bundle +21 -0
  98. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-2/trust.bundle +21 -0
  99. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-3/trust.bundle +21 -0
  100. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-4/trust.bundle +21 -0
  101. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-5/trust.bundle +21 -0
  102. package/evals/fixtures/learning-review-proposals/pattern-present/economics.jsonl +6 -0
  103. package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.json +30 -0
  104. package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.md +66 -0
  105. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/gate-review.inquiries.json +26 -0
  106. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/trust.bundle +21 -0
  107. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/gate-review.inquiries.json +26 -0
  108. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/trust.bundle +21 -0
  109. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/gate-review.inquiries.json +26 -0
  110. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/trust.bundle +21 -0
  111. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/gate-review.inquiries.json +26 -0
  112. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/trust.bundle +21 -0
  113. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-5/trust.bundle +21 -0
  114. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-6/trust.bundle +21 -0
  115. package/evals/fixtures/learning-review-proposals/repeat-window/economics.jsonl +6 -0
  116. package/evals/fixtures/learning-review-proposals/under-threshold/economics.jsonl +3 -0
  117. package/evals/fixtures/telemetry/usage-transcript-sample.jsonl +4 -0
  118. package/evals/fixtures/trust-reconcile-exploits/mcp-degrade.json +42 -0
  119. package/evals/integration/test_builder_entry_enforcement.sh +241 -0
  120. package/evals/integration/test_builder_step_producers.sh +18 -10
  121. package/evals/integration/test_bundle_install.sh +172 -0
  122. package/evals/integration/test_console_tenant_isolation.sh +167 -0
  123. package/evals/integration/test_critique_supersession_roundtrip.sh +4 -1
  124. package/evals/integration/test_dual_emit_flow_step.sh +10 -4
  125. package/evals/integration/test_economics_record.sh +674 -0
  126. package/evals/integration/test_effective_backlog_settings.sh +1 -1
  127. package/evals/integration/test_evidence_capture_hook.sh +17 -2
  128. package/evals/integration/test_exemption_usage_review.sh +198 -0
  129. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  130. package/evals/integration/test_flow_kit_install_git.sh +83 -0
  131. package/evals/integration/test_flowdef_session_activation.sh +0 -1
  132. package/evals/integration/test_flowdef_session_history_preservation.sh +13 -3
  133. package/evals/integration/test_gate_lockdown.sh +7 -0
  134. package/evals/integration/test_gate_review_inquiry_records.sh +9 -1
  135. package/evals/integration/test_goal_fit_hook.sh +2031 -0
  136. package/evals/integration/test_hook_category_behaviors.sh +8 -1
  137. package/evals/integration/test_hook_influence_cases.sh +25 -1
  138. package/evals/integration/test_install_merge.sh +227 -2
  139. package/evals/integration/test_kit_conformance_levels.sh +6 -6
  140. package/evals/integration/test_learning_review_proposals.sh +329 -0
  141. package/evals/integration/test_liveness_conflict_injection.sh +26 -22
  142. package/evals/integration/test_liveness_console_relay.sh +166 -0
  143. package/evals/integration/test_liveness_heartbeat.sh +17 -17
  144. package/evals/integration/test_liveness_worktree_root.sh +575 -0
  145. package/evals/integration/test_phase_map_and_gate_claim.sh +6 -1
  146. package/evals/integration/test_publish_delivery.sh +331 -1
  147. package/evals/integration/test_pull_work_board.sh +200 -0
  148. package/evals/integration/test_pull_work_provider.sh +1 -1
  149. package/evals/integration/test_record_check.sh +378 -0
  150. package/evals/integration/test_routing_efficiency.sh +71 -0
  151. package/evals/integration/test_runtime_adapter_activation.sh +28 -0
  152. package/evals/integration/test_session_resume_roundtrip.sh +16 -19
  153. package/evals/integration/test_skill_drift_check.sh +870 -0
  154. package/evals/integration/test_telemetry.sh +445 -0
  155. package/evals/integration/test_telemetry_doctor.sh +66 -0
  156. package/evals/integration/test_telemetry_usage_pipeline.sh +228 -0
  157. package/evals/integration/test_trust_reconcile_negatives.sh +30 -13
  158. package/evals/integration/test_trust_reconcile_trailer_diagnostic.sh +247 -0
  159. package/evals/integration/test_usage_cost.sh +61 -0
  160. package/evals/integration/test_workflow_sidecar_writer.sh +1395 -0
  161. package/evals/integration/test_workflow_steering_hook.sh +157 -16
  162. package/evals/integration/test_workspace_settings.sh +176 -0
  163. package/evals/lib/env.sh +26 -0
  164. package/evals/lib/node.sh +8 -0
  165. package/evals/run.sh +29 -0
  166. package/evals/static/test_ci_integration_coverage.sh +115 -0
  167. package/evals/static/test_declared_scope_forms_documented.sh +114 -0
  168. package/evals/static/test_universal_bundles.sh +34 -0
  169. package/evals/static/test_validate_source_kit_asset_scope.sh +259 -0
  170. package/evals/static/test_workflow_skills.sh +1 -1
  171. package/kits/builder/flows/build.flow.json +9 -18
  172. package/kits/builder/flows/publish-learn.flow.json +5 -1
  173. package/kits/builder/kit.json +120 -0
  174. package/kits/builder/skills/deliver/SKILL.md +42 -0
  175. package/kits/builder/skills/evidence-gate/SKILL.md +12 -0
  176. package/kits/builder/skills/execute-plan/SKILL.md +9 -0
  177. package/kits/builder/skills/learning-review/SKILL.md +51 -0
  178. package/kits/builder/skills/plan-work/SKILL.md +17 -20
  179. package/kits/builder/skills/pull-work/SKILL.md +21 -0
  180. package/kits/builder/skills/release-readiness/SKILL.md +12 -0
  181. package/kits/knowledge/kit.json +9 -0
  182. package/kits/veritas-governance/docs/README.md +35 -7
  183. package/kits/veritas-governance/fixtures/exemption-review/mixed-fresh-stale.DECLARED.json +14 -0
  184. package/kits/veritas-governance/kit.json +14 -0
  185. package/kits/veritas-governance/skills/exemption-usage-review/SKILL.md +128 -0
  186. package/kits/veritas-governance/skills/exemption-usage-review/review-exemptions.mjs +231 -0
  187. package/package.json +2 -2
  188. package/packaging/manifest.json +29 -0
  189. package/schemas/backlog-provider-settings.schema.json +13 -0
  190. package/schemas/workflow-state.schema.json +44 -0
  191. package/scripts/README.md +4 -0
  192. package/scripts/check-content-boundary.cjs +8 -1
  193. package/scripts/ci/trust-reconcile.js +136 -0
  194. package/scripts/hooks/codex-hook-adapter.js +77 -2
  195. package/scripts/hooks/evidence-capture.js +38 -5
  196. package/scripts/hooks/lib/codex-exit-code.js +316 -0
  197. package/scripts/hooks/lib/kit-catalog.js +235 -0
  198. package/scripts/hooks/lib/liveness-write.js +28 -1
  199. package/scripts/hooks/lib/local-artifact-paths.js +97 -1
  200. package/scripts/hooks/lib/runnable-command.js +177 -0
  201. package/scripts/hooks/lib/skill-drift.js +350 -0
  202. package/scripts/hooks/stop-goal-fit.js +278 -48
  203. package/scripts/hooks/workflow-steering.js +121 -21
  204. package/scripts/install-codex-home.sh +97 -47
  205. package/scripts/install-merge.js +72 -14
  206. package/scripts/install-owned-files.js +178 -0
  207. package/scripts/liveness/relay.sh +84 -0
  208. package/scripts/telemetry/economics-record.schema.json +145 -0
  209. package/scripts/telemetry/economics-record.sh +331 -0
  210. package/scripts/telemetry/install-console-config.sh +25 -4
  211. package/scripts/telemetry/learning-review-decide.sh +124 -0
  212. package/scripts/telemetry/learning-review-proposals.schema.json +161 -0
  213. package/scripts/telemetry/learning-review-proposals.sh +484 -0
  214. package/scripts/telemetry/lib/config.sh +102 -12
  215. package/scripts/telemetry/lib/pricing.sh +14 -6
  216. package/scripts/telemetry/lib/session.sh +3 -0
  217. package/scripts/telemetry/lib/transport.sh +133 -15
  218. package/scripts/telemetry/lib/usage.sh +121 -28
  219. package/scripts/telemetry/routing-efficiency.sh +0 -0
  220. package/scripts/telemetry/telemetry.conf +6 -0
  221. package/scripts/telemetry/telemetry.sh +48 -0
  222. package/src/builder-flow-run-adapter.ts +357 -0
  223. package/src/builder-flow-runtime.ts +348 -0
  224. package/src/cli/builder-flow-run-adapter.test.mjs +495 -0
  225. package/src/cli/builder-flow-runtime.test.mjs +213 -0
  226. package/src/cli/builder-run.ts +28 -0
  227. package/src/cli/codex-agent-routing.test.mjs +44 -0
  228. package/src/cli/codex-exit-code.test.mjs +207 -0
  229. package/src/cli/console-connect-options.test.mjs +329 -0
  230. package/src/cli/console-telemetry-validate.test.mjs +157 -0
  231. package/src/cli/effective-backlog-settings.ts +68 -2
  232. package/src/cli/flow-resolver-composition.test.mjs +101 -0
  233. package/src/cli/init.test.mjs +161 -0
  234. package/src/cli/init.ts +407 -62
  235. package/src/cli/kit-metadata-security.test.mjs +443 -0
  236. package/src/cli/kit.ts +50 -12
  237. package/src/cli/pull-work-provider.ts +377 -3
  238. package/src/cli/sidecar-pure-helpers.test.mjs +64 -0
  239. package/src/cli/skill-drift-check.ts +196 -0
  240. package/src/cli/telemetry-doctor.test.mjs +53 -0
  241. package/src/cli/telemetry-doctor.ts +50 -7
  242. package/src/cli/validate-hook-influence.ts +37 -6
  243. package/src/cli/workflow-sidecar.ts +1150 -151
  244. package/src/cli.ts +5 -0
  245. package/src/flow-kit/validate.ts +277 -38
  246. package/src/index.ts +19 -0
  247. package/src/lib/console-connect-options.ts +261 -0
  248. package/src/lib/console-telemetry-validate.ts +88 -0
  249. package/src/lib/flow-resolver.ts +153 -10
  250. package/src/lib/fs.ts +160 -0
  251. package/src/lib/local-artifact-root.ts +129 -3
  252. package/src/runtime-adapters.ts +113 -33
  253. package/src/tools/build-universal-bundles.ts +36 -2
  254. package/src/tools/codex-agent-routing.ts +48 -0
  255. package/src/tools/generate-context-map.ts +1 -0
  256. package/src/tools/validate-source-tree.ts +26 -1
@@ -0,0 +1,228 @@
1
+ #!/usr/bin/env bash
2
+ # test_telemetry_usage_pipeline.sh — Layer 2: hermetic Stop-hook usage pipeline
3
+ #
4
+ # Proves the full telemetry.sh Stop path (add_stop_data_and_emit_usage) yields
5
+ # a session.usage event with real tokens, a concrete (non-"unknown") model,
6
+ # and a non-null estimated_cost_usd when a runtime transcript is supplied —
7
+ # and that tokens still survive (with estimated_cost_usd null) when the
8
+ # pricing registry is forced unavailable. Also guards the kiro-cli
9
+ # non-regression case: with no transcript, model still resolves via the
10
+ # existing usage_get_model() kiro lookup (unaffected by this fix).
11
+ #
12
+ # Uses the same TELEMETRY_DIR resolution convention as test_telemetry.sh
13
+ # (prefers context/scripts/telemetry when present) so this exercises the same
14
+ # copy CI actually runs, while explicitly pointing TELEMETRY_PRICING_FILE at
15
+ # the canonical bundled registry so pricing resolves regardless of which copy
16
+ # is under test (context/scripts/telemetry ships no bundled pricing.json).
17
+ set -uo pipefail
18
+
19
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
20
+ if [[ -d "$ROOT_DIR/context/scripts/telemetry" ]]; then
21
+ TELEMETRY_DIR="$ROOT_DIR/context/scripts/telemetry"
22
+ else
23
+ TELEMETRY_DIR="$HOME/.flow-agents/context/scripts/telemetry"
24
+ fi
25
+ TELEMETRY_SH="${TELEMETRY_DIR}/telemetry.sh"
26
+ PRICING_FILE="$ROOT_DIR/scripts/telemetry/pricing.json"
27
+ FIXTURE_TRANSCRIPT="$ROOT_DIR/evals/fixtures/telemetry/usage-transcript-sample.jsonl"
28
+
29
+ TMPDIR_EVAL=$(mktemp -d /tmp/eval-telemetry-usage-pipeline.XXXXXX)
30
+ TMPLOG="${TMPDIR_EVAL}/test-output.jsonl"
31
+ FAKE_HOME="${TMPDIR_EVAL}/home"
32
+ mkdir -p "$FAKE_HOME" "$TMPDIR_EVAL/sessions"
33
+
34
+ pass=0; fail=0
35
+ _pass() { echo " ✓ $1"; pass=$((pass + 1)); }
36
+ _fail() { echo " ✗ $1"; fail=$((fail + 1)); }
37
+
38
+ echo "=== Layer 2: Telemetry Usage Pipeline (hermetic fixture) ==="
39
+ echo ""
40
+
41
+ if [[ ! -f "$TELEMETRY_SH" ]]; then
42
+ _fail "telemetry.sh not found at $TELEMETRY_SH"
43
+ echo "Cannot continue without telemetry script"
44
+ rm -rf "$TMPDIR_EVAL"
45
+ exit 1
46
+ fi
47
+ if [[ ! -f "$FIXTURE_TRANSCRIPT" ]]; then
48
+ _fail "fixture transcript not found at $FIXTURE_TRANSCRIPT"
49
+ rm -rf "$TMPDIR_EVAL"
50
+ exit 1
51
+ fi
52
+
53
+ # Wait for a new line to land in TMPLOG (telemetry.sh's Stop path emits
54
+ # asynchronously even in foreground mode's background-adjacent callers).
55
+ _wait_for_line() {
56
+ local before_lines="$1" i=0 current_lines
57
+ while [[ $i -lt 50 ]]; do
58
+ current_lines=$(wc -l < "$TMPLOG" 2>/dev/null | tr -d ' ')
59
+ [[ "${current_lines:-0}" -gt "$before_lines" ]] && break
60
+ sleep 0.1; i=$((i + 1))
61
+ done
62
+ }
63
+
64
+ _wait_for_file_line() {
65
+ local file="$1" i=0 current_lines
66
+ while [[ $i -lt 50 ]]; do
67
+ current_lines=$(wc -l < "$file" 2>/dev/null | tr -d ' ')
68
+ [[ "${current_lines:-0}" -gt 0 ]] && break
69
+ sleep 0.1; i=$((i + 1))
70
+ done
71
+ }
72
+
73
+ # Run a real Stop event against a freshly-established session (agentSpawn
74
+ # first, matching real usage — Claude Code always sends SessionStart before
75
+ # Stop). Returns the emitted session.usage event (jq-compact, one line).
76
+ _run_stop() {
77
+ local input="$1"; shift
78
+ local extra_env_count="$#"
79
+ local extra_env=("$@")
80
+ local common_env=(
81
+ HOME="$FAKE_HOME"
82
+ TELEMETRY_ENABLED=true
83
+ TELEMETRY_CHANNELS=full
84
+ TELEMETRY_CHANNEL_FULL_LOG_FILE="$TMPLOG"
85
+ FLOW_AGENTS_TELEMETRY_FOREGROUND=true
86
+ TELEMETRY_CONFIG_FILE="$TMPDIR_EVAL/telemetry.conf"
87
+ TELEMETRY_DATA_DIR="$TMPDIR_EVAL"
88
+ TELEMETRY_SESSION_DIR="$TMPDIR_EVAL/sessions"
89
+ )
90
+
91
+ local before_lines
92
+ touch "$TMPLOG"
93
+ before_lines=$(wc -l < "$TMPLOG" | tr -d ' ')
94
+ echo '{"cwd":"/tmp"}' | env "${common_env[@]}" bash "$TELEMETRY_SH" agentSpawn dev >/dev/null 2>&1
95
+ _wait_for_line "$before_lines"
96
+
97
+ before_lines=$(wc -l < "$TMPLOG" | tr -d ' ')
98
+ if [[ "$extra_env_count" -gt 0 ]]; then
99
+ echo "$input" | env "${common_env[@]}" TELEMETRY_USAGE_TRACKING=true "${extra_env[@]}" \
100
+ bash "$TELEMETRY_SH" Stop dev 2>/dev/null
101
+ else
102
+ echo "$input" | env "${common_env[@]}" TELEMETRY_USAGE_TRACKING=true \
103
+ bash "$TELEMETRY_SH" Stop dev 2>/dev/null
104
+ fi
105
+ _wait_for_line "$before_lines"
106
+
107
+ tail -n +"$((before_lines + 1))" "$TMPLOG" 2>/dev/null | jq -c 'select(.event_type=="session.usage")' | tail -1
108
+ }
109
+
110
+ # --- 1. With transcript + pricing available: real tokens, cost, concrete model
111
+ echo "--- Fixture transcript, pricing available ---"
112
+ input1=$(jq -nc --arg tp "$FIXTURE_TRANSCRIPT" '{session_id:"pipeline-1",transcript_path:$tp,hook_event_name:"Stop"}')
113
+ out1=$(_run_stop "$input1" TELEMETRY_PRICING_FILE="$PRICING_FILE")
114
+
115
+ if [[ -n "$out1" ]]; then
116
+ model=$(echo "$out1" | jq -r '.usage.model')
117
+ it=$(echo "$out1" | jq -r '.usage.input_tokens')
118
+ ot=$(echo "$out1" | jq -r '.usage.output_tokens')
119
+ cost=$(echo "$out1" | jq -r '.usage.estimated_cost_usd')
120
+ by_model_len=$(echo "$out1" | jq -r '.usage.by_model | length')
121
+
122
+ [[ "$model" != "unknown" && "$model" != "null" && -n "$model" ]] && _pass "model is concrete (got: $model)" || _fail "model should not be unknown (got: $model)"
123
+ [[ "$model" == "claude-opus-4-8" ]] && _pass "model is the dominant-by-tokens model (claude-opus-4-8)" || _fail "expected dominant model claude-opus-4-8, got $model"
124
+ [[ "$it" != "null" && "$it" -gt 0 ]] && _pass "input_tokens is real and non-null (got: $it)" || _fail "input_tokens should be non-null/positive (got: $it)"
125
+ [[ "$ot" != "null" && "$ot" -gt 0 ]] && _pass "output_tokens is real and non-null (got: $ot)" || _fail "output_tokens should be non-null/positive (got: $ot)"
126
+ cost_positive=$(echo "$out1" | jq -r '(.usage.estimated_cost_usd // 0) > 0')
127
+ [[ "$cost" != "null" && "$cost_positive" == "true" ]] && _pass "estimated_cost_usd is real and non-null (got: $cost)" || _fail "estimated_cost_usd should be non-null/positive (got: $cost)"
128
+ [[ "$by_model_len" == "2" ]] && _pass "by_model has 2 entries" || _fail "expected 2 by_model entries, got $by_model_len"
129
+ else
130
+ _fail "no session.usage event emitted for fixture transcript"
131
+ fi
132
+
133
+ # --- 1b. Economics attribution: canonical current pointer beats stale legacy
134
+ echo ""
135
+ echo "--- Economics attribution uses canonical current pointer ---"
136
+ ATTR_CWD="${TMPDIR_EVAL}/workspace"
137
+ CANON_SLUG="canonical-task"
138
+ LEGACY_SLUG="legacy-task"
139
+ ECON_LOG="${TMPDIR_EVAL}/economics.jsonl"
140
+ mkdir -p "$ATTR_CWD/.kontourai/flow-agents/$CANON_SLUG" "$ATTR_CWD/.flow-agents/$LEGACY_SLUG"
141
+ : > "$ECON_LOG"
142
+ printf '%s\n' "{\"active_slug\":\"$CANON_SLUG\"}" > "$ATTR_CWD/.kontourai/flow-agents/current.json"
143
+ printf '%s\n' "{\"active_slug\":\"$LEGACY_SLUG\"}" > "$ATTR_CWD/.flow-agents/current.json"
144
+ printf '%s\n' '{"schema_version":"1.0","task_slug":"canonical-task","phase":"execution","verification_verdict":"PASS"}' > "$ATTR_CWD/.kontourai/flow-agents/$CANON_SLUG/state.json"
145
+ printf '%s\n' '{"schema_version":"1.0","task_slug":"legacy-task","phase":"execution","verification_verdict":"PASS"}' > "$ATTR_CWD/.flow-agents/$LEGACY_SLUG/state.json"
146
+ input_attr=$(jq -nc --arg tp "$FIXTURE_TRANSCRIPT" --arg cwd "$ATTR_CWD" '{session_id:"pipeline-attribution",transcript_path:$tp,hook_event_name:"Stop",cwd:$cwd}')
147
+ TELEMETRY_SH_SAVED="$TELEMETRY_SH"
148
+ TELEMETRY_SH="$ROOT_DIR/scripts/telemetry/telemetry.sh"
149
+ out_attr=$(_run_stop "$input_attr" TELEMETRY_PRICING_FILE="$PRICING_FILE" TELEMETRY_ECONOMICS_LOG_FILE="$ECON_LOG")
150
+ TELEMETRY_SH="$TELEMETRY_SH_SAVED"
151
+ _wait_for_file_line "$ECON_LOG"
152
+
153
+ if [[ -n "$out_attr" && -s "$ECON_LOG" ]]; then
154
+ attr_task=$(tail -1 "$ECON_LOG" | jq -r '.task_slug')
155
+ attr_phase=$(tail -1 "$ECON_LOG" | jq -r '.phases[0].phase')
156
+ [[ "$attr_task" == "$CANON_SLUG" ]] && _pass "economics record task_slug comes from canonical .kontourai current pointer" || _fail "expected canonical task_slug '$CANON_SLUG', got '$attr_task'"
157
+ [[ "$attr_phase" == "execution" ]] && _pass "economics record phase comes from canonical state.json" || _fail "expected canonical phase execution, got '$attr_phase'"
158
+ else
159
+ _fail "economics attribution record was not emitted for canonical-current test"
160
+ fi
161
+
162
+ # --- 2. Pricing forced unavailable: tokens survive, cost is null ------------
163
+ echo ""
164
+ echo "--- Fixture transcript, pricing forced unavailable ---"
165
+ input2=$(jq -nc --arg tp "$FIXTURE_TRANSCRIPT" '{session_id:"pipeline-2",transcript_path:$tp,hook_event_name:"Stop"}')
166
+ out2=$(_run_stop "$input2" TELEMETRY_PRICING_FILE=/nonexistent/pricing.json TELEMETRY_PRICING_URL="" FLOW_AGENTS_PRICING_FILE="" FLOW_AGENTS_PRICING_URL="")
167
+
168
+ if [[ -n "$out2" ]]; then
169
+ it2=$(echo "$out2" | jq -r '.usage.input_tokens')
170
+ ot2=$(echo "$out2" | jq -r '.usage.output_tokens')
171
+ cost2=$(echo "$out2" | jq -r '.usage.estimated_cost_usd')
172
+ pv2=$(echo "$out2" | jq -r '.usage.pricing_version')
173
+ model2=$(echo "$out2" | jq -r '.usage.model')
174
+
175
+ [[ "$it2" != "null" && "$it2" -gt 0 ]] && _pass "tokens survive when pricing unavailable (input_tokens=$it2)" || _fail "tokens should survive when pricing unavailable (got input_tokens=$it2)"
176
+ [[ "$ot2" != "null" && "$ot2" -gt 0 ]] && _pass "output_tokens survive when pricing unavailable (got: $ot2)" || _fail "output_tokens should survive when pricing unavailable (got: $ot2)"
177
+ [[ "$cost2" == "null" ]] && _pass "estimated_cost_usd is null when pricing unavailable (cost degrades, tokens don't)" || _fail "estimated_cost_usd should be null when pricing unavailable (got: $cost2)"
178
+ [[ "$pv2" == "null" ]] && _pass "pricing_version is null when pricing unavailable" || _fail "pricing_version should be null when pricing unavailable (got: $pv2)"
179
+ [[ "$model2" == "claude-opus-4-8" ]] && _pass "model still resolves from transcript when pricing unavailable" || _fail "expected model claude-opus-4-8, got $model2"
180
+ else
181
+ _fail "no session.usage event emitted when pricing forced unavailable"
182
+ fi
183
+
184
+ # --- 3. No transcript (kiro-cli style): model still resolves via kiro fallback, no regression
185
+ echo ""
186
+ echo "--- No transcript (kiro-cli non-regression) ---"
187
+ input3='{"session_id":"pipeline-3","hook_event_name":"Stop"}'
188
+ out3=$(_run_stop "$input3")
189
+
190
+ if [[ -n "$out3" ]]; then
191
+ model3=$(echo "$out3" | jq -r '.usage.model')
192
+ it3=$(echo "$out3" | jq -r '.usage.input_tokens')
193
+ by_model3=$(echo "$out3" | jq -r '.usage.by_model')
194
+
195
+ # FAKE_HOME has no ~/.kiro/agents spec, so usage_get_model's kiro lookup
196
+ # falls through to "unknown" — this is the pre-existing, unfixed kiro path
197
+ # and must be untouched by the transcript-model override (transcript_usage
198
+ # is null here, so model is never overridden).
199
+ [[ "$model3" == "unknown" ]] && _pass "no-transcript path still resolves via usage_get_model kiro fallback (unknown, no regression)" || _fail "expected kiro fallback 'unknown' with no transcript, got $model3"
200
+ [[ "$it3" == "null" ]] && _pass "input_tokens is null with no transcript (expected)" || _fail "expected null input_tokens with no transcript, got $it3"
201
+ [[ "$by_model3" == "null" ]] && _pass "by_model is null with no transcript (expected)" || _fail "expected null by_model with no transcript, got $by_model3"
202
+ else
203
+ _fail "no session.usage event emitted for no-transcript case"
204
+ fi
205
+
206
+ # --- 4. Empty transcript + TELEMETRY_USAGE_DEBUG=1: debug reason is emitted -
207
+ echo ""
208
+ echo "--- Empty transcript, TELEMETRY_USAGE_DEBUG=1 (debug path) ---"
209
+ EMPTY_TRANSCRIPT="${TMPDIR_EVAL}/empty-transcript.jsonl"
210
+ : > "$EMPTY_TRANSCRIPT"
211
+ DEBUG_DRIFT_LOG="${TMPDIR_EVAL}/debug-drift.log"
212
+ input4=$(jq -nc --arg tp "$EMPTY_TRANSCRIPT" '{session_id:"pipeline-4",transcript_path:$tp,hook_event_name:"Stop"}')
213
+ out4=$(_run_stop "$input4" TELEMETRY_USAGE_DEBUG=1 TELEMETRY_DRIFT_LOG="$DEBUG_DRIFT_LOG")
214
+
215
+ if grep -q '\[telemetry\] usage_parse_transcript:' "$DEBUG_DRIFT_LOG" 2>/dev/null; then
216
+ _pass "debug reason line emitted for empty-transcript no-usage scenario"
217
+ else
218
+ _fail "expected a usage_parse_transcript debug reason line in $DEBUG_DRIFT_LOG"
219
+ fi
220
+
221
+ model4=$(echo "$out4" | jq -r '.usage.model // "unknown"' 2>/dev/null)
222
+ [[ -z "$out4" || "$model4" == "unknown" ]] && _pass "empty-transcript path emits no real usage (no regression)" || _fail "expected no/unknown usage for empty transcript, got model=$model4"
223
+
224
+ rm -rf "$TMPDIR_EVAL"
225
+
226
+ echo ""
227
+ echo "Telemetry usage pipeline: $pass passed, $fail failed"
228
+ [ "$fail" -eq 0 ]
@@ -9,21 +9,23 @@
9
9
  #
10
10
  # 1. no-label-bypass (finding 1) → not-run: a test_output claim with no
11
11
  # manifest-matched execution.label is a divergence, never session-local.
12
- # 2. skip-assumed-bypass (finding 2) → unwaived-assumed: 'assumed' alone is not a pass.
13
- # 3. status-misassertion (finding 3) status-misassertion: CI re-derives status; a
12
+ # 2. mcp-degrade (issue 492) → not-run: an MCP-shaped execution.label is not a
13
+ # manifest command and can never be RECONCILED as a CI-verified command check.
14
+ # 3. skip-assumed-bypass (finding 2) → unwaived-assumed: 'assumed' alone is not a pass.
15
+ # 4. status-misassertion (finding 3) → status-misassertion: CI re-derives status; a
14
16
  # self-reported status that does not match the bundle's own evidence is rejected.
15
- # 4. waived-command-check (finding 4) → waiver-on-command-check: a command-backed
17
+ # 5. waived-command-check (finding 4) → waiver-on-command-check: a command-backed
16
18
  # (test_output) check cannot be waived.
17
- # 5. ws3-old-style-bundle (AC6) → old all-test_output bundle FAILS the same way (exit 1,
19
+ # 6. ws3-old-style-bundle (AC6) → old all-test_output bundle FAILS the same way (exit 1,
18
20
  # divergences) under the new reconciler as under the old one — no soundness regression.
19
- # 6. fabricated-attestation (iteration-4, converged iteration-3 finding, both gates) → a
21
+ # 7. fabricated-attestation (iteration-4, converged iteration-3 finding, both gates) → a
20
22
  # fully self-consistent, hand-fabricated no-command 'security' claim+evidence+event
21
23
  # triple (indistinguishable from a genuine attestation at the reconciler's own
22
24
  # re-derivation layer) MUST still pass (exit 0 — blocking attestations at L0 would break
23
25
  # every honest human-attestation use) BUT MUST be loudly, distinctly marked
24
26
  # 'ATTESTED (not independently verifiable at L0)' plus the summary count line — never a
25
27
  # quiet SESSION-LOCAL OK indistinguishable from a reconciled check. See ADR 0020 Residuals.
26
- # 7. delivery/DECLARED marker regressions (ADR 0022 §1/§2, section 7 below) — bundle-absent
28
+ # 8. delivery/DECLARED marker regressions (ADR 0022 §1/§2, section 7 below) — bundle-absent
27
29
  # fail-closed default, malformed/empty-array/missing-field diagnostics, and all four
28
30
  # matchesScope() forms (ref:, commit: single + range, author:, branch-prefix:) including
29
31
  # positive AND near-miss coverage, plus the compound (space-separated AND) scope form a
@@ -49,10 +51,12 @@ errors=0
49
51
  _pass() { echo " PASS: $1"; }
50
52
  _fail() { echo " FAIL: $1"; errors=$((errors + 1)); }
51
53
 
52
- # run_case <label> <bundle> <needle>
53
- # Asserts: reconciler exits non-zero AND stdout/stderr contains <needle>.
54
+ # run_case <label> <bundle> <needle> [forbidden]
55
+ # Asserts: reconciler exits non-zero, stdout/stderr contains <needle>, and optionally does
56
+ # NOT contain <forbidden>.
54
57
  run_case() {
55
58
  local label="$1" bundle="$2" needle="$3"
59
+ local forbidden="${4:-}"
56
60
  echo "=== $label ==="
57
61
  if [[ ! -f "$bundle" ]]; then _fail "$label: fixture not found at $bundle"; return; fi
58
62
  local out code
@@ -69,25 +73,38 @@ run_case() {
69
73
  else
70
74
  _fail "$label: expected divergence \"$needle\" not found — output: $out"
71
75
  fi
76
+ if [[ -n "$forbidden" ]]; then
77
+ if echo "$out" | grep -qF "$forbidden"; then
78
+ _fail "$label: must NOT emit \"$forbidden\" — output: $out"
79
+ else
80
+ _pass "$label: does not emit \"$forbidden\""
81
+ fi
82
+ fi
72
83
  }
73
84
 
74
85
  # 1. Reviewer's no-label exploit → not-run (test_output must reconcile against the manifest).
75
86
  run_case "no-label-bypass (finding 1)" "$FX/no-label-bypass.json" \
76
87
  "no manifest-matched execution.label"
77
88
 
78
- # 2. Verifier's skip->assumed exploitunwaived-assumed.
89
+ # 2. Issue #492: MCP-shaped self-reported pass not-run (not manifest-matched), never
90
+ # RECONCILED as a CI-verified command check.
91
+ run_case "mcp-degrade (issue 492)" "$FX/mcp-degrade.json" \
92
+ "command is not in the reconcile manifest" \
93
+ "RECONCILED"
94
+
95
+ # 3. Verifier's skip->assumed exploit → unwaived-assumed.
79
96
  run_case "skip-assumed-bypass (finding 2)" "$FX/skip-assumed-bypass.json" \
80
97
  "[unwaived-assumed]"
81
98
 
82
- # 3. Status-misassertion exploit → status re-derived CI-side; asserted != derived.
99
+ # 4. Status-misassertion exploit → status re-derived CI-side; asserted != derived.
83
100
  run_case "status-misassertion (finding 3)" "$FX/status-misassertion.json" \
84
101
  "[status-misassertion]"
85
102
 
86
- # 4. Waived command-backed check → waiver-on-command-check.
103
+ # 5. Waived command-backed check → waiver-on-command-check.
87
104
  run_case "waived-command-check (finding 4)" "$FX/waived-command-check.json" \
88
105
  "[waiver-on-command-check]"
89
106
 
90
- # 5. AC6 backward-compat regression: the real ws3-kit-dependencies-namespacing/trust.bundle
107
+ # 6. AC6 backward-compat regression: the real ws3-kit-dependencies-namespacing/trust.bundle
91
108
  # (an old-style all-test_output bundle whose commands are not manifest-matched) FAILS the
92
109
  # same way under the new reconciler as under the old one — same FAIL verdict, divergences
93
110
  # present, no silent pass introduced.
@@ -115,7 +132,7 @@ else
115
132
  fi
116
133
  fi
117
134
 
118
- # 6. Fabricated-attestation (iteration-4): passes (exit 0) but MUST carry the loud ATTESTED
135
+ # 7. Fabricated-attestation (iteration-4): passes (exit 0) but MUST carry the loud ATTESTED
119
136
  # marker and the summary count line — NOT the old quiet SESSION-LOCAL OK. This is a
120
137
  # visibility assertion, not a divergence assertion (unlike run_case above): a fabricated
121
138
  # self-consistent attestation bundle is, by construction, indistinguishable from a genuine
@@ -0,0 +1,247 @@
1
+ #!/usr/bin/env bash
2
+ # test_trust_reconcile_trailer_diagnostic.sh — Integration eval for scripts/ci/
3
+ # trust-reconcile.js's runtime-session commit-trailer diagnostic (issue #305, ADR 0022 §1).
4
+ #
5
+ # Proves the diagnostic added in findRuntimeSessionTrailers()/logRuntimeSessionTrailers()
6
+ # is audit-only:
7
+ # AC5 trailer-diagnostic-positive: a commit with a Claude-Session: trailer in the
8
+ # resolved range produces the exact line
9
+ # `[trust-reconcile] identified: runtime-session trailer 'Claude-Session' on <ref>`
10
+ # — the trailer KEY only, never the VALUE (session URL) — printed for the range's
11
+ # commit-with-trailer.
12
+ # AC6 trailer-diagnostic-negative: a commit range with no runtime-session trailer
13
+ # produces NO such diagnostic line, and the reconciler's exit code is IDENTICAL
14
+ # to the positive case (same canonical command, same bundle/DECLARED absence,
15
+ # differing only in trailer presence) — the diagnostic never changes the exit path.
16
+ # AC7 trailer-diagnostic-checkout-depth-verified: .github/workflows/trust-reconcile.yml
17
+ # already carries fetch-depth: 0 (no .yml edit needed for #305's depth ask).
18
+ # Base-ref-range vs narrower-fallback: both range-resolution paths
19
+ # (TRUST_RECONCILE_BASE_REF/GITHUB_BASE_REF resolvable vs unresolvable, falling back
20
+ # to just the head commit) are exercised directly against the exported
21
+ # findRuntimeSessionTrailers() helper (unit-style, no reconciler wrapper needed for
22
+ # this sub-case) as well as through the full reconciler CLI (integration-style).
23
+ #
24
+ # Eval-authoring hazards observed (per plan/goal constraints):
25
+ # - No self-recursive harness invocation: this eval shells out to trust-reconcile.js and
26
+ # to git only; it never invokes evals/run.sh or any other eval-suite entry point.
27
+ # - No legacy/deprecated-path literals: trailer fixture values are synthetic/generic
28
+ # (a https://claude.ai/code/session_test-fixture-only placeholder), never a copy of a
29
+ # real committed session URL or a legacy flat delivery/trust.bundle-style literal.
30
+ #
31
+ # Deterministic, no model spend, self-cleaning.
32
+ # Usage: bash evals/integration/test_trust_reconcile_trailer_diagnostic.sh
33
+
34
+ set -uo pipefail
35
+
36
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
37
+ RECONCILE="$ROOT/scripts/ci/trust-reconcile.js"
38
+
39
+ errors=0
40
+ TMP_DIR="$(mktemp -d)"
41
+ trap 'rm -rf "$TMP_DIR"' EXIT
42
+ pass() { echo " ✓ $1"; }
43
+ fail() { echo " ✗ $1"; errors=$((errors + 1)); }
44
+
45
+ echo "=== trust-reconcile.js runtime-session trailer diagnostic — integration eval ==="
46
+
47
+ # Synthetic-only trailer value — never a real/legacy committed session URL.
48
+ TRAILER_VALUE="https://claude.ai/code/session_test-fixture-only"
49
+ RECONCILE_CMD="node -e 'process.exit(0)'"
50
+
51
+ # ─── Fixture: a real scratch git repo with two commits (base, then head) ─────────
52
+ build_repo() {
53
+ local repo_dir="$1"
54
+ local with_trailer="$2" # "true" or "false"
55
+ mkdir -p "$repo_dir"
56
+ git init -q "$repo_dir"
57
+ git -C "$repo_dir" config user.email "eval@local"
58
+ git -C "$repo_dir" config user.name "eval"
59
+ echo "base" > "$repo_dir/f.txt"
60
+ git -C "$repo_dir" add -A
61
+ git -C "$repo_dir" commit -qm "base commit"
62
+ echo "change" >> "$repo_dir/f.txt"
63
+ git -C "$repo_dir" add -A
64
+ if [[ "$with_trailer" == "true" ]]; then
65
+ git -C "$repo_dir" commit -qm "$(printf 'feat: trailer test commit\n\nClaude-Session: %s\n' "$TRAILER_VALUE")"
66
+ else
67
+ git -C "$repo_dir" commit -qm "feat: no-trailer test commit"
68
+ fi
69
+ }
70
+
71
+ POS_REPO="$TMP_DIR/positive-repo"
72
+ NEG_REPO="$TMP_DIR/negative-repo"
73
+ build_repo "$POS_REPO" "true"
74
+ build_repo "$NEG_REPO" "false"
75
+
76
+ POS_BASE="$(git -C "$POS_REPO" rev-list --max-parents=0 HEAD)"
77
+ POS_HEAD="$(git -C "$POS_REPO" rev-parse HEAD)"
78
+ NEG_BASE="$(git -C "$NEG_REPO" rev-list --max-parents=0 HEAD)"
79
+ NEG_HEAD="$(git -C "$NEG_REPO" rev-parse HEAD)"
80
+
81
+ # A THIRD repo where the range-distinguishing case actually differs from POS_REPO/NEG_REPO:
82
+ # the trailer lives on a MIDDLE commit (between base and a later, trailer-free head commit)
83
+ # -- reachable ONLY via the ranged `git log <base>..<head>` walk, never via the narrower
84
+ # head-commit-only fallback. This is the genuine base-ref-range-vs-fallback distinguisher;
85
+ # POS_REPO/NEG_REPO above (trailer-or-not on the head commit itself) would pass under
86
+ # EITHER code path and do not by themselves prove the ranged branch is exercised.
87
+ RANGE_REPO="$TMP_DIR/range-only-repo"
88
+ mkdir -p "$RANGE_REPO"
89
+ git init -q "$RANGE_REPO"
90
+ git -C "$RANGE_REPO" config user.email "eval@local"
91
+ git -C "$RANGE_REPO" config user.name "eval"
92
+ echo "base" > "$RANGE_REPO/f.txt"
93
+ git -C "$RANGE_REPO" add -A
94
+ git -C "$RANGE_REPO" commit -qm "base commit"
95
+ RANGE_BASE="$(git -C "$RANGE_REPO" rev-parse HEAD)"
96
+ echo "middle" >> "$RANGE_REPO/f.txt"
97
+ git -C "$RANGE_REPO" add -A
98
+ git -C "$RANGE_REPO" commit -qm "$(printf 'feat: middle commit carries the trailer
99
+
100
+ Claude-Session: %s
101
+ ' "$TRAILER_VALUE")"
102
+ echo "head" >> "$RANGE_REPO/f.txt"
103
+ git -C "$RANGE_REPO" add -A
104
+ git -C "$RANGE_REPO" commit -qm "feat: head commit itself has no trailer"
105
+ RANGE_HEAD="$(git -C "$RANGE_REPO" rev-parse HEAD)"
106
+
107
+ # --- AC5: positive case — exact diagnostic line, key only, no value leak --------
108
+ echo "--- AC5: positive — Claude-Session trailer present ---"
109
+ pos_out="$(TRUST_RECONCILE_BASE_REF="$POS_BASE" TRUST_RECONCILE_SHA="$POS_HEAD" TRUST_RECONCILE_REF="test-branch" \
110
+ TRUST_RECONCILE_COMMANDS="$RECONCILE_CMD" node "$RECONCILE" --repo-root "$POS_REPO" 2>&1)"
111
+ pos_code=$?
112
+
113
+ if echo "$pos_out" | grep -qF "[trust-reconcile] identified: runtime-session trailer 'Claude-Session' on test-branch"; then
114
+ pass "AC5: exact diagnostic line printed for the Claude-Session trailer"
115
+ else
116
+ fail "AC5: expected diagnostic line not found — output: $pos_out"
117
+ fi
118
+
119
+ if echo "$pos_out" | grep -qF "$TRAILER_VALUE"; then
120
+ fail "AC5: trailer VALUE ($TRAILER_VALUE) leaked into stdout — diagnostic must log the KEY only"
121
+ else
122
+ pass "AC5: trailer VALUE does not appear anywhere in stdout (key-only logging, per ADR 0022 §1's exact quoted format)"
123
+ fi
124
+
125
+ # --- AC6: negative case — no trailer, no line, same canonical command/DECLARED state --
126
+ echo "--- AC6: negative — no runtime-session trailer present ---"
127
+ neg_out="$(TRUST_RECONCILE_BASE_REF="$NEG_BASE" TRUST_RECONCILE_SHA="$NEG_HEAD" TRUST_RECONCILE_REF="test-branch" \
128
+ TRUST_RECONCILE_COMMANDS="$RECONCILE_CMD" node "$RECONCILE" --repo-root "$NEG_REPO" 2>&1)"
129
+ neg_code=$?
130
+
131
+ if echo "$neg_out" | grep -q "identified: runtime-session trailer"; then
132
+ fail "AC6: an 'identified: runtime-session trailer' line was printed with no trailer present"
133
+ else
134
+ pass "AC6: no 'identified: runtime-session trailer' line printed (no trailer present)"
135
+ fi
136
+
137
+ if [[ "$pos_code" == "$neg_code" ]]; then
138
+ pass "AC6: reconciler exit code is IDENTICAL between the trailer-present and no-trailer cases (both: $pos_code) — diagnostic never changes the exit path"
139
+ else
140
+ fail "AC6: reconciler exit code DIVERGED between trailer-present ($pos_code) and no-trailer ($neg_code) cases — the diagnostic must never affect the exit path"
141
+ fi
142
+
143
+ # --- Base-ref-range vs narrower-fallback, exercised directly on the exported helper --
144
+ # RANGE_REPO's trailer lives on a MIDDLE commit, absent from the head commit itself — this
145
+ # genuinely distinguishes the two code paths (see RANGE_REPO fixture comment above).
146
+ echo "--- base-ref-range vs narrower-fallback (direct helper exercise, genuinely distinguishing fixture) ---"
147
+ range_result="$(TRUST_RECONCILE_BASE_REF="$RANGE_BASE" node -e "
148
+ const { findRuntimeSessionTrailers } = require('$RECONCILE');
149
+ const found = findRuntimeSessionTrailers('$RANGE_REPO', { sha: '$RANGE_HEAD', ref: 'test-branch' });
150
+ console.log(JSON.stringify(found));
151
+ " 2>&1)"
152
+ if echo "$range_result" | grep -q '"trailerName":"Claude-Session"'; then
153
+ pass "base-ref-range path: findRuntimeSessionTrailers finds a trailer on a MIDDLE commit (base..head range) that is absent from the head commit itself, when TRUST_RECONCILE_BASE_REF resolves"
154
+ else
155
+ fail "base-ref-range path: findRuntimeSessionTrailers did not find the middle-commit trailer via the ranged walk — output: $range_result"
156
+ fi
157
+
158
+ # Narrower fallback: no base ref env var set at all (helper's own internal base-ref
159
+ # resolution reads process.env directly, so unset both env vars for this call) — against
160
+ # the SAME range-only repo, this must NOT find the middle-commit trailer (it is invisible
161
+ # to a head-commit-only scan), proving the fallback is genuinely narrower, not just an
162
+ # equivalent restatement of the ranged path.
163
+ fallback_result="$(env -u TRUST_RECONCILE_BASE_REF -u GITHUB_BASE_REF node -e "
164
+ const { findRuntimeSessionTrailers } = require('$RECONCILE');
165
+ const found = findRuntimeSessionTrailers('$RANGE_REPO', { sha: '$RANGE_HEAD', ref: 'test-branch' });
166
+ console.log(JSON.stringify(found));
167
+ " 2>&1)"
168
+ if [[ "$fallback_result" == "[]" ]]; then
169
+ pass "narrower-fallback path: findRuntimeSessionTrailers does NOT find the middle-commit-only trailer when no base ref is resolvable (head-commit-only scan is genuinely narrower)"
170
+ else
171
+ fail "narrower-fallback path: expected an empty array (middle-commit trailer invisible to head-only scan), got: $fallback_result"
172
+ fi
173
+
174
+ # Narrower fallback, positive: against POS_REPO (trailer ON the head commit itself), the
175
+ # no-base-ref fallback still finds it — proving the fallback is not simply "always empty".
176
+ fallback_pos_result="$(env -u TRUST_RECONCILE_BASE_REF -u GITHUB_BASE_REF node -e "
177
+ const { findRuntimeSessionTrailers } = require('$RECONCILE');
178
+ const found = findRuntimeSessionTrailers('$POS_REPO', { sha: '$POS_HEAD', ref: 'test-branch' });
179
+ console.log(JSON.stringify(found));
180
+ " 2>&1)"
181
+ if echo "$fallback_pos_result" | grep -q '"trailerName":"Claude-Session"'; then
182
+ pass "narrower-fallback path: still finds a trailer that IS on the head commit itself (fallback scans the head commit, not nothing)"
183
+ else
184
+ fail "narrower-fallback path: did not find the head-commit trailer with no base ref set — output: $fallback_pos_result"
185
+ fi
186
+
187
+ # Narrower-fallback negative: no base ref, no trailer on the head commit itself (NEG_REPO).
188
+ fallback_neg_result="$(env -u TRUST_RECONCILE_BASE_REF -u GITHUB_BASE_REF node -e "
189
+ const { findRuntimeSessionTrailers } = require('$RECONCILE');
190
+ const found = findRuntimeSessionTrailers('$NEG_REPO', { sha: '$NEG_HEAD', ref: 'test-branch' });
191
+ console.log(JSON.stringify(found));
192
+ " 2>&1)"
193
+ if [[ "$fallback_neg_result" == "[]" ]]; then
194
+ pass "narrower-fallback path: findRuntimeSessionTrailers returns an empty array when the head commit itself has no trailer and no base ref is resolvable"
195
+ else
196
+ fail "narrower-fallback path: expected an empty array, got: $fallback_neg_result"
197
+ fi
198
+
199
+ # --- No-crash: unresolvable sha degrades to empty array, never throws -----------
200
+ degrade_result="$(node -e "
201
+ const { findRuntimeSessionTrailers } = require('$RECONCILE');
202
+ const found = findRuntimeSessionTrailers('$POS_REPO', { sha: '', ref: 'test-branch' });
203
+ console.log(JSON.stringify(found));
204
+ " 2>&1)"
205
+ if [[ "$degrade_result" == "[]" ]]; then
206
+ pass "empty sha degrades to an empty array (never throws, never crashes the caller)"
207
+ else
208
+ fail "empty sha did not degrade cleanly — got: $degrade_result"
209
+ fi
210
+
211
+ # --- AC7: checkout-depth already verified (no .yml edit needed) -----------------
212
+ echo "--- AC7: .github/workflows/trust-reconcile.yml already carries fetch-depth: 0 ---"
213
+ WORKFLOW_FILE="$ROOT/.github/workflows/trust-reconcile.yml"
214
+ if grep -n "fetch-depth: 0" "$WORKFLOW_FILE" >/dev/null 2>&1; then
215
+ pass "AC7: fetch-depth: 0 already present in trust-reconcile.yml (Addendum part 3) — no .yml change needed for #305's depth ask"
216
+ else
217
+ fail "AC7: fetch-depth: 0 not found in trust-reconcile.yml — expected it to already be present"
218
+ fi
219
+
220
+ if command -v python3 >/dev/null 2>&1 && python3 -c "import yaml" 2>/dev/null; then
221
+ if python3 - "$WORKFLOW_FILE" << 'PY' 2>/dev/null
222
+ import sys, yaml
223
+ try:
224
+ yaml.safe_load(open(sys.argv[1]).read())
225
+ sys.exit(0)
226
+ except yaml.YAMLError:
227
+ sys.exit(1)
228
+ PY
229
+ then
230
+ pass "AC7: trust-reconcile.yml parses as valid YAML (python3 yaml)"
231
+ else
232
+ fail "AC7: trust-reconcile.yml failed python3 yaml parse"
233
+ fi
234
+ else
235
+ grep -q "^name:" "$WORKFLOW_FILE" && grep -q "trust-reconcile" "$WORKFLOW_FILE" && \
236
+ pass "AC7: trust-reconcile.yml has expected structural fields (yaml parser unavailable, structural fallback)" || \
237
+ fail "AC7: trust-reconcile.yml missing expected structural fields"
238
+ fi
239
+
240
+ echo ""
241
+ if [[ "$errors" -eq 0 ]]; then
242
+ echo "PASS: trust-reconcile.js runtime-session trailer diagnostic (audit-only, key-only, exit-code-identical)"
243
+ exit 0
244
+ else
245
+ echo "FAIL: $errors check(s) failed"
246
+ exit 1
247
+ fi