@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
@@ -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 = []) {
@@ -244,6 +267,9 @@ function stateSteering(root) {
244
267
  `STATE: ${state.task_slug || path.basename(path.dirname(current.file))} is status:${state.status} phase:${state.phase}.`,
245
268
  ];
246
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)}`);
247
273
  if (next.target_phase) parts.push(`Target phase: ${safeStateText(next.target_phase, 80)}.`);
248
274
  if (next.status === 'needs_user' || state.status === 'needs_decision' || state.status === 'not_verified') {
249
275
  parts.push('Do not deliver as complete until the user decision or accepted gap is recorded.');
@@ -265,6 +291,52 @@ function contextMapSteering(root) {
265
291
  ].join(' ');
266
292
  }
267
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
+
268
340
  function promptText(input) {
269
341
  const candidates = [
270
342
  input && input.prompt,
@@ -279,7 +351,7 @@ function promptText(input) {
279
351
  return '';
280
352
  }
281
353
 
282
- function looksLikeBuilderWork(text) {
354
+ function looksLikeImplementationWork(text) {
283
355
  const normalized = safeStateText(text, 2000).toLowerCase();
284
356
  if (!normalized) return false;
285
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);
@@ -298,17 +370,29 @@ function looksLikeBuilderWork(text) {
298
370
  return hasWorkVerb || (hasWorkObject && hasRequestLanguage);
299
371
  }
300
372
 
301
- function builderWorkflowSteering(input) {
302
- if (!looksLikeBuilderWork(promptText(input))) return '';
303
- return [
304
- 'BUILDER WORKFLOW ROUTE: this user prompt looks like coding/build work.',
305
- 'Builder lifecycle: shape raw ideas -> build selected work -> publish verified branch/PR -> learn correction feedback.',
306
- 'Before source edits or implementation commands, activate Builder Kit delivery workflow.',
307
- 'If the user explicitly requested TDD, activate `tdd-workflow`; otherwise activate `deliver` and keep the session on `builder.build`.',
308
- 'Use `npm run workflow:sidecar -- ensure-session --flow-id builder.build ...` when the repo provides the sidecar writer.',
309
- 'After local verification, continue to publish/release-readiness and learning-review; do not treat local verification as terminal delivery.',
310
- '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.',
311
- ].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
+ }
312
396
  }
313
397
 
314
398
  /**
@@ -341,6 +425,13 @@ function resumeSteering(root, current) {
341
425
  // Full next action (240-char display path, not the 80-char normalization)
342
426
  const nextSummary = next.summary ? safeStateText(next.summary, 240) : 'none';
343
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)}`);
344
435
 
345
436
  // Plan artifact path
346
437
  let planPath = 'not found';
@@ -505,9 +596,9 @@ function run(rawInput) {
505
596
  }
506
597
 
507
598
  if (event === 'UserPromptSubmit') {
508
- const builderHint = builderWorkflowSteering(input);
509
- if (builderHint) {
510
- hints.push(builderHint);
599
+ const kitHint = kitWorkflowSteering(input, root);
600
+ if (kitHint) {
601
+ hints.push(kitHint);
511
602
  const contextHint = contextMapSteering(root);
512
603
  if (contextHint) hints.push(contextHint);
513
604
  }
@@ -528,6 +619,14 @@ function run(rawInput) {
528
619
  if (supersessionHint) hints.push(supersessionHint);
529
620
  }
530
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);
628
+ }
629
+
531
630
  if (shouldAppendWorkflowContext) {
532
631
  const stateHint = stateSteering(root);
533
632
  if (stateHint) hints.push(stateHint);
@@ -558,6 +657,7 @@ module.exports = {
558
657
  stateSteering,
559
658
  critiqueSteering,
560
659
  contextMapSteering,
660
+ skillDriftSteering,
561
661
  latestWorkflowState,
562
662
  findRepoRoot,
563
663
  safeStateText,
@@ -565,6 +665,6 @@ module.exports = {
565
665
  resumeSteering,
566
666
  supersessionSteering,
567
667
  promptText,
568
- looksLikeBuilderWork,
569
- builderWorkflowSteering,
668
+ looksLikeImplementationWork,
669
+ kitWorkflowSteering,
570
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
  }
@@ -46,6 +46,78 @@ console_telemetry_timeout_seconds() {
46
46
  fi
47
47
  }
48
48
 
49
+ # Derive a coarse, path-free project label for console attribution, most-stable-first so the SAME
50
+ # project resolves to the SAME label across developers and machines (folder names differ between
51
+ # clones and worktrees; the project manifest and git remote do not). Precedence:
52
+ # 1. FLOW_AGENTS_PROJECT — explicit operator override, always wins
53
+ # 2. nearest package.json name — walking up from cwd (monorepo-granular, committed => consistent)
54
+ # 3. git remote origin org/repo — repo-level identity, stable across clones
55
+ # 4. git toplevel dir basename — repo dir even from a worktree/subdir
56
+ # 5. cwd basename — last resort
57
+ # Path-free by construction (never the full local path). Cached per cwd under the telemetry data
58
+ # dir so the git/manifest reads run once per project, not per event; session_cleanup bounds the
59
+ # cache lifetime so a project rename (package.json name / git remote) self-heals within a day.
60
+ # Failure signals via empty output; the sole caller wraps the call as `$(...) || proj=""` so even if
61
+ # an internal command fails under `set -e`, telemetry is relayed unchanged (see
62
+ # console_telemetry_emit). Do not call this unwrapped from a `set -e` context.
63
+ console_project_label() {
64
+ local cwd="$1"
65
+ [[ -z "$cwd" || ! -d "$cwd" ]] && return 0
66
+ [[ -n "${FLOW_AGENTS_PROJECT:-}" ]] && { printf '%s' "$FLOW_AGENTS_PROJECT"; return 0; }
67
+
68
+ local cache="" key
69
+ if [[ -n "${TELEMETRY_SESSION_DIR:-}" && -d "${TELEMETRY_SESSION_DIR:-}" ]]; then
70
+ key=$(printf '%s' "$cwd" | cksum | cut -d' ' -f1)
71
+ cache="${TELEMETRY_SESSION_DIR%/}/project-label.${key}"
72
+ [[ -s "$cache" ]] && { cat "$cache"; return 0; }
73
+ fi
74
+
75
+ local label="" dir name url top after path repo rest org
76
+ dir="$cwd"
77
+ while [[ -n "$dir" && "$dir" != "/" ]]; do
78
+ if [[ -f "$dir/package.json" ]]; then
79
+ name=$(jq -r '.name // empty' "$dir/package.json" 2>/dev/null) || name=""
80
+ [[ -n "$name" ]] && { label="$name"; break; }
81
+ fi
82
+ dir=$(dirname "$dir")
83
+ done
84
+ if [[ -z "$label" ]]; then
85
+ url=$(git -C "$cwd" config --get remote.origin.url 2>/dev/null) || url=""
86
+ if [[ -n "$url" ]]; then
87
+ # Reduce a git remote to a coarse, host-free org/repo. Strip scheme+host STRUCTURALLY (not by
88
+ # a dot-in-host heuristic) so a self-hosted or dot-less host, a port, or a user@ prefix can
89
+ # never leak into the label; a remote with no org tier (single path segment) falls through.
90
+ url="${url%/}"; url="${url%.git}"
91
+ path=""
92
+ if [[ "$url" == *"://"* ]]; then
93
+ # Only network VCS schemes carry an org/repo path; file:// and other local schemes fall through.
94
+ if [[ "$url" =~ ^(https?|ssh|git|ftps?):// ]]; then
95
+ after="${url#*://}" # [user@]host[:port]/org/repo
96
+ [[ "$after" == */* ]] && path="${after#*/}" # drop host[:port] (and any user@), keep path
97
+ fi
98
+ elif [[ "$url" == *:* && "${url%%:*}" != */* ]]; then
99
+ path="${url#*:}" # scp form [user@]host:org/repo
100
+ elif [[ "$url" != /* ]]; then
101
+ path="$url" # bare relative org/repo shorthand (not a local abs path)
102
+ fi
103
+ if [[ "$path" == */* ]]; then
104
+ repo="${path##*/}"; rest="${path%/*}"; org="${rest##*/}" # last two path segments
105
+ [[ -n "$org" && -n "$repo" ]] && label="$org/$repo"
106
+ fi
107
+ fi
108
+ fi
109
+ if [[ -z "$label" ]]; then
110
+ top=$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null) || top=""
111
+ [[ -n "$top" ]] && label=$(basename "$top")
112
+ fi
113
+ [[ -z "$label" ]] && label=$(basename "$cwd")
114
+
115
+ if [[ -n "$cache" ]]; then
116
+ printf '%s' "$label" > "${cache}.tmp.$$" 2>/dev/null && mv "${cache}.tmp.$$" "$cache" 2>/dev/null
117
+ fi
118
+ printf '%s' "$label"
119
+ }
120
+
49
121
  console_telemetry_emit() {
50
122
  local event="$1"
51
123
  local endpoint_url
@@ -53,6 +125,21 @@ console_telemetry_emit() {
53
125
  [[ -z "$endpoint_url" ]] && return
54
126
  console_telemetry_endpoint_allowed "$endpoint_url" || return
55
127
 
128
+ # Attribution: stamp a coarse, path-free project label (see console_project_label) before redaction
129
+ # so the console buckets by project consistently across developers. The full context.cwd is still
130
+ # redacted below — only the label leaves the machine. Every substitution is `|| var=""`-guarded so
131
+ # that even under `set -e` any failure (bad JSON, missing cwd, no git) relays the event unchanged.
132
+ local ev_cwd proj labeled_event
133
+ ev_cwd=$(printf '%s' "$event" | jq -r '.context.cwd // empty' 2>/dev/null) || ev_cwd=""
134
+ if [[ -n "$ev_cwd" ]]; then
135
+ proj=$(console_project_label "$ev_cwd" 2>/dev/null) || proj=""
136
+ if [[ -n "$proj" ]]; then
137
+ labeled_event=$(printf '%s' "$event" | jq -c --arg p "$proj" '
138
+ if ((.context.project // "") | length) == 0 then .context.project = $p else . end' 2>/dev/null) || labeled_event=""
139
+ [[ -n "$labeled_event" ]] && event="$labeled_event"
140
+ fi
141
+ fi
142
+
56
143
  local processed_event
57
144
  processed_event=$(redact_event "$event" "${CONSOLE_TELEMETRY_REDACT:-${TELEMETRY_CHANNEL_ANALYTICS_REDACT:-}}")
58
145