@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.
- package/commands/list.js +8 -5
- package/commands/list.js.map +1 -1
- package/commands/resume.js +5 -1
- package/commands/resume.js.map +1 -1
- package/commands/schedule.d.ts +3 -0
- package/commands/schedule.d.ts.map +1 -0
- package/commands/schedule.js +57 -0
- package/commands/schedule.js.map +1 -0
- package/package.json +5 -5
- package/program.d.ts.map +1 -1
- package/program.js +2 -0
- package/program.js.map +1 -1
- package/registry/metadata.d.ts.map +1 -1
- package/registry/metadata.js +2 -0
- package/registry/metadata.js.map +1 -1
- package/registry/verb-support.d.ts.map +1 -1
- package/registry/verb-support.js +29 -21
- package/registry/verb-support.js.map +1 -1
- package/registry/verbs.d.ts.map +1 -1
- package/registry/verbs.js +0 -1
- package/registry/verbs.js.map +1 -1
- package/resources/apply/apply.d.ts +6 -1
- package/resources/apply/apply.d.ts.map +1 -1
- package/resources/apply/apply.js +14 -24
- package/resources/apply/apply.js.map +1 -1
- package/resources/apply/handlers.d.ts +9 -1
- package/resources/apply/handlers.d.ts.map +1 -1
- package/resources/apply/handlers.js +50 -148
- package/resources/apply/handlers.js.map +1 -1
- package/resources/delete.d.ts +12 -0
- package/resources/delete.d.ts.map +1 -1
- package/resources/delete.js +8 -4
- package/resources/delete.js.map +1 -1
- package/resources/execution.d.ts +14 -4
- package/resources/execution.d.ts.map +1 -1
- package/resources/execution.js +16 -6
- package/resources/execution.js.map +1 -1
- package/resources/get-bindings.d.ts +1 -0
- package/resources/get-bindings.d.ts.map +1 -1
- package/resources/get-bindings.js +8 -1
- package/resources/get-bindings.js.map +1 -1
- package/resources/list.d.ts +4 -0
- package/resources/list.d.ts.map +1 -1
- package/resources/list.js +95 -27
- package/resources/list.js.map +1 -1
- package/resources/reference.d.ts +2 -0
- package/resources/reference.d.ts.map +1 -1
- package/resources/reference.js +4 -0
- package/resources/reference.js.map +1 -1
- package/resources/schedule.d.ts +19 -0
- package/resources/schedule.d.ts.map +1 -0
- package/resources/schedule.js +96 -0
- package/resources/schedule.js.map +1 -0
- package/src/commands/list.ts +8 -5
- package/src/commands/resume.ts +8 -1
- package/src/commands/schedule.ts +70 -0
- package/src/program.ts +2 -0
- package/src/registry/metadata.ts +2 -0
- package/src/registry/registry.test.ts +113 -8
- package/src/registry/verb-support.ts +29 -21
- package/src/registry/verbs.ts +0 -1
- package/src/resources/apply/apply.integration.test.ts +132 -0
- package/src/resources/apply/apply.ts +16 -25
- package/src/resources/apply/handlers.ts +60 -150
- package/src/resources/delete.ts +8 -4
- package/src/resources/execution.ts +20 -6
- package/src/resources/get-bindings.ts +8 -1
- package/src/resources/list.ts +107 -31
- package/src/resources/mutate-verbs.integration.test.ts +78 -4
- package/src/resources/read-verbs.integration.test.ts +124 -5
- package/src/resources/reference.ts +5 -0
- package/src/resources/schedule.ts +115 -0
|
@@ -34,9 +34,15 @@ import { EnvironmentQueryController } from "@stigmer/protos/ai/stigmer/agentic/e
|
|
|
34
34
|
import { McpServerSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
35
35
|
import { McpServerCommandController } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/command_pb";
|
|
36
36
|
import { McpServerQueryController } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/query_pb";
|
|
37
|
+
import { ScheduleSchema } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/api_pb";
|
|
38
|
+
import { ScheduleCommandController } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/command_pb";
|
|
39
|
+
import { ScheduleQueryController } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/query_pb";
|
|
37
40
|
import { WorkflowSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/api_pb";
|
|
38
41
|
import { WorkflowCommandController } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/command_pb";
|
|
39
42
|
import { WorkflowQueryController } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/query_pb";
|
|
43
|
+
import { WorkflowInstanceSchema } from "@stigmer/protos/ai/stigmer/agentic/workflowinstance/v1/api_pb";
|
|
44
|
+
import { WorkflowInstanceCommandController } from "@stigmer/protos/ai/stigmer/agentic/workflowinstance/v1/command_pb";
|
|
45
|
+
import { WorkflowInstanceQueryController } from "@stigmer/protos/ai/stigmer/agentic/workflowinstance/v1/query_pb";
|
|
40
46
|
import { createNodeClient, normalizeEndpoint } from "@stigmer/sdk/node";
|
|
41
47
|
import type { Stigmer } from "@stigmer/sdk";
|
|
42
48
|
import { createServer as createHttp2Server, type Http2Server, type ServerHttp2Session } from "node:http2";
|
|
@@ -85,6 +91,14 @@ const knownChannelApp = create(ChannelAppSchema, {
|
|
|
85
91
|
metadata: { name: "clinic-meta-app", slug: "clinic-meta-app", org: "acme", id: "chapp_1" },
|
|
86
92
|
});
|
|
87
93
|
|
|
94
|
+
const knownSchedule = create(ScheduleSchema, {
|
|
95
|
+
metadata: { name: "daily-fee-reminders", slug: "daily-fee-reminders", org: "acme", id: "sch_1" },
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const knownWorkflowInstance = create(WorkflowInstanceSchema, {
|
|
99
|
+
metadata: { name: "deploy-default", slug: "deploy-default", org: "acme", id: "win_1" },
|
|
100
|
+
});
|
|
101
|
+
|
|
88
102
|
// Pending execution (cancellable) vs. an already-terminal one.
|
|
89
103
|
const pendingExecution = create(AgentExecutionSchema, {
|
|
90
104
|
metadata: { id: "aex_run" },
|
|
@@ -110,8 +124,11 @@ let tagCalls: { workflowId: string; versionHash: string; tag: string }[] = [];
|
|
|
110
124
|
let datastoreDeletes: { resourceId: string; force: boolean }[] = [];
|
|
111
125
|
let environmentDeletes: { resourceId: string; force: boolean }[] = [];
|
|
112
126
|
let channelAppDeletes: { resourceId: string; force: boolean }[] = [];
|
|
113
|
-
// agent_channel
|
|
127
|
+
// agent_channel, schedule, and workflow_instance delete by typed ID — no
|
|
128
|
+
// force field exists on their wire contracts.
|
|
114
129
|
let channelDeleteIds: string[] = [];
|
|
130
|
+
let scheduleDeleteIds: string[] = [];
|
|
131
|
+
let workflowInstanceDeleteIds: string[] = [];
|
|
115
132
|
|
|
116
133
|
beforeEach(() => {
|
|
117
134
|
cancelCalls = [];
|
|
@@ -120,6 +137,8 @@ beforeEach(() => {
|
|
|
120
137
|
environmentDeletes = [];
|
|
121
138
|
channelAppDeletes = [];
|
|
122
139
|
channelDeleteIds = [];
|
|
140
|
+
scheduleDeleteIds = [];
|
|
141
|
+
workflowInstanceDeleteIds = [];
|
|
123
142
|
});
|
|
124
143
|
|
|
125
144
|
beforeAll(async () => {
|
|
@@ -216,6 +235,32 @@ beforeAll(async () => {
|
|
|
216
235
|
},
|
|
217
236
|
});
|
|
218
237
|
|
|
238
|
+
router.service(ScheduleQueryController, {
|
|
239
|
+
getByReference: (req) => {
|
|
240
|
+
if (req.slug !== "daily-fee-reminders") throw new ConnectError("schedule not found", Code.NotFound);
|
|
241
|
+
return knownSchedule;
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
router.service(ScheduleCommandController, {
|
|
245
|
+
delete: (req) => {
|
|
246
|
+
scheduleDeleteIds.push(req.value);
|
|
247
|
+
return knownSchedule;
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
router.service(WorkflowInstanceQueryController, {
|
|
252
|
+
getByReference: (req) => {
|
|
253
|
+
if (req.slug !== "deploy-default") throw new ConnectError("workflow instance not found", Code.NotFound);
|
|
254
|
+
return knownWorkflowInstance;
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
router.service(WorkflowInstanceCommandController, {
|
|
258
|
+
delete: (req) => {
|
|
259
|
+
workflowInstanceDeleteIds.push(req.value);
|
|
260
|
+
return knownWorkflowInstance;
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
|
|
219
264
|
router.service(WorkflowQueryController, {
|
|
220
265
|
getByReference: (req) => {
|
|
221
266
|
if (req.slug !== "deploy") throw new ConnectError("workflow not found", Code.NotFound);
|
|
@@ -306,22 +351,51 @@ describe("delete (standard kinds)", () => {
|
|
|
306
351
|
expect(classify(err)?.exitCode).toBe(ExitCode.NotFound);
|
|
307
352
|
});
|
|
308
353
|
|
|
354
|
+
it("deletes a schedule via its typed ID (tears down the Temporal artifact server-side)", async () => {
|
|
355
|
+
const plan = await planDelete(client, "schedule", "daily-fee-reminders", "acme");
|
|
356
|
+
const result = await plan.perform();
|
|
357
|
+
expect(result.status).toBe("success");
|
|
358
|
+
expect(result.message).toBe("Schedule deleted successfully");
|
|
359
|
+
expect(scheduleDeleteIds).toEqual(["sch_1"]);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("deletes a workflow instance via its typed ID", async () => {
|
|
363
|
+
const plan = await planDelete(client, "workflow-instance", "deploy-default", "acme");
|
|
364
|
+
const result = await plan.perform();
|
|
365
|
+
expect(result.status).toBe("success");
|
|
366
|
+
expect(result.message).toBe("Workflow Instance deleted successfully");
|
|
367
|
+
expect(workflowInstanceDeleteIds).toEqual(["win_1"]);
|
|
368
|
+
});
|
|
369
|
+
|
|
309
370
|
it("rejects an unknown type and lists exactly the wired types", async () => {
|
|
310
371
|
const err = await planDelete(client, "bogus", "x", "acme").catch((e) => e);
|
|
311
372
|
expect(classify(err)?.exitCode).toBe(ExitCode.Usage);
|
|
312
373
|
// The list is derived from DELETE_HANDLERS (+ the two special cases), so
|
|
313
|
-
// it must name the wired kinds and never the
|
|
374
|
+
// it must name the wired kinds and never the narrowed ones (#354).
|
|
314
375
|
const message = String(err.message);
|
|
315
|
-
for (const wired of [
|
|
376
|
+
for (const wired of [
|
|
377
|
+
"datastore",
|
|
378
|
+
"environment",
|
|
379
|
+
"agentchannel",
|
|
380
|
+
"channelapp",
|
|
381
|
+
"schedule",
|
|
382
|
+
"workflowinstance",
|
|
383
|
+
"execution",
|
|
384
|
+
"organization",
|
|
385
|
+
]) {
|
|
316
386
|
expect(message).toContain(wired);
|
|
317
387
|
}
|
|
318
388
|
expect(message).not.toContain("session");
|
|
319
389
|
expect(message).not.toContain("oauthapp");
|
|
320
390
|
});
|
|
321
391
|
|
|
322
|
-
it("rejects a kind
|
|
392
|
+
it("rejects a narrowed kind at the verb gate with a usage error", async () => {
|
|
393
|
+
// session's delete verb is narrowed out of the matrix
|
|
394
|
+
// (stigmer/stigmer#354), so the refusal now happens at the verb-support
|
|
395
|
+
// gate ("does not support") rather than the dispatch fall-through.
|
|
323
396
|
const err = await planDelete(client, "session", "ses_1", "acme").catch((e) => e);
|
|
324
397
|
expect(classify(err)?.exitCode).toBe(ExitCode.Usage);
|
|
398
|
+
expect(String(err.message)).toContain("does not support");
|
|
325
399
|
});
|
|
326
400
|
});
|
|
327
401
|
|
|
@@ -21,6 +21,8 @@ import { ChannelAppSchema } from "@stigmer/protos/ai/stigmer/agentic/channelapp/
|
|
|
21
21
|
import { ChannelAppQueryController } from "@stigmer/protos/ai/stigmer/agentic/channelapp/v1/query_pb";
|
|
22
22
|
import { EnvironmentSchema } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/api_pb";
|
|
23
23
|
import { EnvironmentQueryController } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/query_pb";
|
|
24
|
+
import { ScheduleSchema } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/api_pb";
|
|
25
|
+
import { ScheduleQueryController } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/query_pb";
|
|
24
26
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
25
27
|
import { ApiKeySchema } from "@stigmer/protos/ai/stigmer/iam/apikey/v1/api_pb";
|
|
26
28
|
import { ApiKeyQueryController } from "@stigmer/protos/ai/stigmer/iam/apikey/v1/query_pb";
|
|
@@ -142,6 +144,44 @@ const secondChannelApp = create(ChannelAppSchema, {
|
|
|
142
144
|
},
|
|
143
145
|
});
|
|
144
146
|
|
|
147
|
+
// Spec/status populated to exercise every SCHEDULE_TABLE column — and the
|
|
148
|
+
// exact condition the table exists to surface (stigmer/stigmer#352): the
|
|
149
|
+
// owner's switch is ON (spec.enabled) while the platform's failure latch is
|
|
150
|
+
// SET (status.paused_reason), two different states with two different
|
|
151
|
+
// remedies.
|
|
152
|
+
const pausedSchedule = create(ScheduleSchema, {
|
|
153
|
+
apiVersion: "agentic.stigmer.ai/v1",
|
|
154
|
+
kind: "Schedule",
|
|
155
|
+
metadata: { name: "daily-fee-reminders", slug: "daily-fee-reminders", org: "acme", id: "sch_1" },
|
|
156
|
+
spec: {
|
|
157
|
+
cron: "0 9 * * *",
|
|
158
|
+
timeZone: "Asia/Kolkata",
|
|
159
|
+
enabled: true,
|
|
160
|
+
target: {
|
|
161
|
+
case: "agent",
|
|
162
|
+
value: { agentRef: { kind: ApiResourceKind.agent, org: "acme", slug: "clinic-assistant" } },
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
status: { pausedReason: "5 consecutive failed runs", consecutiveFailures: 5 },
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Owner-disabled and never fired: empty status, so the row must derive
|
|
169
|
+
// "active" from an absent paused_reason and print no bogus timestamps.
|
|
170
|
+
const disabledSchedule = create(ScheduleSchema, {
|
|
171
|
+
apiVersion: "agentic.stigmer.ai/v1",
|
|
172
|
+
kind: "Schedule",
|
|
173
|
+
metadata: { name: "weekly-digest", slug: "weekly-digest", org: "acme", id: "sch_2" },
|
|
174
|
+
spec: {
|
|
175
|
+
cron: "30 6 * * 1",
|
|
176
|
+
timeZone: "UTC",
|
|
177
|
+
enabled: false,
|
|
178
|
+
target: {
|
|
179
|
+
case: "agent",
|
|
180
|
+
value: { agentRef: { kind: ApiResourceKind.agent, org: "acme", slug: "reviewer" } },
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
|
|
145
185
|
let backend: Http2Server;
|
|
146
186
|
let client: Stigmer;
|
|
147
187
|
const openSessions = new Set<ServerHttp2Session>();
|
|
@@ -226,6 +266,20 @@ beforeAll(async () => {
|
|
|
226
266
|
return { entries: [knownChannelApp, secondChannelApp] };
|
|
227
267
|
},
|
|
228
268
|
});
|
|
269
|
+
router.service(ScheduleQueryController, {
|
|
270
|
+
get: (req) => {
|
|
271
|
+
if (req.value !== "sch_1") throw new ConnectError("schedule not found", Code.NotFound);
|
|
272
|
+
return pausedSchedule;
|
|
273
|
+
},
|
|
274
|
+
getByReference: (req) => {
|
|
275
|
+
if (req.slug !== "daily-fee-reminders") throw new ConnectError("schedule not found", Code.NotFound);
|
|
276
|
+
return pausedSchedule;
|
|
277
|
+
},
|
|
278
|
+
list: (req) => {
|
|
279
|
+
if (req.org !== "acme") throw new ConnectError("org is required", Code.InvalidArgument);
|
|
280
|
+
return { totalCount: 2, items: [pausedSchedule, disabledSchedule] };
|
|
281
|
+
},
|
|
282
|
+
});
|
|
229
283
|
};
|
|
230
284
|
backend = createHttp2Server(connectNodeAdapter({ routes }));
|
|
231
285
|
backend.on("session", (session) => {
|
|
@@ -341,6 +395,27 @@ describe("get integration", () => {
|
|
|
341
395
|
});
|
|
342
396
|
});
|
|
343
397
|
|
|
398
|
+
it("fetches a schedule by org/slug and renders backend protojson", async () => {
|
|
399
|
+
const { schema, message } = await fetchResource(client, ApiResourceKind.schedule, {
|
|
400
|
+
kind: "ref",
|
|
401
|
+
org: "acme",
|
|
402
|
+
slug: "daily-fee-reminders",
|
|
403
|
+
});
|
|
404
|
+
const rendered = JSON.parse(renderResource(schema, message, "json"));
|
|
405
|
+
expect(rendered).toEqual(toJson(ScheduleSchema, pausedSchedule, { useProtoFieldName: true }));
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("fetches a schedule by ID, exposing the pause diagnosis get exists for", async () => {
|
|
409
|
+
const { message } = await fetchResource(client, ApiResourceKind.schedule, { kind: "id", id: "sch_1" });
|
|
410
|
+
// The full resource is where an operator reads WHY the platform paused
|
|
411
|
+
// a schedule (paused_reason + the streak) — the list table only flags it.
|
|
412
|
+
expect(JSON.parse(renderResource(ScheduleSchema, message, "json"))).toMatchObject({
|
|
413
|
+
metadata: { id: "sch_1" },
|
|
414
|
+
spec: { enabled: true },
|
|
415
|
+
status: { paused_reason: "5 consecutive failed runs", consecutive_failures: 5 },
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
|
|
344
419
|
it("maps a NotFound backend error to ExitCode.NotFound", async () => {
|
|
345
420
|
const err = await fetchResource(client, ApiResourceKind.agent, { kind: "id", id: "missing" }).catch((e) => e);
|
|
346
421
|
const classified = classify(err);
|
|
@@ -348,8 +423,11 @@ describe("get integration", () => {
|
|
|
348
423
|
});
|
|
349
424
|
|
|
350
425
|
it("rejects an unsupported kind with a usage error", async () => {
|
|
351
|
-
// session
|
|
352
|
-
//
|
|
426
|
+
// session's read verbs are narrowed out of the verb matrix
|
|
427
|
+
// (stigmer/stigmer#354): the command layer refuses at the gate, and this
|
|
428
|
+
// resource-layer fallback answers anyone bypassing the gate. Wiring
|
|
429
|
+
// session get requires BOTH the binding and the matrix line — the
|
|
430
|
+
// registry conformance test enforces the pairing.
|
|
353
431
|
const err = await fetchResource(client, ApiResourceKind.session, { kind: "id", id: "x" }).catch((e) => e);
|
|
354
432
|
expect(classify(err)?.exitCode).toBe(ExitCode.Usage);
|
|
355
433
|
});
|
|
@@ -441,10 +519,51 @@ describe("list integration", () => {
|
|
|
441
519
|
expect(JSON.parse(out)).toEqual([toJson(ChannelAppSchema, knownChannelApp, { useProtoFieldName: true })]);
|
|
442
520
|
});
|
|
443
521
|
|
|
522
|
+
it("lists schedules as a table keeping the owner switch and the platform latch distinct", async () => {
|
|
523
|
+
const out = await listResources(client, ApiResourceKind.schedule, "acme", 50, "table");
|
|
524
|
+
for (const header of ["ID", "SLUG", "TARGET", "CRON", "TZ", "ENABLED", "STATE"]) {
|
|
525
|
+
expect(out).toContain(header);
|
|
526
|
+
}
|
|
527
|
+
const rows = out.split("\n");
|
|
528
|
+
// The stigmer/stigmer#352 condition: enabled by the owner, paused by the
|
|
529
|
+
// platform. Collapsing these into one column would hide exactly this.
|
|
530
|
+
const pausedRow = rows.find((line) => line.includes("sch_1"));
|
|
531
|
+
expect(pausedRow).toContain("acme/clinic-assistant");
|
|
532
|
+
expect(pausedRow).toContain("0 9 * * *");
|
|
533
|
+
expect(pausedRow).toContain("Asia/Kolkata");
|
|
534
|
+
expect(pausedRow).toContain("true");
|
|
535
|
+
expect(pausedRow).toContain("paused");
|
|
536
|
+
// Owner-disabled with an empty status: derived state is "active" (no
|
|
537
|
+
// platform latch), and the absent timestamps render nothing bogus.
|
|
538
|
+
const disabledRow = rows.find((line) => line.includes("sch_2"));
|
|
539
|
+
expect(disabledRow).toContain("false");
|
|
540
|
+
expect(disabledRow).toContain("active");
|
|
541
|
+
expect(disabledRow).not.toContain("paused");
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it("lists schedules as JSON matching backend protojson", async () => {
|
|
545
|
+
const out = await listResources(client, ApiResourceKind.schedule, "acme", 50, "json");
|
|
546
|
+
expect(JSON.parse(out)).toEqual([
|
|
547
|
+
toJson(ScheduleSchema, pausedSchedule, { useProtoFieldName: true }),
|
|
548
|
+
toJson(ScheduleSchema, disabledSchedule, { useProtoFieldName: true }),
|
|
549
|
+
]);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it("refuses to list schedules without an org context, before any RPC", async () => {
|
|
553
|
+
// ListSchedulesRequest requires org (min_len 1); an unset cloud context
|
|
554
|
+
// resolves to "". The branch refuses with actionable copy instead of
|
|
555
|
+
// relaying the server's raw validation error.
|
|
556
|
+
const err = await listResources(client, ApiResourceKind.schedule, "", 50, "table").catch((e) => e);
|
|
557
|
+
expect(classify(err)?.exitCode).toBe(ExitCode.Usage);
|
|
558
|
+
expect(String(err.message)).toContain("--org");
|
|
559
|
+
});
|
|
560
|
+
|
|
444
561
|
it("rejects a kind whose list is unwired with a usage error", async () => {
|
|
445
|
-
// agent_share
|
|
446
|
-
// the
|
|
447
|
-
//
|
|
562
|
+
// agent_share's read verbs are narrowed out of the verb matrix
|
|
563
|
+
// (stigmer/stigmer#354): the command layer refuses at the gate, and this
|
|
564
|
+
// resource-layer fallback answers anyone bypassing the gate. Wiring
|
|
565
|
+
// agent_share list requires BOTH the LIST_HANDLERS entry and the matrix
|
|
566
|
+
// line — the registry conformance test enforces the pairing.
|
|
448
567
|
const err = await listResources(client, ApiResourceKind.agent_share, "acme", 50, "json").catch((e) => e);
|
|
449
568
|
expect(classify(err)?.exitCode).toBe(ExitCode.Usage);
|
|
450
569
|
});
|
|
@@ -86,6 +86,11 @@ export function isSessionId(ref: string): boolean {
|
|
|
86
86
|
return hasKindPrefix(ref, idPrefixFor(ApiResourceKind.session));
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/** True if `ref` carries the schedule ID prefix (`sch_…`/`sch-…`). */
|
|
90
|
+
export function isScheduleId(ref: string): boolean {
|
|
91
|
+
return hasKindPrefix(ref, idPrefixFor(ApiResourceKind.schedule));
|
|
92
|
+
}
|
|
93
|
+
|
|
89
94
|
/**
|
|
90
95
|
* True if `ref` starts with ANY known resource-ID prefix (length-agnostic).
|
|
91
96
|
* Mirrors Go's reference.HasResourceIDPrefix: use it to detect *intent* (the
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// `schedule resume` dispatch: clear a platform auto-pause from a schedule
|
|
2
|
+
// and report what was cleared.
|
|
3
|
+
//
|
|
4
|
+
// "Paused" is the platform's latch (status.paused_reason, written after
|
|
5
|
+
// repeated failed runs), distinct from "disabled", the owner's switch
|
|
6
|
+
// (spec.enabled) — two words, two levers (see docs/vocabulary.md). The
|
|
7
|
+
// resume RPC is deliberately the only path that clears the latch: applying
|
|
8
|
+
// a manifest preserves status verbatim, so no declarative workflow can
|
|
9
|
+
// silently un-pause a failing schedule.
|
|
10
|
+
//
|
|
11
|
+
// The pre-state read exists for honest messaging only: the RPC is
|
|
12
|
+
// idempotent and its response always shows a cleared latch, so "resumed"
|
|
13
|
+
// versus "was not paused" can only be told apart by looking first. The
|
|
14
|
+
// resume call is made either way — on the no-op path it doubles as a
|
|
15
|
+
// harmless re-arm of the schedule's clock.
|
|
16
|
+
|
|
17
|
+
import { timestampDate } from "@bufbuild/protobuf/wkt";
|
|
18
|
+
import type { Schedule } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/api_pb";
|
|
19
|
+
import type { Stigmer } from "@stigmer/sdk";
|
|
20
|
+
import { UsageError } from "../errors/index.js";
|
|
21
|
+
import { CommandResult } from "../output/index.js";
|
|
22
|
+
import { parseReference } from "./reference.js";
|
|
23
|
+
|
|
24
|
+
/** The schedule kind's canonical id prefix (proto kind_meta). */
|
|
25
|
+
const SCHEDULE_ID_PREFIX = "sch";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Resolve `ref` (a `sch_…` id, `org/slug`, or bare slug against `org`),
|
|
29
|
+
* clear its platform pause, and describe the outcome.
|
|
30
|
+
*/
|
|
31
|
+
export async function resumeSchedule(stigmer: Stigmer, ref: string, org: string): Promise<CommandResult> {
|
|
32
|
+
const before = await loadSchedule(stigmer, ref, org);
|
|
33
|
+
const id = before.metadata?.id ?? "";
|
|
34
|
+
if (id === "") {
|
|
35
|
+
throw new UsageError(`Schedule '${ref}' has no id — cannot resume`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const wasPausedReason = before.status?.pausedReason ?? "";
|
|
39
|
+
const wasFailures = before.status?.consecutiveFailures ?? 0;
|
|
40
|
+
|
|
41
|
+
const resumed = await stigmer.schedule.resume(id);
|
|
42
|
+
|
|
43
|
+
return describeOutcome(resumed, wasPausedReason, wasFailures);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function loadSchedule(stigmer: Stigmer, ref: string, org: string): Promise<Schedule> {
|
|
47
|
+
const parsed = parseReference(ref, org, SCHEDULE_ID_PREFIX);
|
|
48
|
+
if (parsed.kind === "id") {
|
|
49
|
+
return stigmer.schedule.get(parsed.id);
|
|
50
|
+
}
|
|
51
|
+
if (parsed.org === "") {
|
|
52
|
+
throw new UsageError(
|
|
53
|
+
"organization not set\n\nSet it with:\n stigmer config context set --org <org>\n stigmer schedule resume --org <org> ...",
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
return stigmer.schedule.getByReference({ org: parsed.org, slug: parsed.slug });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Resolve `ref` (a `sch_…` id, `org/slug`, or bare slug against `org`) and
|
|
61
|
+
* fire it once, immediately (project DD-014).
|
|
62
|
+
*
|
|
63
|
+
* The fire is asynchronous: the RPC answers with the schedule, and the run
|
|
64
|
+
* lands on status (last_fire_at / last_execution_id) as it starts — the
|
|
65
|
+
* same observability contract cron fires have. Disabled and platform-paused
|
|
66
|
+
* schedules are refused by the server with teaching copy; the CLI relays
|
|
67
|
+
* the server's message verbatim (never pattern-match refusal text).
|
|
68
|
+
*/
|
|
69
|
+
export async function triggerSchedule(stigmer: Stigmer, ref: string, org: string): Promise<CommandResult> {
|
|
70
|
+
const before = await loadSchedule(stigmer, ref, org);
|
|
71
|
+
const id = before.metadata?.id ?? "";
|
|
72
|
+
if (id === "") {
|
|
73
|
+
throw new UsageError(`Schedule '${ref}' has no id — cannot trigger`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const triggered = await stigmer.schedule.trigger(id);
|
|
77
|
+
|
|
78
|
+
const slug = triggered.metadata?.slug ?? "";
|
|
79
|
+
const name = triggered.metadata?.name || slug;
|
|
80
|
+
const result = CommandResult.success(`Schedule '${name}' triggered — a run is starting`);
|
|
81
|
+
|
|
82
|
+
const section = result.addSection();
|
|
83
|
+
const orgSlug = triggered.metadata?.org ? `${triggered.metadata.org}/${slug}` : slug;
|
|
84
|
+
section.field("Watch it land", `stigmer get schedule ${orgSlug} (status.last_execution_id)`);
|
|
85
|
+
if (triggered.status?.nextFireAt !== undefined) {
|
|
86
|
+
section.field("Next cron fire", timestampDate(triggered.status.nextFireAt).toISOString());
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function describeOutcome(schedule: Schedule, wasPausedReason: string, wasFailures: number): CommandResult {
|
|
93
|
+
const slug = schedule.metadata?.slug ?? "";
|
|
94
|
+
const name = schedule.metadata?.name || slug;
|
|
95
|
+
const wasPaused = wasPausedReason !== "" || wasFailures > 0;
|
|
96
|
+
|
|
97
|
+
const result = CommandResult.success(
|
|
98
|
+
wasPaused ? `Schedule '${name}' resumed` : `Schedule '${name}' was not paused — nothing to clear`,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
if (wasPaused) {
|
|
102
|
+
const section = result.addSection("Cleared");
|
|
103
|
+
if (wasPausedReason !== "") section.field("Pause reason", wasPausedReason);
|
|
104
|
+
if (wasFailures > 0) section.field("Consecutive failures", String(wasFailures));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (schedule.spec?.enabled !== true) {
|
|
108
|
+
result.hint("This schedule is disabled (enabled: false) — it will not fire until you enable it:");
|
|
109
|
+
result.hint(` edit the manifest to 'enabled: true' and run: stigmer apply -f <file>`);
|
|
110
|
+
} else if (schedule.status?.nextFireAt !== undefined) {
|
|
111
|
+
result.addSection().field("Next fire", timestampDate(schedule.status.nextFireAt).toISOString());
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return result;
|
|
115
|
+
}
|