orionfold-relay 0.18.0 → 0.20.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 (35) hide show
  1. package/README.md +7 -1
  2. package/dist/cli.js +188 -19
  3. package/package.json +1 -1
  4. package/src/lib/apps/app-schedule-id.ts +37 -0
  5. package/src/lib/apps/manifest-trigger-dispatch.ts +57 -0
  6. package/src/lib/apps/registry.ts +27 -1
  7. package/src/lib/packs/cli.ts +2 -1
  8. package/src/lib/packs/install.ts +133 -21
  9. package/src/lib/packs/templates/relay-agency-pro/base/blueprints/relay-agency-pro--client-audit-export.yaml +53 -0
  10. package/src/lib/packs/templates/relay-agency-pro/base/blueprints/relay-agency-pro--cre-renewal-engine.yaml +93 -0
  11. package/src/lib/packs/templates/relay-agency-pro/base/blueprints/relay-agency-pro--intake-pipeline.yaml +69 -0
  12. package/src/lib/packs/templates/relay-agency-pro/base/blueprints/relay-agency-pro--month-end-close.yaml +72 -0
  13. package/src/lib/packs/templates/relay-agency-pro/base/blueprints/relay-agency-pro--new-business.yaml +84 -0
  14. package/src/lib/packs/templates/relay-agency-pro/base/manifest.yaml +120 -0
  15. package/src/lib/packs/templates/relay-agency-pro/base/profiles/relay-agency-pro--cre-renewal-analyst/SKILL.md +69 -0
  16. package/src/lib/packs/templates/relay-agency-pro/base/profiles/relay-agency-pro--cre-renewal-analyst/profile.yaml +19 -0
  17. package/src/lib/packs/templates/relay-agency-pro/base/profiles/relay-agency-pro--finance-controller/SKILL.md +32 -0
  18. package/src/lib/packs/templates/relay-agency-pro/base/profiles/relay-agency-pro--finance-controller/profile.yaml +17 -0
  19. package/src/lib/packs/templates/relay-agency-pro/base/profiles/relay-agency-pro--governance-auditor/SKILL.md +28 -0
  20. package/src/lib/packs/templates/relay-agency-pro/base/profiles/relay-agency-pro--governance-auditor/profile.yaml +17 -0
  21. package/src/lib/packs/templates/relay-agency-pro/base/profiles/relay-agency-pro--intake-coordinator/SKILL.md +28 -0
  22. package/src/lib/packs/templates/relay-agency-pro/base/profiles/relay-agency-pro--intake-coordinator/profile.yaml +17 -0
  23. package/src/lib/packs/templates/relay-agency-pro/base/profiles/relay-agency-pro--proposal-writer/SKILL.md +19 -0
  24. package/src/lib/packs/templates/relay-agency-pro/base/profiles/relay-agency-pro--proposal-writer/profile.yaml +17 -0
  25. package/src/lib/packs/templates/relay-agency-pro/base/profiles/relay-agency-pro--prospect-researcher/SKILL.md +20 -0
  26. package/src/lib/packs/templates/relay-agency-pro/base/profiles/relay-agency-pro--prospect-researcher/profile.yaml +19 -0
  27. package/src/lib/packs/templates/relay-agency-pro/base/profiles/relay-agency-pro--sensitive-client-analyst/SKILL.md +24 -0
  28. package/src/lib/packs/templates/relay-agency-pro/base/profiles/relay-agency-pro--sensitive-client-analyst/profile.yaml +21 -0
  29. package/src/lib/packs/templates/relay-agency-pro/pack.yaml +23 -0
  30. package/src/lib/plugins/examples/echo-server/plugin.yaml +1 -1
  31. package/src/lib/plugins/examples/finance-pack/plugin.yaml +1 -1
  32. package/src/lib/plugins/examples/reading-radar/plugin.yaml +1 -1
  33. package/src/lib/plugins/registry.ts +1 -1
  34. package/src/lib/plugins/sdk/types.ts +1 -1
  35. package/src/lib/schedules/scheduler.ts +118 -2
package/README.md CHANGED
@@ -104,10 +104,13 @@ The governance is *in* the workflow, not bolted on. A blueprint is a fixed step
104
104
  ## Why it stays trustworthy
105
105
 
106
106
  - **Local-first** — SQLite database, no cloud dependency, `npx orionfold-relay` and go
107
+ - **Never phones home** — no telemetry, no update checks, no license server; the complete outbound-network inventory is documented and code-linked in [docs/trust/data-flow.md](docs/trust/data-flow.md)
107
108
  - **Your rules, enforced** — tool permissions, inbox approvals, and audit trails for every agent action
108
109
  - **Your AI team** — 21 specialist profiles ready to deploy, each with instructions, tool policies, and runtime tuning
109
110
  - **Know what you spend** — usage metering, budgets, and per-provider/per-model spend visibility on governed runs
110
- - **Open source** — Apache-2.0, read the engine and run it yourself
111
+ - **Open source & verifiable** — Apache-2.0, read the engine and run it yourself; every npm release ships with a provenance attestation and a CycloneDX SBOM ([docs/trust/supply-chain.md](docs/trust/supply-chain.md))
112
+
113
+ Evaluating Relay for an enterprise? The full trust pack — [security packet](docs/trust/security-packet.md), [data-flow disclosure](docs/trust/data-flow.md), [supply-chain verification](docs/trust/supply-chain.md), [plain-language license terms](docs/trust/license-terms.md), and [continuity statement](docs/trust/continuity.md) — lives in [`docs/trust/`](docs/trust/). Vulnerability reports: [SECURITY.md](SECURITY.md).
111
114
 
112
115
  <img src="https://raw.githubusercontent.com/orionfold/relay/main/public/readme/inbox-list.png" alt="The governance command center: tool-permission approvals, agent questions, and a permission queue — nothing reaches a client without sign-off" width="1200" />
113
116
 
@@ -139,6 +142,9 @@ relay license remove <license-id> # forget a license
139
142
  - **What's free stays free.** Capabilities never move from the free engine into a paid
140
143
  pack. Paid packs are new content, not repossessed features.
141
144
 
145
+ The full terms in plain language — seats, transfer, what expiry does and doesn't do —
146
+ are in [docs/trust/license-terms.md](docs/trust/license-terms.md).
147
+
142
148
  ---
143
149
 
144
150
  ## Runtime bridge
package/dist/cli.js CHANGED
@@ -1186,7 +1186,7 @@ var CURRENT_PLUGIN_API_VERSION, CAPABILITY_VALUES, ORIGIN_VALUES, PrimitivesBund
1186
1186
  var init_types = __esm({
1187
1187
  "src/lib/plugins/sdk/types.ts"() {
1188
1188
  "use strict";
1189
- CURRENT_PLUGIN_API_VERSION = "0.17";
1189
+ CURRENT_PLUGIN_API_VERSION = "0.20";
1190
1190
  CAPABILITY_VALUES = ["fs", "net", "child_process", "env"];
1191
1191
  ORIGIN_VALUES = ["ainative-internal", "third-party"];
1192
1192
  PrimitivesBundleManifestSchema = z.object({
@@ -3458,7 +3458,8 @@ async function deleteAppCascade(appId, options = {}) {
3458
3458
  filesRemoved: false,
3459
3459
  projectRemoved: false,
3460
3460
  profilesRemoved: 0,
3461
- blueprintsRemoved: 0
3461
+ blueprintsRemoved: 0,
3462
+ schedulesRemoved: 0
3462
3463
  };
3463
3464
  const resolvedApps = path2.resolve(appsDir);
3464
3465
  const rootDir = path2.resolve(appsDir, appId);
@@ -3478,7 +3479,23 @@ async function deleteAppCascade(appId, options = {}) {
3478
3479
  const filesRemoved = deleteApp(appId, appsDir);
3479
3480
  const profilesRemoved = sweepNamespacedProfiles(profilesDir, appId);
3480
3481
  const blueprintsRemoved = sweepNamespacedBlueprints(blueprintsDir, appId);
3481
- return { projectRemoved, filesRemoved, profilesRemoved, blueprintsRemoved };
3482
+ let schedulesRemoved = 0;
3483
+ try {
3484
+ const { db: db3 } = await Promise.resolve().then(() => (init_db(), db_exports));
3485
+ const { schedules: schedules2 } = await Promise.resolve().then(() => (init_schema(), schema_exports));
3486
+ const { like: like8 } = await import("drizzle-orm");
3487
+ const result = db3.delete(schedules2).where(like8(schedules2.id, `app:${appId}:%`)).run();
3488
+ schedulesRemoved = result.changes;
3489
+ } catch (err2) {
3490
+ console.error(`[registry] schedule sweep failed for app "${appId}":`, err2);
3491
+ }
3492
+ return {
3493
+ projectRemoved,
3494
+ filesRemoved,
3495
+ profilesRemoved,
3496
+ blueprintsRemoved,
3497
+ schedulesRemoved
3498
+ };
3482
3499
  }
3483
3500
  var AppArtifactRefSchema, AppBlueprintRefSchema, KitIdSchema, BindingRefSchema, LeafKpiSourceSchema, RatioKpiSourceSchema, KpiSourceSchema, KpiSpecSchema, ViewSchema, AppTableRefSchema, AppScheduleRefSchema, AppManifestSchema, DOW, APPS_CACHE_TTL_MS, appsCache, appsDetailCache, SLUG_RE;
3484
3501
  var init_registry = __esm({
@@ -12411,6 +12428,14 @@ var init_registry5 = __esm({
12411
12428
  });
12412
12429
 
12413
12430
  // src/lib/schedules/installer.ts
12431
+ var installer_exports = {};
12432
+ __export(installer_exports, {
12433
+ installPluginSchedules: () => installPluginSchedules,
12434
+ installSchedulesFromSpecs: () => installSchedulesFromSpecs,
12435
+ listInstalledPluginScheduleIds: () => listInstalledPluginScheduleIds,
12436
+ removeOrphanSchedules: () => removeOrphanSchedules,
12437
+ removePluginSchedules: () => removePluginSchedules
12438
+ });
12414
12439
  import { and as and16, like as like5, notInArray } from "drizzle-orm";
12415
12440
  function pluginScheduleId(pluginId, scheduleId) {
12416
12441
  return `${PLUGIN_SCHEDULE_PREFIX}${pluginId}:${scheduleId}`;
@@ -12508,6 +12533,10 @@ function removeOrphanSchedules(pluginId, keepIds) {
12508
12533
  db.delete(schedules).where(and16(like5(schedules.id, pattern), notInArray(schedules.id, keepIds))).run();
12509
12534
  }
12510
12535
  }
12536
+ function listInstalledPluginScheduleIds(pluginId) {
12537
+ const pattern = `${PLUGIN_SCHEDULE_PREFIX}${pluginId}:%`;
12538
+ return db.select({ id: schedules.id }).from(schedules).where(like5(schedules.id, pattern)).all().map((r) => r.id);
12539
+ }
12511
12540
  var PLUGIN_SCHEDULE_PREFIX;
12512
12541
  var init_installer = __esm({
12513
12542
  "src/lib/schedules/installer.ts"() {
@@ -12862,7 +12891,7 @@ var init_registry6 = __esm({
12862
12891
  init_registry5();
12863
12892
  init_installer();
12864
12893
  init_schedule_spec();
12865
- SUPPORTED_API_VERSIONS = /* @__PURE__ */ new Set([CURRENT_PLUGIN_API_VERSION, "0.16"]);
12894
+ SUPPORTED_API_VERSIONS = /* @__PURE__ */ new Set([CURRENT_PLUGIN_API_VERSION, "0.19"]);
12866
12895
  pluginCache = null;
12867
12896
  lastLoadedPluginIds = /* @__PURE__ */ new Set();
12868
12897
  PluginTableSchema = z16.object({
@@ -24900,6 +24929,7 @@ var init_engine = __esm({
24900
24929
  var manifest_trigger_dispatch_exports = {};
24901
24930
  __export(manifest_trigger_dispatch_exports, {
24902
24931
  dispatchBlueprintForRow: () => dispatchBlueprintForRow,
24932
+ dispatchScheduledBlueprint: () => dispatchScheduledBlueprint,
24903
24933
  evaluateManifestTriggers: () => evaluateManifestTriggers
24904
24934
  });
24905
24935
  async function evaluateManifestTriggers(tableId, rowId, rowData) {
@@ -24975,6 +25005,41 @@ async function dispatchBlueprintForRow(input) {
24975
25005
  return null;
24976
25006
  }
24977
25007
  }
25008
+ async function dispatchScheduledBlueprint(input) {
25009
+ const { appId, blueprintId, scheduleId } = input;
25010
+ try {
25011
+ const { instantiateBlueprint: instantiateBlueprint2 } = await Promise.resolve().then(() => (init_instantiator(), instantiator_exports));
25012
+ const { executeWorkflow: executeWorkflow2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
25013
+ const { workflowId } = await instantiateBlueprint2(blueprintId, {}, appId);
25014
+ executeWorkflow2(workflowId).catch((err2) => {
25015
+ console.error(
25016
+ `[manifest-trigger-dispatch] executeWorkflow ${workflowId} failed:`,
25017
+ err2
25018
+ );
25019
+ });
25020
+ return { workflowId };
25021
+ } catch (err2) {
25022
+ const message = err2 instanceof Error ? err2.message : String(err2);
25023
+ console.error(
25024
+ `[manifest-trigger-dispatch] scheduled dispatch failed for app=${appId} blueprint=${blueprintId}:`,
25025
+ err2
25026
+ );
25027
+ try {
25028
+ await db.insert(notifications).values({
25029
+ id: crypto.randomUUID(),
25030
+ taskId: null,
25031
+ type: "task_failed",
25032
+ title: `Schedule failure in app "${appId}"`,
25033
+ body: `Blueprint "${blueprintId}" failed for schedule "${scheduleId}": ${message}`,
25034
+ read: false,
25035
+ createdAt: /* @__PURE__ */ new Date()
25036
+ });
25037
+ } catch (nerr) {
25038
+ console.error(`[manifest-trigger-dispatch] notification write failed:`, nerr);
25039
+ }
25040
+ return null;
25041
+ }
25042
+ }
24978
25043
  function findMatchingSubscriptions(apps, tableId) {
24979
25044
  const out = [];
24980
25045
  for (const app of apps) {
@@ -25517,6 +25582,35 @@ var init_tables = __esm({
25517
25582
  }
25518
25583
  });
25519
25584
 
25585
+ // src/lib/apps/app-schedule-id.ts
25586
+ var app_schedule_id_exports = {};
25587
+ __export(app_schedule_id_exports, {
25588
+ APP_SCHEDULE_PREFIX: () => APP_SCHEDULE_PREFIX,
25589
+ appScheduleId: () => appScheduleId,
25590
+ isAppScheduleId: () => isAppScheduleId,
25591
+ parseAppScheduleId: () => parseAppScheduleId
25592
+ });
25593
+ function appScheduleId(appId, scheduleId) {
25594
+ return `${APP_SCHEDULE_PREFIX}${appId}:${scheduleId}`;
25595
+ }
25596
+ function isAppScheduleId(id) {
25597
+ return id.startsWith(APP_SCHEDULE_PREFIX);
25598
+ }
25599
+ function parseAppScheduleId(id) {
25600
+ if (!isAppScheduleId(id)) return null;
25601
+ const rest = id.slice(APP_SCHEDULE_PREFIX.length);
25602
+ const sep = rest.indexOf(":");
25603
+ if (sep <= 0 || sep === rest.length - 1) return null;
25604
+ return { appId: rest.slice(0, sep), scheduleId: rest.slice(sep + 1) };
25605
+ }
25606
+ var APP_SCHEDULE_PREFIX;
25607
+ var init_app_schedule_id = __esm({
25608
+ "src/lib/apps/app-schedule-id.ts"() {
25609
+ "use strict";
25610
+ APP_SCHEDULE_PREFIX = "app:";
25611
+ }
25612
+ });
25613
+
25520
25614
  // src/lib/packs/install.ts
25521
25615
  var install_exports = {};
25522
25616
  __export(install_exports, {
@@ -25529,8 +25623,8 @@ import { execFileSync as execFileSync3 } from "child_process";
25529
25623
  import yaml12 from "js-yaml";
25530
25624
  import semver from "semver";
25531
25625
  function relayCoreVersion() {
25532
- if (semver.valid("0.18.0")) {
25533
- return "0.18.0";
25626
+ if (semver.valid("0.20.0")) {
25627
+ return "0.20.0";
25534
25628
  }
25535
25629
  try {
25536
25630
  const root = getAppRoot(import.meta.dirname, 3);
@@ -25577,6 +25671,26 @@ async function installPack(source, options = {}) {
25577
25671
  }
25578
25672
  }
25579
25673
  const resolved = resolvePackLayer(pack);
25674
+ const declaredBlueprints = new Set(
25675
+ pack.manifest.blueprints.map((bp) => bp.id)
25676
+ );
25677
+ for (const sched of pack.manifest.schedules) {
25678
+ if (!sched.cron) {
25679
+ throw new PackValidationError(
25680
+ `Manifest schedule "${sched.id}" has no cron expression.`
25681
+ );
25682
+ }
25683
+ if (!sched.runs) {
25684
+ throw new PackValidationError(
25685
+ `Manifest schedule "${sched.id}" has no "runs" blueprint.`
25686
+ );
25687
+ }
25688
+ if (!declaredBlueprints.has(sched.runs)) {
25689
+ throw new PackValidationError(
25690
+ `Manifest schedule "${sched.id}" runs blueprint "${sched.runs}", which the manifest does not declare.`
25691
+ );
25692
+ }
25693
+ }
25580
25694
  const { ensureAppProject: ensureAppProject2 } = await Promise.resolve().then(() => (init_compose_integration(), compose_integration_exports));
25581
25695
  const { ensureCustomer: ensureCustomer2 } = await Promise.resolve().then(() => (init_customers(), customers_exports));
25582
25696
  const { createTable: createTable2, addRows: addRows2, listTables: listTables2 } = await Promise.resolve().then(() => (init_tables(), tables_exports));
@@ -25627,7 +25741,44 @@ async function installPack(source, options = {}) {
25627
25741
  });
25628
25742
  customersSeeded += 1;
25629
25743
  }
25630
- const droppedManifest = rewriteTableRefs(pack.manifest, logicalToReal);
25744
+ const scheduleLogicalToReal = /* @__PURE__ */ new Map();
25745
+ let schedulesRegistered = 0;
25746
+ if (pack.manifest.schedules.length > 0) {
25747
+ const { appScheduleId: appScheduleId2 } = await Promise.resolve().then(() => (init_app_schedule_id(), app_schedule_id_exports));
25748
+ const { installSchedulesFromSpecs: installSchedulesFromSpecs2 } = await Promise.resolve().then(() => (init_installer(), installer_exports));
25749
+ const specs = pack.manifest.schedules.map((sched) => {
25750
+ const compositeId = appScheduleId2(pack.meta.id, sched.id);
25751
+ scheduleLogicalToReal.set(sched.id, compositeId);
25752
+ const name = typeof sched.name === "string" ? sched.name : titleCase3(sched.id);
25753
+ return {
25754
+ id: compositeId,
25755
+ name: `${name} (${pack.meta.id})`,
25756
+ version: pack.meta.version,
25757
+ // Display-only: the scheduler branches on the app: id prefix and
25758
+ // dispatches the blueprint; this prompt is never sent to an agent.
25759
+ prompt: `App schedule for "${pack.meta.id}" \u2014 runs blueprint "${sched.runs}".`,
25760
+ cronExpression: sched.cron,
25761
+ recurs: true,
25762
+ type: "scheduled"
25763
+ };
25764
+ });
25765
+ installSchedulesFromSpecs2(specs);
25766
+ schedulesRegistered = specs.length;
25767
+ const { db: db3 } = await Promise.resolve().then(() => (init_db(), db_exports));
25768
+ const { schedules: schedulesTable } = await Promise.resolve().then(() => (init_schema(), schema_exports));
25769
+ const { and: and24, inArray: inArray7, isNull: isNull7 } = await import("drizzle-orm");
25770
+ db3.update(schedulesTable).set({ projectId: pack.meta.id }).where(
25771
+ and24(
25772
+ inArray7(schedulesTable.id, specs.map((s) => s.id)),
25773
+ isNull7(schedulesTable.projectId)
25774
+ )
25775
+ ).run();
25776
+ }
25777
+ const droppedManifest = rewriteTableRefs(
25778
+ pack.manifest,
25779
+ logicalToReal,
25780
+ scheduleLogicalToReal
25781
+ );
25631
25782
  if (pack.meta.entitlement) {
25632
25783
  droppedManifest.entitlement = pack.meta.entitlement;
25633
25784
  }
@@ -25637,6 +25788,10 @@ async function installPack(source, options = {}) {
25637
25788
  profilesDir,
25638
25789
  blueprintsDir
25639
25790
  );
25791
+ if (blueprintsDropped > 0) {
25792
+ const { reloadBlueprints: reloadBlueprints2 } = await Promise.resolve().then(() => (init_registry3(), registry_exports3));
25793
+ reloadBlueprints2();
25794
+ }
25640
25795
  return {
25641
25796
  packId: pack.meta.id,
25642
25797
  packVersion: pack.meta.version,
@@ -25645,7 +25800,8 @@ async function installPack(source, options = {}) {
25645
25800
  customersSeeded,
25646
25801
  profilesDropped,
25647
25802
  blueprintsDropped,
25648
- rowsSeeded
25803
+ rowsSeeded,
25804
+ schedulesRegistered
25649
25805
  };
25650
25806
  } finally {
25651
25807
  cleanup();
@@ -25706,33 +25862,46 @@ function readTableSeed(resolved, logicalId) {
25706
25862
  }
25707
25863
  return [];
25708
25864
  }
25709
- function rewriteTableRefs(manifest, logicalToReal) {
25865
+ function rewriteTableRefs(manifest, logicalToReal, scheduleLogicalToReal = /* @__PURE__ */ new Map()) {
25710
25866
  const rewritten = {
25711
25867
  ...manifest,
25712
25868
  tables: manifest.tables.map((t) => {
25713
25869
  const real2 = logicalToReal.get(t.id);
25714
25870
  return real2 ? { ...t, id: real2 } : t;
25871
+ }),
25872
+ // Row-insert triggers bind to tables by the SAME logical id. Dispatch
25873
+ // (manifest-trigger-dispatch) matches trigger.table against the REAL
25874
+ // UUID, so an unrewritten ref silently never fires.
25875
+ blueprints: manifest.blueprints.map((bp) => {
25876
+ const triggerTable = bp.trigger?.table;
25877
+ if (!triggerTable) return bp;
25878
+ const real2 = logicalToReal.get(triggerTable);
25879
+ return real2 ? { ...bp, trigger: { ...bp.trigger, table: real2 } } : bp;
25880
+ }),
25881
+ schedules: manifest.schedules.map((s) => {
25882
+ const real2 = scheduleLogicalToReal.get(s.id);
25883
+ return real2 ? { ...s, id: real2 } : s;
25715
25884
  })
25716
25885
  };
25717
25886
  if (rewritten.view) {
25718
- rewritten.view = rewriteViewTableRefs(
25719
- rewritten.view,
25720
- logicalToReal
25721
- );
25887
+ rewritten.view = rewriteViewRefs(rewritten.view, {
25888
+ table: logicalToReal,
25889
+ schedule: scheduleLogicalToReal
25890
+ });
25722
25891
  }
25723
25892
  return rewritten;
25724
25893
  }
25725
- function rewriteViewTableRefs(view, logicalToReal) {
25894
+ function rewriteViewRefs(view, maps) {
25726
25895
  if (Array.isArray(view)) {
25727
- return view.map((v) => rewriteViewTableRefs(v, logicalToReal));
25896
+ return view.map((v) => rewriteViewRefs(v, maps));
25728
25897
  }
25729
25898
  if (view && typeof view === "object") {
25730
25899
  const out = {};
25731
25900
  for (const [key, value] of Object.entries(view)) {
25732
- if (key === "table" && typeof value === "string") {
25733
- out[key] = logicalToReal.get(value) ?? value;
25901
+ if ((key === "table" || key === "schedule") && typeof value === "string") {
25902
+ out[key] = maps[key].get(value) ?? value;
25734
25903
  } else {
25735
- out[key] = rewriteViewTableRefs(value, logicalToReal);
25904
+ out[key] = rewriteViewRefs(value, maps);
25736
25905
  }
25737
25906
  }
25738
25907
  return out;
@@ -25832,7 +26001,7 @@ async function runAdd(source, licenseUrl, io) {
25832
26001
  licenseUrl
25833
26002
  });
25834
26003
  io.log(
25835
- `Installed ${report.packId}@${report.packVersion}: ${report.projectCreated ? "project created" : "project reused"}, ${report.tablesCreated} table(s) (${report.rowsSeeded} row(s)), ${report.customersSeeded} customer(s), ${report.profilesDropped} profile(s), ${report.blueprintsDropped} blueprint(s).`
26004
+ `Installed ${report.packId}@${report.packVersion}: ${report.projectCreated ? "project created" : "project reused"}, ${report.tablesCreated} table(s) (${report.rowsSeeded} row(s)), ${report.customersSeeded} customer(s), ${report.profilesDropped} profile(s), ${report.blueprintsDropped} blueprint(s), ${report.schedulesRegistered} schedule(s).`
25836
26005
  );
25837
26006
  return 0;
25838
26007
  } catch (err2) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orionfold-relay",
3
- "version": "0.18.0",
3
+ "version": "0.20.0",
4
4
  "description": "Orionfold Relay — a local-first, multi-agent orchestration runtime and builder scaffold for AI-native work.",
5
5
  "keywords": [
6
6
  "ai",
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Composite DB id for app-manifest schedules: `app:<appId>:<scheduleId>`.
3
+ *
4
+ * Mirrors the plugin-schedule convention (`plugin:<pluginId>:<specId>`,
5
+ * src/lib/schedules/installer.ts). A deterministic id makes the install-time
6
+ * upsert idempotent and lets the scheduler recover the owning app at fire
7
+ * time without a schema change.
8
+ *
9
+ * Kept in its own module (no DB imports) so both the pack installer and the
10
+ * scheduler can import it statically without touching the runtime-registry
11
+ * chain (TDR-032).
12
+ */
13
+
14
+ export const APP_SCHEDULE_PREFIX = "app:";
15
+
16
+ export function appScheduleId(appId: string, scheduleId: string): string {
17
+ return `${APP_SCHEDULE_PREFIX}${appId}:${scheduleId}`;
18
+ }
19
+
20
+ export function isAppScheduleId(id: string): boolean {
21
+ return id.startsWith(APP_SCHEDULE_PREFIX);
22
+ }
23
+
24
+ /**
25
+ * Recover `{ appId, scheduleId }` from a composite id, or null when the id
26
+ * is not app-owned. appId is a clean slug (no colons), so the first two
27
+ * colon-separated segments are unambiguous.
28
+ */
29
+ export function parseAppScheduleId(
30
+ id: string
31
+ ): { appId: string; scheduleId: string } | null {
32
+ if (!isAppScheduleId(id)) return null;
33
+ const rest = id.slice(APP_SCHEDULE_PREFIX.length);
34
+ const sep = rest.indexOf(":");
35
+ if (sep <= 0 || sep === rest.length - 1) return null;
36
+ return { appId: rest.slice(0, sep), scheduleId: rest.slice(sep + 1) };
37
+ }
@@ -137,6 +137,63 @@ export async function dispatchBlueprintForRow(input: {
137
137
  }
138
138
  }
139
139
 
140
+ /**
141
+ * Cron-fired blueprint dispatch for app-manifest schedules
142
+ * (`manifest.schedules[].runs`). The schedule-side sibling of
143
+ * `dispatchBlueprintForRow` — same instantiate → execute chokepoint, same
144
+ * notification-on-failure contract, but with no row context: variables come
145
+ * entirely from the blueprint's declared defaults, so a pack author must
146
+ * give every required variable a default for a scheduled blueprint.
147
+ *
148
+ * Returns `{ workflowId }` on success, `null` on failure (already logged +
149
+ * recorded in `notifications`).
150
+ */
151
+ export async function dispatchScheduledBlueprint(input: {
152
+ appId: string;
153
+ blueprintId: string;
154
+ scheduleId: string;
155
+ }): Promise<{ workflowId: string } | null> {
156
+ const { appId, blueprintId, scheduleId } = input;
157
+ try {
158
+ const { instantiateBlueprint } = await import(
159
+ "@/lib/workflows/blueprints/instantiator"
160
+ );
161
+ const { executeWorkflow } = await import("@/lib/workflows/engine");
162
+
163
+ const { workflowId } = await instantiateBlueprint(blueprintId, {}, appId);
164
+
165
+ // Fire-and-forget — workflow may run for minutes
166
+ executeWorkflow(workflowId).catch((err) => {
167
+ console.error(
168
+ `[manifest-trigger-dispatch] executeWorkflow ${workflowId} failed:`,
169
+ err
170
+ );
171
+ });
172
+
173
+ return { workflowId };
174
+ } catch (err) {
175
+ const message = err instanceof Error ? err.message : String(err);
176
+ console.error(
177
+ `[manifest-trigger-dispatch] scheduled dispatch failed for app=${appId} blueprint=${blueprintId}:`,
178
+ err
179
+ );
180
+ try {
181
+ await db.insert(notifications).values({
182
+ id: crypto.randomUUID(),
183
+ taskId: null,
184
+ type: "task_failed",
185
+ title: `Schedule failure in app "${appId}"`,
186
+ body: `Blueprint "${blueprintId}" failed for schedule "${scheduleId}": ${message}`,
187
+ read: false,
188
+ createdAt: new Date(),
189
+ });
190
+ } catch (nerr) {
191
+ console.error(`[manifest-trigger-dispatch] notification write failed:`, nerr);
192
+ }
193
+ return null;
194
+ }
195
+ }
196
+
140
197
  interface MatchingSubscription {
141
198
  appId: string;
142
199
  blueprintId: string;
@@ -486,6 +486,8 @@ export interface DeleteAppCascadeResult {
486
486
  profilesRemoved: number;
487
487
  /** Number of `<appId>--*.yaml` blueprint files removed from the blueprints dir. */
488
488
  blueprintsRemoved: number;
489
+ /** Number of `app:<appId>:*` schedule rows removed from the schedules table. */
490
+ schedulesRemoved: number;
489
491
  }
490
492
 
491
493
  export interface DeleteAppCascadeOptions {
@@ -553,6 +555,7 @@ export async function deleteAppCascade(
553
555
  projectRemoved: false,
554
556
  profilesRemoved: 0,
555
557
  blueprintsRemoved: 0,
558
+ schedulesRemoved: 0,
556
559
  };
557
560
 
558
561
  const resolvedApps = path.resolve(appsDir);
@@ -577,5 +580,28 @@ export async function deleteAppCascade(
577
580
  const profilesRemoved = sweepNamespacedProfiles(profilesDir, appId);
578
581
  const blueprintsRemoved = sweepNamespacedBlueprints(blueprintsDir, appId);
579
582
 
580
- return { projectRemoved, filesRemoved, profilesRemoved, blueprintsRemoved };
583
+ // Sweep app-owned schedule rows (`app:<appId>:*`, registered by the pack
584
+ // installer) so an uninstalled app's schedules don't refire into nothing.
585
+ // Dynamic import — this module must stay out of the DB static import graph.
586
+ let schedulesRemoved = 0;
587
+ try {
588
+ const { db } = await import("@/lib/db");
589
+ const { schedules } = await import("@/lib/db/schema");
590
+ const { like } = await import("drizzle-orm");
591
+ const result = db
592
+ .delete(schedules)
593
+ .where(like(schedules.id, `app:${appId}:%`))
594
+ .run();
595
+ schedulesRemoved = result.changes;
596
+ } catch (err) {
597
+ console.error(`[registry] schedule sweep failed for app "${appId}":`, err);
598
+ }
599
+
600
+ return {
601
+ projectRemoved,
602
+ filesRemoved,
603
+ profilesRemoved,
604
+ blueprintsRemoved,
605
+ schedulesRemoved,
606
+ };
581
607
  }
@@ -104,7 +104,8 @@ async function runAdd(
104
104
  `${report.tablesCreated} table(s) (${report.rowsSeeded} row(s)), ` +
105
105
  `${report.customersSeeded} customer(s), ` +
106
106
  `${report.profilesDropped} profile(s), ` +
107
- `${report.blueprintsDropped} blueprint(s).`
107
+ `${report.blueprintsDropped} blueprint(s), ` +
108
+ `${report.schedulesRegistered} schedule(s).`
108
109
  );
109
110
  return 0;
110
111
  } catch (err) {