@kontourai/flow-agents 3.3.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.
- package/.github/workflows/add-to-project.yml +15 -0
- package/.github/workflows/ci.yml +161 -0
- package/CHANGELOG.md +41 -0
- package/CONTEXT.md +5 -1
- package/README.md +19 -8
- package/build/src/builder-flow-run-adapter.d.ts +80 -0
- package/build/src/builder-flow-run-adapter.js +241 -0
- package/build/src/builder-flow-runtime.d.ts +16 -0
- package/build/src/builder-flow-runtime.js +290 -0
- package/build/src/cli/builder-run.d.ts +1 -0
- package/build/src/cli/builder-run.js +27 -0
- package/build/src/cli/effective-backlog-settings.js +70 -2
- package/build/src/cli/init.d.ts +34 -0
- package/build/src/cli/init.js +341 -61
- package/build/src/cli/kit.js +55 -12
- package/build/src/cli/pull-work-provider.js +346 -5
- package/build/src/cli/skill-drift-check.d.ts +1 -0
- package/build/src/cli/skill-drift-check.js +165 -0
- package/build/src/cli/telemetry-doctor.d.ts +37 -0
- package/build/src/cli/telemetry-doctor.js +53 -6
- package/build/src/cli/validate-hook-influence.js +37 -7
- package/build/src/cli/workflow-sidecar.d.ts +93 -8
- package/build/src/cli/workflow-sidecar.js +1175 -158
- package/build/src/cli.js +5 -0
- package/build/src/flow-kit/validate.d.ts +54 -34
- package/build/src/flow-kit/validate.js +237 -26
- package/build/src/index.d.ts +2 -0
- package/build/src/index.js +1 -0
- package/build/src/lib/console-connect-options.d.ts +97 -0
- package/build/src/lib/console-connect-options.js +199 -0
- package/build/src/lib/console-telemetry-validate.d.ts +49 -0
- package/build/src/lib/console-telemetry-validate.js +91 -0
- package/build/src/lib/flow-resolver.d.ts +54 -1
- package/build/src/lib/flow-resolver.js +112 -5
- package/build/src/lib/fs.d.ts +17 -0
- package/build/src/lib/fs.js +172 -0
- package/build/src/lib/local-artifact-root.d.ts +44 -1
- package/build/src/lib/local-artifact-root.js +131 -3
- package/build/src/runtime-adapters.d.ts +39 -3
- package/build/src/runtime-adapters.js +77 -31
- package/build/src/tools/build-universal-bundles.js +40 -2
- package/build/src/tools/codex-agent-routing.d.ts +2 -0
- package/build/src/tools/codex-agent-routing.js +49 -0
- package/build/src/tools/generate-context-map.js +1 -0
- package/build/src/tools/validate-source-tree.js +27 -1
- package/context/scripts/hooks/lib/kit-catalog.js +235 -0
- package/context/scripts/hooks/lib/runnable-command.js +177 -0
- package/context/scripts/hooks/stop-goal-fit.js +278 -48
- package/context/scripts/hooks/workflow-steering.js +121 -21
- package/context/scripts/package.json +3 -0
- package/context/scripts/telemetry/install-console-config.sh +25 -4
- package/context/scripts/telemetry/lib/config.sh +102 -12
- package/context/scripts/telemetry/lib/pricing.sh +50 -0
- package/context/scripts/telemetry/lib/session.sh +3 -0
- package/context/scripts/telemetry/lib/transport.sh +87 -0
- package/context/scripts/telemetry/lib/usage.sh +205 -4
- package/context/scripts/telemetry/telemetry.conf +6 -0
- package/context/scripts/telemetry/telemetry.sh +48 -0
- package/context/settings/workspace-backlog-provider-settings.example.json +48 -0
- package/docs/agent-usage-feedback-loop.md +35 -0
- package/docs/architecture-engine-and-kits.md +110 -0
- package/docs/context-map.md +2 -0
- package/docs/decisions/embeddable-engine.md +152 -0
- package/docs/decisions/index.md +3 -1
- package/docs/decisions/trust-ledger-retention.md +88 -0
- package/docs/decisions/workflow-enforcement.md +31 -9
- package/docs/fixture-ownership.md +3 -0
- package/docs/implementing-trust-reconciliation.md +129 -0
- package/docs/index.md +19 -9
- package/docs/integrations/flow-agents-console.md +167 -0
- package/docs/kit-authoring-guide.md +52 -21
- package/docs/spec/builder-flow-runtime.md +80 -0
- package/docs/spec/runtime-hook-surface.md +45 -1
- package/docs/specs/economics-record-contract.md +270 -0
- package/docs/specs/harness-capability-matrix.md +74 -0
- package/docs/specs/learning-review-proposals-contract.md +340 -0
- package/docs/specs/routing-efficiency-review.md +59 -0
- package/docs/verifiable-trust.md +74 -25
- package/docs/workflow-usage-guide.md +10 -0
- package/evals/acceptance/prove-capture-teeth.sh +132 -0
- package/evals/ci/antigaming-suite.sh +1 -0
- package/evals/ci/run-baseline.sh +72 -4
- package/evals/fixtures/economics/acceptance.json +12 -0
- package/evals/fixtures/economics/agents/tool-worker-1/events.jsonl +2 -0
- package/evals/fixtures/economics/agents/tool-worker-2/events.jsonl +2 -0
- package/evals/fixtures/economics/agents/tool-worker-3/events.jsonl +2 -0
- package/evals/fixtures/economics/agents/tool-worker-4/events.jsonl +1 -0
- package/evals/fixtures/economics/agents/tool-worker-5/events.jsonl +2 -0
- package/evals/fixtures/economics/critique.json +22 -0
- package/evals/fixtures/economics/expected-record.json +71 -0
- package/evals/fixtures/economics/session-usage-event.json +1 -0
- package/evals/fixtures/economics/state.json +11 -0
- package/evals/fixtures/economics/transcript.jsonl +3 -0
- package/evals/fixtures/hook-influence/cases.json +7 -7
- package/evals/fixtures/learning-review-proposals/balanced/economics.jsonl +6 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/economics.jsonl +5 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-1/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-2/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-3/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-4/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-5/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/economics.jsonl +6 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.json +30 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.md +66 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-5/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-6/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/repeat-window/economics.jsonl +6 -0
- package/evals/fixtures/learning-review-proposals/under-threshold/economics.jsonl +3 -0
- package/evals/fixtures/telemetry/usage-transcript-sample.jsonl +4 -0
- package/evals/fixtures/trust-reconcile-exploits/mcp-degrade.json +42 -0
- package/evals/integration/test_builder_entry_enforcement.sh +241 -0
- package/evals/integration/test_builder_step_producers.sh +18 -10
- package/evals/integration/test_bundle_install.sh +172 -0
- package/evals/integration/test_console_tenant_isolation.sh +167 -0
- package/evals/integration/test_critique_supersession_roundtrip.sh +4 -1
- package/evals/integration/test_dual_emit_flow_step.sh +10 -4
- package/evals/integration/test_economics_record.sh +674 -0
- package/evals/integration/test_effective_backlog_settings.sh +1 -1
- package/evals/integration/test_evidence_capture_hook.sh +17 -2
- package/evals/integration/test_exemption_usage_review.sh +198 -0
- package/evals/integration/test_fixture_retirement_audit.sh +2 -2
- package/evals/integration/test_flow_kit_install_git.sh +83 -0
- package/evals/integration/test_flowdef_session_activation.sh +0 -1
- package/evals/integration/test_flowdef_session_history_preservation.sh +13 -3
- package/evals/integration/test_gate_lockdown.sh +7 -0
- package/evals/integration/test_gate_review_inquiry_records.sh +9 -1
- package/evals/integration/test_goal_fit_hook.sh +2031 -0
- package/evals/integration/test_hook_category_behaviors.sh +8 -1
- package/evals/integration/test_hook_influence_cases.sh +25 -1
- package/evals/integration/test_install_merge.sh +227 -2
- package/evals/integration/test_kit_conformance_levels.sh +6 -6
- package/evals/integration/test_learning_review_proposals.sh +329 -0
- package/evals/integration/test_liveness_conflict_injection.sh +26 -22
- package/evals/integration/test_liveness_console_relay.sh +166 -0
- package/evals/integration/test_liveness_heartbeat.sh +17 -17
- package/evals/integration/test_liveness_worktree_root.sh +575 -0
- package/evals/integration/test_phase_map_and_gate_claim.sh +6 -1
- package/evals/integration/test_publish_delivery.sh +331 -1
- package/evals/integration/test_pull_work_board.sh +200 -0
- package/evals/integration/test_pull_work_provider.sh +1 -1
- package/evals/integration/test_record_check.sh +378 -0
- package/evals/integration/test_routing_efficiency.sh +71 -0
- package/evals/integration/test_runtime_adapter_activation.sh +28 -0
- package/evals/integration/test_session_resume_roundtrip.sh +16 -19
- package/evals/integration/test_skill_drift_check.sh +870 -0
- package/evals/integration/test_telemetry.sh +445 -0
- package/evals/integration/test_telemetry_doctor.sh +66 -0
- package/evals/integration/test_telemetry_usage_pipeline.sh +228 -0
- package/evals/integration/test_trust_reconcile_negatives.sh +30 -13
- package/evals/integration/test_trust_reconcile_trailer_diagnostic.sh +247 -0
- package/evals/integration/test_usage_cost.sh +61 -0
- package/evals/integration/test_workflow_sidecar_writer.sh +1395 -0
- package/evals/integration/test_workflow_steering_hook.sh +157 -16
- package/evals/integration/test_workspace_settings.sh +176 -0
- package/evals/lib/env.sh +26 -0
- package/evals/lib/node.sh +8 -0
- package/evals/run.sh +29 -0
- package/evals/static/test_ci_integration_coverage.sh +115 -0
- package/evals/static/test_declared_scope_forms_documented.sh +114 -0
- package/evals/static/test_universal_bundles.sh +34 -0
- package/evals/static/test_validate_source_kit_asset_scope.sh +259 -0
- package/evals/static/test_workflow_skills.sh +1 -1
- package/kits/builder/flows/build.flow.json +9 -18
- package/kits/builder/flows/publish-learn.flow.json +5 -1
- package/kits/builder/kit.json +120 -0
- package/kits/builder/skills/deliver/SKILL.md +42 -0
- package/kits/builder/skills/evidence-gate/SKILL.md +12 -0
- package/kits/builder/skills/execute-plan/SKILL.md +9 -0
- package/kits/builder/skills/learning-review/SKILL.md +51 -0
- package/kits/builder/skills/plan-work/SKILL.md +17 -20
- package/kits/builder/skills/pull-work/SKILL.md +21 -0
- package/kits/builder/skills/release-readiness/SKILL.md +12 -0
- package/kits/knowledge/kit.json +9 -0
- package/kits/veritas-governance/docs/README.md +35 -7
- package/kits/veritas-governance/fixtures/exemption-review/mixed-fresh-stale.DECLARED.json +14 -0
- package/kits/veritas-governance/kit.json +14 -0
- package/kits/veritas-governance/skills/exemption-usage-review/SKILL.md +128 -0
- package/kits/veritas-governance/skills/exemption-usage-review/review-exemptions.mjs +231 -0
- package/package.json +2 -2
- package/packaging/manifest.json +29 -0
- package/schemas/backlog-provider-settings.schema.json +13 -0
- package/schemas/workflow-state.schema.json +44 -0
- package/scripts/README.md +4 -0
- package/scripts/check-content-boundary.cjs +8 -1
- package/scripts/ci/trust-reconcile.js +136 -0
- package/scripts/hooks/codex-hook-adapter.js +77 -2
- package/scripts/hooks/evidence-capture.js +38 -5
- package/scripts/hooks/lib/codex-exit-code.js +316 -0
- package/scripts/hooks/lib/kit-catalog.js +235 -0
- package/scripts/hooks/lib/liveness-write.js +28 -1
- package/scripts/hooks/lib/local-artifact-paths.js +97 -1
- package/scripts/hooks/lib/runnable-command.js +177 -0
- package/scripts/hooks/lib/skill-drift.js +350 -0
- package/scripts/hooks/stop-goal-fit.js +278 -48
- package/scripts/hooks/workflow-steering.js +121 -21
- package/scripts/install-codex-home.sh +97 -47
- package/scripts/install-merge.js +72 -14
- package/scripts/install-owned-files.js +178 -0
- package/scripts/liveness/relay.sh +84 -0
- package/scripts/telemetry/economics-record.schema.json +145 -0
- package/scripts/telemetry/economics-record.sh +331 -0
- package/scripts/telemetry/install-console-config.sh +25 -4
- package/scripts/telemetry/learning-review-decide.sh +124 -0
- package/scripts/telemetry/learning-review-proposals.schema.json +161 -0
- package/scripts/telemetry/learning-review-proposals.sh +484 -0
- package/scripts/telemetry/lib/config.sh +102 -12
- package/scripts/telemetry/lib/pricing.sh +14 -6
- package/scripts/telemetry/lib/session.sh +3 -0
- package/scripts/telemetry/lib/transport.sh +133 -15
- package/scripts/telemetry/lib/usage.sh +121 -28
- package/scripts/telemetry/routing-efficiency.sh +0 -0
- package/scripts/telemetry/telemetry.conf +6 -0
- package/scripts/telemetry/telemetry.sh +48 -0
- package/src/builder-flow-run-adapter.ts +357 -0
- package/src/builder-flow-runtime.ts +348 -0
- package/src/cli/builder-flow-run-adapter.test.mjs +495 -0
- package/src/cli/builder-flow-runtime.test.mjs +213 -0
- package/src/cli/builder-run.ts +28 -0
- package/src/cli/codex-agent-routing.test.mjs +44 -0
- package/src/cli/codex-exit-code.test.mjs +207 -0
- package/src/cli/console-connect-options.test.mjs +329 -0
- package/src/cli/console-telemetry-validate.test.mjs +157 -0
- package/src/cli/effective-backlog-settings.ts +68 -2
- package/src/cli/flow-resolver-composition.test.mjs +72 -0
- package/src/cli/init.test.mjs +161 -0
- package/src/cli/init.ts +407 -62
- package/src/cli/kit-metadata-security.test.mjs +443 -0
- package/src/cli/kit.ts +50 -12
- package/src/cli/pull-work-provider.ts +377 -3
- package/src/cli/sidecar-pure-helpers.test.mjs +64 -0
- package/src/cli/skill-drift-check.ts +196 -0
- package/src/cli/telemetry-doctor.test.mjs +53 -0
- package/src/cli/telemetry-doctor.ts +50 -7
- package/src/cli/validate-hook-influence.ts +37 -6
- package/src/cli/workflow-sidecar.ts +1150 -151
- package/src/cli.ts +5 -0
- package/src/flow-kit/validate.ts +277 -38
- package/src/index.ts +19 -0
- package/src/lib/console-connect-options.ts +261 -0
- package/src/lib/console-telemetry-validate.ts +88 -0
- package/src/lib/flow-resolver.ts +117 -4
- package/src/lib/fs.ts +160 -0
- package/src/lib/local-artifact-root.ts +129 -3
- package/src/runtime-adapters.ts +113 -33
- package/src/tools/build-universal-bundles.ts +36 -2
- package/src/tools/codex-agent-routing.ts +48 -0
- package/src/tools/generate-context-map.ts +1 -0
- 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
|
-
|
|
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'))
|
|
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
|
|
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
|
|
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
|
|
302
|
-
|
|
303
|
-
return
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
]
|
|
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
|
|
509
|
-
if (
|
|
510
|
-
hints.push(
|
|
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
|
-
|
|
569
|
-
|
|
668
|
+
looksLikeImplementationWork,
|
|
669
|
+
kitWorkflowSteering,
|
|
570
670
|
};
|
|
@@ -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 [[
|
|
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 "$
|
|
208
|
-
|
|
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 "
|
|
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
|
-
|
|
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
|
-
#
|
|
10
|
-
#
|
|
11
|
-
|
|
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
|
|
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
|
-
#
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|