@skrr-ai/cli 0.1.36 → 0.1.37

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 (152) hide show
  1. package/README.md +17 -4
  2. package/dist/commands/agents/actions/create.d.ts +5 -0
  3. package/dist/commands/agents/actions/create.js +26 -0
  4. package/dist/commands/agents/actions/update.d.ts +5 -0
  5. package/dist/commands/agents/actions/update.js +24 -0
  6. package/dist/commands/agents/api-actions/add.d.ts +41 -0
  7. package/dist/commands/agents/api-actions/add.js +202 -0
  8. package/dist/commands/agents/api-actions/index.d.ts +28 -0
  9. package/dist/commands/agents/api-actions/index.js +77 -0
  10. package/dist/commands/agents/api-actions/remove.d.ts +14 -0
  11. package/dist/commands/agents/api-actions/remove.js +36 -0
  12. package/dist/commands/agents/avatar/default/apply.d.ts +25 -0
  13. package/dist/commands/agents/avatar/default/apply.js +56 -0
  14. package/dist/commands/agents/avatar/default/index.d.ts +18 -0
  15. package/dist/commands/agents/avatar/default/index.js +44 -0
  16. package/dist/commands/agents/avatar/default/set.d.ts +23 -0
  17. package/dist/commands/agents/avatar/default/set.js +95 -0
  18. package/dist/commands/agents/avatar/library/delete.d.ts +20 -0
  19. package/dist/commands/agents/avatar/library/delete.js +47 -0
  20. package/dist/commands/agents/avatar/library/index.d.ts +16 -0
  21. package/dist/commands/agents/avatar/library/index.js +56 -0
  22. package/dist/commands/agents/avatar/presets.d.ts +19 -0
  23. package/dist/commands/agents/avatar/presets.js +56 -0
  24. package/dist/commands/agents/categories/create.d.ts +14 -0
  25. package/dist/commands/agents/categories/create.js +46 -0
  26. package/dist/commands/agents/categories/delete.d.ts +13 -0
  27. package/dist/commands/agents/categories/delete.js +33 -0
  28. package/dist/commands/agents/categories/index.d.ts +20 -0
  29. package/dist/commands/agents/categories/index.js +82 -0
  30. package/dist/commands/agents/categories/update.d.ts +22 -0
  31. package/dist/commands/agents/categories/update.js +59 -0
  32. package/dist/commands/agents/create.d.ts +10 -0
  33. package/dist/commands/agents/create.js +57 -3
  34. package/dist/commands/agents/ensure-default.d.ts +27 -0
  35. package/dist/commands/agents/ensure-default.js +65 -0
  36. package/dist/commands/agents/mute.d.ts +16 -0
  37. package/dist/commands/agents/mute.js +38 -0
  38. package/dist/commands/agents/pin.d.ts +16 -0
  39. package/dist/commands/agents/pin.js +38 -0
  40. package/dist/commands/agents/tools/auth.d.ts +19 -0
  41. package/dist/commands/agents/tools/auth.js +41 -0
  42. package/dist/commands/agents/tools/calls.d.ts +18 -0
  43. package/dist/commands/agents/tools/calls.js +54 -0
  44. package/dist/commands/agents/tools/index.d.ts +20 -0
  45. package/dist/commands/agents/tools/index.js +77 -0
  46. package/dist/commands/agents/unmute.d.ts +16 -0
  47. package/dist/commands/agents/unmute.js +38 -0
  48. package/dist/commands/agents/unpin.d.ts +16 -0
  49. package/dist/commands/agents/unpin.js +38 -0
  50. package/dist/commands/agents/update.d.ts +12 -0
  51. package/dist/commands/agents/update.js +64 -3
  52. package/dist/commands/followups/act.d.ts +22 -0
  53. package/dist/commands/followups/act.js +102 -0
  54. package/dist/commands/followups/cancel.d.ts +17 -0
  55. package/dist/commands/followups/cancel.js +40 -0
  56. package/dist/commands/followups/checkin.d.ts +27 -0
  57. package/dist/commands/followups/checkin.js +107 -0
  58. package/dist/commands/followups/list.d.ts +24 -0
  59. package/dist/commands/followups/list.js +90 -0
  60. package/dist/commands/followups/remind.d.ts +26 -0
  61. package/dist/commands/followups/remind.js +106 -0
  62. package/dist/commands/followups/reschedule.d.ts +15 -0
  63. package/dist/commands/followups/reschedule.js +48 -0
  64. package/dist/commands/followups/resolve.d.ts +23 -0
  65. package/dist/commands/followups/resolve.js +54 -0
  66. package/dist/commands/followups/show.d.ts +12 -0
  67. package/dist/commands/followups/show.js +45 -0
  68. package/dist/commands/followups/watch.d.ts +51 -0
  69. package/dist/commands/followups/watch.js +207 -0
  70. package/dist/commands/goals/labels/add.js +19 -2
  71. package/dist/commands/goals/labels/remove.js +22 -2
  72. package/dist/commands/labels/list.d.ts +24 -1
  73. package/dist/commands/labels/list.js +62 -19
  74. package/dist/commands/memory/governance/index.d.ts +21 -0
  75. package/dist/commands/memory/governance/index.js +64 -0
  76. package/dist/commands/memory/governance/set.d.ts +24 -0
  77. package/dist/commands/memory/governance/set.js +95 -0
  78. package/dist/commands/memory/provenance.d.ts +18 -0
  79. package/dist/commands/memory/provenance.js +66 -0
  80. package/dist/commands/spaces/create.d.ts +3 -0
  81. package/dist/commands/spaces/create.js +32 -2
  82. package/dist/commands/spaces/labels/add.js +19 -2
  83. package/dist/commands/spaces/labels/remove.js +22 -2
  84. package/dist/commands/spaces/show.js +2 -0
  85. package/dist/commands/spaces/update.d.ts +5 -0
  86. package/dist/commands/spaces/update.js +41 -4
  87. package/dist/commands/spaces/workflows/lifecycle-diagnostics.d.ts +11 -0
  88. package/dist/commands/spaces/workflows/lifecycle-diagnostics.js +45 -0
  89. package/dist/commands/tasks/complete.d.ts +1 -0
  90. package/dist/commands/tasks/complete.js +8 -1
  91. package/dist/commands/tasks/create.d.ts +3 -0
  92. package/dist/commands/tasks/create.js +34 -2
  93. package/dist/commands/tasks/expectations/assess.d.ts +11 -0
  94. package/dist/commands/tasks/expectations/assess.js +85 -14
  95. package/dist/commands/tasks/expectations.js +19 -2
  96. package/dist/commands/tasks/labels/attach.js +20 -3
  97. package/dist/commands/tasks/labels/detach.js +21 -3
  98. package/dist/commands/tasks/lifecycle/attach.d.ts +14 -0
  99. package/dist/commands/tasks/lifecycle/attach.js +47 -0
  100. package/dist/commands/tasks/lifecycle/definitions.d.ts +11 -0
  101. package/dist/commands/tasks/lifecycle/definitions.js +40 -0
  102. package/dist/commands/tasks/lifecycle/detach.d.ts +12 -0
  103. package/dist/commands/tasks/lifecycle/detach.js +32 -0
  104. package/dist/commands/tasks/lifecycle/history.d.ts +13 -0
  105. package/dist/commands/tasks/lifecycle/history.js +38 -0
  106. package/dist/commands/tasks/lifecycle/migrate.d.ts +12 -0
  107. package/dist/commands/tasks/lifecycle/migrate.js +29 -0
  108. package/dist/commands/tasks/lifecycle/reconcile.d.ts +11 -0
  109. package/dist/commands/tasks/lifecycle/reconcile.js +27 -0
  110. package/dist/commands/tasks/lifecycle/reopen.d.ts +13 -0
  111. package/dist/commands/tasks/lifecycle/reopen.js +34 -0
  112. package/dist/commands/tasks/lifecycle/show.d.ts +11 -0
  113. package/dist/commands/tasks/lifecycle/show.js +27 -0
  114. package/dist/commands/tasks/lifecycle/start.d.ts +12 -0
  115. package/dist/commands/tasks/lifecycle/start.js +30 -0
  116. package/dist/commands/tasks/lifecycle/transition.d.ts +15 -0
  117. package/dist/commands/tasks/lifecycle/transition.js +78 -0
  118. package/dist/commands/tasks/move.d.ts +1 -0
  119. package/dist/commands/tasks/move.js +31 -0
  120. package/dist/commands/tasks/result/submit.js +11 -1
  121. package/dist/commands/tasks/review-queue.d.ts +29 -0
  122. package/dist/commands/tasks/review-queue.js +165 -0
  123. package/dist/commands/tasks/show.js +42 -0
  124. package/dist/commands/tasks/update.d.ts +1 -0
  125. package/dist/commands/tasks/update.js +47 -0
  126. package/dist/lib/agent-actions.d.ts +36 -10
  127. package/dist/lib/agent-actions.js +63 -9
  128. package/dist/lib/agent-config.d.ts +11 -0
  129. package/dist/lib/agent-config.js +85 -3
  130. package/dist/lib/agent-pin-mute.d.ts +24 -0
  131. package/dist/lib/agent-pin-mute.js +17 -0
  132. package/dist/lib/agent-visual-refs.d.ts +6 -0
  133. package/dist/lib/agent-visual-refs.js +142 -0
  134. package/dist/lib/first-party-harness-agent.js +7 -0
  135. package/dist/lib/followups.d.ts +107 -0
  136. package/dist/lib/followups.js +102 -0
  137. package/dist/lib/label-ref.d.ts +95 -0
  138. package/dist/lib/label-ref.js +163 -0
  139. package/dist/lib/label-scope.d.ts +2 -2
  140. package/dist/lib/label-scope.js +3 -3
  141. package/dist/lib/task-closure.d.ts +32 -0
  142. package/dist/lib/task-closure.js +106 -0
  143. package/dist/lib/task-lifecycle-output.d.ts +14 -0
  144. package/dist/lib/task-lifecycle-output.js +101 -0
  145. package/dist/lib/tasks.d.ts +3 -0
  146. package/dist/lib/tasks.js +31 -0
  147. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessTrust.js +4 -0
  148. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessTrust.js +4 -0
  149. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  150. package/dist/node_modules/@skrr-ai/data-provider/index.js +5079 -4546
  151. package/oclif.manifest.json +20216 -16002
  152. package/package.json +16 -4
@@ -0,0 +1,6 @@
1
+ import type { AgentAvatarRef, AgentSceneBackground } from '@skrr-ai/data-provider';
2
+ /** Preset ids this build ships, for an error message that can be acted on. */
3
+ export declare function avatarPresetIds(): string[];
4
+ export declare function sceneBackgroundPresetIds(): string[];
5
+ export declare function parseAvatarRefFlag(value: string): AgentAvatarRef;
6
+ export declare function parseSceneBackgroundFlag(value: string): AgentSceneBackground;
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.avatarPresetIds = avatarPresetIds;
4
+ exports.sceneBackgroundPresetIds = sceneBackgroundPresetIds;
5
+ exports.parseAvatarRefFlag = parseAvatarRefFlag;
6
+ exports.parseSceneBackgroundFlag = parseSceneBackgroundFlag;
7
+ /**
8
+ * The two "how does this agent look" fields, parsed from a flag.
9
+ *
10
+ * WHY A PARSER AND NOT A PLAIN STRING
11
+ * ───────────────────────────────────
12
+ * `agent.avatar` and `agent.sceneBackground` are DISCRIMINATED UNIONS on the
13
+ * wire (`packages/api/src/agents/validation.ts`), because a face and a backdrop
14
+ * each have several possible origins — a built-in preset, the caller's own
15
+ * uploaded asset, an explicit "the platform default", a raw storage path — and
16
+ * a bare string cannot say which. `agentUpdateSchema.parse(req.body)` runs with
17
+ * no coercion, so a string reaches the server and is rejected.
18
+ *
19
+ * `--scene-background` shipped as a pass-through string for exactly that
20
+ * reason, which made it a flag with NO value that could ever succeed: every
21
+ * invocation was a 400 naming a zod path rather than the flag. The fix is to
22
+ * own the shorthand here, once, so both fields read the same way and neither
23
+ * can regress into a raw string again.
24
+ *
25
+ * THE SHORTHAND
26
+ * ─────────────
27
+ * default → { type: 'default' }
28
+ * preset:<id> → { type: 'preset', id }
29
+ * asset:<id> → { type: 'asset', id } (avatar only)
30
+ * image:<filepath>@<source> → { type: 'image', filepath, source }
31
+ * {"type":"preset","id":"..."} → parsed verbatim, the escape hatch
32
+ *
33
+ * A bare id is deliberately NOT accepted. `preset:studio` and an asset id are
34
+ * different namespaces, and guessing between them would make the failure mode
35
+ * "your avatar silently became someone else's" rather than a parse error.
36
+ */
37
+ const data_provider_1 = require("@skrr-ai/data-provider");
38
+ /** `image:path/to.png@s3` — the `@` is the LAST one, so a path may contain `@`. */
39
+ function parseImage(rest, flag) {
40
+ const at = rest.lastIndexOf('@');
41
+ if (at <= 0 || at === rest.length - 1) {
42
+ throw new Error(`${flag}: "image:" needs a storage source — write image:<filepath>@<source>, ` +
43
+ 'for example image:images/123/face.png@s3.');
44
+ }
45
+ return { filepath: rest.slice(0, at), source: rest.slice(at + 1) };
46
+ }
47
+ /**
48
+ * A JSON object escape hatch, so a shape this grammar has not learned yet is
49
+ * still expressible without falling back to `--from-json` for the whole agent.
50
+ * Returns `undefined` when the value is not JSON at all, so the caller can go on
51
+ * to the shorthand rather than reporting a JSON syntax error for `preset:foo`.
52
+ */
53
+ function parseJsonObject(value) {
54
+ const trimmed = value.trim();
55
+ if (!trimmed.startsWith('{'))
56
+ return undefined;
57
+ try {
58
+ const parsed = JSON.parse(trimmed);
59
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : undefined;
60
+ }
61
+ catch {
62
+ return undefined;
63
+ }
64
+ }
65
+ /** Preset ids this build ships, for an error message that can be acted on. */
66
+ function avatarPresetIds() {
67
+ return data_provider_1.AGENT_AVATAR_PRESETS.map((preset) => preset.id);
68
+ }
69
+ function sceneBackgroundPresetIds() {
70
+ return data_provider_1.AGENT_SCENE_BACKGROUND_PRESETS.map((preset) => preset.id);
71
+ }
72
+ /**
73
+ * Name a handful of valid ids rather than all of them.
74
+ *
75
+ * There are ~60 avatar presets. An error that prints every one buries the
76
+ * sentence that says what went wrong, so show a few and point at the command
77
+ * that lists the rest.
78
+ */
79
+ function someOf(ids, limit = 6) {
80
+ const head = ids.slice(0, limit).join(', ');
81
+ return ids.length > limit ? `${head}, … (${ids.length} total)` : head;
82
+ }
83
+ function parseAvatarRefFlag(value) {
84
+ const json = parseJsonObject(value);
85
+ if (json)
86
+ return json;
87
+ const raw = value.trim();
88
+ if (raw === 'default')
89
+ return { type: 'default' };
90
+ const colon = raw.indexOf(':');
91
+ const kind = colon === -1 ? raw : raw.slice(0, colon);
92
+ const rest = colon === -1 ? '' : raw.slice(colon + 1);
93
+ if (kind === 'preset') {
94
+ if (!(0, data_provider_1.isAgentAvatarPresetId)(rest)) {
95
+ throw new Error(`--avatar: "${rest}" is not an avatar preset in this build. ` +
96
+ `Try one of: ${someOf(avatarPresetIds())}. ` +
97
+ 'Run `skrr agents avatar presets` for the full catalog.');
98
+ }
99
+ return { type: 'preset', id: rest };
100
+ }
101
+ if (kind === 'asset') {
102
+ if (!rest) {
103
+ throw new Error('--avatar: "asset:" needs an id from your library — ' +
104
+ 'run `skrr agents avatar library` to list them.');
105
+ }
106
+ return { type: 'asset', id: rest };
107
+ }
108
+ if (kind === 'image')
109
+ return { type: 'image', ...parseImage(rest, '--avatar') };
110
+ throw new Error(`--avatar: cannot read "${value}". Accepted: default, preset:<id>, asset:<id>, ` +
111
+ 'image:<filepath>@<source>, or a JSON object. ' +
112
+ 'To upload a new image file instead, use `skrr agents avatar upload --file <path>`.');
113
+ }
114
+ function parseSceneBackgroundFlag(value) {
115
+ const json = parseJsonObject(value);
116
+ if (json)
117
+ return json;
118
+ const raw = value.trim();
119
+ if (raw === 'default')
120
+ return { type: 'default' };
121
+ const colon = raw.indexOf(':');
122
+ const kind = colon === -1 ? raw : raw.slice(0, colon);
123
+ const rest = colon === -1 ? '' : raw.slice(colon + 1);
124
+ if (kind === 'preset') {
125
+ if (!(0, data_provider_1.isAgentSceneBackgroundPresetId)(rest)) {
126
+ throw new Error(`--scene-background: "${rest}" is not a backdrop preset in this build. ` +
127
+ `Valid ids: ${sceneBackgroundPresetIds().join(', ')}.`);
128
+ }
129
+ return { type: 'preset', id: rest };
130
+ }
131
+ if (kind === 'image')
132
+ return { type: 'image', ...parseImage(rest, '--scene-background') };
133
+ // A bare preset id is the single most likely mistake here, because the
134
+ // backdrop catalog is short enough to remember. Say so instead of listing the
135
+ // grammar twice.
136
+ if ((0, data_provider_1.isAgentSceneBackgroundPresetId)(raw)) {
137
+ throw new Error(`--scene-background: "${raw}" is a preset id — write preset:${raw}. ` +
138
+ 'A bare id is refused because preset and image live in different namespaces.');
139
+ }
140
+ throw new Error(`--scene-background: cannot read "${value}". Accepted: default, preset:<id>, ` +
141
+ `image:<filepath>@<source>, or a JSON object. Presets: ${sceneBackgroundPresetIds().join(', ')}.`);
142
+ }
@@ -419,6 +419,13 @@ async function currentUserId(signal) {
419
419
  * function only after the flag, the config key, and the sole-owned-agent branch
420
420
  * have all declined, which is why the extra round trip is acceptable.
421
421
  */
422
+ /**
423
+ * Deliberately NOT `dataService.ensureDefaultAgent`, which `agents
424
+ * ensure-default` uses for the same endpoint. This path takes an `AbortSignal`
425
+ * and the caller below distinguishes an abort from a refusal — reporting "no
426
+ * default agent" for a caller whose deadline expired sends someone to fix the
427
+ * wrong thing. The shared helper cannot carry a signal, so this one stays.
428
+ */
422
429
  async function ensureDefaultAgent(harness, signal) {
423
430
  const response = await (0, api_fetch_1.apiFetch)('/api/agents/default', {
424
431
  method: 'POST',
@@ -0,0 +1,107 @@
1
+ import { withQuery } from './triggers';
2
+ type Query = Parameters<typeof withQuery>[1];
3
+ type Json = Record<string, unknown>;
4
+ export declare const FOLLOWUP_KINDS: readonly ["reminder", "watch", "checkin", "act"];
5
+ export type FollowUpKind = (typeof FOLLOWUP_KINDS)[number];
6
+ export declare const FOLLOWUP_STATES: readonly ["armed", "proposed", "settled", "paused"];
7
+ /** Terminals a caller may name on `resolve` — the rest are the runtime's. */
8
+ export declare const FOLLOWUP_RESOLVE_OUTCOMES: readonly ["resolved", "abandoned"];
9
+ export declare const WATCH_WAIT_KINDS: readonly ["clock", "event", "poll_until"];
10
+ export interface FollowUpRow {
11
+ id: string;
12
+ kind?: string;
13
+ form?: string | null;
14
+ producer?: string;
15
+ why?: string;
16
+ intentKey?: string;
17
+ subject?: {
18
+ type?: string;
19
+ externalKey?: string;
20
+ label?: string;
21
+ url?: string;
22
+ } | null;
23
+ agentId?: string | null;
24
+ state?: string;
25
+ terminal?: string | null;
26
+ terminalAt?: string | null;
27
+ terminalReason?: string | null;
28
+ escalatedInitiativeId?: string | null;
29
+ capsule?: unknown;
30
+ nextRunAt?: string | null;
31
+ deadlineAt?: string | null;
32
+ createdAt?: string;
33
+ label?: string;
34
+ }
35
+ export interface FollowUpCreateResponse {
36
+ followup: FollowUpRow | null;
37
+ reused?: boolean;
38
+ dropped?: false | 'budget';
39
+ proposal?: boolean;
40
+ budget?: Json;
41
+ fallbackTriggerId?: string | null;
42
+ }
43
+ export interface FollowUpObservability {
44
+ generatedAt?: string;
45
+ agents?: Array<{
46
+ agentId: string;
47
+ kinds: Array<{
48
+ kind: string;
49
+ created: number;
50
+ armed: number;
51
+ proposed: number;
52
+ settled: number;
53
+ terminal: Record<string, number>;
54
+ escalationRate: number | null;
55
+ fires: number;
56
+ budget: {
57
+ pool: string | null;
58
+ used: number;
59
+ limit: number | null;
60
+ } | null;
61
+ silenceRate: number | null;
62
+ silentFires: number | null;
63
+ }>;
64
+ }>;
65
+ }
66
+ export type ResolvedFollowUpAgent = {
67
+ id: string;
68
+ /**
69
+ * True when the identity came from the session binding rather than a flag —
70
+ * a follow-up declared inside an agent's own turn is `agent_in_turn`; one
71
+ * declared by a human at a shell is `user_explicit`. The producer field is
72
+ * provenance, not authority, and this is the only honest signal a CLI has.
73
+ */
74
+ boundToSession: boolean;
75
+ };
76
+ /**
77
+ * Which agent a follow-up belongs to.
78
+ *
79
+ * Deliberately NOT `resolveAgentIdentity`: that resolver's last step picks
80
+ * "the only visible agent", and a follow-up bound to the wrong agent wakes
81
+ * the wrong agent in the wrong main chat. The only honest sources are an
82
+ * explicit `--agent` and the session binding the runtime already proves
83
+ * (`OVERSKY_SESSION_AGENT_ID`, or the legacy `OVERSKY_AGENT_ID` a manual
84
+ * local runtime sets). Anything else throws AGENT_CONTEXT_AMBIGUOUS and
85
+ * names the fix rather than guessing.
86
+ */
87
+ export declare function resolveFollowUpAgent(explicitAgentId?: string, env?: Record<string, string | undefined>): ResolvedFollowUpAgent;
88
+ /** A `type:externalKey` subject ref → the subject block the API wants. */
89
+ export declare function parseSubjectRef(ref: string): {
90
+ type: string;
91
+ externalKey: string;
92
+ };
93
+ export declare const followupsApi: {
94
+ create: (body: Json) => Promise<FollowUpCreateResponse>;
95
+ list: (query?: Query) => Promise<{
96
+ followups: FollowUpRow[];
97
+ }>;
98
+ observability: (query?: Query) => Promise<FollowUpObservability>;
99
+ show: (id: string) => Promise<{
100
+ followup: FollowUpRow;
101
+ }>;
102
+ resolve: (id: string, body: Json) => Promise<Json>;
103
+ cancel: (id: string, body?: Json) => Promise<Json>;
104
+ reschedule: (id: string, body: Json) => Promise<Json>;
105
+ };
106
+ export declare function followupSummaryLine(row: FollowUpRow): string;
107
+ export {};
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.followupsApi = exports.WATCH_WAIT_KINDS = exports.FOLLOWUP_RESOLVE_OUTCOMES = exports.FOLLOWUP_STATES = exports.FOLLOWUP_KINDS = void 0;
4
+ exports.resolveFollowUpAgent = resolveFollowUpAgent;
5
+ exports.parseSubjectRef = parseSubjectRef;
6
+ exports.followupSummaryLine = followupSummaryLine;
7
+ /**
8
+ * followups.ts — the `skrr followups` domain lib.
9
+ *
10
+ * A follow-up is ONE resource with four kinds (`reminder`, `watch`, `checkin`,
11
+ * `act`), declared by intent and derived into mechanism entirely server-side
12
+ * (`FollowUps/` + `docs/architecture/followup-service-2026-09-15.md`). The CLI
13
+ * is the universal substrate — a daemon-run local harness reaches the platform
14
+ * only through `skrr` — so every verb the API exposes lives here, and commands
15
+ * own flags only. The `followup_*` MCP tools wrap the same surface.
16
+ *
17
+ * What this file deliberately does NOT contain: executor modes, trigger kinds,
18
+ * destination logic, or budget policy. A caller names `kind` + `subject` +
19
+ * `why` and the server derives the rest; a second derivation here would be
20
+ * exactly the drift the service exists to kill.
21
+ */
22
+ const data_provider_1 = require("@skrr-ai/data-provider");
23
+ const runtime_context_1 = require("./runtime-context");
24
+ const triggers_1 = require("./triggers");
25
+ const agent_resolver_1 = require("./agent-resolver");
26
+ const MOUNT = '/api/followups';
27
+ const base = (id) => `${MOUNT}/${encodeURIComponent(id)}`;
28
+ /* ------------------------------------------------------------------ *
29
+ * Vocabulary (client-side validation only — the server is the authority) *
30
+ * ------------------------------------------------------------------ */
31
+ exports.FOLLOWUP_KINDS = ['reminder', 'watch', 'checkin', 'act'];
32
+ exports.FOLLOWUP_STATES = ['armed', 'proposed', 'settled', 'paused'];
33
+ /** Terminals a caller may name on `resolve` — the rest are the runtime's. */
34
+ exports.FOLLOWUP_RESOLVE_OUTCOMES = ['resolved', 'abandoned'];
35
+ exports.WATCH_WAIT_KINDS = ['clock', 'event', 'poll_until'];
36
+ /**
37
+ * Which agent a follow-up belongs to.
38
+ *
39
+ * Deliberately NOT `resolveAgentIdentity`: that resolver's last step picks
40
+ * "the only visible agent", and a follow-up bound to the wrong agent wakes
41
+ * the wrong agent in the wrong main chat. The only honest sources are an
42
+ * explicit `--agent` and the session binding the runtime already proves
43
+ * (`OVERSKY_SESSION_AGENT_ID`, or the legacy `OVERSKY_AGENT_ID` a manual
44
+ * local runtime sets). Anything else throws AGENT_CONTEXT_AMBIGUOUS and
45
+ * names the fix rather than guessing.
46
+ */
47
+ function resolveFollowUpAgent(explicitAgentId, env = process.env) {
48
+ const runtime = (0, runtime_context_1.readRuntimeContext)(env);
49
+ const sessionAgent = runtime.agent?.source === 'OVERSKY_SESSION_AGENT_ID' ||
50
+ runtime.agent?.source === 'OVERSKY_AGENT_ID'
51
+ ? runtime.agent
52
+ : undefined;
53
+ if (sessionAgent) {
54
+ if (explicitAgentId && explicitAgentId !== sessionAgent.id) {
55
+ throw new agent_resolver_1.AgentResolutionError({
56
+ code: 'AGENT_CONTEXT_CONFLICT',
57
+ message: 'This local runtime is bound to a session agent. Do not override it with another agent ID.',
58
+ suggestion: 'Use the bound session agent, or start a new daemon session for a different agent.',
59
+ details: { explicitAgentId, sessionAgentId: sessionAgent.id, source: sessionAgent.source },
60
+ });
61
+ }
62
+ return { id: sessionAgent.id, boundToSession: true };
63
+ }
64
+ if (explicitAgentId)
65
+ return { id: explicitAgentId, boundToSession: false };
66
+ throw new agent_resolver_1.AgentResolutionError({
67
+ code: 'AGENT_CONTEXT_AMBIGUOUS',
68
+ message: 'No agent context: a follow-up needs an explicit agent and this session supplies none.',
69
+ suggestion: 'Pass `--agent <agent-id>`, run inside a daemon session that sets OVERSKY_SESSION_AGENT_ID, or use `--all-agents` where listing is the intent.',
70
+ details: { runtimeKind: runtime.kind },
71
+ });
72
+ }
73
+ /** A `type:externalKey` subject ref → the subject block the API wants. */
74
+ function parseSubjectRef(ref) {
75
+ const idx = ref.indexOf(':');
76
+ if (idx <= 0 || idx === ref.length - 1) {
77
+ throw new Error(`--subject must be "type:externalKey" (e.g. task:OSK-42, conversation:conv_123), got "${ref}"`);
78
+ }
79
+ return { type: ref.slice(0, idx), externalKey: ref.slice(idx + 1) };
80
+ }
81
+ /* ------------------------------------------------------------------ *
82
+ * API *
83
+ * ------------------------------------------------------------------ */
84
+ exports.followupsApi = {
85
+ create: (body) => data_provider_1.request.post(MOUNT, body),
86
+ list: (query = {}) => data_provider_1.request.get((0, triggers_1.withQuery)(MOUNT, query)),
87
+ observability: (query = {}) => data_provider_1.request.get((0, triggers_1.withQuery)(`${MOUNT}/observability`, query)),
88
+ show: (id) => data_provider_1.request.get(base(id)),
89
+ resolve: (id, body) => data_provider_1.request.post(`${base(id)}/resolve`, body),
90
+ cancel: (id, body = {}) => data_provider_1.request.post(`${base(id)}/cancel`, body),
91
+ reschedule: (id, body) => data_provider_1.request.post(`${base(id)}/reschedule`, body),
92
+ };
93
+ /* ------------------------------------------------------------------ *
94
+ * Rendering *
95
+ * ------------------------------------------------------------------ */
96
+ const fmtWhen = (iso) => (iso ? iso.slice(0, 16).replace('T', ' ') + 'Z' : '—');
97
+ function followupSummaryLine(row) {
98
+ const subject = row.subject ? `${row.subject.type}:${row.subject.externalKey}` : '—';
99
+ const kindForm = row.form ? `${row.kind}/${row.form}` : (row.kind ?? '—');
100
+ const when = row.state === 'settled' ? `settled:${row.terminal ?? '?'}` : `next:${fmtWhen(row.nextRunAt)}`;
101
+ return `${row.id} [${row.state ?? '?'}] ${kindForm} ${subject} ${when}`;
102
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * label-ref.ts — let a human type `--label dogfood` instead of a uuid.
3
+ *
4
+ * WHY
5
+ * ---
6
+ * Labels are addressed by id everywhere: `spaces labels add <space> --label
7
+ * <uuid>`. That is correct for the wire and hostile at a keyboard, and the cost
8
+ * was measurable — the fleet reached nine labels, every one of them
9
+ * `applicableTo: ['task']`, and not one space-applicable label had ever been
10
+ * created. Applying one meant `labels list`, eyeball the uuid, paste it. Three
11
+ * steps to do the thing the flag already existed for.
12
+ *
13
+ * THE RULE, AND WHY IT IS NOT "CREATE IT IF MISSING"
14
+ * -------------------------------------------------
15
+ * An unknown name is REFUSED, with the closest matches named. It is never
16
+ * created.
17
+ *
18
+ * Auto-create is the obvious convenience and it is the one thing that destroys
19
+ * the vocabulary: `--label dogfod` would mint a second label nobody meant, and
20
+ * a label is worth exactly as much as the completeness of a filter on it. The
21
+ * server draws the same line for every agent-supplied key — the operator
22
+ * declares the vocabulary, the caller chooses from it (`WakeIntentResolver`,
23
+ * the `agent_run` watched source, and `Labels/labelGuidance.js`). This is that
24
+ * rule at the keyboard instead of at the model.
25
+ *
26
+ * Refusing is also what makes the typo cheap: the caller sees
27
+ * `did you mean: dogfood?` and retries, instead of discovering six weeks later
28
+ * that half the program is under a name nobody can spell twice.
29
+ *
30
+ * An id passes through UNTOUCHED and costs no round trip — scripts and agents
31
+ * that already hold ids keep working exactly as before, and nothing here is on
32
+ * their path.
33
+ */
34
+ import { dataService } from '@skrr-ai/data-provider';
35
+ import type { LabelApplicable } from './label-scope';
36
+ export declare function looksLikeLabelId(ref: string): boolean;
37
+ /**
38
+ * A label row as the API returns it. Narrow on purpose at the fields this
39
+ * module reasons about; everything else rides along untouched so callers that
40
+ * RENDER labels (`labels list`) can use the same merge.
41
+ */
42
+ export interface LabelRow {
43
+ id: string;
44
+ name: string;
45
+ scope?: string;
46
+ scopeId?: string;
47
+ color?: string;
48
+ applicableTo?: string[];
49
+ usageCount?: number;
50
+ archivedAt?: string | null;
51
+ [key: string]: unknown;
52
+ }
53
+ /**
54
+ * Names close enough to be worth printing. A suggestion list that includes
55
+ * everything is the same as no suggestion list — the caller still has to read
56
+ * all of it — so the threshold scales with the typed length and caps at three.
57
+ */
58
+ export declare function closestNames(typed: string, names: string[], limit?: number): string[];
59
+ export interface LabelVocabularyContext {
60
+ /** Narrow to labels attachable to this entity type. Omit for all of them. */
61
+ applicableTo?: LabelApplicable;
62
+ /** Narrows to the workspace scope; `--workspace` / OVERSKY_WORKSPACE_ID. */
63
+ workspaceId?: string;
64
+ /** Archived labels are excluded unless asked for. */
65
+ includeArchived?: boolean;
66
+ /** Injected in tests. */
67
+ getLabels?: typeof dataService.getLabels;
68
+ }
69
+ export interface LabelRefContext extends LabelVocabularyContext {
70
+ /** Which entity the resolved labels will be attached to. */
71
+ applicableTo: LabelApplicable;
72
+ }
73
+ /**
74
+ * Every scope this caller could legitimately attach from, merged.
75
+ *
76
+ * `GET /api/labels` takes ONE scope, and a label the caller may attach can live
77
+ * in either of two. Asking for one and calling it "the vocabulary" would refuse
78
+ * a name that exists — which is the failure mode that makes people stop using
79
+ * names at all. A scope that cannot be read contributes nothing and throws
80
+ * nothing: this runs to produce a better error message, and must never become
81
+ * the error.
82
+ */
83
+ export declare function collectLabelVocabulary(context: LabelVocabularyContext): Promise<LabelRow[]>;
84
+ export declare class LabelRefError extends Error {
85
+ }
86
+ /**
87
+ * Resolve `--label` / `--label-id` values, each either an id or a name.
88
+ *
89
+ * Throws `LabelRefError` with an actionable message when a name does not
90
+ * resolve, or resolves to more than one label. Ambiguity is a refusal rather
91
+ * than a pick: the same name may legitimately exist in a user scope and a
92
+ * workspace scope, and guessing which one the caller meant is how the wrong
93
+ * label ends up attached to a year of work.
94
+ */
95
+ export declare function resolveLabelRefs(refs: string[] | undefined, context: LabelRefContext): Promise<string[]>;
@@ -0,0 +1,163 @@
1
+ "use strict";
2
+ /**
3
+ * label-ref.ts — let a human type `--label dogfood` instead of a uuid.
4
+ *
5
+ * WHY
6
+ * ---
7
+ * Labels are addressed by id everywhere: `spaces labels add <space> --label
8
+ * <uuid>`. That is correct for the wire and hostile at a keyboard, and the cost
9
+ * was measurable — the fleet reached nine labels, every one of them
10
+ * `applicableTo: ['task']`, and not one space-applicable label had ever been
11
+ * created. Applying one meant `labels list`, eyeball the uuid, paste it. Three
12
+ * steps to do the thing the flag already existed for.
13
+ *
14
+ * THE RULE, AND WHY IT IS NOT "CREATE IT IF MISSING"
15
+ * -------------------------------------------------
16
+ * An unknown name is REFUSED, with the closest matches named. It is never
17
+ * created.
18
+ *
19
+ * Auto-create is the obvious convenience and it is the one thing that destroys
20
+ * the vocabulary: `--label dogfod` would mint a second label nobody meant, and
21
+ * a label is worth exactly as much as the completeness of a filter on it. The
22
+ * server draws the same line for every agent-supplied key — the operator
23
+ * declares the vocabulary, the caller chooses from it (`WakeIntentResolver`,
24
+ * the `agent_run` watched source, and `Labels/labelGuidance.js`). This is that
25
+ * rule at the keyboard instead of at the model.
26
+ *
27
+ * Refusing is also what makes the typo cheap: the caller sees
28
+ * `did you mean: dogfood?` and retries, instead of discovering six weeks later
29
+ * that half the program is under a name nobody can spell twice.
30
+ *
31
+ * An id passes through UNTOUCHED and costs no round trip — scripts and agents
32
+ * that already hold ids keep working exactly as before, and nothing here is on
33
+ * their path.
34
+ */
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.LabelRefError = void 0;
37
+ exports.looksLikeLabelId = looksLikeLabelId;
38
+ exports.closestNames = closestNames;
39
+ exports.collectLabelVocabulary = collectLabelVocabulary;
40
+ exports.resolveLabelRefs = resolveLabelRefs;
41
+ const data_provider_1 = require("@skrr-ai/data-provider");
42
+ /** A canonical v4 uuid, which is what every label id is. */
43
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
44
+ function looksLikeLabelId(ref) {
45
+ return UUID_RE.test(ref.trim());
46
+ }
47
+ /** Levenshtein, iterative, two rows. Only ever runs on a refusal path. */
48
+ function distance(a, b) {
49
+ if (a === b)
50
+ return 0;
51
+ if (!a.length)
52
+ return b.length;
53
+ if (!b.length)
54
+ return a.length;
55
+ let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
56
+ for (let i = 1; i <= a.length; i += 1) {
57
+ const row = [i];
58
+ for (let j = 1; j <= b.length; j += 1) {
59
+ row[j] = Math.min(prev[j] + 1, row[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
60
+ }
61
+ prev = row;
62
+ }
63
+ return prev[b.length];
64
+ }
65
+ /**
66
+ * Names close enough to be worth printing. A suggestion list that includes
67
+ * everything is the same as no suggestion list — the caller still has to read
68
+ * all of it — so the threshold scales with the typed length and caps at three.
69
+ */
70
+ function closestNames(typed, names, limit = 3) {
71
+ const needle = typed.trim().toLowerCase();
72
+ const budget = Math.max(2, Math.ceil(needle.length / 3));
73
+ return names
74
+ .map((name) => ({ name, d: distance(needle, name.toLowerCase()) }))
75
+ .filter((row) => row.d <= budget || row.name.toLowerCase().includes(needle))
76
+ .sort((a, b) => a.d - b.d)
77
+ .slice(0, limit)
78
+ .map((row) => row.name);
79
+ }
80
+ /**
81
+ * Every scope this caller could legitimately attach from, merged.
82
+ *
83
+ * `GET /api/labels` takes ONE scope, and a label the caller may attach can live
84
+ * in either of two. Asking for one and calling it "the vocabulary" would refuse
85
+ * a name that exists — which is the failure mode that makes people stop using
86
+ * names at all. A scope that cannot be read contributes nothing and throws
87
+ * nothing: this runs to produce a better error message, and must never become
88
+ * the error.
89
+ */
90
+ async function collectLabelVocabulary(context) {
91
+ const getLabels = context.getLabels ?? data_provider_1.dataService.getLabels;
92
+ const requests = [{ scope: 'user' }];
93
+ if (context.workspaceId)
94
+ requests.push({ scope: 'workspace', scopeId: context.workspaceId });
95
+ const settled = await Promise.allSettled(requests.map((request) => getLabels({
96
+ ...request,
97
+ ...(context.applicableTo ? { applicableTo: context.applicableTo } : {}),
98
+ ...(context.includeArchived ? { includeArchived: true } : {}),
99
+ })));
100
+ const byId = new Map();
101
+ for (const outcome of settled) {
102
+ if (outcome.status !== 'fulfilled')
103
+ continue;
104
+ for (const row of outcome.value?.data ?? []) {
105
+ if (row?.id && !byId.has(row.id))
106
+ byId.set(row.id, row);
107
+ }
108
+ }
109
+ return [...byId.values()];
110
+ }
111
+ class LabelRefError extends Error {
112
+ }
113
+ exports.LabelRefError = LabelRefError;
114
+ /**
115
+ * Resolve `--label` / `--label-id` values, each either an id or a name.
116
+ *
117
+ * Throws `LabelRefError` with an actionable message when a name does not
118
+ * resolve, or resolves to more than one label. Ambiguity is a refusal rather
119
+ * than a pick: the same name may legitimately exist in a user scope and a
120
+ * workspace scope, and guessing which one the caller meant is how the wrong
121
+ * label ends up attached to a year of work.
122
+ */
123
+ async function resolveLabelRefs(refs, context) {
124
+ const input = (refs ?? []).map((ref) => ref.trim()).filter(Boolean);
125
+ if (input.length === 0)
126
+ return [];
127
+ // Ids are the wire form and the common case for scripts. Resolve nothing,
128
+ // fetch nothing, and let the server be the authority on whether they exist.
129
+ if (input.every(looksLikeLabelId))
130
+ return input;
131
+ const vocabulary = await collectLabelVocabulary(context);
132
+ const resolved = [];
133
+ for (const ref of input) {
134
+ if (looksLikeLabelId(ref)) {
135
+ resolved.push(ref);
136
+ continue;
137
+ }
138
+ const needle = ref.toLowerCase();
139
+ const matches = vocabulary.filter((row) => (row.name ?? '').toLowerCase() === needle);
140
+ if (matches.length === 1) {
141
+ resolved.push(matches[0].id);
142
+ continue;
143
+ }
144
+ if (matches.length > 1) {
145
+ const listed = matches
146
+ .map((row) => ` ${row.id} ${row.name}${row.scope ? ` (${row.scope})` : ''}`)
147
+ .join('\n');
148
+ throw new LabelRefError(`"${ref}" names ${matches.length} labels. Pass the id you mean:\n${listed}`);
149
+ }
150
+ if (vocabulary.length === 0) {
151
+ throw new LabelRefError(`No label named "${ref}" — and no label applies to ${context.applicableTo}s yet. ` +
152
+ `Create one with \`labels create --name ${ref} --applicable-to ${context.applicableTo}\`.`);
153
+ }
154
+ const near = closestNames(ref, vocabulary.map((row) => row.name));
155
+ throw new LabelRefError(`No label named "${ref}" applies to ${context.applicableTo}s.` +
156
+ (near.length ? ` Did you mean: ${near.join(', ')}?` : '') +
157
+ ` Run \`labels list\` for the vocabulary, or create it deliberately with ` +
158
+ `\`labels create --name ${ref} --applicable-to ${context.applicableTo}\` — ` +
159
+ 'a name is never created implicitly, because a second spelling of a label ' +
160
+ 'is worse than no label.');
161
+ }
162
+ return resolved;
163
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Shared plumbing for the unified label commands (`skrr labels …`).
3
3
  *
4
- * Labels are anchored to exactly one scope — a user, a space, or a workspace —
4
+ * Labels are anchored to exactly one scope — a user or a workspace —
5
5
  * via the `(scope, scopeId)` pair. That pair is simultaneously the uniqueness
6
6
  * key and the permission boundary server-side (see
7
7
  * `api/server/services/Labels/LabelService.js`), so every label command has to
@@ -9,7 +9,7 @@
9
9
  * always explicit; `user` is the default because that is the only scope whose
10
10
  * id the server can infer (it falls back to the caller).
11
11
  */
12
- export declare const LABEL_SCOPES: readonly ["user", "space", "workspace"];
12
+ export declare const LABEL_SCOPES: readonly ["user", "workspace"];
13
13
  export type LabelScope = (typeof LABEL_SCOPES)[number];
14
14
  export declare const LABEL_APPLICABLE: readonly ["task", "space", "goal", "routine"];
15
15
  export type LabelApplicable = (typeof LABEL_APPLICABLE)[number];