@claude-flow/cli 3.25.5 → 3.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/.claude/.proven-config-version +1 -0
  2. package/.claude/helpers/.helpers-version +1 -1
  3. package/.claude/helpers/helpers.manifest.json +5 -4
  4. package/.claude/helpers/hook-handler.cjs +129 -0
  5. package/.claude/helpers/statusline.cjs +771 -505
  6. package/.claude/proven-config.json +42 -0
  7. package/catalog-manifest.json +12 -0
  8. package/dist/src/commands/advisor.d.ts +15 -0
  9. package/dist/src/commands/advisor.js +94 -0
  10. package/dist/src/commands/doctor.js +151 -32
  11. package/dist/src/commands/funnel.d.ts +13 -0
  12. package/dist/src/commands/funnel.js +103 -0
  13. package/dist/src/commands/hooks.js +112 -46
  14. package/dist/src/commands/index.js +10 -0
  15. package/dist/src/commands/init.js +44 -0
  16. package/dist/src/commands/proxy.d.ts +21 -0
  17. package/dist/src/commands/proxy.js +310 -0
  18. package/dist/src/commands/security.js +29 -0
  19. package/dist/src/commands/settings.d.ts +19 -0
  20. package/dist/src/commands/settings.js +180 -0
  21. package/dist/src/commands/version.d.ts +42 -0
  22. package/dist/src/commands/version.js +106 -0
  23. package/dist/src/funnel/advisor-tip.d.ts +58 -0
  24. package/dist/src/funnel/advisor-tip.js +92 -0
  25. package/dist/src/funnel/attribution.d.ts +37 -0
  26. package/dist/src/funnel/attribution.js +101 -0
  27. package/dist/src/funnel/consent.d.ts +22 -0
  28. package/dist/src/funnel/consent.js +55 -0
  29. package/dist/src/funnel/credit-errors.d.ts +31 -0
  30. package/dist/src/funnel/credit-errors.js +88 -0
  31. package/dist/src/funnel/credit-notifier.d.ts +44 -0
  32. package/dist/src/funnel/credit-notifier.js +74 -0
  33. package/dist/src/funnel/disclosure.d.ts +46 -0
  34. package/dist/src/funnel/disclosure.js +97 -0
  35. package/dist/src/funnel/enrollment.d.ts +36 -0
  36. package/dist/src/funnel/enrollment.js +64 -0
  37. package/dist/src/funnel/environment.d.ts +17 -0
  38. package/dist/src/funnel/environment.js +39 -0
  39. package/dist/src/funnel/event-transport.d.ts +51 -0
  40. package/dist/src/funnel/event-transport.js +199 -0
  41. package/dist/src/funnel/events.d.ts +42 -0
  42. package/dist/src/funnel/events.js +150 -0
  43. package/dist/src/funnel/index.d.ts +20 -0
  44. package/dist/src/funnel/index.js +20 -0
  45. package/dist/src/funnel/insights.d.ts +49 -0
  46. package/dist/src/funnel/insights.js +120 -0
  47. package/dist/src/funnel/local-signals.d.ts +15 -0
  48. package/dist/src/funnel/local-signals.js +68 -0
  49. package/dist/src/funnel/message-transport.d.ts +51 -0
  50. package/dist/src/funnel/message-transport.js +149 -0
  51. package/dist/src/funnel/messages.d.ts +55 -0
  52. package/dist/src/funnel/messages.js +160 -0
  53. package/dist/src/funnel/power-saver-notifier.d.ts +44 -0
  54. package/dist/src/funnel/power-saver-notifier.js +92 -0
  55. package/dist/src/funnel/precedence.d.ts +16 -0
  56. package/dist/src/funnel/precedence.js +85 -0
  57. package/dist/src/funnel/promo.d.ts +41 -0
  58. package/dist/src/funnel/promo.js +144 -0
  59. package/dist/src/funnel/rate-limit-notifier.d.ts +55 -0
  60. package/dist/src/funnel/rate-limit-notifier.js +102 -0
  61. package/dist/src/funnel/rotation.d.ts +19 -0
  62. package/dist/src/funnel/rotation.js +70 -0
  63. package/dist/src/funnel/state.d.ts +13 -0
  64. package/dist/src/funnel/state.js +52 -0
  65. package/dist/src/funnel/toggle-cooldown.d.ts +17 -0
  66. package/dist/src/funnel/toggle-cooldown.js +32 -0
  67. package/dist/src/funnel/types.d.ts +86 -0
  68. package/dist/src/funnel/types.js +26 -0
  69. package/dist/src/init/executor.js +25 -19
  70. package/dist/src/init/helper-refresh.d.ts +33 -2
  71. package/dist/src/init/helper-refresh.js +102 -12
  72. package/dist/src/init/helpers-generator.js +52 -0
  73. package/dist/src/init/mcp-generator.js +11 -6
  74. package/dist/src/init/statusline-generator.js +282 -116
  75. package/dist/src/services/daemon-autostart.js +28 -5
  76. package/dist/src/services/fable-harness.d.ts +39 -0
  77. package/dist/src/services/fable-harness.js +41 -0
  78. package/package.json +16 -4
  79. package/plugins/ruflo-metaharness/scripts/smoke.sh +18 -5
  80. package/dist/src/ruvector/lattice-wasm.d.ts +0 -14
  81. package/dist/src/ruvector/lattice-wasm.js +0 -144
  82. package/plugins/ruflo-metaharness/.claude-flow/data/pending-insights.jsonl +0 -5
  83. package/plugins/ruflo-metaharness/.claude-flow/neural/stats.json +0 -6
@@ -0,0 +1,19 @@
1
+ /**
2
+ * `ruflo settings` — user-facing preferences router (ADR-311 amendment).
3
+ *
4
+ * The name change from `ruflo funnel …` is deliberate copy discipline:
5
+ * "funnel" is internal analytics terminology; it should not appear in the
6
+ * command surface end users see. Every subcommand here forwards to the
7
+ * existing `ruflo funnel` primitives so behavior stays identical — this
8
+ * is a friendlier wrapper, not a re-implementation.
9
+ *
10
+ * Subcommands:
11
+ * ruflo settings Show effective settings + how to change them
12
+ * ruflo settings notices status Show whether the notices row is on
13
+ * ruflo settings notices off Turn off the notices row (persistent)
14
+ * ruflo settings notices on Re-enable the notices row
15
+ * ruflo settings notices id Show the pseudonymous notices id
16
+ */
17
+ import type { Command } from '../types.js';
18
+ export declare const settingsCommand: Command;
19
+ //# sourceMappingURL=settings.d.ts.map
@@ -0,0 +1,180 @@
1
+ /**
2
+ * `ruflo settings` — user-facing preferences router (ADR-311 amendment).
3
+ *
4
+ * The name change from `ruflo funnel …` is deliberate copy discipline:
5
+ * "funnel" is internal analytics terminology; it should not appear in the
6
+ * command surface end users see. Every subcommand here forwards to the
7
+ * existing `ruflo funnel` primitives so behavior stays identical — this
8
+ * is a friendlier wrapper, not a re-implementation.
9
+ *
10
+ * Subcommands:
11
+ * ruflo settings Show effective settings + how to change them
12
+ * ruflo settings notices status Show whether the notices row is on
13
+ * ruflo settings notices off Turn off the notices row (persistent)
14
+ * ruflo settings notices on Re-enable the notices row
15
+ * ruflo settings notices id Show the pseudonymous notices id
16
+ */
17
+ import { output } from '../output.js';
18
+ import { deleteFunnelData, funnelStateDir, getDisclosure, getFunnelId, hasConsent, readConsents, recordDisclosureDeclined, recordDisclosureReenabled, resolveFunnelEnabled, } from '../funnel/index.js';
19
+ import { readStateJson, writeStateJson } from '../funnel/state.js';
20
+ import { clearRateLimitStatus, markRateLimited, rateLimitNotice, readRateLimitStatus, } from '../funnel/rate-limit-notifier.js';
21
+ import { clearQuotaLowStatus, markQuotaLow, quotaLowNotice, readQuotaLowStatus, } from '../funnel/power-saver-notifier.js';
22
+ function setUserConfigEnabled(enabled) {
23
+ const cfg = readStateJson('funnel.json') ?? {};
24
+ cfg.enabled = enabled;
25
+ writeStateJson('funnel.json', cfg);
26
+ }
27
+ const noticesStatusSub = {
28
+ name: 'status',
29
+ description: 'Show whether notices are on and which source decided it',
30
+ action: async () => {
31
+ const decision = resolveFunnelEnabled();
32
+ const disclosure = getDisclosure();
33
+ output.writeln(`Notices: ${decision.enabled ? 'enabled' : 'disabled'} (decided by: ${decision.decidedBy})`);
34
+ output.writeln(`Disclosure: ${disclosure.state}`);
35
+ output.writeln(`Telemetry: ${hasConsent('telemetry') ? 'consent granted' : 'no consent'}`);
36
+ return { success: true, data: { decision, disclosure: disclosure.state } };
37
+ },
38
+ };
39
+ const noticesOffSub = {
40
+ name: 'off',
41
+ description: 'Turn off statusline notices (persistent, user-level)',
42
+ action: async () => {
43
+ setUserConfigEnabled(false);
44
+ recordDisclosureDeclined();
45
+ deleteFunnelData();
46
+ output.printSuccess('Notices disabled. Local notice data deleted.');
47
+ return { success: true };
48
+ },
49
+ };
50
+ const noticesOnSub = {
51
+ name: 'on',
52
+ description: 'Re-enable statusline notices',
53
+ action: async () => {
54
+ setUserConfigEnabled(true);
55
+ recordDisclosureReenabled();
56
+ const decision = resolveFunnelEnabled();
57
+ if (decision.enabled) {
58
+ output.printSuccess('Notices enabled.');
59
+ }
60
+ else {
61
+ output.printWarning(`User preference recorded, but notices stay off (decided by: ${decision.decidedBy})`);
62
+ }
63
+ return { success: true, data: decision };
64
+ },
65
+ };
66
+ const noticesIdSub = {
67
+ name: 'id',
68
+ description: 'Print the pseudonymous notices ID (telemetry consent required)',
69
+ action: async () => {
70
+ const id = getFunnelId();
71
+ output.writeln(id ?? '(no id — telemetry consent not granted, or notices are off)');
72
+ return { success: true, data: { id } };
73
+ },
74
+ };
75
+ const rateLimitedSub = {
76
+ name: 'rate-limited',
77
+ description: 'Manually flag that you have hit a Claude usage limit (ADR-312 Phase 0)',
78
+ options: [
79
+ { name: 'clear', description: 'Clear the flag', type: 'boolean', default: false },
80
+ ],
81
+ action: async (ctx) => {
82
+ if (ctx.flags.clear) {
83
+ const changed = clearRateLimitStatus();
84
+ if (!changed) {
85
+ output.printError('Rate-limit flag was just toggled — try again in a few minutes (ADR-314 anti-abuse cooldown).');
86
+ return { success: false };
87
+ }
88
+ output.printSuccess('Rate-limit flag cleared.');
89
+ return { success: true };
90
+ }
91
+ const changed = markRateLimited();
92
+ if (!changed) {
93
+ output.printError('Rate-limit flag was just toggled — try again in a few minutes (ADR-314 anti-abuse cooldown).');
94
+ return { success: false };
95
+ }
96
+ output.printSuccess('Rate-limit flag set.');
97
+ output.writeln('');
98
+ output.writeln('This is a manual, self-reported flag — ruflo cannot detect Claude\'s');
99
+ output.writeln('usage-limit state automatically today (see ADR-312). While flagged,');
100
+ output.writeln('the notices row may suggest sponsored Cognitum capacity as a bridge');
101
+ output.writeln('until your own limit resets: ruflo proxy sponsor-enable');
102
+ output.writeln('');
103
+ output.writeln('Clear it any time: ruflo settings notices rate-limited --clear');
104
+ const notice = rateLimitNotice();
105
+ return { success: true, data: { notice, status: readRateLimitStatus() } };
106
+ },
107
+ };
108
+ const quotaLowSub = {
109
+ name: 'quota-low',
110
+ description: 'Manually flag that your Claude quota is running low (ADR-314 power saver)',
111
+ options: [
112
+ { name: 'clear', description: 'Clear the flag', type: 'boolean', default: false },
113
+ ],
114
+ action: async (ctx) => {
115
+ if (ctx.flags.clear) {
116
+ const changed = clearQuotaLowStatus();
117
+ if (!changed) {
118
+ output.printError('Quota-low flag was just toggled — try again in a few minutes (ADR-314 anti-abuse cooldown).');
119
+ return { success: false };
120
+ }
121
+ output.printSuccess('Quota-low flag cleared.');
122
+ return { success: true };
123
+ }
124
+ const changed = markQuotaLow();
125
+ if (!changed) {
126
+ output.printError('Quota-low flag was just toggled — try again in a few minutes (ADR-314 anti-abuse cooldown).');
127
+ return { success: false };
128
+ }
129
+ output.printSuccess('Quota-low flag set.');
130
+ output.writeln('');
131
+ output.writeln('This is a manual, self-reported flag — ruflo cannot read your actual');
132
+ output.writeln('quota percentage today (see ADR-312/314). While flagged, and once you');
133
+ output.writeln('enable power saver mode, everyday requests route through Cognitum\'s');
134
+ output.writeln('own difficulty-based router (billed to your own Cognitum account):');
135
+ output.writeln(' ruflo proxy power-saver-enable');
136
+ output.writeln('');
137
+ output.writeln('Clear it any time: ruflo settings notices quota-low --clear');
138
+ const notice = quotaLowNotice();
139
+ return { success: true, data: { notice, status: readQuotaLowStatus() } };
140
+ },
141
+ };
142
+ const noticesCommand = {
143
+ name: 'notices',
144
+ description: 'Control the statusline notices row',
145
+ subcommands: [noticesStatusSub, noticesOffSub, noticesOnSub, noticesIdSub, rateLimitedSub, quotaLowSub],
146
+ action: noticesStatusSub.action,
147
+ };
148
+ const overviewAction = async () => {
149
+ const decision = resolveFunnelEnabled();
150
+ const disclosure = getDisclosure();
151
+ const consents = readConsents();
152
+ output.writeln('ruflo settings — user preferences');
153
+ output.writeln('');
154
+ output.writeln('Notices (statusline tips + product updates)');
155
+ output.writeln(' ruflo settings notices status Show current state');
156
+ output.writeln(' ruflo settings notices off Turn off all notices');
157
+ output.writeln(' ruflo settings notices on Re-enable');
158
+ output.writeln(' ruflo settings notices id Show pseudonymous notices id');
159
+ output.writeln('');
160
+ output.writeln(` current: ${decision.enabled ? 'enabled' : 'disabled'} (${decision.decidedBy})`);
161
+ output.writeln(` disclosure: ${disclosure.state}`);
162
+ const domains = Object.keys(consents);
163
+ if (domains.length) {
164
+ output.writeln(` consents: ${domains.join(', ')}`);
165
+ }
166
+ output.writeln(` state dir: ${funnelStateDir()}`);
167
+ return { success: true, data: { decision, disclosure: disclosure.state, consents } };
168
+ };
169
+ export const settingsCommand = {
170
+ name: 'settings',
171
+ description: 'View and change user preferences (notices, consents)',
172
+ subcommands: [noticesCommand],
173
+ examples: [
174
+ { command: 'ruflo settings', description: 'Overview + current state' },
175
+ { command: 'ruflo settings notices off', description: 'Turn off statusline notices' },
176
+ { command: 'ruflo settings notices status', description: 'Show current notices state' },
177
+ ],
178
+ action: overviewAction,
179
+ };
180
+ //# sourceMappingURL=settings.js.map
@@ -0,0 +1,42 @@
1
+ /**
2
+ * `ruflo version` — ANV (Agent-Native Versioning) Phase 1.
3
+ * https://gist.github.com/ruvnet/0d858ad440a4439b4a2281a40c39b1a0
4
+ *
5
+ * Plain `ruflo --version` / `-V` (index.ts's showVersion()) is UNCHANGED —
6
+ * it stays bare semver so scripts parsing that output never see a surprise
7
+ * suffix. This is a separate subcommand: `ruflo version` prints the same
8
+ * bare semver by default, and `--explain` additionally renders the ANV
9
+ * catalog/benchmark breakdown when a catalog-manifest.json ships with this
10
+ * install. No catalog-manifest.json (e.g. an old install, or a dev checkout
11
+ * that hasn't run the generator) degrades to bare semver — the suffix is
12
+ * advisory, never load-bearing for npm range resolution or CLI behavior.
13
+ */
14
+ import type { Command } from '../types.js';
15
+ interface CatalogManifest {
16
+ schemaVersion: number;
17
+ generation: number;
18
+ generatedAt: string;
19
+ gitSha: string;
20
+ catalog: {
21
+ agents: number;
22
+ tools: number;
23
+ skills: number;
24
+ };
25
+ benchmark: {
26
+ tier: number;
27
+ verifiedAt: string;
28
+ signature?: string;
29
+ } | null;
30
+ }
31
+ /**
32
+ * The advisory suffix itself (semver build metadata, `+` prefix — legal per
33
+ * semver.org §10, ignored by npm for range resolution/precedence).
34
+ * +ad.<release-sequence>.g<gitSha>.cat<generation>[.hal<tier>]
35
+ * `.hal<tier>` is only appended when a real, signed benchmark submission
36
+ * exists for THIS catalog generation — never fabricated (ANV's own
37
+ * "verifiable, no unverifiable claims" principle).
38
+ */
39
+ export declare function buildAdvisorySuffix(manifest: CatalogManifest, releaseSequence?: number): string;
40
+ export declare const versionCommand: Command;
41
+ export {};
42
+ //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1,106 @@
1
+ import { output } from '../output.js';
2
+ import { existsSync, readFileSync } from 'fs';
3
+ import { join, dirname } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { createRequire } from 'module';
6
+ import { getInstalledCliVersion } from '../init/helper-refresh.js';
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ /** Locate catalog-manifest.json next to the installed package root. */
9
+ function findCatalogManifest() {
10
+ const candidates = [];
11
+ try {
12
+ const esmRequire = createRequire(import.meta.url);
13
+ const pkgRoot = dirname(esmRequire.resolve('@claude-flow/cli/package.json'));
14
+ candidates.push(join(pkgRoot, 'catalog-manifest.json'));
15
+ }
16
+ catch { /* not resolvable via package resolution */ }
17
+ // Dev checkout fallback: this compiled file lives at dist/src/commands/version.js
18
+ // (three levels under the package root) when built, or is run via tsx one level
19
+ // shallower from src/commands/version.ts — try both.
20
+ candidates.push(join(__dirname, '..', '..', 'catalog-manifest.json'));
21
+ candidates.push(join(__dirname, '..', '..', '..', 'catalog-manifest.json'));
22
+ for (const c of candidates) {
23
+ if (!existsSync(c))
24
+ continue;
25
+ try {
26
+ const parsed = JSON.parse(readFileSync(c, 'utf-8'));
27
+ if (parsed && typeof parsed.generation === 'number' && parsed.catalog)
28
+ return parsed;
29
+ }
30
+ catch { /* corrupt manifest — treat as absent */ }
31
+ }
32
+ return null;
33
+ }
34
+ /**
35
+ * The advisory suffix itself (semver build metadata, `+` prefix — legal per
36
+ * semver.org §10, ignored by npm for range resolution/precedence).
37
+ * +ad.<release-sequence>.g<gitSha>.cat<generation>[.hal<tier>]
38
+ * `.hal<tier>` is only appended when a real, signed benchmark submission
39
+ * exists for THIS catalog generation — never fabricated (ANV's own
40
+ * "verifiable, no unverifiable claims" principle).
41
+ */
42
+ export function buildAdvisorySuffix(manifest, releaseSequence = 1) {
43
+ const parts = [`ad.${releaseSequence}`, `g${manifest.gitSha}`, `cat${manifest.generation}`];
44
+ if (manifest.benchmark)
45
+ parts.push(`hal${manifest.benchmark.tier}`);
46
+ return `+${parts.join('.')}`;
47
+ }
48
+ export const versionCommand = {
49
+ name: 'version',
50
+ description: 'Show installed version, with --explain for the ANV catalog breakdown',
51
+ options: [
52
+ {
53
+ name: 'explain',
54
+ description: 'Show the full ANV breakdown (catalog generation, counts, benchmark status)',
55
+ type: 'boolean',
56
+ default: false,
57
+ },
58
+ {
59
+ name: 'require-catalog-gte',
60
+ description: 'Exit non-zero unless the installed catalog generation is >= N (capability gating for scripts)',
61
+ type: 'number',
62
+ },
63
+ ],
64
+ examples: [
65
+ { command: 'ruflo version', description: 'Print the installed semver' },
66
+ { command: 'ruflo version --explain', description: 'Print the full ANV catalog/benchmark breakdown' },
67
+ { command: 'ruflo version --require-catalog-gte 40', description: 'Gate a script on a minimum catalog generation' },
68
+ ],
69
+ action: async (ctx) => {
70
+ const version = getInstalledCliVersion();
71
+ const manifest = findCatalogManifest();
72
+ const requireGte = ctx.flags.requireCatalogGte;
73
+ if (typeof requireGte === 'number') {
74
+ const generation = manifest?.generation ?? 0;
75
+ if (generation >= requireGte) {
76
+ output.writeln(`OK (installed catalog is ${generation})`);
77
+ return { success: true };
78
+ }
79
+ output.printError(`Installed catalog generation ${generation} is below required ${requireGte}`);
80
+ return { success: false };
81
+ }
82
+ if (!ctx.flags.explain) {
83
+ output.writeln(`${version}`);
84
+ return { success: true };
85
+ }
86
+ if (!manifest) {
87
+ output.writeln(`Installed: ruflo@${version}`);
88
+ output.writeln(output.dim(' (no catalog-manifest.json — plain semver, pre-ANV or dev checkout)'));
89
+ return { success: true };
90
+ }
91
+ const suffix = buildAdvisorySuffix(manifest);
92
+ output.writeln(`Installed: ${output.bold(`ruflo@${version}${suffix}`)}`);
93
+ output.writeln();
94
+ output.writeln(`Era: AD (Agent Descent) — 1st generation`);
95
+ output.writeln(`Catalog: generation ${manifest.generation} ` +
96
+ `(agents: ${manifest.catalog.agents} types, tools: ${manifest.catalog.tools} MCP, skills: ${manifest.catalog.skills})`);
97
+ if (manifest.benchmark) {
98
+ output.writeln(`Benchmark: GAIA tier ${manifest.benchmark.tier} (verified ${manifest.benchmark.verifiedAt.slice(0, 10)}, signed)`);
99
+ }
100
+ else {
101
+ output.writeln(output.dim('Benchmark: not yet submitted (no verified GAIA/HAL score for this catalog generation)'));
102
+ }
103
+ return { success: true };
104
+ },
105
+ };
106
+ //# sourceMappingURL=version.js.map
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Advisor co-pilot tip — ADR-316. A periodic, consent-gated, budget-capped
3
+ * suggestion generated by ruflo's existing Fable Advisor Harness
4
+ * (services/fable-harness.ts, ADR-172), surfaced in the local insight
5
+ * ticker (insights.ts) alongside the CVE/uncommitted/power-saver signals.
6
+ *
7
+ * This is the ONE insight source that spends real money and makes a real
8
+ * network call — every other source in insights.ts is synchronous, local,
9
+ * $0 by construction. Three hard gates keep that safe:
10
+ * 1. consent: 'advisor-tips' domain (types.ts/consent.ts), never bundled
11
+ * with anything else, off by default.
12
+ * 2. cooldown: at most once per ADVISOR_REFRESH_TTL_MS regardless of how
13
+ * many sessions run in that window — a file-based TTL checked BEFORE
14
+ * spawning, not a per-call budget hope.
15
+ * 3. budget: FableHarness's own --max-budget-usd hard cap, passed through
16
+ * to the underlying `claude -p` call.
17
+ *
18
+ * The snapshot sent to the model is built ONLY from the same structural
19
+ * signals insights.ts already uses (funnel/local-signals.ts) — never raw
20
+ * prompts, commands, or file contents. Matches ADR-309's "no raw content,
21
+ * ever" bar even though ADR-309 is about telemetry and this is a different,
22
+ * separately-consented data flow (an opt-in advisor query, not analytics).
23
+ *
24
+ * Cheapness/latency discipline for the CALLER (never this module itself):
25
+ * refreshAdvisorTipIfStale performs a real network-bound `claude -p` spawn
26
+ * when stale — callers MUST run it from a properly-awaited, DETACHED
27
+ * background process (mirroring the funnel-cache fix's
28
+ * spawnDetachedFunnelRefresh pattern), never inline in a statusline render.
29
+ */
30
+ import { FableHarness, type CoPilotSnapshot } from '../services/fable-harness.js';
31
+ /** At most one real spend per real day, regardless of session count. */
32
+ export declare const ADVISOR_REFRESH_TTL_MS: number;
33
+ /** Conservative default — one adviseCoPilotTip call is a single, unbatched
34
+ * item, so the ~$0.34 clean-cwd anchor (not the $0.02 batched anchor)
35
+ * applies. Override via RUFLO_ADVISOR_MAX_BUDGET_USD for advanced users. */
36
+ export declare const ADVISOR_DEFAULT_BUDGET_USD = 0.4;
37
+ export interface CachedAdvisorTip {
38
+ headline: string;
39
+ detail: string;
40
+ }
41
+ /** Synchronous, $0, no network — safe to call from the insight ticker's hot path. */
42
+ export declare function readAdvisorTip(now?: Date): CachedAdvisorTip | null;
43
+ export interface AdvisorRefreshResult {
44
+ refreshed: boolean;
45
+ reason?: 'not-consented' | 'fresh' | 'no-tip' | 'error';
46
+ }
47
+ /**
48
+ * Refresh the cached advisor tip if consent is granted AND the cache is
49
+ * stale. Spends real money via FableHarness exactly once per call site,
50
+ * gated by the TTL check above — callers get to decide cadence (e.g.
51
+ * "call this on every session-restore"; the TTL makes repeated calls cheap
52
+ * no-ops within the same window).
53
+ */
54
+ export declare function refreshAdvisorTipIfStale(snapshot: CoPilotSnapshot, opts?: {
55
+ now?: Date;
56
+ harness?: FableHarness;
57
+ }): Promise<AdvisorRefreshResult>;
58
+ //# sourceMappingURL=advisor-tip.d.ts.map
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Advisor co-pilot tip — ADR-316. A periodic, consent-gated, budget-capped
3
+ * suggestion generated by ruflo's existing Fable Advisor Harness
4
+ * (services/fable-harness.ts, ADR-172), surfaced in the local insight
5
+ * ticker (insights.ts) alongside the CVE/uncommitted/power-saver signals.
6
+ *
7
+ * This is the ONE insight source that spends real money and makes a real
8
+ * network call — every other source in insights.ts is synchronous, local,
9
+ * $0 by construction. Three hard gates keep that safe:
10
+ * 1. consent: 'advisor-tips' domain (types.ts/consent.ts), never bundled
11
+ * with anything else, off by default.
12
+ * 2. cooldown: at most once per ADVISOR_REFRESH_TTL_MS regardless of how
13
+ * many sessions run in that window — a file-based TTL checked BEFORE
14
+ * spawning, not a per-call budget hope.
15
+ * 3. budget: FableHarness's own --max-budget-usd hard cap, passed through
16
+ * to the underlying `claude -p` call.
17
+ *
18
+ * The snapshot sent to the model is built ONLY from the same structural
19
+ * signals insights.ts already uses (funnel/local-signals.ts) — never raw
20
+ * prompts, commands, or file contents. Matches ADR-309's "no raw content,
21
+ * ever" bar even though ADR-309 is about telemetry and this is a different,
22
+ * separately-consented data flow (an opt-in advisor query, not analytics).
23
+ *
24
+ * Cheapness/latency discipline for the CALLER (never this module itself):
25
+ * refreshAdvisorTipIfStale performs a real network-bound `claude -p` spawn
26
+ * when stale — callers MUST run it from a properly-awaited, DETACHED
27
+ * background process (mirroring the funnel-cache fix's
28
+ * spawnDetachedFunnelRefresh pattern), never inline in a statusline render.
29
+ */
30
+ import { hasConsent } from './consent.js';
31
+ import { readStateJson, writeStateJson } from './state.js';
32
+ import { FableHarness } from '../services/fable-harness.js';
33
+ const ADVISOR_CACHE_FILE = 'advisor-tip.json';
34
+ /** At most one real spend per real day, regardless of session count. */
35
+ export const ADVISOR_REFRESH_TTL_MS = 24 * 60 * 60 * 1000;
36
+ /** Conservative default — one adviseCoPilotTip call is a single, unbatched
37
+ * item, so the ~$0.34 clean-cwd anchor (not the $0.02 batched anchor)
38
+ * applies. Override via RUFLO_ADVISOR_MAX_BUDGET_USD for advanced users. */
39
+ export const ADVISOR_DEFAULT_BUDGET_USD = 0.4;
40
+ /** Synchronous, $0, no network — safe to call from the insight ticker's hot path. */
41
+ export function readAdvisorTip(now = new Date()) {
42
+ const cache = readStateJson(ADVISOR_CACHE_FILE);
43
+ // `!cache._ts` would wrongly treat a legitimate epoch-zero timestamp as
44
+ // absent (0 is falsy) — check the type explicitly instead.
45
+ if (!cache || !cache.headline || typeof cache._ts !== 'number')
46
+ return null;
47
+ if (now.getTime() - cache._ts >= ADVISOR_REFRESH_TTL_MS)
48
+ return null;
49
+ return { headline: cache.headline, detail: cache.detail ?? '' };
50
+ }
51
+ function isCacheStale(now) {
52
+ const cache = readStateJson(ADVISOR_CACHE_FILE);
53
+ if (!cache || typeof cache._ts !== 'number')
54
+ return true;
55
+ return now.getTime() - cache._ts >= ADVISOR_REFRESH_TTL_MS;
56
+ }
57
+ function resolveBudgetUsd() {
58
+ const raw = process.env.RUFLO_ADVISOR_MAX_BUDGET_USD;
59
+ const n = raw !== undefined ? Number(raw) : NaN;
60
+ return Number.isFinite(n) && n > 0 ? n : ADVISOR_DEFAULT_BUDGET_USD;
61
+ }
62
+ /**
63
+ * Refresh the cached advisor tip if consent is granted AND the cache is
64
+ * stale. Spends real money via FableHarness exactly once per call site,
65
+ * gated by the TTL check above — callers get to decide cadence (e.g.
66
+ * "call this on every session-restore"; the TTL makes repeated calls cheap
67
+ * no-ops within the same window).
68
+ */
69
+ export async function refreshAdvisorTipIfStale(snapshot, opts = {}) {
70
+ const now = opts.now ?? new Date();
71
+ if (!hasConsent('advisor-tips'))
72
+ return { refreshed: false, reason: 'not-consented' };
73
+ if (!isCacheStale(now))
74
+ return { refreshed: false, reason: 'fresh' };
75
+ const harness = opts.harness ?? new FableHarness({ maxBudgetUsd: resolveBudgetUsd() });
76
+ try {
77
+ const tip = await harness.adviseCoPilotTip(snapshot);
78
+ if (!tip) {
79
+ // The model looked and found nothing worth surfacing — still stamp
80
+ // _ts so we don't re-spend inside the same TTL window re-asking the
81
+ // same question and getting the same "nothing to say" answer.
82
+ writeStateJson(ADVISOR_CACHE_FILE, { _ts: now.getTime() });
83
+ return { refreshed: true, reason: 'no-tip' };
84
+ }
85
+ writeStateJson(ADVISOR_CACHE_FILE, { _ts: now.getTime(), headline: tip.headline, detail: tip.detail });
86
+ return { refreshed: true };
87
+ }
88
+ catch {
89
+ return { refreshed: false, reason: 'error' };
90
+ }
91
+ }
92
+ //# sourceMappingURL=advisor-tip.js.map
@@ -0,0 +1,37 @@
1
+ export interface AttributionInput {
2
+ /** UTM `medium` — the surface (e.g. 'statusline', 'enrollment', 'exhaustion'). */
3
+ medium: string;
4
+ /** UTM `campaign` — the message kind ('disclosure' | 'promotional' | 'educational'). */
5
+ campaign: string;
6
+ /** UTM `content` — the specific message id ('promo-meta-llm-routing' etc.). */
7
+ content: string;
8
+ /** Static UTM `source` — always 'ruflo' but overridable for tests. */
9
+ source?: string;
10
+ /** Injectable for deterministic tests. Defaults to the live clock. */
11
+ now?: Date;
12
+ }
13
+ /**
14
+ * Return `url` with UTM parameters appended, and — when telemetry consent is
15
+ * granted — a `fid` query parameter carrying the pseudonymous funnel ID.
16
+ * Preserves any query parameters the base URL already carried.
17
+ */
18
+ export declare function attributionUrl(url: string, input: AttributionInput): string;
19
+ /**
20
+ * Wrap the target URL in a server-side click-redirect so the analytics
21
+ * function fires a `promo_open` event + records coarse geo before 302ing to
22
+ * the real destination.
23
+ *
24
+ * Only applied to promotional messages (they have a real URL destination).
25
+ * Disclosure/educational rows call attributionUrl directly.
26
+ *
27
+ * The click endpoint URL structure is:
28
+ * https://funnel.ruv.io/v1/click/{messageId}?to=<utm-decorated-target>
29
+ *
30
+ * The server validates `to` against its own host allowlist, records the
31
+ * event, and 302s. If the click endpoint is unreachable at OSC 8 time,
32
+ * the browser falls back to the terminal's normal error page — but since
33
+ * this is a rare failure path and impressions are already recorded, the
34
+ * loss is bounded.
35
+ */
36
+ export declare function clickTrackedUrl(messageId: string, targetUrl: string, input: AttributionInput): string;
37
+ //# sourceMappingURL=attribution.d.ts.map
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Attribution URL builder — appends UTM parameters and the pseudonymous funnel
3
+ * ID to outbound funnel links so cognitum.one traffic analytics can attribute
4
+ * a landing back to the surface, message, and campaign that produced it
5
+ * (ADR-305 measurement plane).
6
+ *
7
+ * Failure discipline (ADR-301 no-runtime-network invariant, Phase 1):
8
+ * - This function is PURE — it never makes a network call. Attribution is
9
+ * shipped as query params on a link the user's own browser resolves; the
10
+ * CLI has zero runtime dependency on cognitum.one being reachable.
11
+ * If the whole API is down, the OSC 8 label still renders correctly, the
12
+ * click still leaves the terminal, and only the landing page fails (which
13
+ * is a browser-visible problem, not a statusline problem).
14
+ * - If the base URL is malformed, we return it verbatim rather than
15
+ * synthesizing a broken analytics endpoint. The OSC 8 renderer then
16
+ * re-validates the host against its allowlist and, on failure, drops
17
+ * the escape entirely and shows the plain label — never a raw URL.
18
+ *
19
+ * Privacy discipline (ADR-309):
20
+ * - `fid` (the pseudonymous funnel ID) is appended only when telemetry
21
+ * consent is present. Without consent, getFunnelId() returns null and
22
+ * the URL carries only the UTM fields — same landing page, no join key.
23
+ * - The base URL must already be an allowlisted https target; the OSC 8
24
+ * renderer re-validates the host at render time regardless.
25
+ */
26
+ import { getFunnelId } from './events.js';
27
+ /**
28
+ * Server-side click-redirect endpoint. When set, promotional URLs route
29
+ * through here so the server can record a `promo_open` event + coarse
30
+ * geo (from CF-IPCountry / X-Appengine-Country) before 302ing to the
31
+ * real cognitum.one / agentics.org / etc. target.
32
+ */
33
+ const CLICK_ENDPOINT_BASE = process.env.RUFLO_FUNNEL_CLICK_ENDPOINT ?? 'https://funnel.ruv.io/v1/click';
34
+ /**
35
+ * Return `url` with UTM parameters appended, and — when telemetry consent is
36
+ * granted — a `fid` query parameter carrying the pseudonymous funnel ID.
37
+ * Preserves any query parameters the base URL already carried.
38
+ */
39
+ export function attributionUrl(url, input) {
40
+ let parsed;
41
+ try {
42
+ parsed = new URL(url);
43
+ }
44
+ catch {
45
+ return url;
46
+ }
47
+ // Defense in depth: reject non-https schemes at the builder stage. The OSC 8
48
+ // renderer allowlist would drop them at render time too, but decorating a
49
+ // `javascript:` or `data:` URL with a valid-looking `fid` would still be a
50
+ // record we don't want to emit. Fail closed by returning the input verbatim.
51
+ if (parsed.protocol !== 'https:')
52
+ return url;
53
+ const params = parsed.searchParams;
54
+ params.set('utm_source', input.source ?? 'ruflo');
55
+ params.set('utm_medium', input.medium);
56
+ params.set('utm_campaign', input.campaign);
57
+ params.set('utm_content', input.content);
58
+ const fid = getFunnelId(input.now ?? new Date());
59
+ if (fid)
60
+ params.set('fid', fid);
61
+ return parsed.toString();
62
+ }
63
+ /**
64
+ * Wrap the target URL in a server-side click-redirect so the analytics
65
+ * function fires a `promo_open` event + records coarse geo before 302ing to
66
+ * the real destination.
67
+ *
68
+ * Only applied to promotional messages (they have a real URL destination).
69
+ * Disclosure/educational rows call attributionUrl directly.
70
+ *
71
+ * The click endpoint URL structure is:
72
+ * https://funnel.ruv.io/v1/click/{messageId}?to=<utm-decorated-target>
73
+ *
74
+ * The server validates `to` against its own host allowlist, records the
75
+ * event, and 302s. If the click endpoint is unreachable at OSC 8 time,
76
+ * the browser falls back to the terminal's normal error page — but since
77
+ * this is a rare failure path and impressions are already recorded, the
78
+ * loss is bounded.
79
+ */
80
+ export function clickTrackedUrl(messageId, targetUrl, input) {
81
+ const attributed = attributionUrl(targetUrl, input);
82
+ // If attribution rejected the URL (non-https, malformed), pass through
83
+ // verbatim — never smuggle a bad URL into the click endpoint.
84
+ if (attributed === targetUrl) {
85
+ try {
86
+ const p = new URL(targetUrl);
87
+ if (p.protocol !== 'https:')
88
+ return targetUrl;
89
+ }
90
+ catch {
91
+ return targetUrl;
92
+ }
93
+ }
94
+ // Sanitize the message id — allowlist [a-z0-9-] so a malformed id can't
95
+ // shape-shift the click endpoint path.
96
+ if (!/^[a-z0-9][a-z0-9-]{0,63}$/i.test(messageId))
97
+ return attributed;
98
+ const params = new URLSearchParams({ to: attributed });
99
+ return `${CLICK_ENDPOINT_BASE}/${encodeURIComponent(messageId)}?${params.toString()}`;
100
+ }
101
+ //# sourceMappingURL=attribution.js.map
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Consent domains and versioned receipts — ADR-302.
3
+ *
4
+ * Four+ distinct decisions, never bundled: account, proxy-install,
5
+ * telemetry, cloud-routing, hosted-memory. A receipt is written on both
6
+ * grant AND decline (a decline is a decision, not an absence). A stale
7
+ * policyVersion means the consent must be re-asked before the capability
8
+ * activates — never silently carried forward.
9
+ */
10
+ import type { ConsentDomain, ConsentFile, ConsentReceipt } from './types.js';
11
+ export declare const CONSENT_DOMAINS: ConsentDomain[];
12
+ export declare function readConsents(): ConsentFile;
13
+ export declare function getConsent(domain: ConsentDomain): ConsentReceipt;
14
+ /**
15
+ * Effective consent: granted AND at the current policy version. A receipt
16
+ * from an older policy version is treated as not-consented (re-ask, never
17
+ * carry forward).
18
+ */
19
+ export declare function hasConsent(domain: ConsentDomain): boolean;
20
+ export declare function recordConsent(domain: ConsentDomain, granted: boolean, surface: string, now?: Date): ConsentReceipt;
21
+ export declare function revokeConsent(domain: ConsentDomain, surface: string): ConsentReceipt;
22
+ //# sourceMappingURL=consent.d.ts.map