@stigmer/cli 3.5.3 → 3.6.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 (72) hide show
  1. package/commands/list.js +8 -5
  2. package/commands/list.js.map +1 -1
  3. package/commands/resume.js +5 -1
  4. package/commands/resume.js.map +1 -1
  5. package/commands/schedule.d.ts +3 -0
  6. package/commands/schedule.d.ts.map +1 -0
  7. package/commands/schedule.js +57 -0
  8. package/commands/schedule.js.map +1 -0
  9. package/package.json +5 -5
  10. package/program.d.ts.map +1 -1
  11. package/program.js +2 -0
  12. package/program.js.map +1 -1
  13. package/registry/metadata.d.ts.map +1 -1
  14. package/registry/metadata.js +2 -0
  15. package/registry/metadata.js.map +1 -1
  16. package/registry/verb-support.d.ts.map +1 -1
  17. package/registry/verb-support.js +29 -21
  18. package/registry/verb-support.js.map +1 -1
  19. package/registry/verbs.d.ts.map +1 -1
  20. package/registry/verbs.js +0 -1
  21. package/registry/verbs.js.map +1 -1
  22. package/resources/apply/apply.d.ts +6 -1
  23. package/resources/apply/apply.d.ts.map +1 -1
  24. package/resources/apply/apply.js +14 -24
  25. package/resources/apply/apply.js.map +1 -1
  26. package/resources/apply/handlers.d.ts +9 -1
  27. package/resources/apply/handlers.d.ts.map +1 -1
  28. package/resources/apply/handlers.js +50 -148
  29. package/resources/apply/handlers.js.map +1 -1
  30. package/resources/delete.d.ts +12 -0
  31. package/resources/delete.d.ts.map +1 -1
  32. package/resources/delete.js +8 -4
  33. package/resources/delete.js.map +1 -1
  34. package/resources/execution.d.ts +14 -4
  35. package/resources/execution.d.ts.map +1 -1
  36. package/resources/execution.js +16 -6
  37. package/resources/execution.js.map +1 -1
  38. package/resources/get-bindings.d.ts +1 -0
  39. package/resources/get-bindings.d.ts.map +1 -1
  40. package/resources/get-bindings.js +8 -1
  41. package/resources/get-bindings.js.map +1 -1
  42. package/resources/list.d.ts +4 -0
  43. package/resources/list.d.ts.map +1 -1
  44. package/resources/list.js +95 -27
  45. package/resources/list.js.map +1 -1
  46. package/resources/reference.d.ts +2 -0
  47. package/resources/reference.d.ts.map +1 -1
  48. package/resources/reference.js +4 -0
  49. package/resources/reference.js.map +1 -1
  50. package/resources/schedule.d.ts +19 -0
  51. package/resources/schedule.d.ts.map +1 -0
  52. package/resources/schedule.js +96 -0
  53. package/resources/schedule.js.map +1 -0
  54. package/src/commands/list.ts +8 -5
  55. package/src/commands/resume.ts +8 -1
  56. package/src/commands/schedule.ts +70 -0
  57. package/src/program.ts +2 -0
  58. package/src/registry/metadata.ts +2 -0
  59. package/src/registry/registry.test.ts +113 -8
  60. package/src/registry/verb-support.ts +29 -21
  61. package/src/registry/verbs.ts +0 -1
  62. package/src/resources/apply/apply.integration.test.ts +132 -0
  63. package/src/resources/apply/apply.ts +16 -25
  64. package/src/resources/apply/handlers.ts +60 -150
  65. package/src/resources/delete.ts +8 -4
  66. package/src/resources/execution.ts +20 -6
  67. package/src/resources/get-bindings.ts +8 -1
  68. package/src/resources/list.ts +107 -31
  69. package/src/resources/mutate-verbs.integration.test.ts +78 -4
  70. package/src/resources/read-verbs.integration.test.ts +124 -5
  71. package/src/resources/reference.ts +5 -0
  72. package/src/resources/schedule.ts +115 -0
@@ -1,6 +1,9 @@
1
1
  import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
2
2
  import { describe, expect, it } from "vitest";
3
3
  import { APPLY_HANDLERS } from "../resources/apply/handlers.js";
4
+ import { DELETE_HANDLERS } from "../resources/delete.js";
5
+ import { GET_BINDINGS } from "../resources/get-bindings.js";
6
+ import { LIST_HANDLERS, SEARCH_KINDS } from "../resources/list.js";
4
7
  import { defaultRegistry } from "./registry.js";
5
8
  import { Verb } from "./verbs.js";
6
9
 
@@ -72,7 +75,7 @@ describe("registry — verb support matrix", () => {
72
75
  expect(registry.supportsVerb(ApiResourceKind.workflow_instance, Verb.List)).toBe(false);
73
76
  });
74
77
 
75
- it("agent_channel supports the declarative verbs, matching agent_share", () => {
78
+ it("agent_channel supports the full declarative verb set", () => {
76
79
  for (const v of [Verb.Apply, Verb.Get, Verb.List, Verb.Delete]) {
77
80
  expect(registry.supportsVerb(ApiResourceKind.agent_channel, v)).toBe(true);
78
81
  }
@@ -81,6 +84,23 @@ describe("registry — verb support matrix", () => {
81
84
  expect(registry.supportsVerb(ApiResourceKind.agent_channel, Verb.Search)).toBe(false);
82
85
  });
83
86
 
87
+ it("schedule supports the full declarative verb set", () => {
88
+ for (const v of [Verb.Apply, Verb.Get, Verb.List, Verb.Delete]) {
89
+ expect(registry.supportsVerb(ApiResourceKind.schedule, v)).toBe(true);
90
+ }
91
+ // Firing rides `stigmer schedule trigger`, never the generic run verb.
92
+ expect(registry.supportsVerb(ApiResourceKind.schedule, Verb.Run)).toBe(false);
93
+ });
94
+
95
+ it("session and agent_share promise only apply (narrowed — stigmer/stigmer#354)", () => {
96
+ for (const kind of [ApiResourceKind.session, ApiResourceKind.agent_share]) {
97
+ expect(registry.supportsVerb(kind, Verb.Apply)).toBe(true);
98
+ for (const v of [Verb.Get, Verb.List, Verb.Delete]) {
99
+ expect(registry.supportsVerb(kind, v)).toBe(false);
100
+ }
101
+ }
102
+ });
103
+
84
104
  it("typesForVerb(get) includes the core read kinds", () => {
85
105
  const kinds = new Set(registry.typesForVerb(Verb.Get).map((t) => t.kind));
86
106
  expect(kinds).toContain(ApiResourceKind.agent);
@@ -98,13 +118,98 @@ describe("registry — completeness", () => {
98
118
  }
99
119
  });
100
120
 
101
- // The apply verb dispatches through an explicit handler registry, not
102
- // the kind registry — a kind can advertise Apply in the verb matrix yet
103
- // fail at dispatch if its handler entry is missing. Pin the wiring for
104
- // the kinds whose apply path the declarative flow depends on.
105
- it("every declarative kind with an Apply verb intent has an apply handler", () => {
106
- for (const kind of [ApiResourceKind.agent_share, ApiResourceKind.agent_channel]) {
107
- expect(APPLY_HANDLERS.get(kind), `apply handler missing for kind ${kind}`).toBeDefined();
121
+ });
122
+
123
+ // The verb matrix is a PROMISE (`list types` prints it; the command layer
124
+ // gates on it), but each verb actually runs through its own dispatch
125
+ // registry. A pair present in the matrix but absent from its registry fails
126
+ // at the point of use with "not implemented" — the stigmer/stigmer#353 bug
127
+ // class. This suite holds the two to strict bidirectional equality so the
128
+ // drift is a red test instead of a runtime surprise. Verbs deliberately
129
+ // withheld are narrowed out of the matrix (recorded in stigmer/stigmer#354),
130
+ // never exempted here — an exemption category would legitimize exactly the
131
+ // state this suite exists to reject.
132
+ describe("registry — verb/dispatch conformance", () => {
133
+ // Special cases are kind/verb pairs that genuinely work through bespoke
134
+ // code instead of a registry entry; each names its dispatch site.
135
+ const DISPATCH: ReadonlyArray<{
136
+ label: string;
137
+ verb: Verb;
138
+ wired: ReadonlySet<ApiResourceKind>;
139
+ specialCases: ReadonlyMap<ApiResourceKind, string>;
140
+ }> = [
141
+ {
142
+ label: "apply",
143
+ verb: Verb.Apply,
144
+ wired: new Set(APPLY_HANDLERS.keys()),
145
+ specialCases: new Map([
146
+ [
147
+ ApiResourceKind.project,
148
+ "the stigmer.yaml declarative/synthesis track (resources/apply/declarative.ts); " +
149
+ "file mode refuses with a pointer there (resolveHandlerForKind)",
150
+ ],
151
+ ]),
152
+ },
153
+ {
154
+ label: "get",
155
+ verb: Verb.Get,
156
+ wired: new Set(GET_BINDINGS.keys()),
157
+ specialCases: new Map([
158
+ [
159
+ ApiResourceKind.organization,
160
+ "fetchOrganization in resources/get.ts (not org-scoped; a slug resolves via findMyOrganizations)",
161
+ ],
162
+ ]),
163
+ },
164
+ {
165
+ label: "list",
166
+ verb: Verb.List,
167
+ wired: new Set([...LIST_HANDLERS.keys(), ...SEARCH_KINDS]),
168
+ specialCases: new Map(),
169
+ },
170
+ {
171
+ label: "delete",
172
+ verb: Verb.Delete,
173
+ wired: new Set(DELETE_HANDLERS.keys()),
174
+ specialCases: new Map([
175
+ [
176
+ ApiResourceKind.organization,
177
+ "planOrganizationDelete in resources/delete.ts (not org-scoped; resolved via memberships)",
178
+ ],
179
+ ]),
180
+ },
181
+ ];
182
+
183
+ it.each(DISPATCH)("every kind promising '$label' has a dispatch entry", ({ verb, wired, specialCases }) => {
184
+ for (const info of registry.all()) {
185
+ if (!info.supportedVerbs.has(verb)) continue;
186
+ if (specialCases.has(info.kind)) continue;
187
+ expect(
188
+ wired.has(info.kind),
189
+ `${info.name} promises '${verb}' in the verb matrix but has no dispatch entry — ` +
190
+ "wire it or narrow the matrix (stigmer/stigmer#353, #354)",
191
+ ).toBe(true);
192
+ }
193
+ });
194
+
195
+ it.each(DISPATCH)("every '$label' dispatch entry is promised in the matrix", ({ verb, wired }) => {
196
+ for (const kind of wired) {
197
+ expect(
198
+ registry.supportsVerb(kind, verb),
199
+ `${ApiResourceKind[kind]} is wired for '${verb}' but the verb matrix does not promise it — ` +
200
+ "dead dispatch the command-layer gate blocks",
201
+ ).toBe(true);
202
+ }
203
+ });
204
+
205
+ it("special cases do not shadow a real dispatch entry", () => {
206
+ for (const { label, wired, specialCases } of DISPATCH) {
207
+ for (const kind of specialCases.keys()) {
208
+ expect(
209
+ wired.has(kind),
210
+ `${ApiResourceKind[kind]} '${label}' is documented as a special case yet also has a registry entry — drop one`,
211
+ ).toBe(false);
212
+ }
108
213
  }
109
214
  });
110
215
  });
@@ -1,20 +1,14 @@
1
1
  // Verb-support matrix — which verbs each resource kind supports.
2
2
  //
3
- // This is CLI-specific policy (not stored in proto), ported verbatim from Go's
4
- // types.verbSupport so both CLIs expose the same surface. Keep in lockstep.
5
- //
6
- // | Kind | apply | validate | get | list | delete | run | push | search | download |
7
- // |--------------|-------|----------|-----|------|--------|-----|------|--------|----------|
8
- // | Organization | Y | - | Y | Y | Y | - | - | - | - |
9
- // | Agent | Y | Y | Y | Y | Y | Y | - | Y | - |
10
- // | Workflow | Y | Y | Y | Y | Y | Y | - | Y | - |
11
- // | Skill | - | - | Y | Y | Y | - | Y | - | - |
12
- // | McpServer | Y | Y | Y | Y | Y | - | - | - | - |
13
- // | Project | Y* | Y | Y | Y | Y | - | - | - | - |
14
- // | Execution | - | - | Y | Y | Y** | - | - | - | Y |
15
- //
16
- // * Project "apply" triggers SDK synthesis mode.
17
- // ** Execution "delete" maps to a cancel operation.
3
+ // This is CLI-specific policy (not stored in proto), and it is a PROMISE:
4
+ // `stigmer list types` prints it, the docs type tables are written from it,
5
+ // and the command layer gates on it before dispatch. The conformance test in
6
+ // registry.test.ts holds every promised apply/get/list/delete verb to an
7
+ // actual dispatch entry — and every dispatch entry back to a promise — so a
8
+ // verb listed here either works or fails CI. "Advertised but not
9
+ // implemented" (stigmer/stigmer#353) is a rejected state, not a backlog;
10
+ // verbs deliberately withheld pending a real ops story are recorded in
11
+ // stigmer/stigmer#354, not here.
18
12
 
19
13
  import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
20
14
  import { Verb } from "./verbs.js";
@@ -31,17 +25,24 @@ export const VERB_SUPPORT: ReadonlyMap<ApiResourceKind, ReadonlySet<Verb>> = new
31
25
  ],
32
26
  [ApiResourceKind.skill, new Set<Verb>([Verb.Get, Verb.List, Verb.Delete, Verb.Push])],
33
27
  [ApiResourceKind.mcp_server, new Set<Verb>([Verb.Apply, Verb.Validate, Verb.Get, Verb.List, Verb.Delete])],
28
+ // Project apply is the stigmer.yaml declarative/synthesis track, not
29
+ // `apply -f project.yaml` — resolveHandlerForKind carries the teaching
30
+ // refusal, and the conformance test documents it as a special case.
34
31
  [ApiResourceKind.project, new Set<Verb>([Verb.Apply, Verb.Validate, Verb.Get, Verb.List, Verb.Delete])],
35
32
  [ApiResourceKind.api_key, new Set<Verb>([Verb.Get, Verb.List, Verb.Delete])],
36
33
  // agent_execution is special — uses dedicated AgentExecutionQueryController
37
34
  // RPCs, not the unified SearchService. delete maps to cancel.
38
35
  [ApiResourceKind.agent_execution, new Set<Verb>([Verb.Get, Verb.List, Verb.Delete, Verb.Download])],
39
- [ApiResourceKind.identity_provider, new Set<Verb>([Verb.Apply, Verb.Get, Verb.List, Verb.Delete])],
40
- [ApiResourceKind.oauth_app, new Set<Verb>([Verb.Apply, Verb.Get, Verb.List, Verb.Delete])],
36
+ // IAM apps are configured declaratively; read/ops verbs are deliberately
37
+ // not promised (never wired, no demand — stigmer/stigmer#354). To add one:
38
+ // dispatch entry + this line, in the same change.
39
+ [ApiResourceKind.identity_provider, new Set<Verb>([Verb.Apply])],
40
+ [ApiResourceKind.oauth_app, new Set<Verb>([Verb.Apply])],
41
41
  [ApiResourceKind.environment, new Set<Verb>([Verb.Apply, Verb.Get, Verb.List, Verb.Delete])],
42
- // Enable/disable flows through `stigmer share agent`; the generic verbs
43
- // cover the declarative path (apply a manifest, inspect, tear down).
44
- [ApiResourceKind.agent_share, new Set<Verb>([Verb.Apply, Verb.Get, Verb.List, Verb.Delete])],
42
+ // Enable/disable flows through `stigmer share agent`, which is the whole
43
+ // read/ops surface today; only the declarative apply path is promised
44
+ // (stigmer/stigmer#354 records the narrowing).
45
+ [ApiResourceKind.agent_share, new Set<Verb>([Verb.Apply])],
45
46
  // The provider install flow is console-driven and cloud-only; the generic
46
47
  // verbs cover the declarative path (apply a manifest, inspect, tear down).
47
48
  [ApiResourceKind.agent_channel, new Set<Verb>([Verb.Apply, Verb.Get, Verb.List, Verb.Delete])],
@@ -52,10 +53,17 @@ export const VERB_SUPPORT: ReadonlyMap<ApiResourceKind, ReadonlySet<Verb>> = new
52
53
  // generic verbs cover the declarative path (apply a manifest, inspect,
53
54
  // tear down — delete is guarded server-side for non-empty datastores).
54
55
  [ApiResourceKind.datastore, new Set<Verb>([Verb.Apply, Verb.Validate, Verb.Get, Verb.List, Verb.Delete])],
56
+ // Firing is the platform's job (Temporal Schedules); the generic verbs
57
+ // cover the declarative path, and get/list surface the state the dedicated
58
+ // `stigmer schedule` commands act on — spec.enabled (the owner's switch)
59
+ // versus status.paused_reason (the platform's failure latch).
60
+ [ApiResourceKind.schedule, new Set<Verb>([Verb.Apply, Verb.Get, Verb.List, Verb.Delete])],
55
61
  [ApiResourceKind.agent_instance, new Set<Verb>([Verb.Apply, Verb.Get, Verb.List, Verb.Delete])],
56
62
  // No list: proto exposes getByWorkflow (requires workflow_id), not a generic list.
57
63
  [ApiResourceKind.workflow_instance, new Set<Verb>([Verb.Apply, Verb.Get, Verb.Delete])],
58
- [ApiResourceKind.session, new Set<Verb>([Verb.Apply, Verb.Get, Verb.List, Verb.Delete])],
64
+ // Sessions are runtime conversation state with no CLI read/ops story yet;
65
+ // get/list/delete are deliberately not promised (stigmer/stigmer#354).
66
+ [ApiResourceKind.session, new Set<Verb>([Verb.Apply])],
59
67
  ]);
60
68
 
61
69
  export function verbsForKind(kind: ApiResourceKind): ReadonlySet<Verb> {
@@ -1,5 +1,4 @@
1
1
  // CLI verbs — operations that can be performed on a resource.
2
- // Mirrors the Go types.Verb set so the two CLIs stay behaviorally identical.
3
2
 
4
3
  export const Verb = {
5
4
  Apply: "apply",
@@ -17,6 +17,8 @@ import { type ConnectRouter, createClient } from "@connectrpc/connect";
17
17
  import { connectNodeAdapter } from "@connectrpc/connect-node";
18
18
  import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
19
19
  import { AgentCommandController } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/command_pb";
20
+ import type { ChannelApp } from "@stigmer/protos/ai/stigmer/agentic/channelapp/v1/api_pb";
21
+ import { ChannelAppCommandController } from "@stigmer/protos/ai/stigmer/agentic/channelapp/v1/command_pb";
20
22
  import type { Datastore } from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/api_pb";
21
23
  import { DatastoreCommandController } from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/command_pb";
22
24
  import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
@@ -33,11 +35,13 @@ const openSessions = new Set<ServerHttp2Session>();
33
35
  let appliedAgents: Agent[] = [];
34
36
  let appliedMcps: McpServer[] = [];
35
37
  let appliedDatastores: Datastore[] = [];
38
+ let appliedChannelApps: ChannelApp[] = [];
36
39
 
37
40
  beforeEach(() => {
38
41
  appliedAgents = [];
39
42
  appliedMcps = [];
40
43
  appliedDatastores = [];
44
+ appliedChannelApps = [];
41
45
  });
42
46
 
43
47
  function writeYaml(dir: string, name: string, body: string): string {
@@ -66,6 +70,12 @@ beforeAll(async () => {
66
70
  return req;
67
71
  },
68
72
  });
73
+ router.service(ChannelAppCommandController, {
74
+ apply: (req) => {
75
+ appliedChannelApps.push(req);
76
+ return req;
77
+ },
78
+ });
69
79
  };
70
80
 
71
81
  backend = createHttp2Server(connectNodeAdapter({ routes }));
@@ -215,6 +225,128 @@ describe("file-mode apply — datastore", () => {
215
225
  });
216
226
  });
217
227
 
228
+ // The stigmer/stigmer#353 regression block: ChannelApp apply used to fail
229
+ // with "apply not implemented for Channel App" because the CLI's handler
230
+ // table was a drifted copy of the SDK manifest registry. The handlers now
231
+ // COME FROM that registry, so this suite pins the whole class: the kind
232
+ // applies, secrets round-trip verbatim, and the ordering the registry
233
+ // encodes (app before the channel that references it) is honored.
234
+ const CHANNEL_APP_YAML = [
235
+ "apiVersion: agentic.stigmer.ai/v1",
236
+ "kind: ChannelApp",
237
+ "metadata:",
238
+ " name: demo-whatsapp",
239
+ " slug: demo-whatsapp",
240
+ "spec:",
241
+ " whatsapp:",
242
+ " app_id: '108954'",
243
+ " app_secret: '***REDACTED***'",
244
+ " access_token: '***REDACTED***'",
245
+ " verify_token: '***REDACTED***'",
246
+ "",
247
+ ].join("\n");
248
+
249
+ const AGENT_CHANNEL_YAML = [
250
+ "apiVersion: agentic.stigmer.ai/v1",
251
+ "kind: AgentChannel",
252
+ "metadata:",
253
+ " name: isc-whatsapp",
254
+ " slug: isc-whatsapp",
255
+ "spec:",
256
+ " enabled: true",
257
+ " agent_ref: { kind: 40, org: acme, slug: clinic-assistant }",
258
+ " whatsapp: { phone_number_id: '106540352242922' }",
259
+ "",
260
+ ].join("\n");
261
+
262
+ const SCHEDULE_YAML = [
263
+ "apiVersion: agentic.stigmer.ai/v1",
264
+ "kind: Schedule",
265
+ "metadata:",
266
+ " name: daily-fee-reminders",
267
+ " slug: daily-fee-reminders",
268
+ "spec:",
269
+ " cron: '0 9 * * *'",
270
+ " time_zone: Asia/Kolkata",
271
+ " enabled: false",
272
+ " agent:",
273
+ " agent_ref: { kind: 40, org: acme, slug: clinic-assistant }",
274
+ " message: run the reminders",
275
+ "",
276
+ ].join("\n");
277
+
278
+ describe("file-mode apply — channel app (#353)", () => {
279
+ it("applies a ChannelApp through the raw controller with the redaction markers intact", async () => {
280
+ const dir = mkdtempSync(join(tmpdir(), "apply-it-"));
281
+ try {
282
+ writeYaml(dir, "chapp.yaml", CHANNEL_APP_YAML);
283
+ const items = resolveApplyItems(dir);
284
+ expect(items).toHaveLength(1);
285
+
286
+ const outcome = await applyItem(controllerFn, items[0], "acme", false);
287
+ expect(outcome.result.message).toBe("Channel App created successfully");
288
+
289
+ expect(appliedChannelApps).toHaveLength(1);
290
+ const spec = appliedChannelApps[0].spec;
291
+ expect(spec?.providerConfig.case).toBe("whatsapp");
292
+ if (spec?.providerConfig.case !== "whatsapp") throw new Error("unreachable");
293
+ expect(spec.providerConfig.value.appId).toBe("108954");
294
+ // The redaction markers must reach the server verbatim — that is the
295
+ // convention by which applying a fetched manifest preserves stored
296
+ // secrets (verb-support's OAuthApp marker note).
297
+ expect(spec.providerConfig.value.appSecret).toBe("***REDACTED***");
298
+ expect(spec.providerConfig.value.accessToken).toBe("***REDACTED***");
299
+ } finally {
300
+ rmSync(dir, { recursive: true, force: true });
301
+ }
302
+ });
303
+
304
+ it("treats a ChannelApp carrying metadata.id as an update", async () => {
305
+ const dir = mkdtempSync(join(tmpdir(), "apply-it-"));
306
+ try {
307
+ writeYaml(
308
+ dir,
309
+ "chapp.yaml",
310
+ CHANNEL_APP_YAML.replace("metadata:", "metadata:\n id: chapp_existing"),
311
+ );
312
+ const items = resolveApplyItems(dir);
313
+ const outcome = await applyItem(controllerFn, items[0], "acme", false);
314
+ expect(outcome.result.message).toBe("Channel App updated successfully");
315
+ expect(appliedChannelApps[0].metadata?.id).toBe("chapp_existing");
316
+ } finally {
317
+ rmSync(dir, { recursive: true, force: true });
318
+ }
319
+ });
320
+
321
+ it("orders one bundle app → channel → schedule regardless of file order", () => {
322
+ const dir = mkdtempSync(join(tmpdir(), "apply-it-"));
323
+ try {
324
+ // File names chosen so lexical file order is exactly WRONG — before
325
+ // the registry unification all three kinds fell to a shared default
326
+ // priority and applied in this (broken) file order.
327
+ writeYaml(dir, "a-schedule.yaml", SCHEDULE_YAML);
328
+ writeYaml(dir, "b-channel.yaml", AGENT_CHANNEL_YAML);
329
+ writeYaml(dir, "c-chapp.yaml", CHANNEL_APP_YAML);
330
+ const items = resolveApplyItems(dir);
331
+ expect(items.map((i) => i.handler.displayName)).toEqual(["Channel App", "Agent Channel", "Schedule"]);
332
+ } finally {
333
+ rmSync(dir, { recursive: true, force: true });
334
+ }
335
+ });
336
+ });
337
+
338
+ describe("file-mode apply — project special case", () => {
339
+ it("refuses a Project manifest with a pointer to the stigmer.yaml flow", () => {
340
+ const dir = mkdtempSync(join(tmpdir(), "apply-it-"));
341
+ try {
342
+ writeYaml(dir, "project.yaml", ["kind: Project", "metadata:", " name: Demo", " slug: demo", ""].join("\n"));
343
+ expect(() => resolveApplyItems(dir)).toThrow(/stigmer\.yaml/);
344
+ } finally {
345
+ rmSync(dir, { recursive: true, force: true });
346
+ }
347
+ });
348
+ });
349
+
218
350
  describe("file-mode apply — orchestration", () => {
219
351
  it("sorts items into dependency order (mcp_server before agent)", () => {
220
352
  const dir = mkdtempSync(join(tmpdir(), "apply-it-"));
@@ -39,26 +39,6 @@ export interface ApplyOutcome {
39
39
  readonly applied?: Message;
40
40
  }
41
41
 
42
- // Dependency apply order (ascending priority); kinds absent get 99 (applied
43
- // last). Mirrors Go's applier.DefaultApplyOrder.
44
- const APPLY_ORDER: ReadonlyMap<ApiResourceKind, number> = new Map([
45
- [ApiResourceKind.organization, 0],
46
- [ApiResourceKind.mcp_server, 1],
47
- // Before agent: an Agent references a Datastore via datastore_usages
48
- // (the McpServer-before-Agent pattern; mirrors the SDK manifest registry).
49
- [ApiResourceKind.datastore, 2],
50
- [ApiResourceKind.agent, 3],
51
- [ApiResourceKind.workflow, 4],
52
- [ApiResourceKind.environment, 5],
53
- [ApiResourceKind.identity_provider, 6],
54
- [ApiResourceKind.oauth_app, 7],
55
- [ApiResourceKind.agent_instance, 8],
56
- [ApiResourceKind.workflow_instance, 9],
57
- [ApiResourceKind.session, 10],
58
- // After agent and environment: a share references both.
59
- [ApiResourceKind.agent_share, 11],
60
- ]);
61
-
62
42
  /** Expand a path into ordered, kind-resolved apply items (strict YAML parse). */
63
43
  export function resolveApplyItems(path: string): ApplyItem[] {
64
44
  const files = resolveYamlFiles(path);
@@ -83,14 +63,28 @@ export function resolveHandlerForKind(kind: string, where: string): ApplyHandler
83
63
  const info = defaultRegistry().getByYamlKind(kind);
84
64
  if (info === undefined) throw new UsageError(`unknown resource kind '${kind}' in ${where}`);
85
65
  if (!info.supportedVerbs.has(Verb.Apply)) throw new UsageError(`${info.displayName} does not support 'apply'`);
66
+ if (info.kind === ApiResourceKind.project) {
67
+ // Project's Apply verb is the stigmer.yaml track, deliberately not a
68
+ // manifest handler: a file-mode apply would create a second, member-less
69
+ // way to apply a project, bypassing the membership reconciler.
70
+ throw new UsageError(
71
+ `a Project is not applied from a resource manifest (${where}): ` +
72
+ "run `stigmer apply` in the directory containing stigmer.yaml and the project is reconciled from its members",
73
+ );
74
+ }
86
75
  const handler = APPLY_HANDLERS.get(info.kind);
87
76
  if (handler === undefined) throw new UsageError(`apply not implemented for ${info.displayName}`);
88
77
  return handler;
89
78
  }
90
79
 
91
- /** Stable sort by dependency priority (Array.prototype.sort is stable in V8). */
80
+ /**
81
+ * Stable sort by dependency priority (Array.prototype.sort is stable in V8).
82
+ * The priority rides each handler (`applyOrder`, sourced from the SDK
83
+ * manifest registry) — there is deliberately no local ordering table to
84
+ * drift from it.
85
+ */
92
86
  export function sortApplyItems(items: ApplyItem[]): ApplyItem[] {
93
- items.sort((a, b) => priorityOf(a.handler.kind) - priorityOf(b.handler.kind));
87
+ items.sort((a, b) => a.handler.applyOrder - b.handler.applyOrder);
94
88
  return items;
95
89
  }
96
90
 
@@ -202,6 +196,3 @@ function buildDryRunResult(handler: ApplyHandler, message: Message): CommandResu
202
196
  return result;
203
197
  }
204
198
 
205
- function priorityOf(kind: ApiResourceKind): number {
206
- return APPLY_ORDER.get(kind) ?? 99;
207
- }