@oxygen-agent/cli 1.894.0 → 1.917.5
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/README.md +1 -1
- package/dist/command-manifest.js +11 -3
- package/dist/index.js +348 -43
- package/node_modules/@oxygen/formula/dist/expression.d.ts +21 -0
- package/node_modules/@oxygen/formula/dist/expression.js +42 -1
- package/node_modules/@oxygen/formula/dist/formula-functions.d.ts +1 -1
- package/node_modules/@oxygen/formula/dist/formula-functions.js +10 -1
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +30 -7
- package/node_modules/@oxygen/shared/dist/copilot-errors.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/copilot-errors.js +9 -0
- package/node_modules/@oxygen/shared/dist/copilot-plan.d.ts +169 -0
- package/node_modules/@oxygen/shared/dist/copilot-plan.js +476 -0
- package/node_modules/@oxygen/shared/dist/dnc-identities.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/dnc-identities.js +23 -0
- package/node_modules/@oxygen/shared/dist/egress-transport-readiness.d.ts +60 -0
- package/node_modules/@oxygen/shared/dist/egress-transport-readiness.js +67 -0
- package/node_modules/@oxygen/shared/dist/index.d.ts +3 -0
- package/node_modules/@oxygen/shared/dist/index.js +3 -0
- package/node_modules/@oxygen/shared/dist/langfuse.d.ts +93 -5
- package/node_modules/@oxygen/shared/dist/langfuse.js +326 -42
- package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.js +16 -5
- package/node_modules/@oxygen/shared/dist/product-briefing-rules.d.ts +58 -0
- package/node_modules/@oxygen/shared/dist/product-briefing-rules.js +291 -0
- package/node_modules/@oxygen/shared/dist/product-doctrine.d.ts +11 -0
- package/node_modules/@oxygen/shared/dist/product-doctrine.js +70 -0
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +18 -11
- package/node_modules/@oxygen/shared/dist/sequences.js +47 -13
- package/node_modules/@oxygen/shared/dist/user-capability-routing.js +23 -2
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/node_modules/@oxygen/shared/package.json +10 -0
- package/package.json +5 -2
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The briefing contract: every behavioural rule the OXYGEN session briefing has
|
|
3
|
+
* to state, and which half of it owns that rule.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS FILE EXISTS. The briefing is one string an agent reads once per
|
|
6
|
+
* session, so its only failure mode is silent omission: nothing crashes, no
|
|
7
|
+
* type breaks, and the rule simply stops being said. That happened. Splitting
|
|
8
|
+
* the MCP `instructions` into shared doctrine plus an MCP remainder dropped
|
|
9
|
+
* sixteen rules — sender rotation, the webhook/event/wait triggers, "never
|
|
10
|
+
* sends raw provider messages", the Knowledge citation and near-duplicate
|
|
11
|
+
* discipline, `ui://`, Crustdata routing, and more — while every existing
|
|
12
|
+
* assertion stayed green, because those assertions pinned lengths, the derived
|
|
13
|
+
* roster, and a handful of literal phrases, none of which is the rule set.
|
|
14
|
+
*
|
|
15
|
+
* WHY THIS SHAPE. There is no way to derive "this prose still tells an agent to
|
|
16
|
+
* rotate senders" from code, so the rule set has to be written down. What the
|
|
17
|
+
* ledger adds over sixteen inline `toContain` calls is:
|
|
18
|
+
*
|
|
19
|
+
* - Each entry names the behaviour in `rule`, so a future editor deleting a
|
|
20
|
+
* sentence sees what they are deleting rather than an opaque magic string.
|
|
21
|
+
* - `probes` is a disjunction: ANY match satisfies the rule. The briefing lives
|
|
22
|
+
* under a hard character budget, so it gets compressed often; probes key on
|
|
23
|
+
* the distinctive noun ("sender rotation") and accept alternate phrasings, so
|
|
24
|
+
* honest rewording stays green while deletion goes red.
|
|
25
|
+
* - `home` makes the shared/MCP split testable in both directions: a doctrine
|
|
26
|
+
* rule missing from the doctrine is red, an MCP-mechanics rule that leaks into
|
|
27
|
+
* the surface-neutral doctrine is red, and a rule stated in both halves is red
|
|
28
|
+
* (duplication is what the split was meant to end, and it is paid for twice in
|
|
29
|
+
* every session's context).
|
|
30
|
+
*
|
|
31
|
+
* WHAT IT DOES NOT CATCH, stated plainly so nobody over-trusts it:
|
|
32
|
+
*
|
|
33
|
+
* - Truth. A probe proves a phrase is present, not that the surrounding
|
|
34
|
+
* sentence is correct or still says the right thing.
|
|
35
|
+
* - Rules never entered here. New behaviour has to be added to this ledger by
|
|
36
|
+
* hand; the file is only as complete as its last review. `MINIMUM_RULE_COUNT`
|
|
37
|
+
* in the tests is a ratchet against quietly gutting the ledger itself, not a
|
|
38
|
+
* proof of completeness.
|
|
39
|
+
* - Whether an agent obeys any of it at runtime. That is an eval, not a test.
|
|
40
|
+
*/
|
|
41
|
+
/** Which half of the composed briefing must state a rule. */
|
|
42
|
+
export type BriefingHome = "doctrine" | "mcp";
|
|
43
|
+
export interface BriefingRule {
|
|
44
|
+
/** Stable identifier. Rename the prose, never this. */
|
|
45
|
+
id: string;
|
|
46
|
+
/** The behaviour in plain English: what an agent loses if this goes missing. */
|
|
47
|
+
rule: string;
|
|
48
|
+
/**
|
|
49
|
+
* `doctrine` — surface-neutral product truth, equally valid for a Copilot
|
|
50
|
+
* turn, an Agent run, and an MCP session; lives in `product-doctrine.ts`.
|
|
51
|
+
* `mcp` — MCP mechanics or literal `oxygen_*` tool names; lives in the MCP
|
|
52
|
+
* server's remainder, and must stay out of the doctrine.
|
|
53
|
+
*/
|
|
54
|
+
home: BriefingHome;
|
|
55
|
+
/** Accepted phrasings. Any one match satisfies the rule. */
|
|
56
|
+
probes: readonly RegExp[];
|
|
57
|
+
}
|
|
58
|
+
export declare const OXYGEN_BRIEFING_RULES: readonly BriefingRule[];
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The briefing contract: every behavioural rule the OXYGEN session briefing has
|
|
3
|
+
* to state, and which half of it owns that rule.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS FILE EXISTS. The briefing is one string an agent reads once per
|
|
6
|
+
* session, so its only failure mode is silent omission: nothing crashes, no
|
|
7
|
+
* type breaks, and the rule simply stops being said. That happened. Splitting
|
|
8
|
+
* the MCP `instructions` into shared doctrine plus an MCP remainder dropped
|
|
9
|
+
* sixteen rules — sender rotation, the webhook/event/wait triggers, "never
|
|
10
|
+
* sends raw provider messages", the Knowledge citation and near-duplicate
|
|
11
|
+
* discipline, `ui://`, Crustdata routing, and more — while every existing
|
|
12
|
+
* assertion stayed green, because those assertions pinned lengths, the derived
|
|
13
|
+
* roster, and a handful of literal phrases, none of which is the rule set.
|
|
14
|
+
*
|
|
15
|
+
* WHY THIS SHAPE. There is no way to derive "this prose still tells an agent to
|
|
16
|
+
* rotate senders" from code, so the rule set has to be written down. What the
|
|
17
|
+
* ledger adds over sixteen inline `toContain` calls is:
|
|
18
|
+
*
|
|
19
|
+
* - Each entry names the behaviour in `rule`, so a future editor deleting a
|
|
20
|
+
* sentence sees what they are deleting rather than an opaque magic string.
|
|
21
|
+
* - `probes` is a disjunction: ANY match satisfies the rule. The briefing lives
|
|
22
|
+
* under a hard character budget, so it gets compressed often; probes key on
|
|
23
|
+
* the distinctive noun ("sender rotation") and accept alternate phrasings, so
|
|
24
|
+
* honest rewording stays green while deletion goes red.
|
|
25
|
+
* - `home` makes the shared/MCP split testable in both directions: a doctrine
|
|
26
|
+
* rule missing from the doctrine is red, an MCP-mechanics rule that leaks into
|
|
27
|
+
* the surface-neutral doctrine is red, and a rule stated in both halves is red
|
|
28
|
+
* (duplication is what the split was meant to end, and it is paid for twice in
|
|
29
|
+
* every session's context).
|
|
30
|
+
*
|
|
31
|
+
* WHAT IT DOES NOT CATCH, stated plainly so nobody over-trusts it:
|
|
32
|
+
*
|
|
33
|
+
* - Truth. A probe proves a phrase is present, not that the surrounding
|
|
34
|
+
* sentence is correct or still says the right thing.
|
|
35
|
+
* - Rules never entered here. New behaviour has to be added to this ledger by
|
|
36
|
+
* hand; the file is only as complete as its last review. `MINIMUM_RULE_COUNT`
|
|
37
|
+
* in the tests is a ratchet against quietly gutting the ledger itself, not a
|
|
38
|
+
* proof of completeness.
|
|
39
|
+
* - Whether an agent obeys any of it at runtime. That is an eval, not a test.
|
|
40
|
+
*/
|
|
41
|
+
export const OXYGEN_BRIEFING_RULES = [
|
|
42
|
+
// ---- Surface-neutral product doctrine ----------------------------------
|
|
43
|
+
{
|
|
44
|
+
id: "native-primitives-only",
|
|
45
|
+
rule: "Use the hosted primitives; never rebuild one with local scripts, cron, files, or a second engine.",
|
|
46
|
+
home: "doctrine",
|
|
47
|
+
probes: [/never rebuild [^.]*scripts, cron, files/i],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "records-are-canon",
|
|
51
|
+
rule: "Records hold canonical truth; a Table row is a candidate promoted onto Records, never the canon.",
|
|
52
|
+
home: "doctrine",
|
|
53
|
+
probes: [/Tables hold [^.]*promoted onto Records/i, /Records hold canonical truth/i],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: "tables-own-column-work",
|
|
57
|
+
rule: "Tables own typed rows, formulas, AI/tool columns, waterfalls, cell state, and run provenance.",
|
|
58
|
+
home: "doctrine",
|
|
59
|
+
probes: [/waterfalls/i],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "row-dependencies-stay-columns",
|
|
63
|
+
rule: "Simple row dependencies stay chained Table columns instead of becoming a Workflow.",
|
|
64
|
+
home: "doctrine",
|
|
65
|
+
probes: [/row dependencies stay chained Table columns/i, /chained Table columns/i],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "linkedin-routing-by-interaction-state",
|
|
69
|
+
rule: "Interaction state, not recipient count, routes LinkedIn work: net-new is Sequences even for one recipient and one step; an existing thread or a single direct email is Messages.",
|
|
70
|
+
home: "doctrine",
|
|
71
|
+
probes: [/Interaction state, not recipient count/i],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "sequences-own-sender-rotation",
|
|
75
|
+
rule: "Sequences own sender rotation — rotating senders is never hand-rolled outside the primitive.",
|
|
76
|
+
home: "doctrine",
|
|
77
|
+
probes: [/sender rotation/i, /rotating senders/i],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: "sequences-own-suppression",
|
|
81
|
+
rule: "Sequences own suppression.",
|
|
82
|
+
home: "doctrine",
|
|
83
|
+
probes: [/suppression/i],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "sequences-own-reply-stop",
|
|
87
|
+
rule: "Sequences own reply-stop.",
|
|
88
|
+
home: "doctrine",
|
|
89
|
+
probes: [/reply-stop/i],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: "workflows-own-deterministic-orchestration",
|
|
93
|
+
rule: "Deterministic orchestration belongs to hosted Workflows, not to a local runner.",
|
|
94
|
+
home: "doctrine",
|
|
95
|
+
probes: [/belongs to hosted OXYGEN Workflows/i],
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: "workflow-triggers-include-webhook-and-event",
|
|
99
|
+
rule: "Workflow triggers include scheduled/cron, webhook, and event delivery — not only manual calls.",
|
|
100
|
+
home: "doctrine",
|
|
101
|
+
probes: [/webhook, event/i],
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: "workflows-can-wait",
|
|
105
|
+
rule: "Waiting is a Workflow step, so a delay does not justify a local script.",
|
|
106
|
+
home: "doctrine",
|
|
107
|
+
probes: [/branching, waiting/i],
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: "workflow-files-are-authoring-inputs",
|
|
111
|
+
rule: "Local workflow files are authoring inputs only; the hosted definition is the runtime.",
|
|
112
|
+
home: "doctrine",
|
|
113
|
+
probes: [/local files are authoring inputs only/i],
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: "workflow-outreach-enrolls-never-raw-sends",
|
|
117
|
+
rule: "Workflow outreach enrolls into an active, bounded Sequence and never sends raw provider messages.",
|
|
118
|
+
home: "doctrine",
|
|
119
|
+
probes: [/never sends raw provider messages/i, /never a raw provider send/i],
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: "agents-own-adaptive-runs",
|
|
123
|
+
rule: "Adaptive goals, threads, and checkpoints belong to governed Agents, which own first-class runs and may call Workflows as child actions.",
|
|
124
|
+
home: "doctrine",
|
|
125
|
+
probes: [/governed Agents/i],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: "no-competing-store",
|
|
129
|
+
rule: "Neither Workflows nor Agents is a store: a draft is Messages, a note is Records activity.",
|
|
130
|
+
home: "doctrine",
|
|
131
|
+
probes: [/a draft is Messages/i],
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: "messages-stored-once",
|
|
135
|
+
rule: "Messages hold conversation content, stored once across channels.",
|
|
136
|
+
home: "doctrine",
|
|
137
|
+
probes: [/conversation content, stored once/i],
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: "signals-only-record",
|
|
141
|
+
rule: "Signals record that something happened; acting on one is Workflows or Sequences.",
|
|
142
|
+
home: "doctrine",
|
|
143
|
+
probes: [/Signals hold durable typed events/i],
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: "knowledge-cite-slugs",
|
|
147
|
+
rule: "Cite the slugs of the Knowledge pages an answer rests on.",
|
|
148
|
+
home: "doctrine",
|
|
149
|
+
probes: [/cite their slugs/i, /cite the slugs/i, /by slug/i],
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
id: "knowledge-no-near-duplicates",
|
|
153
|
+
rule: "Search before minting a near-duplicate Knowledge page.",
|
|
154
|
+
home: "doctrine",
|
|
155
|
+
probes: [/near-duplicate/i],
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: "knowledge-file-findings-back",
|
|
159
|
+
rule: "File durable findings back into Knowledge instead of leaving them in chat memory.",
|
|
160
|
+
home: "doctrine",
|
|
161
|
+
probes: [/file durable findings/i, /file findings back/i],
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
id: "knowledge-canonical-is-gated",
|
|
165
|
+
rule: "Canonical voice and positioning changes stay proposal-gated.",
|
|
166
|
+
home: "doctrine",
|
|
167
|
+
probes: [/proposal-gated/i],
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: "knowledge-pinned-context-and-off-voice",
|
|
171
|
+
rule: "Pinned canonical context is auto-applied to AI copy; if it is reported missing, say so rather than write off-voice copy.",
|
|
172
|
+
home: "doctrine",
|
|
173
|
+
probes: [/Pinned canonical context/i],
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
id: "knowledge-never-off-voice",
|
|
177
|
+
rule: "Never silently produce off-voice copy when the voice context is missing.",
|
|
178
|
+
home: "doctrine",
|
|
179
|
+
probes: [/off-voice/i],
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
id: "paid-work-needs-preview-scope-ceiling",
|
|
183
|
+
rule: "Paid provider work and external writes need a preview on a small sample, exact scope, approval, and a hard ceiling.",
|
|
184
|
+
home: "doctrine",
|
|
185
|
+
probes: [/preview on a small sample/i],
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
id: "spend-authority-is-not-tool-authority",
|
|
189
|
+
rule: "Never infer spend authority from tool authority.",
|
|
190
|
+
home: "doctrine",
|
|
191
|
+
probes: [/Never infer spend authority from tool authority/i],
|
|
192
|
+
},
|
|
193
|
+
// ---- MCP mechanics and literal tool names -------------------------------
|
|
194
|
+
{
|
|
195
|
+
id: "capability-search-returns-gateways",
|
|
196
|
+
rule: "Capability search returns ownership, boundaries, spend posture, gateways, and ranked tools — read it before guessing.",
|
|
197
|
+
home: "mcp",
|
|
198
|
+
probes: [/gateways/i],
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
id: "session-starts-with-whoami",
|
|
202
|
+
rule: "A session starts at `oxygen_whoami` for user, org, and factual onboarding markers.",
|
|
203
|
+
home: "mcp",
|
|
204
|
+
probes: [/oxygen_whoami/],
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
id: "context-resolve-before-gtm-work",
|
|
208
|
+
rule: "Call `oxygen_context_resolve` before context-dependent GTM work.",
|
|
209
|
+
home: "mcp",
|
|
210
|
+
probes: [/oxygen_context_resolve/],
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
id: "onboarding-never-blocks",
|
|
214
|
+
rule: "The onboarding gate is advisory: if the user declines, continue — never block a concrete ask.",
|
|
215
|
+
home: "mcp",
|
|
216
|
+
probes: [/never block a concrete ask/i],
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
id: "toolset-pack-recovery",
|
|
220
|
+
rule: "On 'No such tool available', reconnect with the returned `?toolset=<toolset_pack>`; never load a full manifest by default.",
|
|
221
|
+
home: "mcp",
|
|
222
|
+
probes: [/\?toolset=<toolset_pack>/],
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
// Both halves of the full-profile rule are load-bearing and they were carried
|
|
226
|
+
// separately in the pre-refactor text: the restraint (not by default) and the
|
|
227
|
+
// sanctioned case (registry-spanning work). Dropping the second half leaves an
|
|
228
|
+
// agent knowing full is discouraged but never knowing when it is earned.
|
|
229
|
+
id: "toolset-full-is-for-registry-spanning-work",
|
|
230
|
+
rule: "`?toolset=full` is reserved for work spanning the registry, never the default profile.",
|
|
231
|
+
home: "mcp",
|
|
232
|
+
probes: [/for registry-spanning work/i],
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
// Distinct from `onboarding-never-blocks`: that one governs the missing-context
|
|
236
|
+
// gate, this one governs the recipe detour. The pre-refactor text stated both and
|
|
237
|
+
// the first refactor collapsed them into one, which is why each is pinned alone.
|
|
238
|
+
id: "route-a-concrete-ask-directly",
|
|
239
|
+
rule: "A concrete ask is routed directly; playbook discovery is never pushed in front of it.",
|
|
240
|
+
home: "mcp",
|
|
241
|
+
probes: [/route a concrete ask directly/i],
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
id: "knowledge-is-index-first",
|
|
245
|
+
rule: "Knowledge work is index-first through `oxygen_knowledge_index`.",
|
|
246
|
+
home: "mcp",
|
|
247
|
+
probes: [/oxygen_knowledge_index/],
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
id: "open-only-relevant-pages",
|
|
251
|
+
rule: "Open only the relevant Knowledge pages rather than pulling the whole wiki into context.",
|
|
252
|
+
home: "mcp",
|
|
253
|
+
probes: [/open(?:ing)? only (?:the )?relevant pages/i],
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
id: "linkedin-public-read-is-cookieless",
|
|
257
|
+
rule: "Public or third-party LinkedIn reads use cookieless `scraper.*`; `linkedin.*` (Unipile) is only for the connected account.",
|
|
258
|
+
home: "mcp",
|
|
259
|
+
probes: [/cookieless `scraper\.\*`/],
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
id: "crustdata-only-for-indexed-datasets",
|
|
263
|
+
rule: "Use Crustdata only for a distinct indexed or longitudinal dataset the native scraper does not provide.",
|
|
264
|
+
home: "mcp",
|
|
265
|
+
probes: [/Crustdata only for an? [^.]*dataset/i],
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
id: "no-silent-costlier-fallback",
|
|
269
|
+
rule: "Never silently fall back to a costlier provider; surface unavailability.",
|
|
270
|
+
home: "mcp",
|
|
271
|
+
probes: [/Never silently fall back to a costlier provider/i],
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
id: "surface-deep-links",
|
|
275
|
+
rule: "Surface the `web_url` deep-links responses return.",
|
|
276
|
+
home: "mcp",
|
|
277
|
+
probes: [/web_url/],
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
id: "surface-inline-widgets",
|
|
281
|
+
rule: "Surface the inline `ui://` widgets responses return.",
|
|
282
|
+
home: "mcp",
|
|
283
|
+
probes: [/ui:\/\//],
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
id: "customer-terminals-use-oxygen",
|
|
287
|
+
rule: "Customer terminals use `oxygen`; `oxygen-dev` is internal unless requested.",
|
|
288
|
+
home: "mcp",
|
|
289
|
+
probes: [/`oxygen-dev` is internal/],
|
|
290
|
+
},
|
|
291
|
+
];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type PrimitiveRouteCard } from "./capability-discovery.js";
|
|
2
|
+
/** The only route-card fields the doctrine reads, so a test can inject a roster. */
|
|
3
|
+
export type DoctrinePrimitive = Pick<PrimitiveRouteCard, "layer" | "primitive">;
|
|
4
|
+
/**
|
|
5
|
+
* Render the doctrine for a roster of primitives. Callers pass nothing; the
|
|
6
|
+
* parameter is the seam that lets a test prove the roster is derived by feeding
|
|
7
|
+
* a grown or shrunk route array and watching the output follow.
|
|
8
|
+
*/
|
|
9
|
+
export declare function renderOxygenProductDoctrine(routes?: readonly DoctrinePrimitive[]): string;
|
|
10
|
+
/** The rendered doctrine for the shipped primitive roster. */
|
|
11
|
+
export declare const OXYGEN_PRODUCT_DOCTRINE: string;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { OXYGEN_PRIMITIVE_ROUTES } from "./capability-discovery.js";
|
|
2
|
+
/**
|
|
3
|
+
* The surface-neutral OXYGEN product briefing: what OXYGEN is, which primitive
|
|
4
|
+
* owns which fact, and what paid work costs in authority.
|
|
5
|
+
*
|
|
6
|
+
* It exists because the briefing used to live only in the MCP server's
|
|
7
|
+
* `instructions` string, so an external Claude Code session was told the five
|
|
8
|
+
* layers and the ownership law while our own Workspace Copilot and Agent runs —
|
|
9
|
+
* the surfaces a non-expert customer actually uses — were told neither. Anything
|
|
10
|
+
* here must be true of EVERY surface: no tool names, no connector/profile
|
|
11
|
+
* mechanics, no host quirks. Those stay with the surface that owns them.
|
|
12
|
+
*
|
|
13
|
+
* The primitive roster is DERIVED from the capability route cards rather than
|
|
14
|
+
* restated. `capability-discovery.ts` already throws at module load if its cards
|
|
15
|
+
* drift from `RECIPE_PRIMITIVES`, so deriving here inherits that check: a
|
|
16
|
+
* sixteenth primitive, or a retired one, cannot ship with the doctrine still
|
|
17
|
+
* describing the old fifteen. A hand-typed roster is exactly the copy that rots
|
|
18
|
+
* silently, which is how it rotted in the first place.
|
|
19
|
+
*
|
|
20
|
+
* Every behavioural rule this text must state is pinned in
|
|
21
|
+
* `product-briefing-rules.ts`. Compress the prose freely; dropping a rule is a
|
|
22
|
+
* product decision that belongs in that ledger, not a side effect of a refactor.
|
|
23
|
+
*/
|
|
24
|
+
/** Rendering order for the stack. Only layers that own a primitive get a line. */
|
|
25
|
+
const LAYER_ORDER = ["Control", "Knowledge", "Data", "Action", "External"];
|
|
26
|
+
/** `knowledge-graph` -> `Knowledge Graph`; the slug is the source, the label is derived. */
|
|
27
|
+
function primitiveLabel(primitive) {
|
|
28
|
+
return primitive
|
|
29
|
+
.split("-")
|
|
30
|
+
.map((word) => (word ? word[0].toUpperCase() + word.slice(1) : word))
|
|
31
|
+
.join(" ");
|
|
32
|
+
}
|
|
33
|
+
function joinLabels(labels) {
|
|
34
|
+
if (labels.length <= 1)
|
|
35
|
+
return labels[0] ?? "";
|
|
36
|
+
if (labels.length === 2)
|
|
37
|
+
return `${labels[0]} and ${labels[1]}`;
|
|
38
|
+
return `${labels.slice(0, -1).join(", ")}, and ${labels[labels.length - 1]}`;
|
|
39
|
+
}
|
|
40
|
+
function rosterLines(routes) {
|
|
41
|
+
return LAYER_ORDER.flatMap((layer) => {
|
|
42
|
+
const labels = routes.filter((card) => card.layer === layer).map((card) => primitiveLabel(card.primitive));
|
|
43
|
+
return labels.length ? [`- ${layer}: ${joinLabels(labels)}.`] : [];
|
|
44
|
+
}).join("\n");
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Render the doctrine for a roster of primitives. Callers pass nothing; the
|
|
48
|
+
* parameter is the seam that lets a test prove the roster is derived by feeding
|
|
49
|
+
* a grown or shrunk route array and watching the output follow.
|
|
50
|
+
*/
|
|
51
|
+
export function renderOxygenProductDoctrine(routes = OXYGEN_PRIMITIVE_ROUTES) {
|
|
52
|
+
return `OXYGEN is hosted revenue infrastructure for B2B startups; OXYGEN OS is its five-layer stack. Control surfaces (CLI, MCP, web, desktop, Copilot) call one contract, own no state; External is the platforms OXYGEN acts on. Between them, the ${routes.length} primitives, each owning its facts:
|
|
53
|
+
|
|
54
|
+
${rosterLines(routes)}
|
|
55
|
+
|
|
56
|
+
Use native primitives so state, approvals, costs, provenance, and runs stay hosted and inspectable; never rebuild one with scripts, cron, files, or a second engine.
|
|
57
|
+
|
|
58
|
+
Ownership law — one owner per fact, no primitive re-implements another:
|
|
59
|
+
|
|
60
|
+
- Records hold canonical truth; Tables hold disposable work promoted onto Records — a row is a candidate, not canon. Tables own typed rows, formulas, AI and tool columns, waterfalls, cell state, and provenance; simple row dependencies stay chained Table columns, not Workflows.
|
|
61
|
+
- Interaction state, not recipient count, owns LinkedIn routing: net-new outreach is Sequences even for one recipient and one step; a reply in an existing thread or one direct email is Messages, holding conversation content, stored once. Cadence, enrollment, sender rotation, suppression, and reply-stop are Sequences.
|
|
62
|
+
- Signals hold durable typed events and record only that something happened; acting on one is Workflows or Sequences; an inbound reply is both, a reaction only a Signal.
|
|
63
|
+
- Deterministic scheduled, cron, webhook, event, branching, waiting, retrying, or approval-gated orchestration belongs to hosted OXYGEN Workflows: schema → lint → apply → call, and local files are authoring inputs only. Adaptive goals, persistent threads, and checkpoints belong to governed Agents, owning first-class runs and calling Workflows as child actions; neither compiles into the other nor is a store — a draft is Messages, a note Records activity, and Workflow outreach enrolls into an active, bounded Sequence and never sends raw provider messages.
|
|
64
|
+
- Posts hold broadcast artifacts and engagement telemetry; Publishing owns approval-gated scheduling, dispatch, retries, and provenance; Ads owns competitor ad intelligence; Tags are the cross-primitive label vocabulary; Dashboards curates native reporting; Observability is the read lens over runs, costs, and failures, never a second engine.
|
|
65
|
+
- The Knowledge Graph describes kinds of customer (ICP, personas, offers, rubrics, voice); Records holds the companies and people they describe. Durable knowledge belongs there, not chat memory: read pages, cite their slugs, search before minting a near-duplicate, file durable findings back. Canonical voice and positioning stay proposal-gated. Pinned canonical context is auto-applied to AI copy; if reported missing, say so rather than write off-voice. Recipes package motions that read it, advisory only.
|
|
66
|
+
|
|
67
|
+
Paid provider work and external writes require a preview on a small sample, exact scope, approval, and a hard credit or action ceiling. Never infer spend authority from tool authority. Surface deep-links, costs, failures, retries, and approval needs, not a bare success; name missing context, never guess.`;
|
|
68
|
+
}
|
|
69
|
+
/** The rendered doctrine for the shipped primitive roster. */
|
|
70
|
+
export const OXYGEN_PRODUCT_DOCTRINE = renderOxygenProductDoctrine();
|
|
@@ -193,20 +193,21 @@ export declare function espFromMxHosts(hosts: readonly string[] | null | undefin
|
|
|
193
193
|
/**
|
|
194
194
|
* row_values keys an enrollment's phone number may live under, in
|
|
195
195
|
* send-precedence order, for WhatsApp sends. The enroll path resolves the FIRST
|
|
196
|
-
* present key to a Unipile WhatsApp attendee id
|
|
196
|
+
* present key to a Unipile WhatsApp attendee id. Mirrors
|
|
197
197
|
* SEQUENCE_EMAIL_COLUMN_KEYS so the WhatsApp send + lookup paths can't drift.
|
|
198
198
|
*/
|
|
199
199
|
export declare const SEQUENCE_PHONE_COLUMN_KEYS: readonly ["phone", "phone_number", "mobile", "mobile_phone", "Phone"];
|
|
200
200
|
/**
|
|
201
|
-
* Normalize a
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
201
|
+
* Normalize a phone-shaped value to the digits Oxygen uses as its stable local
|
|
202
|
+
* WhatsApp identity. A classic WhatsApp phone JID is accepted and reduced to its
|
|
203
|
+
* local digits; a privacy-preserving `@lid` value is not a phone and is rejected.
|
|
204
|
+
*/
|
|
205
|
+
export declare function whatsAppPhoneDigits(phone: string): string | null;
|
|
206
|
+
/**
|
|
207
|
+
* Normalize a raw phone number to the exact recipient identifier Unipile's
|
|
208
|
+
* WhatsApp `chats_create` accepts. A real phone becomes the classic
|
|
209
|
+
* `<digits>@s.whatsapp.net` JID. Already-resolved classic or `@lid` identifiers
|
|
210
|
+
* pass through unchanged so replaying a durable action never double-suffixes it.
|
|
210
211
|
*/
|
|
211
212
|
export declare function whatsAppAttendeeIdForPhone(phone: string): string | null;
|
|
212
213
|
/**
|
|
@@ -216,10 +217,16 @@ export declare function whatsAppAttendeeIdForPhone(phone: string): string | null
|
|
|
216
217
|
* enroll/plan path and any preview count can't drift.
|
|
217
218
|
*/
|
|
218
219
|
export declare function whatsAppAttendeeIdFromRow(rowValues: Record<string, unknown> | null | undefined, phoneColumnKey?: string | null): string | null;
|
|
220
|
+
/**
|
|
221
|
+
* Resolve the stable local phone identity from a lead row without leaking the
|
|
222
|
+
* provider-specific JID into a Table phone column. This deliberately shares the
|
|
223
|
+
* same source-key precedence as WhatsApp dispatch.
|
|
224
|
+
*/
|
|
225
|
+
export declare function whatsAppPhoneDigitsFromRow(rowValues: Record<string, unknown> | null | undefined, phoneColumnKey?: string | null): string | null;
|
|
219
226
|
/**
|
|
220
227
|
* Normalize a raw phone number to strict E.164 (`+` then 2-15 digits, no leading
|
|
221
228
|
* zero) for the CALL channel. Deliberately stricter than
|
|
222
|
-
*
|
|
229
|
+
* whatsAppPhoneDigits, which accepts any 7-15 bare digits: on WhatsApp a
|
|
223
230
|
* bad guess fails an API call, but on the phone it rings a real stranger.
|
|
224
231
|
*
|
|
225
232
|
* Rules — anything ambiguous returns null rather than guessing:
|
|
@@ -457,27 +457,42 @@ export function espFromMxHosts(hosts) {
|
|
|
457
457
|
/**
|
|
458
458
|
* row_values keys an enrollment's phone number may live under, in
|
|
459
459
|
* send-precedence order, for WhatsApp sends. The enroll path resolves the FIRST
|
|
460
|
-
* present key to a Unipile WhatsApp attendee id
|
|
460
|
+
* present key to a Unipile WhatsApp attendee id. Mirrors
|
|
461
461
|
* SEQUENCE_EMAIL_COLUMN_KEYS so the WhatsApp send + lookup paths can't drift.
|
|
462
462
|
*/
|
|
463
463
|
export const SEQUENCE_PHONE_COLUMN_KEYS = ["phone", "phone_number", "mobile", "mobile_phone", "Phone"];
|
|
464
464
|
/**
|
|
465
|
-
* Normalize a
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
* NOTE (R1 — the one unconfirmed Unipile contract): the exact attendee format
|
|
470
|
-
* Unipile wants to open a COLD WhatsApp chat is the single thing to confirm
|
|
471
|
-
* against a live WhatsApp account. If Unipile needs a different shape (a resolved
|
|
472
|
-
* attendee id, or a "<digits>@s.whatsapp.net" jid), THIS function is the only
|
|
473
|
-
* seam to change — every caller routes through it.
|
|
465
|
+
* Normalize a phone-shaped value to the digits Oxygen uses as its stable local
|
|
466
|
+
* WhatsApp identity. A classic WhatsApp phone JID is accepted and reduced to its
|
|
467
|
+
* local digits; a privacy-preserving `@lid` value is not a phone and is rejected.
|
|
474
468
|
*/
|
|
475
|
-
export function
|
|
476
|
-
const
|
|
469
|
+
export function whatsAppPhoneDigits(phone) {
|
|
470
|
+
const trimmed = phone.trim().toLowerCase();
|
|
471
|
+
if (/^\d{7,15}@lid$/u.test(trimmed))
|
|
472
|
+
return null;
|
|
473
|
+
const classicJid = /^(\d{7,15})@s\.whatsapp\.net$/u.exec(trimmed);
|
|
474
|
+
if (classicJid?.[1])
|
|
475
|
+
return classicJid[1];
|
|
476
|
+
if (trimmed.includes("@"))
|
|
477
|
+
return null;
|
|
478
|
+
const digits = trimmed.replace(/[^\d]/g, "");
|
|
477
479
|
if (digits.length < 7 || digits.length > 15)
|
|
478
480
|
return null;
|
|
479
481
|
return digits;
|
|
480
482
|
}
|
|
483
|
+
/**
|
|
484
|
+
* Normalize a raw phone number to the exact recipient identifier Unipile's
|
|
485
|
+
* WhatsApp `chats_create` accepts. A real phone becomes the classic
|
|
486
|
+
* `<digits>@s.whatsapp.net` JID. Already-resolved classic or `@lid` identifiers
|
|
487
|
+
* pass through unchanged so replaying a durable action never double-suffixes it.
|
|
488
|
+
*/
|
|
489
|
+
export function whatsAppAttendeeIdForPhone(phone) {
|
|
490
|
+
const trimmed = phone.trim().toLowerCase();
|
|
491
|
+
if (/^\d{7,15}@(s\.whatsapp\.net|lid)$/u.test(trimmed))
|
|
492
|
+
return trimmed;
|
|
493
|
+
const digits = whatsAppPhoneDigits(trimmed);
|
|
494
|
+
return digits ? `${digits}@s.whatsapp.net` : null;
|
|
495
|
+
}
|
|
481
496
|
/**
|
|
482
497
|
* Resolve a lead's WhatsApp attendee id from its row_values: read the configured
|
|
483
498
|
* phone column (or fall back to SEQUENCE_PHONE_COLUMN_KEYS in precedence order),
|
|
@@ -498,10 +513,29 @@ export function whatsAppAttendeeIdFromRow(rowValues, phoneColumnKey) {
|
|
|
498
513
|
}
|
|
499
514
|
return null;
|
|
500
515
|
}
|
|
516
|
+
/**
|
|
517
|
+
* Resolve the stable local phone identity from a lead row without leaking the
|
|
518
|
+
* provider-specific JID into a Table phone column. This deliberately shares the
|
|
519
|
+
* same source-key precedence as WhatsApp dispatch.
|
|
520
|
+
*/
|
|
521
|
+
export function whatsAppPhoneDigitsFromRow(rowValues, phoneColumnKey) {
|
|
522
|
+
if (!rowValues)
|
|
523
|
+
return null;
|
|
524
|
+
const keys = phoneColumnKey ? [phoneColumnKey, ...SEQUENCE_PHONE_COLUMN_KEYS] : [...SEQUENCE_PHONE_COLUMN_KEYS];
|
|
525
|
+
for (const key of keys) {
|
|
526
|
+
const value = rowValues[key];
|
|
527
|
+
if (typeof value === "string" && value.trim()) {
|
|
528
|
+
const digits = whatsAppPhoneDigits(value);
|
|
529
|
+
if (digits)
|
|
530
|
+
return digits;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
501
535
|
/**
|
|
502
536
|
* Normalize a raw phone number to strict E.164 (`+` then 2-15 digits, no leading
|
|
503
537
|
* zero) for the CALL channel. Deliberately stricter than
|
|
504
|
-
*
|
|
538
|
+
* whatsAppPhoneDigits, which accepts any 7-15 bare digits: on WhatsApp a
|
|
505
539
|
* bad guess fails an API call, but on the phone it rings a real stranger.
|
|
506
540
|
*
|
|
507
541
|
* Rules — anything ambiguous returns null rather than guessing:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getCapabilityRouteMatch, inferCapabilityRoute, } from "./capability-discovery.js";
|
|
1
|
+
import { getCapabilityRoute, getCapabilityRouteMatch, inferCapabilityRoute, } from "./capability-discovery.js";
|
|
2
2
|
// Provider brands are intentionally not duplicated into the static capability
|
|
3
3
|
// catalog. A short "connect <provider>" query would otherwise match Tables via
|
|
4
4
|
// its relationship-oriented "connect" term. Correct that narrow ambiguity at
|
|
@@ -13,9 +13,30 @@ export function inferUserCapabilityRoute(query) {
|
|
|
13
13
|
}
|
|
14
14
|
return getCapabilityRouteMatch("connected-integrations") ?? route;
|
|
15
15
|
}
|
|
16
|
+
// The words that mean "this is Tables work, not a provider connection". Hand-typing
|
|
17
|
+
// them drifted: the list covered table/row/column/dataset/join/relate but not csv,
|
|
18
|
+
// import, enrich, waterfall, formula, lookup or score -- all of which the Tables card
|
|
19
|
+
// itself claims. The measured cost was 11 queries ("connect my csv", "connect my
|
|
20
|
+
// enrichment", "connect my formula") losing their Tables tools and being handed
|
|
21
|
+
// oxygen_integrations_connect instead, on ALL THREE surfaces that call this wrapper.
|
|
22
|
+
//
|
|
23
|
+
// So derive from the card rather than restating it. `connect` is excluded because it
|
|
24
|
+
// is the ambiguity itself -- it is a Tables intent term AND the verb this function
|
|
25
|
+
// keys on, and leaving it in would make the guard reject every query the wrapper
|
|
26
|
+
// exists to correct.
|
|
27
|
+
const TABLES_INTENT_AMBIGUITY_TRIGGERS = new Set(["connect"]);
|
|
28
|
+
function tablesIntentGuard() {
|
|
29
|
+
const terms = (getCapabilityRoute("tables")?.intentTerms ?? [])
|
|
30
|
+
.filter((term) => !TABLES_INTENT_AMBIGUITY_TRIGGERS.has(term))
|
|
31
|
+
.flatMap((term) => (term.includes(" ") ? [term] : [term, `${term}s`]))
|
|
32
|
+
.map((term) => term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
|
33
|
+
.sort((a, b) => b.length - a.length);
|
|
34
|
+
return new RegExp(`\\b(?:${terms.join("|")})\\b`);
|
|
35
|
+
}
|
|
36
|
+
const TABLES_INTENT_GUARD = tablesIntentGuard();
|
|
16
37
|
function isSimpleProviderConnectionIntent(query) {
|
|
17
38
|
const normalized = query.toLowerCase().replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim();
|
|
18
|
-
if (
|
|
39
|
+
if (TABLES_INTENT_GUARD.test(normalized)) {
|
|
19
40
|
return false;
|
|
20
41
|
}
|
|
21
42
|
if (/\b(integration|provider|oauth|byok|api key|connected account)\b/.test(normalized)) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const OXYGEN_VERSION = "1.
|
|
1
|
+
export declare const OXYGEN_VERSION = "1.917.5";
|
|
2
2
|
export declare const OXYGEN_MINIMUM_CLI_VERSION = "1.181.0";
|
|
3
3
|
export declare const MANAGED_INBOX_MINIMUM_CLI_VERSION = "1.326.2";
|
|
4
4
|
export declare const SUPPORT_AGENT_REPLY_MINIMUM_CLI_VERSION = "1.747.0";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const OXYGEN_VERSION = "1.
|
|
1
|
+
export const OXYGEN_VERSION = "1.917.5";
|
|
2
2
|
// The GLOBAL CLI compatibility floor: the oldest CLI allowed to call any
|
|
3
3
|
// operational route. Raising it hard-rejects every older CLI from the entire
|
|
4
4
|
// product, so it obeys one law, enforced by scripts/ci/cli-min-version-gate.mjs:
|
|
@@ -141,6 +141,11 @@
|
|
|
141
141
|
"import": "./dist/pricing-sheet.js",
|
|
142
142
|
"default": "./dist/pricing-sheet.js"
|
|
143
143
|
},
|
|
144
|
+
"./copilot-plan": {
|
|
145
|
+
"types": "./dist/copilot-plan.d.ts",
|
|
146
|
+
"import": "./dist/copilot-plan.js",
|
|
147
|
+
"default": "./dist/copilot-plan.js"
|
|
148
|
+
},
|
|
144
149
|
"./copilot-journeys": {
|
|
145
150
|
"types": "./dist/copilot-journeys.d.ts",
|
|
146
151
|
"import": "./dist/copilot-journeys.js",
|
|
@@ -171,6 +176,11 @@
|
|
|
171
176
|
"import": "./dist/schedule-label.js",
|
|
172
177
|
"default": "./dist/schedule-label.js"
|
|
173
178
|
},
|
|
179
|
+
"./product-briefing-rules": {
|
|
180
|
+
"types": "./dist/product-briefing-rules.d.ts",
|
|
181
|
+
"import": "./dist/product-briefing-rules.js",
|
|
182
|
+
"default": "./dist/product-briefing-rules.js"
|
|
183
|
+
},
|
|
174
184
|
"./publishing-limits": {
|
|
175
185
|
"types": "./dist/publishing-limits.d.ts",
|
|
176
186
|
"import": "./dist/publishing-limits.js",
|