@kontourai/flow-agents 3.2.0 → 3.4.0

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 (281) hide show
  1. package/.github/workflows/add-to-project.yml +15 -0
  2. package/.github/workflows/ci.yml +173 -0
  3. package/CHANGELOG.md +54 -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/assignment-provider.js +10 -1
  11. package/build/src/cli/builder-run.d.ts +1 -0
  12. package/build/src/cli/builder-run.js +27 -0
  13. package/build/src/cli/effective-backlog-settings.js +70 -2
  14. package/build/src/cli/init.d.ts +34 -0
  15. package/build/src/cli/init.js +341 -61
  16. package/build/src/cli/kit.js +55 -12
  17. package/build/src/cli/pull-work-provider.js +346 -5
  18. package/build/src/cli/skill-drift-check.d.ts +1 -0
  19. package/build/src/cli/skill-drift-check.js +165 -0
  20. package/build/src/cli/telemetry-doctor.d.ts +37 -0
  21. package/build/src/cli/telemetry-doctor.js +53 -6
  22. package/build/src/cli/validate-hook-influence.js +37 -7
  23. package/build/src/cli/workflow-artifact-cleanup-audit.js +418 -11
  24. package/build/src/cli/workflow-sidecar.d.ts +310 -1
  25. package/build/src/cli/workflow-sidecar.js +1914 -126
  26. package/build/src/cli.js +5 -0
  27. package/build/src/flow-kit/validate.d.ts +54 -34
  28. package/build/src/flow-kit/validate.js +237 -26
  29. package/build/src/index.d.ts +2 -0
  30. package/build/src/index.js +1 -0
  31. package/build/src/lib/console-connect-options.d.ts +97 -0
  32. package/build/src/lib/console-connect-options.js +199 -0
  33. package/build/src/lib/console-telemetry-validate.d.ts +49 -0
  34. package/build/src/lib/console-telemetry-validate.js +91 -0
  35. package/build/src/lib/flow-resolver.d.ts +54 -1
  36. package/build/src/lib/flow-resolver.js +112 -5
  37. package/build/src/lib/fs.d.ts +17 -0
  38. package/build/src/lib/fs.js +172 -0
  39. package/build/src/lib/local-artifact-root.d.ts +44 -1
  40. package/build/src/lib/local-artifact-root.js +131 -3
  41. package/build/src/runtime-adapters.d.ts +39 -3
  42. package/build/src/runtime-adapters.js +77 -31
  43. package/build/src/tools/build-universal-bundles.js +40 -2
  44. package/build/src/tools/codex-agent-routing.d.ts +2 -0
  45. package/build/src/tools/codex-agent-routing.js +49 -0
  46. package/build/src/tools/generate-context-map.js +1 -0
  47. package/build/src/tools/validate-source-tree.js +30 -3
  48. package/context/contracts/artifact-contract.md +16 -2
  49. package/context/scripts/hooks/lib/kit-catalog.js +235 -0
  50. package/context/scripts/hooks/lib/runnable-command.js +177 -0
  51. package/context/scripts/hooks/stop-goal-fit.js +278 -48
  52. package/context/scripts/hooks/workflow-steering.js +194 -22
  53. package/context/scripts/package.json +3 -0
  54. package/context/scripts/telemetry/install-console-config.sh +25 -4
  55. package/context/scripts/telemetry/lib/config.sh +102 -12
  56. package/context/scripts/telemetry/lib/pricing.sh +50 -0
  57. package/context/scripts/telemetry/lib/session.sh +3 -0
  58. package/context/scripts/telemetry/lib/transport.sh +87 -0
  59. package/context/scripts/telemetry/lib/usage.sh +205 -4
  60. package/context/scripts/telemetry/telemetry.conf +6 -0
  61. package/context/scripts/telemetry/telemetry.sh +48 -0
  62. package/context/settings/workspace-backlog-provider-settings.example.json +48 -0
  63. package/docs/agent-usage-feedback-loop.md +35 -0
  64. package/docs/architecture-engine-and-kits.md +110 -0
  65. package/docs/context-map.md +2 -0
  66. package/docs/coordination-guide.md +370 -0
  67. package/docs/decisions/agent-coordination.md +26 -9
  68. package/docs/decisions/embeddable-engine.md +152 -0
  69. package/docs/decisions/index.md +5 -3
  70. package/docs/decisions/trust-ledger-retention.md +88 -0
  71. package/docs/decisions/trust-reconcile.md +42 -9
  72. package/docs/decisions/workflow-enforcement.md +31 -9
  73. package/docs/fixture-ownership.md +6 -2
  74. package/docs/implementing-trust-reconciliation.md +129 -0
  75. package/docs/index.md +23 -9
  76. package/docs/integrations/flow-agents-console.md +275 -0
  77. package/docs/integrations/index.md +4 -0
  78. package/docs/kit-authoring-guide.md +52 -21
  79. package/docs/spec/builder-flow-runtime.md +80 -0
  80. package/docs/spec/runtime-hook-surface.md +45 -1
  81. package/docs/specs/economics-record-contract.md +270 -0
  82. package/docs/specs/harness-capability-matrix.md +74 -0
  83. package/docs/specs/learning-review-proposals-contract.md +340 -0
  84. package/docs/specs/routing-efficiency-review.md +59 -0
  85. package/docs/verifiable-trust.md +74 -25
  86. package/docs/workflow-artifact-lifecycle.md +38 -1
  87. package/docs/workflow-usage-guide.md +10 -0
  88. package/evals/acceptance/prove-capture-teeth.sh +132 -0
  89. package/evals/ci/antigaming-suite.sh +2 -0
  90. package/evals/ci/run-baseline.sh +78 -4
  91. package/evals/fixtures/economics/acceptance.json +12 -0
  92. package/evals/fixtures/economics/agents/tool-worker-1/events.jsonl +2 -0
  93. package/evals/fixtures/economics/agents/tool-worker-2/events.jsonl +2 -0
  94. package/evals/fixtures/economics/agents/tool-worker-3/events.jsonl +2 -0
  95. package/evals/fixtures/economics/agents/tool-worker-4/events.jsonl +1 -0
  96. package/evals/fixtures/economics/agents/tool-worker-5/events.jsonl +2 -0
  97. package/evals/fixtures/economics/critique.json +22 -0
  98. package/evals/fixtures/economics/expected-record.json +71 -0
  99. package/evals/fixtures/economics/session-usage-event.json +1 -0
  100. package/evals/fixtures/economics/state.json +11 -0
  101. package/evals/fixtures/economics/transcript.jsonl +3 -0
  102. package/evals/fixtures/hook-influence/cases.json +7 -7
  103. package/evals/fixtures/learning-review-proposals/balanced/economics.jsonl +6 -0
  104. package/evals/fixtures/learning-review-proposals/effect-follow-up/economics.jsonl +5 -0
  105. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-1/trust.bundle +21 -0
  106. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-2/trust.bundle +21 -0
  107. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-3/trust.bundle +21 -0
  108. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-4/trust.bundle +21 -0
  109. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-5/trust.bundle +21 -0
  110. package/evals/fixtures/learning-review-proposals/pattern-present/economics.jsonl +6 -0
  111. package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.json +30 -0
  112. package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.md +66 -0
  113. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/gate-review.inquiries.json +26 -0
  114. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/trust.bundle +21 -0
  115. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/gate-review.inquiries.json +26 -0
  116. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/trust.bundle +21 -0
  117. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/gate-review.inquiries.json +26 -0
  118. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/trust.bundle +21 -0
  119. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/gate-review.inquiries.json +26 -0
  120. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/trust.bundle +21 -0
  121. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-5/trust.bundle +21 -0
  122. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-6/trust.bundle +21 -0
  123. package/evals/fixtures/learning-review-proposals/repeat-window/economics.jsonl +6 -0
  124. package/evals/fixtures/learning-review-proposals/under-threshold/economics.jsonl +3 -0
  125. package/evals/fixtures/reconcile-preflight/disputed-critique-unsuperseded.json +48 -0
  126. package/evals/fixtures/reconcile-preflight/standalone-disputed-session-local.json +59 -0
  127. package/evals/fixtures/telemetry/usage-transcript-sample.jsonl +4 -0
  128. package/evals/fixtures/trust-reconcile-exploits/mcp-degrade.json +42 -0
  129. package/evals/integration/test_builder_entry_enforcement.sh +241 -0
  130. package/evals/integration/test_builder_step_producers.sh +18 -10
  131. package/evals/integration/test_bundle_install.sh +172 -0
  132. package/evals/integration/test_checkpoint_signing.sh +10 -2
  133. package/evals/integration/test_ci_actor_identity.sh +221 -0
  134. package/evals/integration/test_console_tenant_isolation.sh +167 -0
  135. package/evals/integration/test_critique_supersession_roundtrip.sh +4 -1
  136. package/evals/integration/test_dual_emit_flow_step.sh +10 -4
  137. package/evals/integration/test_economics_record.sh +674 -0
  138. package/evals/integration/test_effective_backlog_settings.sh +1 -1
  139. package/evals/integration/test_evidence_capture_hook.sh +17 -2
  140. package/evals/integration/test_exemption_usage_review.sh +198 -0
  141. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  142. package/evals/integration/test_flow_kit_install_git.sh +83 -0
  143. package/evals/integration/test_flowdef_session_activation.sh +0 -1
  144. package/evals/integration/test_flowdef_session_history_preservation.sh +13 -3
  145. package/evals/integration/test_gate_lockdown.sh +7 -0
  146. package/evals/integration/test_gate_review_inquiry_records.sh +9 -1
  147. package/evals/integration/test_goal_fit_hook.sh +2031 -0
  148. package/evals/integration/test_hook_category_behaviors.sh +8 -1
  149. package/evals/integration/test_hook_influence_cases.sh +25 -1
  150. package/evals/integration/test_install_merge.sh +227 -2
  151. package/evals/integration/test_kit_conformance_levels.sh +6 -6
  152. package/evals/integration/test_learning_review_proposals.sh +329 -0
  153. package/evals/integration/test_liveness_conflict_injection.sh +26 -22
  154. package/evals/integration/test_liveness_console_relay.sh +166 -0
  155. package/evals/integration/test_liveness_heartbeat.sh +17 -17
  156. package/evals/integration/test_liveness_worktree_root.sh +575 -0
  157. package/evals/integration/test_phase_map_and_gate_claim.sh +6 -1
  158. package/evals/integration/test_publish_delivery.sh +389 -2
  159. package/evals/integration/test_pull_work_board.sh +200 -0
  160. package/evals/integration/test_pull_work_provider.sh +1 -1
  161. package/evals/integration/test_reconcile_preflight.sh +304 -0
  162. package/evals/integration/test_record_check.sh +378 -0
  163. package/evals/integration/test_routing_efficiency.sh +71 -0
  164. package/evals/integration/test_runtime_adapter_activation.sh +28 -0
  165. package/evals/integration/test_session_resume_roundtrip.sh +16 -19
  166. package/evals/integration/test_skill_drift_check.sh +870 -0
  167. package/evals/integration/test_takeover_protocol.sh +340 -0
  168. package/evals/integration/test_telemetry.sh +445 -0
  169. package/evals/integration/test_telemetry_doctor.sh +66 -0
  170. package/evals/integration/test_telemetry_usage_pipeline.sh +228 -0
  171. package/evals/integration/test_trust_reconcile_negatives.sh +121 -13
  172. package/evals/integration/test_trust_reconcile_trailer_diagnostic.sh +247 -0
  173. package/evals/integration/test_usage_cost.sh +61 -0
  174. package/evals/integration/test_verify_hold.sh +910 -0
  175. package/evals/integration/test_veritas_governance_kit.sh +257 -0
  176. package/evals/integration/test_workflow_artifact_cleanup_audit.sh +575 -3
  177. package/evals/integration/test_workflow_sidecar_writer.sh +1395 -0
  178. package/evals/integration/test_workflow_steering_hook.sh +157 -16
  179. package/evals/integration/test_workspace_settings.sh +176 -0
  180. package/evals/lib/env.sh +26 -0
  181. package/evals/lib/node.sh +8 -0
  182. package/evals/run.sh +37 -0
  183. package/evals/static/test_ci_integration_coverage.sh +115 -0
  184. package/evals/static/test_declared_scope_forms_documented.sh +114 -0
  185. package/evals/static/test_universal_bundles.sh +34 -0
  186. package/evals/static/test_validate_source_kit_asset_scope.sh +259 -0
  187. package/evals/static/test_workflow_skills.sh +1 -1
  188. package/kits/builder/flows/build.flow.json +9 -18
  189. package/kits/builder/flows/publish-learn.flow.json +5 -1
  190. package/kits/builder/kit.json +120 -0
  191. package/kits/builder/skills/continue-work/SKILL.md +2 -0
  192. package/kits/builder/skills/deliver/SKILL.md +115 -0
  193. package/kits/builder/skills/evidence-gate/SKILL.md +12 -0
  194. package/kits/builder/skills/execute-plan/SKILL.md +9 -0
  195. package/kits/builder/skills/learning-review/SKILL.md +51 -0
  196. package/kits/builder/skills/plan-work/SKILL.md +17 -20
  197. package/kits/builder/skills/pull-work/SKILL.md +33 -2
  198. package/kits/builder/skills/release-readiness/SKILL.md +12 -0
  199. package/kits/knowledge/kit.json +9 -0
  200. package/kits/veritas-governance/docs/README.md +113 -7
  201. package/kits/veritas-governance/fixtures/exemption/approved.trust-bundle.json +74 -0
  202. package/kits/veritas-governance/fixtures/exemption/not-approved.trust-bundle.json +74 -0
  203. package/kits/veritas-governance/fixtures/exemption-review/mixed-fresh-stale.DECLARED.json +14 -0
  204. package/kits/veritas-governance/flows/exemption-issuance.flow.json +35 -0
  205. package/kits/veritas-governance/kit.json +19 -0
  206. package/kits/veritas-governance/skills/exemption-usage-review/SKILL.md +128 -0
  207. package/kits/veritas-governance/skills/exemption-usage-review/review-exemptions.mjs +231 -0
  208. package/package.json +2 -2
  209. package/packaging/manifest.json +29 -0
  210. package/schemas/backlog-provider-settings.schema.json +13 -0
  211. package/schemas/workflow-state.schema.json +44 -0
  212. package/scripts/README.md +4 -0
  213. package/scripts/check-content-boundary.cjs +8 -1
  214. package/scripts/ci/trust-reconcile.js +214 -253
  215. package/scripts/hooks/codex-hook-adapter.js +77 -2
  216. package/scripts/hooks/evidence-capture.js +38 -5
  217. package/scripts/hooks/lib/actor-identity.js +82 -0
  218. package/scripts/hooks/lib/codex-exit-code.js +316 -0
  219. package/scripts/hooks/lib/kit-catalog.js +235 -0
  220. package/scripts/hooks/lib/liveness-write.js +28 -1
  221. package/scripts/hooks/lib/local-artifact-paths.js +97 -1
  222. package/scripts/hooks/lib/runnable-command.js +177 -0
  223. package/scripts/hooks/lib/skill-drift.js +350 -0
  224. package/scripts/hooks/stop-goal-fit.js +278 -48
  225. package/scripts/hooks/workflow-steering.js +194 -22
  226. package/scripts/install-codex-home.sh +97 -47
  227. package/scripts/install-merge.js +72 -14
  228. package/scripts/install-owned-files.js +178 -0
  229. package/scripts/lib/reconcile-shape.js +381 -0
  230. package/scripts/liveness/relay.sh +84 -0
  231. package/scripts/telemetry/economics-record.schema.json +145 -0
  232. package/scripts/telemetry/economics-record.sh +331 -0
  233. package/scripts/telemetry/install-console-config.sh +25 -4
  234. package/scripts/telemetry/learning-review-decide.sh +124 -0
  235. package/scripts/telemetry/learning-review-proposals.schema.json +161 -0
  236. package/scripts/telemetry/learning-review-proposals.sh +484 -0
  237. package/scripts/telemetry/lib/config.sh +102 -12
  238. package/scripts/telemetry/lib/pricing.sh +14 -6
  239. package/scripts/telemetry/lib/session.sh +3 -0
  240. package/scripts/telemetry/lib/transport.sh +133 -15
  241. package/scripts/telemetry/lib/usage.sh +121 -28
  242. package/scripts/telemetry/routing-efficiency.sh +0 -0
  243. package/scripts/telemetry/telemetry.conf +6 -0
  244. package/scripts/telemetry/telemetry.sh +48 -0
  245. package/src/builder-flow-run-adapter.ts +357 -0
  246. package/src/builder-flow-runtime.ts +348 -0
  247. package/src/cli/assignment-provider.ts +12 -1
  248. package/src/cli/builder-flow-run-adapter.test.mjs +495 -0
  249. package/src/cli/builder-flow-runtime.test.mjs +213 -0
  250. package/src/cli/builder-run.ts +28 -0
  251. package/src/cli/codex-agent-routing.test.mjs +44 -0
  252. package/src/cli/codex-exit-code.test.mjs +207 -0
  253. package/src/cli/console-connect-options.test.mjs +329 -0
  254. package/src/cli/console-telemetry-validate.test.mjs +157 -0
  255. package/src/cli/effective-backlog-settings.ts +68 -2
  256. package/src/cli/flow-resolver-composition.test.mjs +72 -0
  257. package/src/cli/init.test.mjs +161 -0
  258. package/src/cli/init.ts +407 -62
  259. package/src/cli/kit-metadata-security.test.mjs +443 -0
  260. package/src/cli/kit.ts +50 -12
  261. package/src/cli/pull-work-provider.ts +377 -3
  262. package/src/cli/sidecar-pure-helpers.test.mjs +64 -0
  263. package/src/cli/skill-drift-check.ts +196 -0
  264. package/src/cli/telemetry-doctor.test.mjs +53 -0
  265. package/src/cli/telemetry-doctor.ts +50 -7
  266. package/src/cli/validate-hook-influence.ts +37 -6
  267. package/src/cli/workflow-artifact-cleanup-audit.ts +483 -10
  268. package/src/cli/workflow-sidecar.ts +1980 -119
  269. package/src/cli.ts +5 -0
  270. package/src/flow-kit/validate.ts +277 -38
  271. package/src/index.ts +19 -0
  272. package/src/lib/console-connect-options.ts +261 -0
  273. package/src/lib/console-telemetry-validate.ts +88 -0
  274. package/src/lib/flow-resolver.ts +117 -4
  275. package/src/lib/fs.ts +160 -0
  276. package/src/lib/local-artifact-root.ts +129 -3
  277. package/src/runtime-adapters.ts +113 -33
  278. package/src/tools/build-universal-bundles.ts +36 -2
  279. package/src/tools/codex-agent-routing.ts +48 -0
  280. package/src/tools/generate-context-map.ts +1 -0
  281. package/src/tools/validate-source-tree.ts +29 -3
@@ -18,8 +18,9 @@ const fs = require('fs');
18
18
  const path = require('path');
19
19
  const { readLivenessEvents, freshHolders } = require('./lib/liveness-read');
20
20
  const { resolveActor } = require('./lib/actor-identity');
21
- const { flowAgentsArtifactRootsForRead } = require('./lib/local-artifact-paths');
21
+ const { flowAgentsArtifactRootsForRead, resolveSharedRepoRoot, warnIfFailingOpenInsideGitTree } = require('./lib/local-artifact-paths');
22
22
  const { readCurrentPointer } = require('./lib/current-pointer');
23
+ const { workflowTriggersFor } = require('./lib/kit-catalog');
23
24
 
24
25
  const STEERING = {
25
26
  'tool-planner': [
@@ -71,14 +72,36 @@ const ACTIVE_STATE_STATUSES = new Set([
71
72
  ]);
72
73
 
73
74
  function findRepoRoot(startDir) {
74
- let dir = path.resolve(startDir || process.cwd());
75
+ const resolvedStartDir = path.resolve(startDir || process.cwd());
76
+ // #357: prefer git's SHARED common-dir root (the primary checkout's repo root, resolved
77
+ // identically from any linked worktree) before falling back to the plain `.git`/AGENTS.md
78
+ // ancestor walk below. Without this, a worktree's OWN `.git` FILE (not the shared `.git`
79
+ // DIRECTORY) satisfies `fs.existsSync(path.join(dir, '.git'))` at the very first directory
80
+ // checked, so the walk always stopped at the worktree root instead of resolving to the
81
+ // shared primary checkout — see src/lib/local-artifact-root.ts's resolveSharedRepoRoot doc
82
+ // comment for the full rationale. Fails open to the walk below (unchanged) when git is
83
+ // unavailable, `resolvedStartDir` is not inside a git working tree, or the command fails —
84
+ // this is also exactly today's behavior in the single-checkout case, where
85
+ // `--git-common-dir` resolves to `.git` under `resolvedStartDir` itself.
86
+ const sharedRoot = resolveSharedRepoRoot(resolvedStartDir);
87
+ if (sharedRoot) return sharedRoot;
88
+ let dir = resolvedStartDir;
75
89
  const root = path.parse(dir).root;
76
90
  for (let depth = 0; dir && depth < 40; depth++) {
77
- if (fs.existsSync(path.join(dir, '.git')) || fs.existsSync(path.join(dir, 'AGENTS.md'))) return dir;
91
+ if (fs.existsSync(path.join(dir, '.git'))) {
92
+ // #413 iteration-2 Fix 1: resolveSharedRepoRoot failed above even though a `.git` entry
93
+ // exists right here — git resolution was ATTEMPTED and FAILED (corrupted gitlink, bad
94
+ // GIT_DIR, git unavailable, etc.), not merely "no git repo at all". Loud, not silent —
95
+ // see warnIfFailingOpenInsideGitTree's own doc comment for the full rationale. This walk
96
+ // still returns `dir` (the ancestor-walk fallback is preserved unchanged) after warning.
97
+ warnIfFailingOpenInsideGitTree(resolvedStartDir, dir);
98
+ return dir;
99
+ }
100
+ if (fs.existsSync(path.join(dir, 'AGENTS.md'))) return dir;
78
101
  if (dir === root) break;
79
102
  dir = path.dirname(dir);
80
103
  }
81
- return path.resolve(startDir || process.cwd());
104
+ return resolvedStartDir;
82
105
  }
83
106
 
84
107
  function walkStateFiles(dir, out = []) {
@@ -159,8 +182,25 @@ function latestWorkflowState(root) {
159
182
  return states[0] || null;
160
183
  }
161
184
 
185
+ // #293 AC7 (injection discipline): strips C0 (0x00-0x1F), DEL (0x7F), and C1 (0x80-0x9F,
186
+ // which includes ANSI-CSI-adjacent bytes) BEFORE whitespace-collapse/truncation — mirroring
187
+ // src/cli/workflow-sidecar.ts's stripControlCharsForDisplay / assignment-provider.ts's
188
+ // sanitizeDisplayField exactly (same charset, same rationale: holder/actor/assignee strings
189
+ // sourced from the shared multi-writer liveness stream or an attacker-postable GitHub comment
190
+ // are untrusted display input). Previously this function only collapsed whitespace, so a raw
191
+ // ESC/BEL byte embedded in a hostile actor string passed through unstripped into every steering
192
+ // notice that calls safeStateText (resumeSteering's LIVENESS WARNING block, supersessionSteering's
193
+ // SUPERSEDED notice) — fixed here, in the one shared helper, rather than in each call site.
162
194
  function safeStateText(value, maxLength = 240) {
163
- const text = String(value || '').replace(/\s+/g, ' ').trim();
195
+ // Whitespace-collapse FIRST (so an embedded newline/tab becomes a joining space, preserving
196
+ // the existing multiline-summary neutralization behavior), THEN strip control/ANSI bytes
197
+ // (#293 AC7) — reversing this order would strip \n before the collapse ever sees it, losing
198
+ // the join-space between sentences. Only C0/DEL/C1 bytes THAT SURVIVE the collapse (ESC, BEL,
199
+ // and other non-whitespace control bytes — real \s already became a plain space above) are
200
+ // stripped, mirroring src/cli/workflow-sidecar.ts's stripControlCharsForDisplay /
201
+ // assignment-provider.ts's sanitizeDisplayField charset exactly.
202
+ const collapsed = String(value || '').replace(/\s+/g, ' ').trim();
203
+ const text = collapsed.replace(/[\u0000-\u001F\u007F-\u009F]/g, '');
164
204
  if (text.length <= maxLength) return text;
165
205
  return `${text.slice(0, maxLength - 3)}...`;
166
206
  }
@@ -227,6 +267,9 @@ function stateSteering(root) {
227
267
  `STATE: ${state.task_slug || path.basename(path.dirname(current.file))} is status:${state.status} phase:${state.phase}.`,
228
268
  ];
229
269
  if (next.summary) parts.push(`Recorded next_action.summary: "${safeStateText(next.summary)}"`);
270
+ if (Array.isArray(next.skills) && next.skills.length) parts.push(`Required skills: ${next.skills.map(skill => safeStateText(skill, 80)).join(' -> ')}.`);
271
+ if (Array.isArray(next.operations) && next.operations.length) parts.push(`Required operations: ${next.operations.map(operation => safeStateText(operation, 80)).join(' -> ')}.`);
272
+ if (next.command) parts.push(`Run: ${safeStateText(next.command, 240)}`);
230
273
  if (next.target_phase) parts.push(`Target phase: ${safeStateText(next.target_phase, 80)}.`);
231
274
  if (next.status === 'needs_user' || state.status === 'needs_decision' || state.status === 'not_verified') {
232
275
  parts.push('Do not deliver as complete until the user decision or accepted gap is recorded.');
@@ -248,6 +291,52 @@ function contextMapSteering(root) {
248
291
  ].join(' ');
249
292
  }
250
293
 
294
+ /**
295
+ * Compose the SessionStart advisory line for installed-skill drift (#439). Fires only inside a
296
+ * kit-bearing checkout (repo has `kits/` AND a built `dist/claude-code/.claude/skills` bundle) —
297
+ * never triggers a build itself, staying read-only and cheap on every SessionStart. Returns ''
298
+ * (never throws past this function) in every other case: not kit-bearing, no manifest yet, no
299
+ * drift found, or any internal error.
300
+ *
301
+ * Reuses the SINGLE shared classification choke point, `scripts/hooks/lib/skill-drift.js`'s
302
+ * `compareSkillDrift()` — this function must never hand-roll its own drift comparison (same
303
+ * convention the CLI check and the manifest writer follow, see #439 plan).
304
+ *
305
+ * @param {string} root Repository root
306
+ * @returns {string}
307
+ */
308
+ function skillDriftSteering(root) {
309
+ try {
310
+ if (!fs.existsSync(path.join(root, 'kits'))) return '';
311
+ if (!fs.existsSync(path.join(root, 'dist', 'claude-code', '.claude', 'skills'))) return '';
312
+
313
+ const { loadManifest, compareSkillDrift, resolveClaudeGlobalSkillsDir } = require('./lib/skill-drift');
314
+
315
+ const destDir = resolveClaudeGlobalSkillsDir(process.env);
316
+ // Sibling layout: <destDir>/.flow-agents/skills-manifest.json — mirrors
317
+ // src/lib/local-artifact-root.ts's durableFlowAgentsRoot(destDir)/skillsManifestPath exactly
318
+ // (durableFlowAgentsRoot(cwd) resolves to `${cwd}/.flow-agents`, no parent traversal).
319
+ const manifestPath = path.join(destDir, '.flow-agents', 'skills-manifest.json');
320
+ const manifest = loadManifest(manifestPath);
321
+
322
+ const installedDir = path.join(destDir, 'skills');
323
+ const kitSourceDir = path.join(root, 'dist', 'claude-code', '.claude', 'skills');
324
+ const report = compareSkillDrift({ installedDir, kitSourceDir, manifest });
325
+ if (!report.hasDrift) return '';
326
+
327
+ const { kitUpdated, userModified, unbaselined, missingInstall, kitRemoved } = report.summary;
328
+ // kit_removed (#439 review fix) folds into staleCount alongside the other non-in_sync
329
+ // states — this advisory line only ever surfaces kit-updated/user-modified counts by name
330
+ // (see the template string below), so kit_removed does not get its own named count here; it
331
+ // is still reflected in the total. Full per-state detail is always available via
332
+ // `flow-agents skill-drift-check`.
333
+ const staleCount = kitUpdated + userModified + unbaselined + missingInstall + kitRemoved;
334
+ return `[SKILL DRIFT] ${staleCount} installed Claude Code skill file(s) are stale vs this kit (${kitUpdated} kit-updated, ${userModified} user-modified). Run \`flow-agents init --runtime claude-code --global\` to refresh (user-modified files are reported, not overwritten). See \`flow-agents skill-drift-check\` for details.`;
335
+ } catch {
336
+ return '';
337
+ }
338
+ }
339
+
251
340
  function promptText(input) {
252
341
  const candidates = [
253
342
  input && input.prompt,
@@ -262,7 +351,7 @@ function promptText(input) {
262
351
  return '';
263
352
  }
264
353
 
265
- function looksLikeBuilderWork(text) {
354
+ function looksLikeImplementationWork(text) {
266
355
  const normalized = safeStateText(text, 2000).toLowerCase();
267
356
  if (!normalized) return false;
268
357
  const readOnlyIntent = /\b(read[- ]only|review[- ]only|no (source |code |file )?(changes?|edits?|modifications?)|do not (modify|edit|change|write|fix|implement|touch)|don't (modify|edit|change|write|fix|implement|touch)|dont (modify|edit|change|write|fix|implement|touch))\b/.test(normalized);
@@ -281,17 +370,29 @@ function looksLikeBuilderWork(text) {
281
370
  return hasWorkVerb || (hasWorkObject && hasRequestLanguage);
282
371
  }
283
372
 
284
- function builderWorkflowSteering(input) {
285
- if (!looksLikeBuilderWork(promptText(input))) return '';
286
- return [
287
- 'BUILDER WORKFLOW ROUTE: this user prompt looks like coding/build work.',
288
- 'Builder lifecycle: shape raw ideas -> build selected work -> publish verified branch/PR -> learn correction feedback.',
289
- 'Before source edits or implementation commands, activate Builder Kit delivery workflow.',
290
- 'If the user explicitly requested TDD, activate `tdd-workflow`; otherwise activate `deliver` and keep the session on `builder.build`.',
291
- 'Use `npm run workflow:sidecar -- ensure-session --flow-id builder.build ...` when the repo provides the sidecar writer.',
292
- 'After local verification, continue to publish/release-readiness and learning-review; do not treat local verification as terminal delivery.',
293
- 'Do not bypass plan-work -> execute-plan -> review-work -> verify-work for coding tasks; direct implementation is only acceptable when the user explicitly asks for no workflow or explanation-only help.',
294
- ].join(' ');
373
+ function looksLikeKnowledgeWork(text) {
374
+ const normalized = safeStateText(text, 2000).toLowerCase();
375
+ if (!normalized) return false;
376
+ return /\b(remember|capture|save|file|bookmark)\s+(this|that|the|my|our)\b/.test(normalized) ||
377
+ /\bcapture\s+(this\s+)?(decision|learning|lesson|note|context)\b/.test(normalized) ||
378
+ /\bwhat did we learn about\b/.test(normalized);
379
+ }
380
+
381
+ function kitWorkflowSteering(input, root) {
382
+ const prompt = promptText(input);
383
+ const categories = [];
384
+ if (looksLikeImplementationWork(prompt)) categories.push('implementation-work-detected');
385
+ if (looksLikeKnowledgeWork(prompt)) categories.push('knowledge-capture-detected');
386
+ if (!categories.length) return '';
387
+ try {
388
+ return categories
389
+ .flatMap(category => workflowTriggersFor(root, category))
390
+ .map(trigger => trigger.steering)
391
+ .filter(Boolean)
392
+ .join('\n');
393
+ } catch {
394
+ return '';
395
+ }
295
396
  }
296
397
 
297
398
  /**
@@ -324,6 +425,13 @@ function resumeSteering(root, current) {
324
425
  // Full next action (240-char display path, not the 80-char normalization)
325
426
  const nextSummary = next.summary ? safeStateText(next.summary, 240) : 'none';
326
427
  lines.push(`Next action: ${nextSummary}`);
428
+ if (Array.isArray(next.skills) && next.skills.length) {
429
+ lines.push(`Required skills: ${next.skills.map(skill => safeStateText(skill, 80)).join(' -> ')}`);
430
+ }
431
+ if (Array.isArray(next.operations) && next.operations.length) {
432
+ lines.push(`Required operations: ${next.operations.map(operation => safeStateText(operation, 80)).join(' -> ')}`);
433
+ }
434
+ if (next.command) lines.push(`Run: ${safeStateText(next.command, 240)}`);
327
435
 
328
436
  // Plan artifact path
329
437
  let planPath = 'not found';
@@ -411,6 +519,51 @@ function resumeSteering(root, current) {
411
519
  }
412
520
  }
413
521
 
522
+ /**
523
+ * Compose the every-turn SUPERSEDED steering notice (issue #293).
524
+ *
525
+ * Unlike resumeSteering()'s RESUME block (SessionStart only), this notice must
526
+ * surface on EVERY turn (UserPromptSubmit as well as SessionStart) once another
527
+ * actor holds a fresh liveness claim on the caller's own subject — the
528
+ * "don't publish, you may be stale" signal the verify-hold gate backstops at
529
+ * publish time. Reuses the SAME liveness-only join resumeSteering()'s liveness
530
+ * advisory block already performs (freshHolders(events, slug, selfActor, now)),
531
+ * not a second computation.
532
+ *
533
+ * freshHolders() already excludes selfActor internally (see
534
+ * scripts/hooks/lib/liveness-read.js), so any holder returned here is by
535
+ * definition another actor — no additional self-filter is required.
536
+ *
537
+ * Fully fail-open: any error anywhere in this function returns '' rather than
538
+ * throwing, matching every other steering helper in this file.
539
+ *
540
+ * @param {string} root Repository root
541
+ * @param {{ file: string, payload: object }} current Latest active state entry (this actor's own)
542
+ * @returns {string}
543
+ */
544
+ function supersessionSteering(root, current) {
545
+ try {
546
+ const state = current.payload;
547
+ const workflowDir = path.dirname(current.file);
548
+ const slug = state.task_slug || path.basename(workflowDir);
549
+
550
+ const resolved = resolveActor(process.env);
551
+ const selfActor = resolved.actor || 'local';
552
+
553
+ const events = flowAgentsArtifactRootsForRead(root)
554
+ .flatMap(artifactRoot => readLivenessEvents(path.join(artifactRoot, 'liveness', 'events.jsonl')));
555
+ if (events.length === 0) return '';
556
+
557
+ const holders = freshHolders(events, slug, selfActor, Date.now());
558
+ if (holders.length === 0) return '';
559
+
560
+ const holder = holders[0];
561
+ return `[SUPERSEDED: another agent appears live on this work: actor ${safeStateText(holder.actor)}, last seen ${holder.lastAt}. Your claim on this subject may be stale — do not publish until you re-verify hold (see verify-hold gate) or hand off.]`;
562
+ } catch {
563
+ return '';
564
+ }
565
+ }
566
+
414
567
  function run(rawInput) {
415
568
  try {
416
569
  const input = JSON.parse(rawInput);
@@ -443,18 +596,35 @@ function run(rawInput) {
443
596
  }
444
597
 
445
598
  if (event === 'UserPromptSubmit') {
446
- const builderHint = builderWorkflowSteering(input);
447
- if (builderHint) {
448
- hints.push(builderHint);
599
+ const kitHint = kitWorkflowSteering(input, root);
600
+ if (kitHint) {
601
+ hints.push(kitHint);
449
602
  const contextHint = contextMapSteering(root);
450
603
  if (contextHint) hints.push(contextHint);
451
604
  }
452
605
  }
453
606
 
607
+ // Every-turn supersession notice (#293): fires on UserPromptSubmit (every turn),
608
+ // not just SessionStart, so a takeover mid-session keeps surfacing the warning.
609
+ if (event === 'UserPromptSubmit' && current) {
610
+ const supersessionHint = supersessionSteering(root, current);
611
+ if (supersessionHint) hints.push(supersessionHint);
612
+ }
613
+
454
614
  // SessionStart only: append the RESUME block for richer situational awareness
455
615
  if (event === 'SessionStart' && current) {
456
616
  const resumeBlock = resumeSteering(root, current);
457
617
  if (resumeBlock) hints.push(resumeBlock);
618
+ const supersessionHint = supersessionSteering(root, current);
619
+ if (supersessionHint) hints.push(supersessionHint);
620
+ }
621
+
622
+ // SessionStart only, unconditional of `current` (#439): the installed-skill drift advisory
623
+ // fires on every SessionStart inside a kit-bearing checkout, independent of whether an active
624
+ // workflow session exists — do NOT fold this into the `current`-gated block above.
625
+ if (event === 'SessionStart') {
626
+ const driftHint = skillDriftSteering(root);
627
+ if (driftHint) hints.push(driftHint);
458
628
  }
459
629
 
460
630
  if (shouldAppendWorkflowContext) {
@@ -487,12 +657,14 @@ module.exports = {
487
657
  stateSteering,
488
658
  critiqueSteering,
489
659
  contextMapSteering,
660
+ skillDriftSteering,
490
661
  latestWorkflowState,
491
662
  findRepoRoot,
492
663
  safeStateText,
493
664
  stateNeedsAmbientSteering,
494
665
  resumeSteering,
666
+ supersessionSteering,
495
667
  promptText,
496
- looksLikeBuilderWork,
497
- builderWorkflowSteering,
668
+ looksLikeImplementationWork,
669
+ kitWorkflowSteering,
498
670
  };
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -15,6 +15,10 @@ Options:
15
15
  --console-token-file PATH
16
16
  File containing the bearer token.
17
17
  --console-tenant ID Tenant identifier for hosted Console routing.
18
+ --no-economics-relay Write console_economics_relay=0 to opt out of the
19
+ automatic kit-economics relay (default-on once a
20
+ Console telemetry sink is configured; see
21
+ lib/config.sh).
18
22
  EOF
19
23
  }
20
24
 
@@ -105,6 +109,7 @@ main() {
105
109
  local console_token=""
106
110
  local console_token_file="${FLOW_AGENTS_CONSOLE_TOKEN_FILE:-${CONSOLE_TELEMETRY_TOKEN_FILE:-}}"
107
111
  local console_tenant="${FLOW_AGENTS_CONSOLE_TENANT:-${CONSOLE_TENANT_ID:-}}"
112
+ local no_economics_relay=0
108
113
  telemetry_sinks=()
109
114
  if [[ -n "${FLOW_AGENTS_TELEMETRY_SINKS:-}" ]]; then
110
115
  append_sinks "$FLOW_AGENTS_TELEMETRY_SINKS"
@@ -139,6 +144,10 @@ main() {
139
144
  console_tenant="$2"
140
145
  shift 2
141
146
  ;;
147
+ --no-economics-relay)
148
+ no_economics_relay=1
149
+ shift
150
+ ;;
142
151
  --help|-h)
143
152
  usage
144
153
  exit 0
@@ -181,7 +190,12 @@ main() {
181
190
  done
182
191
  [[ "$console_sink_count" -le 1 ]] || die "select at most one Console telemetry sink"
183
192
 
184
- if [[ -z "$console_url" && -z "$console_endpoint" && -z "$console_token" && -z "$console_tenant" ]]; then
193
+ if [[ "$console_sink_count" -gt 0 && -z "$console_tenant" ]]; then
194
+ echo "warning: install-console-config.sh: a Console telemetry sink was selected with no --console-tenant; records will be untenanted" >&2
195
+ fi
196
+
197
+ if [[ -z "$console_url" && -z "$console_endpoint" && -z "$console_token" && -z "$console_tenant" \
198
+ && "$no_economics_relay" -eq 0 ]]; then
185
199
  return 0
186
200
  fi
187
201
  if [[ -z "$console_url" && -z "$console_endpoint" && ( -n "$console_token" || -n "$console_tenant" ) ]]; then
@@ -199,15 +213,22 @@ main() {
199
213
  set_config_key "$conf" "console_telemetry_endpoint_url" "$console_endpoint"
200
214
  set_config_key "$conf" "console_telemetry_token" "$console_token"
201
215
  set_config_key "$conf" "console_tenant_id" "$console_tenant"
216
+ if [[ "$no_economics_relay" -eq 1 ]]; then
217
+ set_config_key "$conf" "console_economics_relay" "0"
218
+ fi
202
219
  if [[ -n "$console_token" ]]; then
203
220
  chmod 600 "$conf" 2>/dev/null || true
204
221
  fi
205
222
 
206
223
  local target="${console_endpoint:-$console_url}"
207
- if [[ -n "$console_tenant" ]]; then
208
- echo "Configured Console telemetry in $conf for $target (tenant: $console_tenant)"
224
+ if [[ -n "$target" ]]; then
225
+ if [[ -n "$console_tenant" ]]; then
226
+ echo "Configured Console telemetry in $conf for $target (tenant: $console_tenant)"
227
+ else
228
+ echo "Configured Console telemetry in $conf for $target"
229
+ fi
209
230
  else
210
- echo "Configured Console telemetry in $conf for $target"
231
+ echo "Recorded economics-relay opt-out in $conf"
211
232
  fi
212
233
  }
213
234
 
@@ -2,14 +2,78 @@
2
2
  # config.sh — Load telemetry configuration with defaults
3
3
 
4
4
  TELEMETRY_DIR="${TELEMETRY_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
5
- TELEMETRY_CONFIG_FILE="${TELEMETRY_CONFIG_FILE:-${TELEMETRY_DIR}/telemetry.conf}"
5
+ # TELEMETRY_DIR is <workspace>/scripts/telemetry, so the workspace root is
6
+ # two levels up.
7
+ TELEMETRY_WORKSPACE_ROOT="${TELEMETRY_WORKSPACE_ROOT:-$(cd "${TELEMETRY_DIR}/../.." && pwd)}"
8
+
9
+ # A workspace/global conf only counts as operator-created (and thus trusted
10
+ # with credentials) if it is mode 600 and owned by the current user.
11
+ # install-console-config.sh always chmod 600s the conf it writes; git can
12
+ # only store 644/755 (never 600), so this gate rejects a conf smuggled in via
13
+ # clone/tarball/PR/supply-chain even if it happens to land at the expected
14
+ # path. Portable across macOS/BSD stat (-f) and GNU/Linux stat (-c).
15
+ telemetry_conf_trusted() {
16
+ local file="$1" mode owner
17
+ # Reject symlinks outright: stat below reports the link's own mode/owner
18
+ # (lstat semantics, no -L), but the config file is later read by following
19
+ # the link, so a symlink lets `chmod -h 600 <link>` pass this gate while
20
+ # the read lands on a different, untrusted target. Never trust a link.
21
+ [[ -L "$file" ]] && return 1
22
+ [[ -f "$file" ]] || return 1
23
+ mode=$(stat -f '%Lp' "$file" 2>/dev/null) || mode=$(stat -c '%a' "$file" 2>/dev/null)
24
+ owner=$(stat -f '%u' "$file" 2>/dev/null) || owner=$(stat -c '%u' "$file" 2>/dev/null)
25
+ [[ "$mode" == "600" && -n "$owner" && "$owner" == "$(id -u)" ]]
26
+ }
27
+
28
+ # A candidate that exists on disk but fails telemetry_conf_trusted would
29
+ # otherwise be silently ignored (telemetry stays fail-open with no signal to
30
+ # the operator). Print a one-time, visible warning naming the path and the
31
+ # fix whenever this happens. Never warn when the file is simply absent, and
32
+ # never change which file TELEMETRY_CONFIG_FILE resolves to below -- this is
33
+ # purely additive/observational.
34
+ telemetry_conf_warn_untrusted() {
35
+ local file="$1"
36
+ # `-e` catches a regular file; `-L` additionally catches a symlink that
37
+ # `-e` would miss (a symlink to a directory, or a dangling symlink) --
38
+ # both of those are rejected by telemetry_conf_trusted's `-L` check above
39
+ # but must still surface this warning, not silently emit none.
40
+ [[ -e "$file" || -L "$file" ]] || return 0
41
+ printf 'warning: config.sh: %s exists but is not trusted (must be mode 600, owned by the current user, and not a symlink); telemetry will ignore it and stay fail-open. Fix with: chmod 600 "%s"\n' "$file" "$file" >&2
42
+ }
43
+
44
+ # Config file resolution order: (1) explicit TELEMETRY_CONFIG_FILE env always
45
+ # wins; (2) a gitignored per-workspace conf at .kontourai/telemetry-console.conf
46
+ # if present and operator-trusted (project-specific override); (3) a
47
+ # gitignored user-global conf at ~/.flow-agents/telemetry-console.conf if
48
+ # present and operator-trusted, matching the existing ~/.flow-agents
49
+ # install-home convention (see scripts/discover-agents.sh,
50
+ # scripts/context-budget/budget-scan.sh) so one machine-wide install can carry owner
51
+ # credentials without per-workspace wiring; (4) the shipped telemetry.conf
52
+ # default. The shipped scripts/telemetry/telemetry.conf is tracked and ships
53
+ # into every packaged dist bundle, so it must never carry owner credentials.
54
+ # A candidate that exists but fails the trust gate falls through silently
55
+ # (telemetry stays fail-open).
56
+ TELEMETRY_LOCAL_CONFIG_FILE="${TELEMETRY_WORKSPACE_ROOT}/.kontourai/telemetry-console.conf"
57
+ TELEMETRY_GLOBAL_CONFIG_FILE="${HOME:-}/.flow-agents/telemetry-console.conf"
58
+ if [[ -n "${TELEMETRY_CONFIG_FILE:-}" ]]; then
59
+ :
60
+ elif telemetry_conf_trusted "$TELEMETRY_LOCAL_CONFIG_FILE"; then
61
+ TELEMETRY_CONFIG_FILE="$TELEMETRY_LOCAL_CONFIG_FILE"
62
+ else
63
+ telemetry_conf_warn_untrusted "$TELEMETRY_LOCAL_CONFIG_FILE"
64
+ if [[ -n "${HOME:-}" ]] && telemetry_conf_trusted "$TELEMETRY_GLOBAL_CONFIG_FILE"; then
65
+ TELEMETRY_CONFIG_FILE="$TELEMETRY_GLOBAL_CONFIG_FILE"
66
+ else
67
+ [[ -n "${HOME:-}" ]] && telemetry_conf_warn_untrusted "$TELEMETRY_GLOBAL_CONFIG_FILE"
68
+ TELEMETRY_CONFIG_FILE="${TELEMETRY_DIR}/telemetry.conf"
69
+ fi
70
+ fi
6
71
 
7
72
  # Defaults
8
73
  TELEMETRY_ENABLED="${TELEMETRY_ENABLED:-true}"
9
- # TELEMETRY_DIR is <workspace>/scripts/telemetry, so the workspace root is
10
- # two levels up. Local runtime telemetry defaults under .kontourai/telemetry;
11
- # explicit TELEMETRY_DATA_DIR still wins.
12
- TELEMETRY_DATA_DIR="${TELEMETRY_DATA_DIR:-$(cd "${TELEMETRY_DIR}/../.." && pwd)/.kontourai/telemetry}"
74
+ # Local runtime telemetry defaults under .kontourai/telemetry; explicit
75
+ # TELEMETRY_DATA_DIR still wins.
76
+ TELEMETRY_DATA_DIR="${TELEMETRY_DATA_DIR:-${TELEMETRY_WORKSPACE_ROOT}/.kontourai/telemetry}"
13
77
  TELEMETRY_SESSION_DIR="${TELEMETRY_SESSION_DIR:-${TELEMETRY_DATA_DIR}/sessions}"
14
78
  TELEMETRY_ENRICH_SYSTEM="${TELEMETRY_ENRICH_SYSTEM:-true}"
15
79
  TELEMETRY_ENRICH_WORKSPACE="${TELEMETRY_ENRICH_WORKSPACE:-true}"
@@ -37,9 +101,16 @@ CONSOLE_TELEMETRY_URL="${CONSOLE_TELEMETRY_URL:-${CONSOLE_URL:-}}"
37
101
  CONSOLE_TELEMETRY_ENDPOINT_URL="${CONSOLE_TELEMETRY_ENDPOINT_URL:-}"
38
102
  CONSOLE_TELEMETRY_TOKEN="${CONSOLE_TELEMETRY_TOKEN:-${CONSOLE_AUTH_TOKEN:-}}"
39
103
  CONSOLE_TENANT_ID="${CONSOLE_TENANT_ID:-}"
104
+ # Economics relay (#469): a caller-pre-set env var is preserved as the starting point; the
105
+ # config-file key (parsed below) can override it, and if no key is present at all the
106
+ # default-on rule below turns it on once a console telemetry sink resolves.
107
+ FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY="${FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY:-}"
108
+ FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL="${FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL:-}"
109
+ # Set (non-empty) only when the config file carries an explicit console_economics_relay key —
110
+ # distinguishes "operator said 0/1" from "key absent" for the default-on rule below.
111
+ console_economics_relay_raw=""
40
112
  # Pricing registry source (consumed by lib/pricing.sh). Explicit file/URL win;
41
- # otherwise the URL is derived from the console below so all runtimes read one
42
- # live pricing source. Falls back to the bundled pricing.json offline.
113
+ # otherwise lib/pricing.sh uses the bundled pricing.json offline.
43
114
  TELEMETRY_PRICING_FILE="${TELEMETRY_PRICING_FILE:-${FLOW_AGENTS_PRICING_FILE:-}}"
44
115
  TELEMETRY_PRICING_URL="${TELEMETRY_PRICING_URL:-${FLOW_AGENTS_PRICING_URL:-}}"
45
116
 
@@ -82,6 +153,17 @@ if [[ -f "$TELEMETRY_CONFIG_FILE" ]]; then
82
153
  console_telemetry_token) CONSOLE_TELEMETRY_TOKEN="$value" ;;
83
154
  console_tenant_id) CONSOLE_TENANT_ID="$value" ;;
84
155
  console_telemetry_redact) CONSOLE_TELEMETRY_REDACT="$value" ;;
156
+ console_economics_relay)
157
+ case "$(echo "$value" | tr '[:upper:]' '[:lower:]')" in
158
+ 1|true|yes|on) console_economics_relay_raw="1" ;;
159
+ 0|false|no|off) console_economics_relay_raw="0" ;;
160
+ *)
161
+ printf 'warning: config.sh: unrecognized console_economics_relay value %q; treating as off\n' "$value" >&2
162
+ console_economics_relay_raw="$value"
163
+ ;;
164
+ esac
165
+ ;;
166
+ console_economics_endpoint_url) FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL="$value" ;;
85
167
  console_pricing_url) TELEMETRY_PRICING_URL="$value" ;;
86
168
  pricing_url) TELEMETRY_PRICING_URL="$value" ;;
87
169
  pricing_file) TELEMETRY_PRICING_FILE="$value" ;;
@@ -92,12 +174,20 @@ fi
92
174
 
93
175
  CONSOLE_TELEMETRY_REDACT="${CONSOLE_TELEMETRY_REDACT:-${TELEMETRY_CHANNEL_ANALYTICS_REDACT}}"
94
176
 
95
- # Derive the live pricing source from the console when not set explicitly, the
96
- # same way the transport derives /api/telemetry/records. One live source for
97
- # bash/Python/TS runtimes; lib/pricing.sh caches it and falls back to bundled.
98
- if [[ -z "${TELEMETRY_PRICING_URL:-}" && -n "${CONSOLE_TELEMETRY_URL:-}" ]]; then
99
- TELEMETRY_PRICING_URL="${CONSOLE_TELEMETRY_URL%/}/api/telemetry/pricing"
177
+ # Economics relay default-on rule (#469, opt-out not silent-off): an explicit
178
+ # console_economics_relay config key always wins. Otherwise, once a console telemetry sink is
179
+ # resolved (console_telemetry_url/console_telemetry_endpoint_url non-empty) the relay defaults ON
180
+ # unless a caller already pre-set FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY in the environment, which is
181
+ # left untouched. economics-record.sh's own opt-in gate (unchanged) reads this exact variable.
182
+ if [[ -n "$console_economics_relay_raw" ]]; then
183
+ FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY="$console_economics_relay_raw"
184
+ elif [[ -z "$FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY" \
185
+ && ( -n "${CONSOLE_TELEMETRY_URL:-}" || -n "${CONSOLE_TELEMETRY_ENDPOINT_URL:-}" ) ]]; then
186
+ FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY=1
100
187
  fi
101
188
 
189
+ # Pricing URL is explicit-only (env or config). Do not derive the console
190
+ # pricing endpoint by default; the bundled registry is the reliable offline floor.
191
+
102
192
  # Ensure directories exist
103
193
  mkdir -p "$TELEMETRY_DATA_DIR" "$TELEMETRY_SESSION_DIR" 2>/dev/null
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env bash
2
+ # pricing.sh — single-source pricing registry loader.
3
+ #
4
+ # Resolves the versioned pricing registry (pricing.json) from, in priority:
5
+ # 1. explicit local file TELEMETRY_PRICING_FILE / FLOW_AGENTS_PRICING_FILE
6
+ # 2. remote URL (cached) TELEMETRY_PRICING_URL / FLOW_AGENTS_PRICING_URL
7
+ # 3. bundled snapshot <telemetry>/pricing.json
8
+ # This is the one source every runtime + the console read from — local for
9
+ # air-gapped use, remote for a single live registry shared across machines.
10
+
11
+ PRICING_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12
+
13
+ pricing_registry_valid_json() {
14
+ command -v jq >/dev/null 2>&1 || return 1
15
+ jq -e '.current_version and .versions' >/dev/null 2>&1
16
+ }
17
+
18
+ # Echo the raw registry JSON. Returns non-zero if nothing resolvable.
19
+ pricing_registry() {
20
+ local f="${TELEMETRY_PRICING_FILE:-${FLOW_AGENTS_PRICING_FILE:-}}"
21
+ if [[ -n "$f" && -f "$f" ]]; then cat "$f"; return 0; fi
22
+
23
+ local url="${TELEMETRY_PRICING_URL:-${FLOW_AGENTS_PRICING_URL:-}}"
24
+ if [[ -n "$url" ]] && command -v curl >/dev/null 2>&1; then
25
+ local cache="${TMPDIR:-/tmp}/flow-agents-pricing-cache.json"
26
+ local ttl="${TELEMETRY_PRICING_TTL_SEC:-3600}"
27
+ if [[ -f "$cache" ]]; then
28
+ local mtime now age
29
+ mtime=$(stat -c %Y "$cache" 2>/dev/null || stat -f %m "$cache" 2>/dev/null || echo 0)
30
+ [[ "$mtime" =~ ^[0-9]+$ ]] || mtime=0
31
+ now=$(date +%s)
32
+ age=$(( now - mtime ))
33
+ if [[ "$age" -lt "$ttl" ]] && pricing_registry_valid_json < "$cache"; then cat "$cache"; return 0; fi
34
+ fi
35
+ if curl -fsS --max-time 5 "$url" -o "${cache}.tmp" 2>/dev/null && [[ -s "${cache}.tmp" ]]; then
36
+ if pricing_registry_valid_json < "${cache}.tmp"; then
37
+ mv "${cache}.tmp" "$cache"
38
+ cat "$cache"
39
+ return 0
40
+ fi
41
+ fi
42
+ rm -f "${cache}.tmp" 2>/dev/null
43
+ [[ -f "$cache" ]] && pricing_registry_valid_json < "$cache" && { cat "$cache"; return 0; }
44
+ fi
45
+
46
+ local bundled
47
+ bundled="$(cd "${PRICING_LIB_DIR}/.." && pwd)/pricing.json"
48
+ [[ -f "$bundled" ]] && { cat "$bundled"; return 0; }
49
+ return 1
50
+ }
@@ -60,4 +60,7 @@ session_end() {
60
60
 
61
61
  session_cleanup() {
62
62
  find "${TELEMETRY_SESSION_DIR}" -name "*.session" -mtime +1 -delete 2>/dev/null || true
63
+ # Bound the per-cwd project-label cache (see console_project_label) so a project rename
64
+ # (package.json name / git remote) self-heals within the same window instead of caching forever.
65
+ find "${TELEMETRY_SESSION_DIR}" -name "project-label.*" -mtime +1 -delete 2>/dev/null || true
63
66
  }