@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,316 @@
1
+ 'use strict';
2
+ /**
3
+ * codex-exit-code.js — codex-only host-banner exit-code extraction (#470).
4
+ *
5
+ * The codex PostToolUse payload carries no structured exit code (no
6
+ * `tool_response.exitCode`/`exit_code`, no `error`, no `stderr`). The only
7
+ * deterministic host signal is a fixed-format banner the codex CLI itself
8
+ * writes into the session rollout's `function_call_output`:
9
+ *
10
+ * Process exited with code N
11
+ *
12
+ * This is HOST-authored prose (observed on codex-cli 0.142.5), not model
13
+ * narration — regex-matching exactly this banner is a deterministic host
14
+ * signal, distinct from scanning stdout/narration for "error"/"fail" words
15
+ * (which docs/spec/runtime-hook-surface.md §2.5 forbids and this module does
16
+ * not do).
17
+ *
18
+ * SECURITY (iteration 2 / #470 review). The banner lives in the HOST-authored
19
+ * PREAMBLE of a `function_call_output.output` string, BEFORE the model's own
20
+ * stdout, which is appended after a literal `Output:` delimiter:
21
+ *
22
+ * Process exited with code 1
23
+ * Original token count: 25
24
+ * Output:
25
+ * <arbitrary model-produced stdout, which may itself contain the string
26
+ * "Process exited with code 0" — this is NOT a host signal and must never
27
+ * be matched>
28
+ *
29
+ * Both exports are therefore PREAMBLE-ANCHORED: extraction only ever looks
30
+ * BEFORE the first `Output:` delimiter and takes the FIRST banner match there
31
+ * (the host writes it once). The post-delimiter model stdout is never
32
+ * scanned, at any size — this is what makes the extraction fail-safe against
33
+ * a command that deliberately prints a forged banner to stdout.
34
+ *
35
+ * Both exports are pure/fail-open: malformed input, a missing/unreadable file,
36
+ * an oversized/inaccessible rollout, or an ambiguous cross-call correlation
37
+ * all degrade to `null` ("no signal"), never a throw. Callers must treat
38
+ * `null` as "extraction unavailable", not as exit code 0.
39
+ */
40
+
41
+ const fs = require('fs');
42
+ const path = require('path');
43
+ const os = require('os');
44
+
45
+ // The two forms the `Output:` delimiter can take depending on how far the
46
+ // caller has decoded the surrounding JSON:
47
+ // - a JSON.parse'd `output` string carries the delimiter as a REAL newline
48
+ // pair: "...\nOutput:\n...".
49
+ // - a raw (still JSON-escaped) slice of the rollout file — used only by the
50
+ // head-only fallback below, when a line is too large to fully parse —
51
+ // carries it literally escaped: "...\\nOutput:\\n...".
52
+ // extractExitCodeFromBanner accepts either input flavor and anchors on
53
+ // whichever form is present (real callers only ever hand it one flavor at a
54
+ // time; this is not an ambiguity, just support for both call sites).
55
+ const ANCHOR_REAL = '\nOutput:\n';
56
+ const ANCHOR_ESCAPED = '\\nOutput:\\n';
57
+
58
+ const BANNER_RE = /Process exited with code (\d+)/;
59
+
60
+ function splitPreamble(text) {
61
+ const iReal = text.indexOf(ANCHOR_REAL);
62
+ const iEsc = text.indexOf(ANCHOR_ESCAPED);
63
+ let idx = -1;
64
+ if (iReal !== -1 && iEsc !== -1) idx = Math.min(iReal, iEsc);
65
+ else if (iReal !== -1) idx = iReal;
66
+ else if (iEsc !== -1) idx = iEsc;
67
+ // No anchor found at all: fall back to treating the whole text as the
68
+ // preamble (documented residual — the observed codex format always
69
+ // includes the `Output:` delimiter; see docs/spec + plan risk log).
70
+ return idx === -1 ? text : text.slice(0, idx);
71
+ }
72
+
73
+ /**
74
+ * extractExitCodeFromBanner(text) → integer | null
75
+ *
76
+ * Pure regex extraction — no I/O, cannot throw on well-formed string input.
77
+ * PREAMBLE-ANCHORED (#470 iteration 2, CRITICAL finding #1): splits `text` on
78
+ * the FIRST `Output:` delimiter (real or JSON-escaped form) and matches the
79
+ * banner ONLY in the portion before it, returning the FIRST match. The
80
+ * model's stdout after the delimiter is never scanned, so a command that
81
+ * prints a forged `Process exited with code 0` to its own stdout cannot
82
+ * override the real host-reported code in the preamble.
83
+ */
84
+ function extractExitCodeFromBanner(text) {
85
+ if (typeof text !== 'string' || !text) return null;
86
+ const preamble = splitPreamble(text);
87
+ const match = BANNER_RE.exec(preamble);
88
+ if (!match) return null;
89
+ const n = Number(match[1]);
90
+ return Number.isInteger(n) ? n : null;
91
+ }
92
+
93
+ function normalizeCommandText(value) {
94
+ if (typeof value === 'string') return value.trim() || null;
95
+ if (Array.isArray(value)) {
96
+ const joined = value.map(v => String(v)).join(' ').trim();
97
+ return joined || null;
98
+ }
99
+ return null;
100
+ }
101
+
102
+ // Best-effort parse of a rollout `function_call`'s `arguments` field into a
103
+ // normalized command string, used only for the command cross-check
104
+ // correlation (Decision B #2). Any shape mismatch degrades to `null`
105
+ // (unresolvable pairing), never a throw.
106
+ function normalizeCallArguments(argumentsField) {
107
+ let parsed = argumentsField;
108
+ if (typeof argumentsField === 'string') {
109
+ try {
110
+ parsed = JSON.parse(argumentsField);
111
+ } catch {
112
+ return null;
113
+ }
114
+ }
115
+ if (!parsed || typeof parsed !== 'object') return null;
116
+ return normalizeCommandText(parsed.command);
117
+ }
118
+
119
+ const OUTPUT_FIELD_NEEDLE = '"output":"';
120
+
121
+ /**
122
+ * parseCandidateLine(line, maxLineHeadBytes) → candidate | null
123
+ *
124
+ * Interprets one JSONL rollout line as either a `function_call_output` or a
125
+ * `function_call` response_item, HEAD-ANCHORED (#470 iteration 2, MEDIUM
126
+ * finding #5): a line that fits within `maxLineHeadBytes` is fully
127
+ * `JSON.parse`d (cheap, and correct for the real-newline anchor form); a line
128
+ * that exceeds the head window is NEVER fully parsed (that would mean holding
129
+ * arbitrarily large flooded stdout in memory and risks the preamble/banner —
130
+ * which lives at the line HEAD — being reasoned about incorrectly). Instead
131
+ * only the first `maxLineHeadBytes` raw bytes are inspected: the banner and
132
+ * `Output:` delimiter live within the first few hundred bytes of the `output`
133
+ * field regardless of how much stdout follows, so the raw head slice is
134
+ * sufficient for `extractExitCodeFromBanner` (escaped-anchor form).
135
+ */
136
+ function parseCandidateLine(line, maxLineHeadBytes) {
137
+ const byteLen = Buffer.byteLength(line, 'utf8');
138
+ if (byteLen <= maxLineHeadBytes) {
139
+ let entry;
140
+ try {
141
+ entry = JSON.parse(line);
142
+ } catch {
143
+ return null; // malformed/partial line — skip, keep scanning
144
+ }
145
+ const payload = entry && typeof entry === 'object' ? entry.payload : null;
146
+ if (!payload || typeof payload !== 'object') return null;
147
+ const callId = typeof payload.call_id === 'string' ? payload.call_id : null;
148
+ if (payload.type === 'function_call_output' && typeof payload.output === 'string') {
149
+ return { type: 'function_call_output', callId, output: payload.output };
150
+ }
151
+ if (payload.type === 'function_call') {
152
+ return { type: 'function_call', callId, command: normalizeCallArguments(payload.arguments) };
153
+ }
154
+ return null;
155
+ }
156
+
157
+ // Line exceeds the head window: raw, head-only, best-effort extraction.
158
+ // `function_call` entries carry only a short command string in practice and
159
+ // are not expected to exceed the head window; only `function_call_output`
160
+ // (whose stdout can flood) is handled here.
161
+ const head = line.slice(0, maxLineHeadBytes);
162
+ if (head.indexOf('"type":"function_call_output"') === -1) return null;
163
+ const callIdMatch = /"call_id":"([^"]*)"/.exec(head);
164
+ const outputIdx = head.indexOf(OUTPUT_FIELD_NEEDLE);
165
+ if (outputIdx === -1) return null; // output field itself starts beyond the head window
166
+ const rawOutputHead = head.slice(outputIdx + OUTPUT_FIELD_NEEDLE.length);
167
+ return {
168
+ type: 'function_call_output',
169
+ callId: callIdMatch ? callIdMatch[1] : null,
170
+ output: rawOutputHead, // still JSON-escaped; extractExitCodeFromBanner handles both anchor forms
171
+ };
172
+ }
173
+
174
+ // Best-effort containment (#470 iteration 2, LOW finding #8): realpath the
175
+ // transcript path and require it to be a regular file; when the codex
176
+ // sessions root itself resolves on this host, additionally reject a realpath
177
+ // that escapes it (defends against a symlinked transcript_path pointing
178
+ // somewhere unexpected). Fail-open: any resolution error propagates to the
179
+ // caller's try/catch as "unusable" (null); a non-resolvable sessions root
180
+ // (e.g. custom CODEX_HOME, test fixtures) simply skips the containment check.
181
+ function resolveContainedRealPath(transcriptPath) {
182
+ const realPath = fs.realpathSync(transcriptPath);
183
+ const stat = fs.statSync(realPath);
184
+ if (!stat.isFile()) return null;
185
+ try {
186
+ const codexHome = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
187
+ const sessionsRoot = path.join(codexHome, 'sessions');
188
+ const realRoot = fs.realpathSync(sessionsRoot);
189
+ const rel = path.relative(realRoot, realPath);
190
+ if (rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) return null;
191
+ } catch {
192
+ // Sessions root doesn't exist/resolve on this host — best-effort only.
193
+ }
194
+ return realPath;
195
+ }
196
+
197
+ /**
198
+ * readExitCodeFromRollout(transcriptPath, options) → integer | null
199
+ *
200
+ * options:
201
+ * - maxScanBytes (default 1MB): bounded backward-scan window from EOF used
202
+ * to locate candidate `response_item` lines. Beyond this cap, an
203
+ * unresolvable line start (and thus that candidate) is dropped — never
204
+ * read further back, and never a crash.
205
+ * - maxLineHeadBytes (default 64KB): per-line head window; see
206
+ * `parseCandidateLine`. This is what makes extraction survive a flooded
207
+ * `output` field (#470 iteration 2, MEDIUM finding #5) — the preamble
208
+ * banner sits at the line HEAD, so bounding reads to the head can never
209
+ * have the banner pushed out of the window by TAIL stdout volume.
210
+ * - callId: correlate to a specific `function_call_output.call_id`.
211
+ * - command: the claimed command (e.g. `tool_input.command`), used only for
212
+ * the command cross-check correlation below.
213
+ *
214
+ * Correlation policy (Decision B, #470 iteration 2, HIGH finding #4), in
215
+ * priority order:
216
+ * 1. call_id match wins — authoritative.
217
+ * 2. Absent a call_id match: take the newest `function_call_output` in the
218
+ * scanned window, resolve its paired `function_call` (same call_id), and
219
+ * compare that call's `arguments` command (normalized) to `command`. A
220
+ * resolved MATCH uses the newest output; a resolved MISMATCH DECLINES
221
+ * (`null`) rather than knowingly attribute a different call's exit code.
222
+ * 3. If no pairing is resolvable at all (the common single-call case): fall
223
+ * back to the newest `function_call_output` banner.
224
+ *
225
+ * Any failure (missing/unreadable/non-regular file, containment violation,
226
+ * malformed JSON lines, no candidate found) yields `null` — never throws.
227
+ */
228
+ function readExitCodeFromRollout(transcriptPath, options) {
229
+ const opts = options || {};
230
+ const maxScanBytes = Number.isInteger(opts.maxScanBytes) && opts.maxScanBytes > 0
231
+ ? opts.maxScanBytes
232
+ : 1024 * 1024;
233
+ const maxLineHeadBytes = Number.isInteger(opts.maxLineHeadBytes) && opts.maxLineHeadBytes > 0
234
+ ? opts.maxLineHeadBytes
235
+ : 64 * 1024;
236
+ const callId = typeof opts.callId === 'string' && opts.callId ? opts.callId : null;
237
+ const command = normalizeCommandText(opts.command);
238
+
239
+ if (typeof transcriptPath !== 'string' || !transcriptPath) return null;
240
+
241
+ let text;
242
+ let truncated;
243
+ try {
244
+ const realPath = resolveContainedRealPath(transcriptPath);
245
+ if (realPath === null) return null; // containment violation
246
+
247
+ const stat = fs.statSync(realPath);
248
+ const fd = fs.openSync(realPath, 'r');
249
+ try {
250
+ const readLen = Math.min(maxScanBytes, stat.size);
251
+ const start = stat.size - readLen;
252
+ const buf = Buffer.alloc(readLen);
253
+ fs.readSync(fd, buf, 0, readLen, start);
254
+ text = buf.toString('utf8');
255
+ // A read starting after byte 0 means the file is larger than the scan
256
+ // window and the first captured line may be a truncated fragment of a
257
+ // longer line that started before `start` — only then is it dropped.
258
+ truncated = start > 0;
259
+ } finally {
260
+ fs.closeSync(fd);
261
+ }
262
+ } catch {
263
+ return null;
264
+ }
265
+
266
+ let lines;
267
+ try {
268
+ lines = text.split('\n');
269
+ } catch {
270
+ return null;
271
+ }
272
+ if (truncated && lines.length > 1) lines.shift();
273
+
274
+ let matchedByCallId = null;
275
+ let newestOutputEntry = null;
276
+ let pairedCommand = null;
277
+
278
+ for (let i = lines.length - 1; i >= 0; i--) {
279
+ const line = lines[i].trim();
280
+ if (!line) continue;
281
+ const candidate = parseCandidateLine(line, maxLineHeadBytes);
282
+ if (!candidate) continue; // malformed/partial/unrecognized line — skip, keep scanning
283
+
284
+ if (candidate.type === 'function_call_output') {
285
+ if (newestOutputEntry === null) newestOutputEntry = candidate; // first seen scanning backward = newest
286
+ if (callId && candidate.callId === callId) {
287
+ matchedByCallId = candidate;
288
+ break; // call_id match is authoritative — stop scanning
289
+ }
290
+ } else if (candidate.type === 'function_call') {
291
+ if (newestOutputEntry && pairedCommand === null && candidate.callId === newestOutputEntry.callId) {
292
+ pairedCommand = candidate.command;
293
+ }
294
+ }
295
+ }
296
+
297
+ let chosenOutput = null;
298
+ if (matchedByCallId) {
299
+ chosenOutput = matchedByCallId.output;
300
+ } else if (newestOutputEntry) {
301
+ if (pairedCommand !== null && command) {
302
+ if (pairedCommand === command) {
303
+ chosenOutput = newestOutputEntry.output;
304
+ } else {
305
+ return null; // positive command mismatch — decline rather than mis-attribute
306
+ }
307
+ } else {
308
+ chosenOutput = newestOutputEntry.output; // single-call fallback — no pairing resolvable
309
+ }
310
+ }
311
+
312
+ if (chosenOutput === null) return null;
313
+ return extractExitCodeFromBanner(chosenOutput);
314
+ }
315
+
316
+ module.exports = { extractExitCodeFromBanner, readExitCodeFromRollout };
@@ -0,0 +1,235 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const KIT_ID_RE = /^[a-z][a-z0-9-]*$/;
7
+ const WORKFLOW_TRIGGER_IDENTIFIER_RE = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/;
8
+ const MAX_STEERING_VALUE_LENGTH = 240;
9
+
10
+ function readJson(file) {
11
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
12
+ }
13
+
14
+ function containedPath(root, rel) {
15
+ if (typeof rel !== 'string' || path.isAbsolute(rel)) return null;
16
+ const resolved = path.resolve(root, rel);
17
+ const absRoot = path.resolve(root);
18
+ if (resolved === absRoot || resolved.startsWith(`${absRoot}${path.sep}`)) return resolved;
19
+ return null;
20
+ }
21
+
22
+ function loadKitManifest(kitRoot, sourceKind, warnings) {
23
+ const manifestPath = path.join(kitRoot, 'kit.json');
24
+ try {
25
+ if (!fs.existsSync(manifestPath)) return null;
26
+ const manifest = readJson(manifestPath);
27
+ const kitId = typeof manifest.id === 'string' ? manifest.id : '';
28
+ if (!KIT_ID_RE.test(kitId)) {
29
+ warnings.push(`${manifestPath}: kit id must be a kebab-case id (^[a-z][a-z0-9-]*$); skipping kit`);
30
+ return null;
31
+ }
32
+ return { kit_id: kitId, source_kind: sourceKind, manifest };
33
+ } catch (error) {
34
+ warnings.push(`${manifestPath}: ${String(error && error.message ? error.message : error)}`);
35
+ return null;
36
+ }
37
+ }
38
+
39
+ function readKitManifests(root) {
40
+ const warnings = [];
41
+ const kits = [];
42
+ const catalogPath = path.join(root, 'kits', 'catalog.json');
43
+ if (fs.existsSync(catalogPath)) {
44
+ try {
45
+ const catalog = readJson(catalogPath);
46
+ if (Array.isArray(catalog.kits)) {
47
+ for (const entry of catalog.kits) {
48
+ const rel = entry && typeof entry === 'object' ? entry.path : undefined;
49
+ const kitRoot = containedPath(root, rel);
50
+ if (!kitRoot || !fs.existsSync(kitRoot)) continue;
51
+ const loaded = loadKitManifest(kitRoot, 'builtin', warnings);
52
+ if (loaded) kits.push(loaded);
53
+ }
54
+ }
55
+ } catch (error) {
56
+ warnings.push(String(error && error.message ? error.message : error));
57
+ }
58
+ }
59
+
60
+ const registryPath = path.join(root, 'kits', 'local', 'installed-kits.json');
61
+ if (fs.existsSync(registryPath)) {
62
+ try {
63
+ const registry = readJson(registryPath);
64
+ if (Array.isArray(registry.kits)) {
65
+ for (const entry of registry.kits) {
66
+ const id = entry && typeof entry === 'object' ? entry.id : undefined;
67
+ if (typeof id !== 'string') continue;
68
+ const kitRoot = path.join(root, 'kits', 'local', 'repositories', id);
69
+ if (!fs.existsSync(kitRoot)) continue;
70
+ const loaded = loadKitManifest(kitRoot, 'local', warnings);
71
+ if (loaded) kits.push(loaded);
72
+ }
73
+ }
74
+ } catch (error) {
75
+ warnings.push(String(error && error.message ? error.message : error));
76
+ }
77
+ }
78
+ return { kits, warnings };
79
+ }
80
+
81
+ function isWorkflowTriggerIdentifier(value) {
82
+ return typeof value === 'string' && WORKFLOW_TRIGGER_IDENTIFIER_RE.test(value);
83
+ }
84
+
85
+ function optionalWorkflowTriggerIdentifier(value) {
86
+ return value === undefined || isWorkflowTriggerIdentifier(value);
87
+ }
88
+
89
+ function optionalWorkflowTriggerIdentifierList(value) {
90
+ return value === undefined ||
91
+ (Array.isArray(value) && value.length > 0 && value.every(isWorkflowTriggerIdentifier));
92
+ }
93
+
94
+ function optionalConditionalSkills(value) {
95
+ return value === undefined ||
96
+ (Array.isArray(value) && value.every(item =>
97
+ item && typeof item === 'object' &&
98
+ isWorkflowTriggerIdentifier(item.when) &&
99
+ isWorkflowTriggerIdentifier(item.skill)
100
+ ));
101
+ }
102
+
103
+ function renderKitSteering(trigger) {
104
+ const kitId = isWorkflowTriggerIdentifier(trigger.kit_id) ? trigger.kit_id : '';
105
+ const category = isWorkflowTriggerIdentifier(trigger.when) ? trigger.when : 'matching';
106
+ const targetFlowId = isWorkflowTriggerIdentifier(trigger.target_flow_id) ? trigger.target_flow_id : '';
107
+ const defaultSkill = isWorkflowTriggerIdentifier(trigger.default_skill) ? trigger.default_skill : '';
108
+ const conditionalSkills = Array.isArray(trigger.conditional_skills) ? trigger.conditional_skills
109
+ .map(item => ({
110
+ when: isWorkflowTriggerIdentifier(item && item.when) ? item.when : '',
111
+ skill: isWorkflowTriggerIdentifier(item && item.skill) ? item.skill : '',
112
+ }))
113
+ .filter(item => item.when && item.skill) : [];
114
+ const requiredSequence = Array.isArray(trigger.required_sequence)
115
+ ? trigger.required_sequence.filter(isWorkflowTriggerIdentifier)
116
+ : [];
117
+ const postVerifyTargets = Array.isArray(trigger.post_verify_targets)
118
+ ? trigger.post_verify_targets.filter(isWorkflowTriggerIdentifier)
119
+ : [];
120
+
121
+ const lines = [
122
+ `KIT WORKFLOW ROUTE: this user prompt matches ${category}.`,
123
+ ];
124
+ if (kitId && targetFlowId) {
125
+ lines.push(`Before source edits or implementation commands, use the \`${kitId}\` kit's \`${targetFlowId}\` workflow.`);
126
+ } else if (kitId) {
127
+ lines.push(`Before source edits or implementation commands, use the \`${kitId}\` kit workflow.`);
128
+ } else {
129
+ lines.push('Before source edits or implementation commands, use the matching kit workflow.');
130
+ }
131
+ if (conditionalSkills.length || defaultSkill) {
132
+ const conditionalText = conditionalSkills
133
+ .map(item => `If ${item.when}, activate \`${item.skill}\``)
134
+ .join('; ');
135
+ if (conditionalText && defaultSkill) lines.push(`${conditionalText}; otherwise activate \`${defaultSkill}\`.`);
136
+ else if (conditionalText) lines.push(`${conditionalText}.`);
137
+ else lines.push(`Activate \`${defaultSkill}\`.`);
138
+ }
139
+ if (targetFlowId) {
140
+ lines.push(`Keep the session on \`${targetFlowId}\` and use \`npm run workflow:sidecar -- ensure-session --flow-id ${targetFlowId} ...\` when the repo provides the sidecar writer.`);
141
+ }
142
+ if (requiredSequence.length) {
143
+ lines.push(`Do not bypass ${requiredSequence.join(' -> ')} for matching work.`);
144
+ }
145
+ if (postVerifyTargets.length) {
146
+ lines.push(`After local verification, continue to ${postVerifyTargets.join(' and ')}; do not treat local verification as terminal delivery.`);
147
+ }
148
+ return lines.join(' ');
149
+ }
150
+
151
+ function workflowTriggerValidationErrors(manifest, manifestPath) {
152
+ const errors = [];
153
+ const raw = manifest.workflow_triggers;
154
+ if (raw === undefined) return errors;
155
+ if (!Array.isArray(raw)) {
156
+ errors.push(`${manifestPath}: .workflow_triggers must be a list`);
157
+ return errors;
158
+ }
159
+ const seenIds = new Set();
160
+ raw.forEach((trigger, index) => {
161
+ if (!trigger || typeof trigger !== 'object') {
162
+ errors.push(`${manifestPath}: workflow_triggers[${index}] must be an object`);
163
+ return;
164
+ }
165
+ if (!isWorkflowTriggerIdentifier(trigger.id)) {
166
+ errors.push(`${manifestPath}: workflow_triggers[${index}].id must match ^[a-z0-9]+(?:[.-][a-z0-9]+)*$`);
167
+ return;
168
+ }
169
+ if (seenIds.has(trigger.id)) errors.push(`${manifestPath}: workflow_triggers[${index}].id duplicates '${trigger.id}'`);
170
+ seenIds.add(trigger.id);
171
+ if (!isWorkflowTriggerIdentifier(trigger.when)) {
172
+ errors.push(`${manifestPath}: workflow_triggers[${index}].when must match ^[a-z0-9]+(?:[.-][a-z0-9]+)*$`);
173
+ return;
174
+ }
175
+ if (trigger.hint !== undefined) {
176
+ errors.push(`${manifestPath}: workflow_triggers[${index}].hint is retired; use structured steering fields`);
177
+ return;
178
+ }
179
+ if (trigger.display_name !== undefined) {
180
+ errors.push(`${manifestPath}: workflow_triggers[${index}].display_name is not supported in workflow_triggers; use catalog/listing metadata for human-readable names`);
181
+ return;
182
+ }
183
+ if (!optionalWorkflowTriggerIdentifier(trigger.target_flow_id)) {
184
+ errors.push(`${manifestPath}: workflow_triggers[${index}].target_flow_id must match ^[a-z0-9]+(?:[.-][a-z0-9]+)*$ when present`);
185
+ return;
186
+ }
187
+ if (!optionalWorkflowTriggerIdentifier(trigger.default_skill)) {
188
+ errors.push(`${manifestPath}: workflow_triggers[${index}].default_skill must match ^[a-z0-9]+(?:[.-][a-z0-9]+)*$ when present`);
189
+ return;
190
+ }
191
+ if (!optionalConditionalSkills(trigger.conditional_skills)) {
192
+ errors.push(`${manifestPath}: workflow_triggers[${index}].conditional_skills must be a list of { when, skill } identifiers matching ^[a-z0-9]+(?:[.-][a-z0-9]+)*$ when present`);
193
+ return;
194
+ }
195
+ if (!optionalWorkflowTriggerIdentifierList(trigger.required_sequence)) {
196
+ errors.push(`${manifestPath}: workflow_triggers[${index}].required_sequence must be a non-empty identifier list matching ^[a-z0-9]+(?:[.-][a-z0-9]+)*$ when present`);
197
+ return;
198
+ }
199
+ if (!optionalWorkflowTriggerIdentifierList(trigger.post_verify_targets)) {
200
+ errors.push(`${manifestPath}: workflow_triggers[${index}].post_verify_targets must be a non-empty identifier list matching ^[a-z0-9]+(?:[.-][a-z0-9]+)*$ when present`);
201
+ }
202
+ });
203
+ return errors;
204
+ }
205
+
206
+ function workflowTriggersFor(root, when) {
207
+ const result = [];
208
+ const inventory = readKitManifests(root);
209
+ for (const kit of inventory.kits) {
210
+ const triggers = kit.manifest.workflow_triggers;
211
+ if (!Array.isArray(triggers)) continue;
212
+ const manifestPath = path.join(root, kit.source_kind === 'local' ? 'kits/local/repositories' : 'kits', kit.kit_id, 'kit.json');
213
+ const parseErrors = workflowTriggerValidationErrors(kit.manifest, manifestPath);
214
+ if (parseErrors.length) {
215
+ const warning = `${kit.kit_id}: invalid workflow_triggers metadata; skipping workflow_triggers: ${parseErrors.join('; ')}`;
216
+ inventory.warnings.push(warning);
217
+ process.stderr.write(`[flow-agents] ${warning}\n`);
218
+ continue;
219
+ }
220
+ for (const trigger of triggers) {
221
+ if (trigger.when !== when) continue;
222
+ const steering = renderKitSteering({ ...trigger, kit_id: kit.kit_id });
223
+ if (!steering) continue;
224
+ result.push({ kit_id: kit.kit_id, id: trigger.id, when, steering });
225
+ }
226
+ }
227
+ return result;
228
+ }
229
+
230
+ module.exports = {
231
+ MAX_STEERING_VALUE_LENGTH,
232
+ renderKitSteering,
233
+ readKitManifests,
234
+ workflowTriggersFor,
235
+ };
@@ -19,6 +19,32 @@
19
19
 
20
20
  const fs = require('fs');
21
21
  const path = require('path');
22
+ const { spawn } = require('child_process');
23
+
24
+ /**
25
+ * OPTIONAL console liveness relay (#295, ADR 0021 §4/§7). Best-effort, FULLY detached mirror of a
26
+ * liveness event to the hosted Console via `scripts/liveness/relay.sh`. Local-first is sacred: this
27
+ * runs AFTER the durable local append and can never block, throw, or affect it — gated on
28
+ * `FLOW_AGENTS_CONSOLE_LIVENESS_RELAY`, the whole thing wrapped so any failure (missing script,
29
+ * spawn error) is swallowed. No flag ⇒ a single cheap env read and return (true no-op).
30
+ *
31
+ * @param {object} evt The liveness event just written locally.
32
+ * @returns {void}
33
+ */
34
+ function relayLivenessEvent(evt) {
35
+ try {
36
+ const flag = String(process.env.FLOW_AGENTS_CONSOLE_LIVENESS_RELAY || '').toLowerCase();
37
+ if (flag !== '1' && flag !== 'true' && flag !== 'yes' && flag !== 'on') return;
38
+ // scripts/hooks/lib/ -> scripts/liveness/relay.sh (same relative layout in dist/* bundles).
39
+ const relay = path.join(__dirname, '..', '..', 'liveness', 'relay.sh');
40
+ if (!fs.existsSync(relay)) return;
41
+ const child = spawn('bash', [relay, JSON.stringify(evt)], { detached: true, stdio: 'ignore' });
42
+ child.on('error', () => {}); // never surface a spawn failure
43
+ child.unref(); // fully detach — the parent never waits on the relay
44
+ } catch {
45
+ // Best-effort only: the durable local write already succeeded above.
46
+ }
47
+ }
22
48
 
23
49
  /**
24
50
  * Resolve the path to the shared liveness event stream for a given artifact root.
@@ -40,7 +66,8 @@ function livenessStreamFile(root) {
40
66
  function appendLivenessEvent(root, evt) {
41
67
  const file = livenessStreamFile(root);
42
68
  fs.mkdirSync(path.dirname(file), { recursive: true });
43
- fs.appendFileSync(file, `${JSON.stringify(evt)}\n`);
69
+ fs.appendFileSync(file, `${JSON.stringify(evt)}\n`); // local-first: the durable write happens first
70
+ relayLivenessEvent(evt); // then optionally mirror to the Console — best-effort, detached, off by default
44
71
  }
45
72
 
46
73
  module.exports = { livenessStreamFile, appendLivenessEvent };