@oxygen-agent/cli 1.750.4 → 1.766.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 (42) hide show
  1. package/README.md +1 -1
  2. package/dist/command-manifest.js +11 -1
  3. package/dist/help.js +8 -0
  4. package/dist/index.js +481 -94
  5. package/node_modules/@oxygen/shared/dist/billing.d.ts +88 -46
  6. package/node_modules/@oxygen/shared/dist/billing.js +134 -74
  7. package/node_modules/@oxygen/shared/dist/capability-discovery.js +12 -4
  8. package/node_modules/@oxygen/shared/dist/cli-result.js +1 -0
  9. package/node_modules/@oxygen/shared/dist/copilot-journeys.d.ts +8 -0
  10. package/node_modules/@oxygen/shared/dist/copilot-journeys.js +23 -5
  11. package/node_modules/@oxygen/shared/dist/future-signup-events.d.ts +13 -2
  12. package/node_modules/@oxygen/shared/dist/future-signup-events.js +17 -2
  13. package/node_modules/@oxygen/shared/dist/future-signup-lifecycle-projection.d.ts +106 -1
  14. package/node_modules/@oxygen/shared/dist/future-signup-lifecycle-projection.js +156 -45
  15. package/node_modules/@oxygen/shared/dist/index.d.ts +4 -0
  16. package/node_modules/@oxygen/shared/dist/index.js +4 -0
  17. package/node_modules/@oxygen/shared/dist/object-storage.d.ts +33 -0
  18. package/node_modules/@oxygen/shared/dist/object-storage.js +69 -4
  19. package/node_modules/@oxygen/shared/dist/person-name.d.ts +40 -0
  20. package/node_modules/@oxygen/shared/dist/person-name.js +23 -0
  21. package/node_modules/@oxygen/shared/dist/plan-capabilities.d.ts +82 -0
  22. package/node_modules/@oxygen/shared/dist/plan-capabilities.js +130 -0
  23. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +2 -2
  24. package/node_modules/@oxygen/shared/dist/plan-limits.js +18 -2
  25. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +50 -56
  26. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +77 -90
  27. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +62 -0
  28. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +91 -0
  29. package/node_modules/@oxygen/shared/dist/provider-funding-errors.d.ts +44 -0
  30. package/node_modules/@oxygen/shared/dist/provider-funding-errors.js +81 -0
  31. package/node_modules/@oxygen/shared/dist/publishing-limits.d.ts +24 -0
  32. package/node_modules/@oxygen/shared/dist/publishing-limits.js +24 -0
  33. package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +27 -6
  34. package/node_modules/@oxygen/shared/dist/spend-safety.js +34 -6
  35. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
  36. package/node_modules/@oxygen/shared/dist/version.js +6 -3
  37. package/node_modules/@oxygen/shared/package.json +10 -0
  38. package/node_modules/@oxygen/workflows/dist/graph/topology.d.ts +27 -0
  39. package/node_modules/@oxygen/workflows/dist/graph/topology.js +95 -0
  40. package/node_modules/@oxygen/workflows/dist/graph/types.d.ts +20 -0
  41. package/node_modules/@oxygen/workflows/dist/graph/types.js +2 -0
  42. package/package.json +1 -1
@@ -0,0 +1,81 @@
1
+ /**
2
+ * One vocabulary for "the provider refused because of money".
3
+ *
4
+ * A provider 402 has reached customers wearing four different costumes:
5
+ * - `provider_credits_exhausted` (tool runner, managed pool)
6
+ * - `connection_credits_exhausted` (tool runner, BYOK connection)
7
+ * - `provider_insufficient_credits` (enrichment column runner + waterfall)
8
+ * - `provider_credit_exhausted` (worker lane exclusion, singular)
9
+ * plus `provider_plan_limit` for the adjacent "your plan does not include this"
10
+ * refusal, which is also a money problem and also not retryable.
11
+ *
12
+ * Only the first two were known to the run-bottleneck classifier. The others
13
+ * fell through to a `/rate|limit|429/` test — which `provider_plan_limit`
14
+ * matches on the substring "limit" — or to the deferral branch, so a hard 402
15
+ * surfaced to the customer as `bottlenecks: [provider_rate_limit]`. That reads
16
+ * as "wait and retry": a partner agency spent hours lowering concurrency against
17
+ * an account that was simply out of credits, and the real 402 was only visible
18
+ * through a different command.
19
+ *
20
+ * The distinction that matters operationally: a rate limit clears by waiting, a
21
+ * funding failure never does. Anything that decides "should the customer wait?"
22
+ * must ask this predicate, not a regex over the code string.
23
+ */
24
+ /** Codes meaning the provider refused for funding/entitlement reasons. Never retryable by waiting. */
25
+ export const PROVIDER_FUNDING_ERROR_CODES = [
26
+ "provider_credits_exhausted",
27
+ "provider_credits_exhausted_deferred",
28
+ "connection_credits_exhausted",
29
+ "provider_insufficient_credits",
30
+ "provider_credit_exhausted",
31
+ "provider_plan_limit",
32
+ "blocked_provider_funding",
33
+ ];
34
+ const FUNDING_CODE_SET = new Set(PROVIDER_FUNDING_ERROR_CODES);
35
+ /**
36
+ * True when the code means "this failed over money", in any of its spellings.
37
+ *
38
+ * Matches on the exact code first, then falls back to a narrow shape test so a
39
+ * provider-specific variant (`peopledatalabs_credits_exhausted`) is still
40
+ * classified as funding rather than silently treated as retryable. Deliberately
41
+ * does NOT match on "limit" alone — `rate_limit_exceeded` is a real rate limit.
42
+ */
43
+ export function isProviderFundingErrorCode(code) {
44
+ if (!code)
45
+ return false;
46
+ const normalized = code.trim().toLowerCase();
47
+ if (!normalized)
48
+ return false;
49
+ if (FUNDING_CODE_SET.has(normalized))
50
+ return true;
51
+ if (/_credits?_exhausted(_deferred)?$/.test(normalized))
52
+ return true;
53
+ if (/insufficient_credits?$/.test(normalized))
54
+ return true;
55
+ if (/^blocked_provider_funding/.test(normalized))
56
+ return true;
57
+ return /(?:payment_required|plan_limit|quota_exceeded)$/.test(normalized);
58
+ }
59
+ /**
60
+ * True for a genuine rate limit — one that clears by waiting.
61
+ *
62
+ * A funding failure always wins: `provider_plan_limit` contains "limit" and
63
+ * would otherwise read as retryable.
64
+ */
65
+ export function isProviderRateLimitErrorCode(code) {
66
+ if (!code)
67
+ return false;
68
+ if (isProviderFundingErrorCode(code))
69
+ return false;
70
+ return /rate|limit|429|capacity_deferred/i.test(code);
71
+ }
72
+ /** The customer-facing next step for a funding refusal, by credential ownership. */
73
+ export function providerFundingNextStep(credentialMode) {
74
+ if (credentialMode === "byok") {
75
+ return "Top up or upgrade the provider account behind your connected key, then retry. Waiting will not clear this.";
76
+ }
77
+ if (credentialMode === "managed") {
78
+ return "This is OXYGEN's managed provider account, not your credit balance — contact support. Waiting will not clear this; route the run to another provider in the meantime.";
79
+ }
80
+ return "Check the provider account's balance and plan entitlement, then retry. Waiting will not clear this.";
81
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Broadcast character caps, in one place.
3
+ *
4
+ * These were defined three times — the web composer, the tenant-db lint, and an
5
+ * implicit `maxLength` on the MCP tool schema — and the MCP copy had drifted to
6
+ * 20,000. An X Premium account may post 25,000 characters, so a post between the
7
+ * two numbers was rejected by OXYGEN's own schema before X ever saw it, with an
8
+ * error that blamed the length rather than our stale constant. A customer hit
9
+ * exactly that after we told them the Premium cap was supported.
10
+ *
11
+ * `PUBLISHING_CONTENT_MAX_CHARS` is the outermost bound any surface should
12
+ * enforce: it must be >= the largest per-channel cap, because per-channel
13
+ * validation is where a too-long post gets an accurate, channel-specific error.
14
+ */
15
+ /** X free tier. */
16
+ export declare const X_FREE_MAX_CHARS = 280;
17
+ /** X Premium. The largest single-post cap across every supported channel. */
18
+ export declare const X_PREMIUM_MAX_CHARS = 25000;
19
+ /**
20
+ * Outermost content bound for any publishing surface (CLI/MCP schema, API).
21
+ * Never set a surface's cap below this — let the per-channel lint produce the
22
+ * specific error instead of a generic schema rejection.
23
+ */
24
+ export declare const PUBLISHING_CONTENT_MAX_CHARS = 25000;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Broadcast character caps, in one place.
3
+ *
4
+ * These were defined three times — the web composer, the tenant-db lint, and an
5
+ * implicit `maxLength` on the MCP tool schema — and the MCP copy had drifted to
6
+ * 20,000. An X Premium account may post 25,000 characters, so a post between the
7
+ * two numbers was rejected by OXYGEN's own schema before X ever saw it, with an
8
+ * error that blamed the length rather than our stale constant. A customer hit
9
+ * exactly that after we told them the Premium cap was supported.
10
+ *
11
+ * `PUBLISHING_CONTENT_MAX_CHARS` is the outermost bound any surface should
12
+ * enforce: it must be >= the largest per-channel cap, because per-channel
13
+ * validation is where a too-long post gets an accurate, channel-specific error.
14
+ */
15
+ /** X free tier. */
16
+ export const X_FREE_MAX_CHARS = 280;
17
+ /** X Premium. The largest single-post cap across every supported channel. */
18
+ export const X_PREMIUM_MAX_CHARS = 25_000;
19
+ /**
20
+ * Outermost content bound for any publishing surface (CLI/MCP schema, API).
21
+ * Never set a surface's cap below this — let the per-channel lint produce the
22
+ * specific error instead of a generic schema rejection.
23
+ */
24
+ export const PUBLISHING_CONTENT_MAX_CHARS = X_PREMIUM_MAX_CHARS;
@@ -54,13 +54,34 @@ export declare function resolveByokProviderDailyCapEnforcementMode(configured?:
54
54
  /**
55
55
  * Implicit org-level DAILY budget guard, evaluated only when the org has no
56
56
  * explicit org-scope daily budget policy and the plan has finite positive
57
- * monthly credits. Warn when one rolling day spends past WARN_PCT of the
58
- * monthly grant; hard-block at BLOCK_PCT (a loop burning a whole month in a
59
- * day is near-certainly runaway — this also protects large top-up balances).
60
- * Setting any explicit org-daily policy fully suppresses the implicit pair.
57
+ * monthly credits. Expressed as a MULTIPLE of the plan's monthly grant spent in
58
+ * one UTC day: warn past WARN_MULTIPLE, hard-block at BLOCK_MULTIPLE. Setting
59
+ * any explicit org-daily policy fully suppresses the implicit pair.
60
+ *
61
+ * This is a runaway-loop backstop, NOT a spend ceiling. The available balance is
62
+ * the real bound on what a workspace can spend, and legitimate single-day work
63
+ * routinely exceeds a month's grant: standing up cold-email infrastructure
64
+ * (a managed domain plus three warmed inboxes is ~31k credits, so a full sending
65
+ * estate is hundreds of thousands in an afternoon) and large enrichment bursts
66
+ * funded by a top-up are both deliberate purchases, not loops. Raised 20x on
67
+ * 2026-08-17 for exactly that reason — at the previous 1x the guard refused a
68
+ * customer who had already paid for the credits, and the refusal was invisible
69
+ * on every surface. 20x still stops an unattended loop within hours.
70
+ */
71
+ export declare const DEFAULT_ORG_DAILY_SPEND_WARN_MULTIPLE = 5;
72
+ export declare const DEFAULT_ORG_DAILY_SPEND_BLOCK_MULTIPLE = 20;
73
+ /** Resolved credit thresholds of the implicit org-daily guard for one plan. */
74
+ export type OrgDailySpendGuard = {
75
+ warnCredits: number;
76
+ blockCredits: number;
77
+ };
78
+ /**
79
+ * Resolve the implicit guard's thresholds from a plan's monthly grant. `null`
80
+ * means the guard is suppressed because the plan has no finite positive grant to
81
+ * scale from (free at 0, enterprise/custom at null) — the single place that
82
+ * decision is made, so enforcement and the read surfaces cannot drift apart.
61
83
  */
62
- export declare const DEFAULT_ORG_DAILY_SPEND_WARN_PCT = 0.25;
63
- export declare const DEFAULT_ORG_DAILY_SPEND_BLOCK_PCT = 1;
84
+ export declare function resolveOrgDailySpendGuard(monthlyCredits: number | null | undefined): OrgDailySpendGuard | null;
64
85
  export declare function resolveDefaultTriggerRunCreditCeiling(tier: PlanTier): number | null;
65
86
  export declare function resolveDefaultAutoRunBatchCreditCeiling(tier: PlanTier): number | null;
66
87
  export declare function resolveDefaultByokColumnRunMaxRows(tier: PlanTier): number | null;
@@ -71,13 +71,41 @@ export function resolveByokProviderDailyCapEnforcementMode(configured = process.
71
71
  /**
72
72
  * Implicit org-level DAILY budget guard, evaluated only when the org has no
73
73
  * explicit org-scope daily budget policy and the plan has finite positive
74
- * monthly credits. Warn when one rolling day spends past WARN_PCT of the
75
- * monthly grant; hard-block at BLOCK_PCT (a loop burning a whole month in a
76
- * day is near-certainly runaway — this also protects large top-up balances).
77
- * Setting any explicit org-daily policy fully suppresses the implicit pair.
74
+ * monthly credits. Expressed as a MULTIPLE of the plan's monthly grant spent in
75
+ * one UTC day: warn past WARN_MULTIPLE, hard-block at BLOCK_MULTIPLE. Setting
76
+ * any explicit org-daily policy fully suppresses the implicit pair.
77
+ *
78
+ * This is a runaway-loop backstop, NOT a spend ceiling. The available balance is
79
+ * the real bound on what a workspace can spend, and legitimate single-day work
80
+ * routinely exceeds a month's grant: standing up cold-email infrastructure
81
+ * (a managed domain plus three warmed inboxes is ~31k credits, so a full sending
82
+ * estate is hundreds of thousands in an afternoon) and large enrichment bursts
83
+ * funded by a top-up are both deliberate purchases, not loops. Raised 20x on
84
+ * 2026-08-17 for exactly that reason — at the previous 1x the guard refused a
85
+ * customer who had already paid for the credits, and the refusal was invisible
86
+ * on every surface. 20x still stops an unattended loop within hours.
78
87
  */
79
- export const DEFAULT_ORG_DAILY_SPEND_WARN_PCT = 0.25;
80
- export const DEFAULT_ORG_DAILY_SPEND_BLOCK_PCT = 1;
88
+ export const DEFAULT_ORG_DAILY_SPEND_WARN_MULTIPLE = 5;
89
+ export const DEFAULT_ORG_DAILY_SPEND_BLOCK_MULTIPLE = 20;
90
+ /**
91
+ * Resolve the implicit guard's thresholds from a plan's monthly grant. `null`
92
+ * means the guard is suppressed because the plan has no finite positive grant to
93
+ * scale from (free at 0, enterprise/custom at null) — the single place that
94
+ * decision is made, so enforcement and the read surfaces cannot drift apart.
95
+ */
96
+ export function resolveOrgDailySpendGuard(monthlyCredits) {
97
+ if (typeof monthlyCredits !== "number" || !Number.isFinite(monthlyCredits) || monthlyCredits <= 0) {
98
+ return null;
99
+ }
100
+ return {
101
+ warnCredits: roundCredits(monthlyCredits * DEFAULT_ORG_DAILY_SPEND_WARN_MULTIPLE),
102
+ blockCredits: roundCredits(monthlyCredits * DEFAULT_ORG_DAILY_SPEND_BLOCK_MULTIPLE),
103
+ };
104
+ }
105
+ /** Credits carry 3 decimals everywhere (control-DB numeric(18,3)). */
106
+ function roundCredits(value) {
107
+ return Math.round(value * 1000) / 1000;
108
+ }
81
109
  export function resolveDefaultTriggerRunCreditCeiling(tier) {
82
110
  return DEFAULT_TRIGGER_RUN_CREDIT_CEILING[tier];
83
111
  }
@@ -1,4 +1,4 @@
1
- export declare const OXYGEN_VERSION = "1.750.4";
1
+ export declare const OXYGEN_VERSION = "1.766.0";
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.750.4";
1
+ export const OXYGEN_VERSION = "1.766.0";
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:
@@ -26,8 +26,11 @@ export const OXYGEN_VERSION = "1.750.4";
26
26
  // (oxygen sequences|inbox|senders, /api/cli/{sequences,inbox,senders}) and
27
27
  // removed the old /api/cli/linkedin/* routes — older CLIs would 404.
28
28
  export const OXYGEN_MINIMUM_CLI_VERSION = "1.181.0";
29
- // Per-surface floor for the whitelabel/managed-inbox purchase path, enforced only
30
- // by /api/cli/managed-inboxes/subscribe.
29
+ // Per-surface floor for the whitelabel/managed-inbox purchase path, enforced by
30
+ // BOTH paid inbox routes: /api/cli/managed-inboxes/subscribe and
31
+ // /api/cli/managed-inboxes/[domain]/mailboxes. (The comment used to say subscribe
32
+ // only; the expansion route has enforced it too since it shipped, and three
33
+ // separate reads of this file have since reasoned from the wrong half.)
31
34
  //
32
35
  // 1.326.2: Pricing Model 2.0 (2026-07-14) moved whitelabel inbox subscribe from the
33
36
  // USD Stripe rail back to Oxygen credits: the preview now returns
@@ -61,6 +61,11 @@
61
61
  "import": "./dist/object-storage.js",
62
62
  "default": "./dist/object-storage.js"
63
63
  },
64
+ "./person-name": {
65
+ "types": "./dist/person-name.d.ts",
66
+ "import": "./dist/person-name.js",
67
+ "default": "./dist/person-name.js"
68
+ },
64
69
  "./select-options": {
65
70
  "types": "./dist/select-options.d.ts",
66
71
  "import": "./dist/select-options.js",
@@ -145,6 +150,11 @@
145
150
  "types": "./dist/schedule-label.d.ts",
146
151
  "import": "./dist/schedule-label.js",
147
152
  "default": "./dist/schedule-label.js"
153
+ },
154
+ "./publishing-limits": {
155
+ "types": "./dist/publishing-limits.d.ts",
156
+ "import": "./dist/publishing-limits.js",
157
+ "default": "./dist/publishing-limits.js"
148
158
  }
149
159
  },
150
160
  "dependencies": {}
@@ -52,6 +52,33 @@ export declare function topologicalOrder(graph: WorkflowGraphManifest): string[]
52
52
  * execution order and the existing topological analysis.
53
53
  */
54
54
  export declare function dominatingNodeIds(graph: WorkflowGraphManifest, targetNodeId: string): Set<string>;
55
+ export type WorkflowGraphNodeTestPlanIssueCode = "workflow_test_target_not_found" | "workflow_test_target_is_trigger" | "workflow_test_target_disabled" | "workflow_test_target_unreachable";
56
+ export type WorkflowGraphNodeTestPlan = {
57
+ targetNodeId: string;
58
+ /** Original manifest order, suitable for a durable receipt and UI highlight. */
59
+ nodeIds: string[];
60
+ /** Transient execution view; the immutable run manifest remains unchanged. */
61
+ manifest: WorkflowGraphManifest;
62
+ };
63
+ export type WorkflowGraphNodeTestPlanResult = {
64
+ ok: true;
65
+ plan: WorkflowGraphNodeTestPlan;
66
+ } | {
67
+ ok: false;
68
+ code: WorkflowGraphNodeTestPlanIssueCode;
69
+ message: string;
70
+ };
71
+ /**
72
+ * Plan a safe selected-node test over the canonical graph.
73
+ *
74
+ * The slice contains the target and every possible predecessor, not every node
75
+ * that happens to appear earlier in topological order. Legal loop back-edges are
76
+ * ignored while finding predecessors so selecting a node halfway through a loop
77
+ * does not accidentally pull later body nodes into the test. Conversely, when a
78
+ * completed loop is upstream of the target (or is itself the target), its whole
79
+ * body is required because the loop's output includes those executions.
80
+ */
81
+ export declare function planWorkflowGraphNodeTest(graph: WorkflowGraphManifest, targetNodeId: string): WorkflowGraphNodeTestPlanResult;
55
82
  /**
56
83
  * Illegal cycles: one entry per strongly connected component that still contains
57
84
  * a cycle once legal loop back-edges are removed.
@@ -282,6 +282,101 @@ export function dominatingNodeIds(graph, targetNodeId) {
282
282
  }
283
283
  return dominators.get(targetNodeId) ?? new Set();
284
284
  }
285
+ /** Add every forward-graph predecessor of `startIds` to `selected`. */
286
+ function addForwardAncestors(startIds, incoming, selected) {
287
+ const queue = [...startIds];
288
+ let changed = false;
289
+ while (queue.length > 0) {
290
+ const current = queue.pop();
291
+ if (current === undefined || selected.has(current))
292
+ continue;
293
+ selected.add(current);
294
+ changed = true;
295
+ for (const predecessor of incoming.get(current) ?? [])
296
+ queue.push(predecessor);
297
+ }
298
+ return changed;
299
+ }
300
+ /**
301
+ * Plan a safe selected-node test over the canonical graph.
302
+ *
303
+ * The slice contains the target and every possible predecessor, not every node
304
+ * that happens to appear earlier in topological order. Legal loop back-edges are
305
+ * ignored while finding predecessors so selecting a node halfway through a loop
306
+ * does not accidentally pull later body nodes into the test. Conversely, when a
307
+ * completed loop is upstream of the target (or is itself the target), its whole
308
+ * body is required because the loop's output includes those executions.
309
+ */
310
+ export function planWorkflowGraphNodeTest(graph, targetNodeId) {
311
+ const nodes = nodeIndex(graph);
312
+ const target = nodes.get(targetNodeId);
313
+ if (!target) {
314
+ return {
315
+ ok: false,
316
+ code: "workflow_test_target_not_found",
317
+ message: `Workflow node '${targetNodeId}' does not exist in this revision.`,
318
+ };
319
+ }
320
+ if (isTriggerNode(target)) {
321
+ return {
322
+ ok: false,
323
+ code: "workflow_test_target_is_trigger",
324
+ message: "Choose an executable workflow step, not the trigger node.",
325
+ };
326
+ }
327
+ if (target.disabled === true) {
328
+ return {
329
+ ok: false,
330
+ code: "workflow_test_target_disabled",
331
+ message: `Workflow node '${targetNodeId}' is disabled and cannot be tested.`,
332
+ };
333
+ }
334
+ if (!reachableNodeIds(graph).has(targetNodeId)) {
335
+ return {
336
+ ok: false,
337
+ code: "workflow_test_target_unreachable",
338
+ message: `Workflow node '${targetNodeId}' is not reachable from the trigger.`,
339
+ };
340
+ }
341
+ const forward = forwardOutgoing(graph);
342
+ const incoming = new Map();
343
+ for (const id of nodes.keys())
344
+ incoming.set(id, []);
345
+ for (const [source, edges] of forward) {
346
+ for (const edge of edges)
347
+ incoming.get(edge.target)?.push(source);
348
+ }
349
+ const selected = new Set();
350
+ addForwardAncestors([targetNodeId], incoming, selected);
351
+ const loopBodies = graph.nodes
352
+ .filter(isLoopNode)
353
+ .map((loop) => ({ loop, body: loopBodyNodeIds(graph, loop.id) }));
354
+ let expanded = true;
355
+ while (expanded) {
356
+ expanded = false;
357
+ for (const { loop, body } of loopBodies) {
358
+ if (!selected.has(loop.id) || body.has(targetNodeId))
359
+ continue;
360
+ if (addForwardAncestors(body, incoming, selected))
361
+ expanded = true;
362
+ }
363
+ }
364
+ const nodeIds = graph.nodes
365
+ .map((node) => node.id)
366
+ .filter((id) => selected.has(id));
367
+ return {
368
+ ok: true,
369
+ plan: {
370
+ targetNodeId,
371
+ nodeIds,
372
+ manifest: {
373
+ ...graph,
374
+ nodes: graph.nodes.filter((node) => selected.has(node.id)),
375
+ edges: graph.edges.filter((edge) => selected.has(edge.source) && selected.has(edge.target)),
376
+ },
377
+ },
378
+ };
379
+ }
285
380
  /**
286
381
  * Illegal cycles: one entry per strongly connected component that still contains
287
382
  * a cycle once legal loop back-edges are removed.
@@ -3,6 +3,26 @@ export declare const WORKFLOW_GRAPH_MANIFEST_VERSION = 2;
3
3
  export declare const WORKFLOW_GRAPH_COMPILER_VERSION = "oxygen-workflows-v2";
4
4
  export declare const PORTABLE_WORKFLOW_DEFINITION_VERSION = 1;
5
5
  export declare const PORTABLE_WORKFLOW_DEFINITION_COMPILER_VERSION = "oxygen-workflow-definition-v1";
6
+ /** Source-run metadata key for the exact non-live graph scope a test executed. */
7
+ export declare const WORKFLOW_TEST_SCOPE_METADATA_KEY = "workflow_test_scope";
8
+ /**
9
+ * Optional execution scope for a non-live Workflow call.
10
+ *
11
+ * Omission keeps the historic whole-workflow behavior. A node scope is a
12
+ * server-planned backward slice of the same immutable graph revision; callers
13
+ * never submit a second graph or a list of nodes to trust.
14
+ */
15
+ export type WorkflowTestScope = {
16
+ type: "workflow";
17
+ } | {
18
+ type: "node";
19
+ node_id: string;
20
+ };
21
+ /** Durable evidence recorded on the source run after the server plans a test. */
22
+ export type WorkflowTestScopeReceipt = WorkflowTestScope & {
23
+ source_hash: string;
24
+ planned_node_ids: string[];
25
+ };
6
26
  export declare const RESERVED_NODE_IDS: ReadonlySet<string>;
7
27
  /**
8
28
  * Named Code inputs and declared output fields become dotted scope paths and
@@ -2,6 +2,8 @@ export const WORKFLOW_GRAPH_MANIFEST_VERSION = 2;
2
2
  export const WORKFLOW_GRAPH_COMPILER_VERSION = "oxygen-workflows-v2";
3
3
  export const PORTABLE_WORKFLOW_DEFINITION_VERSION = 1;
4
4
  export const PORTABLE_WORKFLOW_DEFINITION_COMPILER_VERSION = "oxygen-workflow-definition-v1";
5
+ /** Source-run metadata key for the exact non-live graph scope a test executed. */
6
+ export const WORKFLOW_TEST_SCOPE_METADATA_KEY = "workflow_test_scope";
5
7
  // Node ids become keys on the runtime scope object (`steps.<id>.output`), exactly
6
8
  // as v1 step ids become keys on ctx.steps. The same JavaScript-internals collision
7
9
  // applies: a node named `__proto__` would silently lose its output.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxygen-agent/cli",
3
- "version": "1.750.4",
3
+ "version": "1.766.0",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",