@skrr-ai/cli 0.1.18 → 0.1.20

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 (49) hide show
  1. package/dist/base-command.d.ts +12 -9
  2. package/dist/base-command.js +27 -26
  3. package/dist/commands/commitments/action-proposals/propose.d.ts +32 -0
  4. package/dist/commands/commitments/action-proposals/propose.js +111 -0
  5. package/dist/commands/commitments/create.d.ts +2 -0
  6. package/dist/commands/commitments/create.js +42 -2
  7. package/dist/commands/commitments/delivery.js +1 -0
  8. package/dist/commands/commitments/doctor.js +11 -0
  9. package/dist/commands/commitments/mode.js +1 -0
  10. package/dist/commands/commitments/update.d.ts +2 -0
  11. package/dist/commands/commitments/update.js +40 -5
  12. package/dist/commands/store/browse.d.ts +14 -0
  13. package/dist/commands/store/browse.js +71 -0
  14. package/dist/commands/store/install.d.ts +16 -0
  15. package/dist/commands/store/install.js +54 -0
  16. package/dist/commands/store/releases.d.ts +19 -0
  17. package/dist/commands/store/releases.js +60 -0
  18. package/dist/commands/store/update.d.ts +15 -0
  19. package/dist/commands/store/update.js +69 -0
  20. package/dist/commands/store/updates.d.ts +24 -0
  21. package/dist/commands/store/updates.js +113 -0
  22. package/dist/commands/subscriptions/cancel.d.ts +14 -0
  23. package/dist/commands/subscriptions/cancel.js +47 -0
  24. package/dist/commands/subscriptions/health.d.ts +24 -0
  25. package/dist/commands/subscriptions/health.js +62 -0
  26. package/dist/commands/subscriptions/list.d.ts +9 -0
  27. package/dist/commands/subscriptions/list.js +52 -0
  28. package/dist/commands/subscriptions/status.d.ts +12 -0
  29. package/dist/commands/subscriptions/status.js +49 -0
  30. package/dist/commands/subscriptions/subscribe.d.ts +15 -0
  31. package/dist/commands/subscriptions/subscribe.js +59 -0
  32. package/dist/commands/tasks/complete.d.ts +10 -4
  33. package/dist/commands/tasks/complete.js +7 -5
  34. package/dist/commands/tasks/self-schedule.d.ts +30 -0
  35. package/dist/commands/tasks/self-schedule.js +114 -0
  36. package/dist/commands/triggers/disable.js +3 -1
  37. package/dist/commands/triggers/enable.js +3 -1
  38. package/dist/commands/triggers/rotate-secret.js +5 -0
  39. package/dist/commands/triggers/show.js +3 -1
  40. package/dist/lib/commitments.d.ts +17 -0
  41. package/dist/lib/commitments.js +59 -0
  42. package/dist/lib/node-adapter.js +4 -0
  43. package/dist/lib/task-extras.d.ts +18 -0
  44. package/dist/lib/task-extras.js +19 -1
  45. package/dist/lib/triggers.d.ts +21 -2
  46. package/dist/lib/triggers.js +48 -1
  47. package/dist/node_modules/@skrr-ai/data-provider/index.js +4063 -3902
  48. package/oclif.manifest.json +2475 -1413
  49. package/package.json +1 -1
@@ -6,12 +6,19 @@ type CompletionStatus = 'completed' | 'failed';
6
6
  type TaskCommentResult = Awaited<ReturnType<typeof dataService.addTaskComment>>;
7
7
  type MovedTaskResult = Awaited<ReturnType<typeof dataService.moveTask>>;
8
8
  /** Wire shape of POST /api/tasks/:taskId/complete (OSK-1969). */
9
+ /** One thing a completion did not have. Never a refusal — `done` has none. */
10
+ export interface CompletionAdvisory {
11
+ code: string;
12
+ message: string;
13
+ }
9
14
  export interface CompleteEndpointResponse {
10
15
  task: MovedTaskResult | null;
11
16
  comment: TaskCommentResult | null;
12
17
  commentFailed?: boolean;
13
- deliveryHeld?: boolean;
14
- deliveryReason?: string;
18
+ /** What the completion was missing. `done` has no preconditions, so every
19
+ * condition that used to refuse one now travels with it instead. The task
20
+ * reached the requested terminal state regardless. */
21
+ advisories?: CompletionAdvisory[];
15
22
  }
16
23
  export interface CompleteTransport {
17
24
  /** POST /api/tasks/:taskId/complete — atomic move + TASK_OUTPUT comment. */
@@ -35,8 +42,7 @@ export interface DurableCompletionResult {
35
42
  movedTask: MovedTaskResult | null;
36
43
  moved: boolean;
37
44
  commentFailed: boolean;
38
- deliveryHeld?: boolean;
39
- deliveryReason?: string;
45
+ advisories?: CompletionAdvisory[];
40
46
  /** true when the atomic /complete endpoint handled the finalization. */
41
47
  usedAtomicEndpoint: boolean;
42
48
  }
@@ -194,8 +194,7 @@ async function completeTaskDurably(opts) {
194
194
  movedTask: response.task ?? null,
195
195
  moved: true,
196
196
  commentFailed: response.commentFailed === true,
197
- ...(response.deliveryHeld ? { deliveryHeld: true } : {}),
198
- ...(response.deliveryReason ? { deliveryReason: response.deliveryReason } : {}),
197
+ ...(response.advisories?.length ? { advisories: response.advisories } : {}),
199
198
  usedAtomicEndpoint: true,
200
199
  };
201
200
  }
@@ -611,7 +610,7 @@ class TasksComplete extends base_command_1.BaseCommand {
611
610
  ...(report ? { report } : {}),
612
611
  body,
613
612
  },
614
- ...(result.deliveryHeld ? { deliveryHeld: true, deliveryReason: result.deliveryReason } : {}),
613
+ ...(result.advisories?.length ? { advisories: result.advisories } : {}),
615
614
  };
616
615
  if (flags.json) {
617
616
  const url = movedTask
@@ -634,8 +633,11 @@ class TasksComplete extends base_command_1.BaseCommand {
634
633
  ? `Task ${completedTaskId} was already ${newStatus}; the outcome was amended.`
635
634
  : `Moved task ${completedTaskId} to ${newStatus}`);
636
635
  }
637
- if (result.deliveryHeld) {
638
- this.warn('Delivery is not verified on the default branch; the task was held at in_review instead of done.');
636
+ for (const advisory of result.advisories ?? []) {
637
+ // One warn per unmet condition. Each already reads as an observation
638
+ // the completion happened — so the CLI relays them rather than
639
+ // re-wording them into something that sounds like a failure.
640
+ this.warn(advisory.message);
639
641
  }
640
642
  if (report) {
641
643
  const url = (0, web_url_1.taskUrlFrom)(this.cliConfig.baseURL, movedTask ?? { id: completedTaskId });
@@ -0,0 +1,30 @@
1
+ import { BaseCommand } from '../../base-command';
2
+ /**
3
+ * `skrr tasks self-schedule` — park a task for a self-initiated return.
4
+ *
5
+ * Distinct from `skrr tasks schedule`, and the distinction is the point.
6
+ * `schedule` writes the AUTHOR's calendar (`scheduledFor` + `recurrence`), the
7
+ * thing the scheduler consumes and re-arms and every calendar surface renders.
8
+ * This writes a separate `self_scheduled` trigger row, so an agent working the
9
+ * task can say "come back in two hours" without editing the human's Monday-9am
10
+ * recurrence to mean something the human never asked for.
11
+ *
12
+ * Both can be set at once. Neither erases the other.
13
+ */
14
+ export default class TasksSelfSchedule extends BaseCommand {
15
+ static description: string;
16
+ static examples: string[];
17
+ static args: {
18
+ id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
19
+ };
20
+ static flags: {
21
+ json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
22
+ at: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
23
+ in: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
24
+ note: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
25
+ 'max-per-day': import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
26
+ show: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
27
+ clear: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
28
+ };
29
+ run(): Promise<void>;
30
+ }
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const base_command_1 = require("../../base-command");
5
+ const task_extras_1 = require("../../lib/task-extras");
6
+ /**
7
+ * `skrr tasks self-schedule` — park a task for a self-initiated return.
8
+ *
9
+ * Distinct from `skrr tasks schedule`, and the distinction is the point.
10
+ * `schedule` writes the AUTHOR's calendar (`scheduledFor` + `recurrence`), the
11
+ * thing the scheduler consumes and re-arms and every calendar surface renders.
12
+ * This writes a separate `self_scheduled` trigger row, so an agent working the
13
+ * task can say "come back in two hours" without editing the human's Monday-9am
14
+ * recurrence to mean something the human never asked for.
15
+ *
16
+ * Both can be set at once. Neither erases the other.
17
+ */
18
+ class TasksSelfSchedule extends base_command_1.BaseCommand {
19
+ static description = "Park a task for a self-initiated return — separate from the author's schedule";
20
+ static examples = [
21
+ '<%= config.bin %> tasks self-schedule <task-id> --in 2h --note "check whether the deploy settled"',
22
+ '<%= config.bin %> tasks self-schedule <task-id> --at 2026-09-09T14:00:00Z',
23
+ '<%= config.bin %> tasks self-schedule <task-id> --show',
24
+ '<%= config.bin %> tasks self-schedule <task-id> --clear',
25
+ ];
26
+ static args = {
27
+ id: core_1.Args.string({ description: 'Task ID', required: true, ignoreStdin: true }),
28
+ };
29
+ static flags = {
30
+ json: core_1.Flags.boolean({ description: 'Output as JSON' }),
31
+ at: core_1.Flags.string({ description: 'Absolute return time (ISO-8601 UTC)' }),
32
+ in: core_1.Flags.string({ description: 'Relative return time such as 90m, 2h, 3d' }),
33
+ note: core_1.Flags.string({
34
+ // Was "handed to the agent as the return prompt", which is not true:
35
+ // `task_execute` builds the run's prompt from the Task, so this string is
36
+ // recorded and never read by the run. See `TaskScheduler/selfSchedule.js`.
37
+ description: 'Why you are coming back — recorded on the return for you to read back',
38
+ }),
39
+ 'max-per-day': core_1.Flags.integer({
40
+ description: 'Cap on self-initiated returns per day (1-24, default 4)',
41
+ }),
42
+ show: core_1.Flags.boolean({ description: 'Print the pending return without changing it' }),
43
+ clear: core_1.Flags.boolean({ description: 'Cancel the pending return' }),
44
+ };
45
+ async run() {
46
+ this.requireAuth();
47
+ const { args, flags } = await this.parse(TasksSelfSchedule);
48
+ if (flags.show && flags.clear) {
49
+ this.error('Pass at most one of --show or --clear.', { exit: 1 });
50
+ }
51
+ let result;
52
+ try {
53
+ if (flags.show) {
54
+ result = await task_extras_1.taskSelfScheduleApi.get(args.id);
55
+ }
56
+ else if (flags.clear) {
57
+ result = await task_extras_1.taskSelfScheduleApi.clear(args.id);
58
+ }
59
+ else {
60
+ const runAt = resolveRunAt(flags.at, flags.in);
61
+ if (!runAt) {
62
+ this.error('Pass --at <ISO-8601>, --in <duration>, or one of --show / --clear.', {
63
+ exit: 1,
64
+ });
65
+ }
66
+ result = await task_extras_1.taskSelfScheduleApi.park(args.id, {
67
+ runAt,
68
+ ...(flags.note ? { note: flags.note } : {}),
69
+ ...(flags['max-per-day'] ? { maxFiresPerDay: flags['max-per-day'] } : {}),
70
+ });
71
+ }
72
+ }
73
+ catch (err) {
74
+ this.handleApiError(err);
75
+ }
76
+ if (flags.json) {
77
+ this.log(JSON.stringify(result ?? {}, null, 2));
78
+ return;
79
+ }
80
+ const runAt = result?.runAt;
81
+ if (!runAt) {
82
+ this.log(`Task ${args.id} has no pending return.`);
83
+ return;
84
+ }
85
+ // `pending` is derived server-side: a return that has already fired keeps
86
+ // its instant on the row, and printing that verbatim would report a return
87
+ // as upcoming an hour after it happened.
88
+ const spent = result?.pending === false;
89
+ this.log(`Task ${args.id} ${spent ? 'last returned at' : 'returns at'} ${runAt}`);
90
+ if (result?.note)
91
+ this.log(`Note: ${result.note}`);
92
+ if (!spent && result?.maxFiresPerDay) {
93
+ this.log(`Cap: ${result.maxFiresPerDay} self-initiated returns/day`);
94
+ }
95
+ }
96
+ }
97
+ exports.default = TasksSelfSchedule;
98
+ /** `90m` / `2h` / `3d`, or an absolute instant. Absolute wins when both are given. */
99
+ function resolveRunAt(at, relative) {
100
+ if (at)
101
+ return at;
102
+ if (!relative)
103
+ return undefined;
104
+ const match = /^(\d+)\s*(m|min|mins|h|hr|hrs|d)$/i.exec(relative.trim());
105
+ if (!match)
106
+ return undefined;
107
+ const value = Number(match[1]);
108
+ const unit = match[2].toLowerCase();
109
+ const MS_PER_UNIT = { m: 60_000, h: 3_600_000, d: 86_400_000 };
110
+ // `min`/`mins`/`hr`/`hrs` collapse onto their first character, which is
111
+ // unambiguous across the three units this accepts.
112
+ const ms = value * (MS_PER_UNIT[unit[0]] ?? MS_PER_UNIT.h);
113
+ return new Date(Date.now() + ms).toISOString();
114
+ }
@@ -26,7 +26,9 @@ class TriggersDisable extends base_command_1.BaseCommand {
26
26
  return;
27
27
  }
28
28
  this.log('Disabled trigger:');
29
- (0, triggers_1.renderTriggerSummary)(trigger, (line) => this.log(line));
29
+ (0, triggers_1.renderTriggerSummary)(trigger, (line) => this.log(line), {
30
+ baseURL: this.cliConfig.baseURL,
31
+ });
30
32
  }
31
33
  }
32
34
  exports.default = TriggersDisable;
@@ -26,7 +26,9 @@ class TriggersEnable extends base_command_1.BaseCommand {
26
26
  return;
27
27
  }
28
28
  this.log('Enabled trigger:');
29
- (0, triggers_1.renderTriggerSummary)(trigger, (line) => this.log(line));
29
+ (0, triggers_1.renderTriggerSummary)(trigger, (line) => this.log(line), {
30
+ baseURL: this.cliConfig.baseURL,
31
+ });
30
32
  }
31
33
  }
32
34
  exports.default = TriggersEnable;
@@ -30,6 +30,11 @@ class TriggersRotateSecret extends base_command_1.BaseCommand {
30
30
  return;
31
31
  }
32
32
  this.log(`Secret: ${String(result.secret ?? '-')}`);
33
+ // The secret alone does not let anyone call in. This command was the natural
34
+ // place to learn the endpoint and printed everything except it, so building
35
+ // the integration meant reading the API source or guessing the path.
36
+ this.log(`URL: POST ${(0, triggers_1.webhookIngestUrl)(this.cliConfig.baseURL, args.id)}`);
37
+ this.log('Sign with header x-trigger-signature; send x-trigger-timestamp alongside it.');
33
38
  this.log(String(result.note ?? 'Save this secret now; it cannot be retrieved later.'));
34
39
  if (result.lastRotatedAt)
35
40
  this.log(`Rotated at: ${String(result.lastRotatedAt)}`);
@@ -29,7 +29,9 @@ class TriggersShow extends base_command_1.BaseCommand {
29
29
  this.log(JSON.stringify(trigger, null, 2));
30
30
  return;
31
31
  }
32
- (0, triggers_1.renderTriggerSummary)(trigger, (line) => this.log(line));
32
+ (0, triggers_1.renderTriggerSummary)(trigger, (line) => this.log(line), {
33
+ baseURL: this.cliConfig.baseURL,
34
+ });
33
35
  }
34
36
  }
35
37
  exports.default = TriggersShow;
@@ -260,6 +260,16 @@ export declare const commitmentApi: {
260
260
  object: "commitment_action_proposal_list";
261
261
  data: CommitmentActionProposal[];
262
262
  }>;
263
+ /**
264
+ * Propose a prepared action for a check.
265
+ *
266
+ * The half that was missing. `decide` and `execute` were both reachable, so a
267
+ * headless operator could only ever REACT to proposals the server made — an
268
+ * asymmetry that leaves the domain half-usable in exactly the way the parity
269
+ * rule exists to catch. A locally-running agent that has investigated a
270
+ * commitment and knows what should be done had no way to say so.
271
+ */
272
+ proposeActionProposal: (id: string, body: Record<string, unknown>) => Promise<Record<string, unknown>>;
263
273
  decideActionProposal: (id: string, proposalId: string, decision: "approve" | "reject") => Promise<Record<string, unknown>>;
264
274
  executeActionProposal: (id: string, proposalId: string) => Promise<Record<string, unknown>>;
265
275
  preflight: (id: string, nextRunCount?: number) => Promise<CommitmentPreflightReport>;
@@ -410,6 +420,13 @@ export declare function readCommitmentAgentDirective({ directive, file, }: {
410
420
  * active or terminal state, but importing it must still create a draft.
411
421
  */
412
422
  export declare function normalizeCreateBody(input: Record<string, unknown>): Record<string, unknown>;
423
+ export declare function parseCommitmentConcurrencyLimit(input: string | undefined): number | null | undefined;
424
+ export declare function commitmentConcurrencyEnvelopePatch({ rawValue, reason, autonomous, }: {
425
+ rawValue?: string;
426
+ reason?: string;
427
+ autonomous?: boolean;
428
+ }): Record<string, unknown> | undefined;
429
+ export declare function describeCommitmentConcurrency(commitment: CommitmentView): string;
413
430
  export declare function parseIntervalToMs(input: string): number;
414
431
  /**
415
432
  * Assemble the `watchedSources` array from CLI flags — one place, so `create`
@@ -6,6 +6,9 @@ exports.authoringAutonomyModeForStored = authoringAutonomyModeForStored;
6
6
  exports.commitmentAutonomyLabel = commitmentAutonomyLabel;
7
7
  exports.readCommitmentAgentDirective = readCommitmentAgentDirective;
8
8
  exports.normalizeCreateBody = normalizeCreateBody;
9
+ exports.parseCommitmentConcurrencyLimit = parseCommitmentConcurrencyLimit;
10
+ exports.commitmentConcurrencyEnvelopePatch = commitmentConcurrencyEnvelopePatch;
11
+ exports.describeCommitmentConcurrency = describeCommitmentConcurrency;
9
12
  exports.parseIntervalToMs = parseIntervalToMs;
10
13
  exports.buildWatchedSources = buildWatchedSources;
11
14
  exports.describeEvidenceHealth = describeEvidenceHealth;
@@ -153,6 +156,16 @@ exports.commitmentApi = {
153
156
  checks: (id, limit) => data_provider_1.request.get((0, triggers_1.withQuery)(`${base(id)}/checks`, { limit })),
154
157
  actionHistory: (id, limit) => data_provider_1.request.get((0, triggers_1.withQuery)(`${base(id)}/action-history`, { limit })),
155
158
  actionProposals: (id, query = {}) => data_provider_1.request.get((0, triggers_1.withQuery)(`${base(id)}/action-proposals`, query)),
159
+ /**
160
+ * Propose a prepared action for a check.
161
+ *
162
+ * The half that was missing. `decide` and `execute` were both reachable, so a
163
+ * headless operator could only ever REACT to proposals the server made — an
164
+ * asymmetry that leaves the domain half-usable in exactly the way the parity
165
+ * rule exists to catch. A locally-running agent that has investigated a
166
+ * commitment and knows what should be done had no way to say so.
167
+ */
168
+ proposeActionProposal: (id, body) => data_provider_1.request.post(`${base(id)}/action-proposals`, body),
156
169
  decideActionProposal: (id, proposalId, decision) => data_provider_1.request.post(`${base(id)}/action-proposals/${encodeURIComponent(proposalId)}/decision`, {
157
170
  decision,
158
171
  }),
@@ -321,6 +334,51 @@ function normalizeCreateBody(input) {
321
334
  }
322
335
  return out;
323
336
  }
337
+ function parseCommitmentConcurrencyLimit(input) {
338
+ if (input === undefined)
339
+ return undefined;
340
+ const raw = String(input).trim().toLowerCase();
341
+ if (['unlimited', 'unbounded', 'none', 'null'].includes(raw))
342
+ return null;
343
+ const value = Number(raw);
344
+ if (!Number.isSafeInteger(value) || value < 1 || value > 100) {
345
+ throw new Error('--max-concurrent-runs must be "unlimited" or an integer from 1 to 100.');
346
+ }
347
+ return value;
348
+ }
349
+ function commitmentConcurrencyEnvelopePatch({ rawValue, reason, autonomous = process.env.OVERSKY_AUTONOMOUS_SESSION === '1', }) {
350
+ const value = parseCommitmentConcurrencyLimit(rawValue);
351
+ const normalizedReason = reason?.trim();
352
+ if (value === undefined) {
353
+ if (normalizedReason) {
354
+ throw new Error('--concurrency-reason requires --max-concurrent-runs.');
355
+ }
356
+ return undefined;
357
+ }
358
+ if (autonomous && value !== null) {
359
+ throw new Error('An autonomous Agent may suggest a finite Commitment concurrency cap, but cannot activate owner policy. Omit the field or use unlimited; an owner can set the cap later.');
360
+ }
361
+ if (value !== null && !normalizedReason) {
362
+ throw new Error('A finite --max-concurrent-runs value requires --concurrency-reason.');
363
+ }
364
+ return {
365
+ maxConcurrentRuns: value,
366
+ ...(normalizedReason ? { maxConcurrentRunsReason: normalizedReason } : {}),
367
+ };
368
+ }
369
+ function describeCommitmentConcurrency(commitment) {
370
+ const envelope = commitment.policy?.autonomyEnvelope;
371
+ const limit = envelope?.maxConcurrentRuns;
372
+ const decision = envelope?.maxConcurrentRunsDecision;
373
+ if (limit == null) {
374
+ const source = decision?.mode === 'unlimited' ? decision.source : 'default';
375
+ return `unlimited (${source})`;
376
+ }
377
+ const authority = decision?.authority || 'unknown';
378
+ const source = decision?.source || 'legacy';
379
+ const review = authority === 'unknown' ? ' — review required' : '';
380
+ return `${limit} (${authority}/${source}${review})`;
381
+ }
324
382
  /**
325
383
  * Parse a human interval ("15m", "6h", "1d", or a bare number of minutes) into
326
384
  * milliseconds. Throws on unparseable input, on trailing junk, and on values
@@ -672,6 +730,7 @@ latestCheck) {
672
730
  log(`Next check:${` ${commitment.cadence.nextCheckAt}`}`);
673
731
  log(`Mode: ${describeAutonomyLine(commitment, latestCheck)}`);
674
732
  log(`Delivery: ${(0, commitment_product_1.deliveryLabel)(commitment.delivery?.mode)}${commitment.delivery?.repo ? ` · ${commitment.delivery.repo}` : ''}${commitment.delivery?.baseBranch ? ` → ${commitment.delivery.baseBranch}` : ''}`);
733
+ log(`Concurrency: ${describeCommitmentConcurrency(commitment)}`);
675
734
  const criteria = commitment.target?.successCriteria || [];
676
735
  const evidenceHealth = latestCheck?.inputs?.evidenceHealth;
677
736
  const evidenceLine = describeEvidenceHealth(evidenceHealth, criteria.length);
@@ -79,6 +79,10 @@ function createNodeAdapter(opts = {}) {
79
79
  const { token } = currentCredential();
80
80
  const headers = {
81
81
  Accept: 'application/json',
82
+ // Server-side authoring provenance. This is attribution, not authority;
83
+ // owner/admin decisions are still authenticated and validated at the
84
+ // route boundary.
85
+ 'X-Skrr-Client': 'cli',
82
86
  ...(extra ?? {}),
83
87
  };
84
88
  if (token) {
@@ -32,6 +32,24 @@ export declare const taskLookupApi: {
32
32
  treePosition: (id: string, body: Json) => Promise<Json>;
33
33
  createAndStart: (body: Json) => Promise<Json>;
34
34
  };
35
+ /**
36
+ * Self-schedule — "come back to this task at T", owned by whoever is working it.
37
+ *
38
+ * A different resource from `/schedule`, not a variant of it. `/schedule` writes
39
+ * `Task.scheduledFor` + `recurrence` (the author's calendar, which the scheduler
40
+ * consumes and re-arms); this writes a `kind:'self_scheduled'` trigger row. Both
41
+ * can be set on one task and neither erases the other, so a headless agent can
42
+ * defer itself without editing the human's recurrence to mean something the
43
+ * human never asked for.
44
+ *
45
+ * PUT, not POST: a task has at most one pending return and parking again
46
+ * replaces it.
47
+ */
48
+ export declare const taskSelfScheduleApi: {
49
+ get: (id: string) => Promise<Json>;
50
+ park: (id: string, body: Json) => Promise<Json>;
51
+ clear: (id: string) => Promise<Json>;
52
+ };
35
53
  /**
36
54
  * The completion URL as a STRING.
37
55
  *
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.taskChatApi = exports.taskLookupApi = exports.taskWorkflowApi = void 0;
3
+ exports.taskChatApi = exports.taskSelfScheduleApi = exports.taskLookupApi = exports.taskWorkflowApi = void 0;
4
4
  exports.taskCompletePath = taskCompletePath;
5
5
  /**
6
6
  * task-extras.ts — the `/api/tasks` endpoints that have no `dataService.*`
@@ -51,6 +51,24 @@ exports.taskLookupApi = {
51
51
  treePosition: (id, body) => data_provider_1.request.patch(`${base(id)}/tree-position`, body),
52
52
  createAndStart: (body) => data_provider_1.request.post(`${MOUNT}/create-and-start`, body),
53
53
  };
54
+ /**
55
+ * Self-schedule — "come back to this task at T", owned by whoever is working it.
56
+ *
57
+ * A different resource from `/schedule`, not a variant of it. `/schedule` writes
58
+ * `Task.scheduledFor` + `recurrence` (the author's calendar, which the scheduler
59
+ * consumes and re-arms); this writes a `kind:'self_scheduled'` trigger row. Both
60
+ * can be set on one task and neither erases the other, so a headless agent can
61
+ * defer itself without editing the human's recurrence to mean something the
62
+ * human never asked for.
63
+ *
64
+ * PUT, not POST: a task has at most one pending return and parking again
65
+ * replaces it.
66
+ */
67
+ exports.taskSelfScheduleApi = {
68
+ get: (id) => data_provider_1.request.get(`${base(id)}/self-schedule`),
69
+ park: (id, body) => data_provider_1.request.put(`${base(id)}/self-schedule`, body),
70
+ clear: (id) => data_provider_1.request.delete(`${base(id)}/self-schedule`),
71
+ };
54
72
  /**
55
73
  * The completion URL as a STRING.
56
74
  *
@@ -18,7 +18,7 @@ export declare const TRIGGER_SCOPE_KINDS: readonly ["agent", "group_chat_subscri
18
18
  * capability was live and unreachable. `watch_until` and `sensor_poll` had been
19
19
  * in that state for longer.
20
20
  */
21
- export declare const TRIGGER_EXECUTOR_MODES: readonly ["send_message", "run_only", "enqueue_group_chat_agent_turn", "enqueue_assistant_turn", "enqueue_space_assistant_turn", "proactive_orchestrator_check", "auto_dream_orchestrator", "task_schedule_fire", "goal_planner_tick", "morning_briefing", "builtin_action", "watch_until", "sensor_poll", "external_work_task", "notify_user", "space_cycle_rollover"];
21
+ export declare const TRIGGER_EXECUTOR_MODES: readonly ["send_message", "run_only", "enqueue_group_chat_agent_turn", "enqueue_assistant_turn", "enqueue_space_assistant_turn", "proactive_orchestrator_check", "auto_dream_orchestrator", "task_schedule_fire", "goal_planner_tick", "morning_briefing", "builtin_action", "task_execute", "watch_until", "sensor_poll", "external_work_task", "notify_user", "space_cycle_rollover"];
22
22
  export declare const TRIGGER_EVENT_FILTER_MODES: readonly ["equals", "contains", "in_set"];
23
23
  export declare const TRIGGER_CONCURRENCY_POLICIES: readonly ["coalesce_if_active", "allow_parallel"];
24
24
  export declare const TRIGGER_FORM_RESPONSE_MODES: readonly ["thank_you_page", "redirect_url", "run_complete"];
@@ -95,6 +95,16 @@ export interface TriggerDefinition {
95
95
  chatMessage?: Record<string, unknown>;
96
96
  audit?: Record<string, unknown>;
97
97
  webhook?: Record<string, unknown>;
98
+ /**
99
+ * Server-DERIVED: maintained by a managed producer, so the mutation routes
100
+ * refuse to edit or remove it. Optional because a server predating the field
101
+ * omits it, which correctly reads as "not producer-owned".
102
+ *
103
+ * Declared here rather than inferred, and never re-derived from an id prefix
104
+ * or an execution mode — the route owns that policy and a local copy is free
105
+ * to disagree with the route that does the refusing.
106
+ */
107
+ producerOwned?: boolean;
98
108
  createdAt?: string;
99
109
  updatedAt?: string;
100
110
  }
@@ -327,7 +337,16 @@ export declare function armedStateLabel(enabled: boolean | undefined): string;
327
337
  * parentheses for scripts.
328
338
  */
329
339
  export declare function writeModeLabel(writeMode: string): string;
330
- export declare function renderTriggerSummary(trigger: TriggerDefinition, log: (line: string) => void): void;
340
+ /**
341
+ * Where an external caller POSTs to fire this trigger.
342
+ *
343
+ * Same origin as every other `/api/...` call the CLI makes, so it is built from
344
+ * the configured base URL rather than guessed.
345
+ */
346
+ export declare function webhookIngestUrl(baseURL: string, triggerId: string): string;
347
+ export declare function renderTriggerSummary(trigger: TriggerDefinition, log: (line: string) => void, options?: {
348
+ baseURL?: string;
349
+ }): void;
331
350
  export declare function renderLegacyRuns(runs: LegacyTriggerRun[], log: (line: string) => void): void;
332
351
  export declare function renderFireAudit(fires: TriggerFireRecord[], log: (line: string) => void): void;
333
352
  /**
@@ -32,6 +32,7 @@ exports.renderTriggerList = renderTriggerList;
32
32
  exports.formatDuplicateSubscriptionWarning = formatDuplicateSubscriptionWarning;
33
33
  exports.armedStateLabel = armedStateLabel;
34
34
  exports.writeModeLabel = writeModeLabel;
35
+ exports.webhookIngestUrl = webhookIngestUrl;
35
36
  exports.renderTriggerSummary = renderTriggerSummary;
36
37
  exports.renderLegacyRuns = renderLegacyRuns;
37
38
  exports.renderFireAudit = renderFireAudit;
@@ -86,6 +87,11 @@ exports.TRIGGER_EXECUTOR_MODES = [
86
87
  'goal_planner_tick',
87
88
  'morning_briefing',
88
89
  'builtin_action',
90
+ // The author-visible way to run a Task from a trigger. Its internal sibling
91
+ // `task_schedule_fire` sits two lines up and the server refuses it from any
92
+ // non-managed caller, so without this entry a headless operator could author
93
+ // every kind of task watcher except one that does the task.
94
+ 'task_execute',
89
95
  // User-authorable capabilities the server allows on the agent scope.
90
96
  'watch_until',
91
97
  'sensor_poll',
@@ -784,11 +790,40 @@ function writeModeLabel(writeMode) {
784
790
  return writeMode;
785
791
  }
786
792
  }
787
- function renderTriggerSummary(trigger, log) {
793
+ /**
794
+ * Kinds that expose a signed inbound HTTP channel. Mirrors `WEBHOOK_KINDS` in
795
+ * `api/server/routes/triggers.js`, which is what actually decides — this copy
796
+ * only chooses whether to PRINT a URL, so being behind costs a missing hint
797
+ * rather than a wrong answer.
798
+ */
799
+ const WEBHOOK_INGEST_KINDS = new Set(['on_demand', 'form', 'chat_message']);
800
+ /**
801
+ * Where an external caller POSTs to fire this trigger.
802
+ *
803
+ * Same origin as every other `/api/...` call the CLI makes, so it is built from
804
+ * the configured base URL rather than guessed.
805
+ */
806
+ function webhookIngestUrl(baseURL, triggerId) {
807
+ return `${String(baseURL).replace(/\/+$/, '')}/api/triggers/webhook/${encodeURIComponent(triggerId)}`;
808
+ }
809
+ function renderTriggerSummary(trigger, log, options = {}) {
788
810
  log(`ID: ${trigger.id ?? '-'}`);
789
811
  log(`Kind: ${trigger.kind ?? '-'}`);
790
812
  log(`Armed: ${armedStateLabel(trigger.enabled)}`);
791
813
  log(`Scope: ${scopeLabel(trigger)}`);
814
+ /*
815
+ * Say when the row is not yours to change.
816
+ *
817
+ * `producerOwned` is derived by the server and reported on every trigger read.
818
+ * Without it a headless operator reads `skrr triggers show task-schedule:<id>`,
819
+ * sees an ordinary-looking trigger, runs `triggers delete`, and learns from a
820
+ * 409 — for a row that was never editable. Same argument the `Contract:` /
821
+ * `Writes:` lines below were added under: a trigger with a property this
822
+ * consequential should state it at the moment it is read.
823
+ */
824
+ if (trigger.producerOwned === true) {
825
+ log('Managed: by the platform — not editable here; change the object that owns it');
826
+ }
792
827
  if (trigger.label)
793
828
  log(`Label: ${trigger.label}`);
794
829
  if (trigger.execution?.mode)
@@ -809,6 +844,18 @@ function renderTriggerSummary(trigger, log) {
809
844
  if (trigger.webhook?.configured !== undefined) {
810
845
  log(`Webhook: ${trigger.webhook.configured ? 'configured' : 'not configured'}`);
811
846
  }
847
+ /*
848
+ * The endpoint, not just "configured".
849
+ *
850
+ * An `on_demand` trigger exists so something outside the platform can fire it,
851
+ * and the CLI could report that a secret was set while never saying where to
852
+ * send it — the one fact the integration cannot be built without. Printed for
853
+ * every webhook-bearing kind, whether or not a secret has been minted yet, so
854
+ * "what is my URL" never requires a rotate.
855
+ */
856
+ if (options.baseURL && trigger.id && WEBHOOK_INGEST_KINDS.has(String(trigger.kind))) {
857
+ log(`Webhook URL: POST ${webhookIngestUrl(options.baseURL, trigger.id)}`);
858
+ }
812
859
  }
813
860
  function renderLegacyRuns(runs, log) {
814
861
  if (runs.length === 0) {