@skrr-ai/cli 0.1.45 → 0.1.47

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 (73) hide show
  1. package/dist/base-command.d.ts +13 -1
  2. package/dist/base-command.js +37 -3
  3. package/dist/commands/agents/actions/create.js +11 -4
  4. package/dist/commands/browser/cloud-agents.js +4 -0
  5. package/dist/commands/code/handover.d.ts +11 -0
  6. package/dist/commands/code/handover.js +104 -1
  7. package/dist/commands/code/install.js +31 -0
  8. package/dist/commands/code/update.d.ts +46 -0
  9. package/dist/commands/code/update.js +242 -0
  10. package/dist/commands/commitments/handover.d.ts +29 -0
  11. package/dist/commands/commitments/handover.js +99 -0
  12. package/dist/commands/convos/handover.d.ts +1 -0
  13. package/dist/commands/convos/handover.js +4 -0
  14. package/dist/commands/daemon/index.js +2 -0
  15. package/dist/commands/daemon/update.d.ts +19 -0
  16. package/dist/commands/daemon/update.js +48 -0
  17. package/dist/commands/followups/watch.js +40 -5
  18. package/dist/commands/spaces/briefs/get.js +8 -0
  19. package/dist/commands/spaces/briefs/list.js +2 -0
  20. package/dist/commands/spaces/briefs/set.d.ts +3 -0
  21. package/dist/commands/spaces/briefs/set.js +78 -13
  22. package/dist/commands/spaces/expertise-suggestions.d.ts +12 -0
  23. package/dist/commands/spaces/expertise-suggestions.js +71 -0
  24. package/dist/commands/spaces/members/add.d.ts +1 -0
  25. package/dist/commands/spaces/members/add.js +11 -3
  26. package/dist/commands/spaces/members/list.js +11 -0
  27. package/dist/commands/tasks/assign.d.ts +9 -0
  28. package/dist/commands/tasks/assign.js +108 -1
  29. package/dist/commands/tasks/fork.d.ts +32 -0
  30. package/dist/commands/tasks/fork.js +171 -0
  31. package/dist/commands/tasks/handover.d.ts +23 -0
  32. package/dist/commands/tasks/handover.js +205 -0
  33. package/dist/commands/tasks/promote.d.ts +23 -0
  34. package/dist/commands/tasks/promote.js +82 -0
  35. package/dist/commands/tasks/runs.d.ts +16 -0
  36. package/dist/commands/tasks/runs.js +46 -1
  37. package/dist/commands/tasks/show.d.ts +23 -0
  38. package/dist/commands/tasks/show.js +59 -0
  39. package/dist/lib/api-fetch.d.ts +7 -1
  40. package/dist/lib/api-fetch.js +9 -2
  41. package/dist/lib/assignee-resolver.d.ts +22 -0
  42. package/dist/lib/assignee-resolver.js +26 -0
  43. package/dist/lib/code-handover.d.ts +50 -0
  44. package/dist/lib/code-handover.js +135 -0
  45. package/dist/lib/commitments.d.ts +22 -0
  46. package/dist/lib/commitments.js +42 -0
  47. package/dist/lib/dedicated-wait.js +22 -1
  48. package/dist/lib/followups.d.ts +18 -0
  49. package/dist/lib/followups.js +44 -0
  50. package/dist/lib/node-adapter.js +6 -0
  51. package/dist/lib/task-extras.d.ts +44 -0
  52. package/dist/lib/task-extras.js +73 -0
  53. package/dist/lib/update-check.d.ts +80 -7
  54. package/dist/lib/update-check.js +365 -33
  55. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.d.ts +16 -1
  56. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.js +15 -0
  57. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessHome.js +45 -0
  58. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessSkillLayout.d.ts +65 -0
  59. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessSkillLayout.js +99 -0
  60. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -0
  61. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +10 -2
  62. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.d.ts +16 -1
  63. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.js +15 -0
  64. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessHome.js +45 -0
  65. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessSkillLayout.d.ts +65 -0
  66. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessSkillLayout.js +94 -0
  67. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -0
  68. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +5 -0
  69. package/dist/node_modules/@skrr-ai/auth-core/package.json +11 -1
  70. package/dist/node_modules/@skrr-ai/data-provider/index.js +2262 -2241
  71. package/dist/node_modules/@skrr-ai/inference-broker/package.json +1 -1
  72. package/oclif.manifest.json +34562 -33947
  73. package/package.json +2 -2
@@ -0,0 +1,29 @@
1
+ import { BaseCommand } from '../../base-command';
2
+ /**
3
+ * `skrr commitments handover` — rebind a commitment's executor to a different
4
+ * agent.
5
+ *
6
+ * `commitment.agentId` is single-valued by design, so this is NOT a patch: the
7
+ * server records the move on the commitment's audit trail (`executorHandovers`)
8
+ * with who initiated it and why (impl contract B4, design doc P4). The
9
+ * canonical case is a commitment whose agent's execution targets are all
10
+ * quota-blocked — this is the "reassign" half of "executor cannot run until
11
+ * <date> — reassign or add a fallback target".
12
+ *
13
+ * Only the WHO moves: the contract, cadence, watched sources and audit trail
14
+ * are unchanged, and the new executor's own execution policy decides WHERE the
15
+ * next check runs.
16
+ */
17
+ export default class CommitmentsHandover extends BaseCommand {
18
+ static description: string;
19
+ static examples: string[];
20
+ static args: {
21
+ id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
22
+ };
23
+ static flags: {
24
+ json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
25
+ to: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
26
+ reason: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
27
+ };
28
+ run(): Promise<void>;
29
+ }
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const data_provider_1 = require("@skrr-ai/data-provider");
5
+ const base_command_1 = require("../../base-command");
6
+ const commitments_1 = require("../../lib/commitments");
7
+ const web_url_1 = require("../../lib/web-url");
8
+ const code_handover_1 = require("../../lib/code-handover");
9
+ /**
10
+ * `skrr commitments handover` — rebind a commitment's executor to a different
11
+ * agent.
12
+ *
13
+ * `commitment.agentId` is single-valued by design, so this is NOT a patch: the
14
+ * server records the move on the commitment's audit trail (`executorHandovers`)
15
+ * with who initiated it and why (impl contract B4, design doc P4). The
16
+ * canonical case is a commitment whose agent's execution targets are all
17
+ * quota-blocked — this is the "reassign" half of "executor cannot run until
18
+ * <date> — reassign or add a fallback target".
19
+ *
20
+ * Only the WHO moves: the contract, cadence, watched sources and audit trail
21
+ * are unchanged, and the new executor's own execution policy decides WHERE the
22
+ * next check runs.
23
+ */
24
+ class CommitmentsHandover extends base_command_1.BaseCommand {
25
+ static description = 'Reassign a commitment to a different executor agent';
26
+ static examples = [
27
+ '<%= config.bin %> commitments handover <commitment-id> --to "Backup Agent"',
28
+ '<%= config.bin %> commitments handover <id> --to agent_abc --reason "claude quota exhausted until Sep 19"',
29
+ '<%= config.bin %> commitments handover <id> --to agent_abc --json',
30
+ ];
31
+ static args = {
32
+ id: core_1.Args.string({ description: 'Commitment ID', required: true, ignoreStdin: true }),
33
+ };
34
+ static flags = {
35
+ json: core_1.Flags.boolean({ description: 'Output as JSON' }),
36
+ to: core_1.Flags.string({
37
+ description: 'Agent that takes over the commitment — id or name',
38
+ required: true,
39
+ }),
40
+ reason: core_1.Flags.string({
41
+ description: 'Why the executor is changing — recorded on the audit trail',
42
+ }),
43
+ };
44
+ async run() {
45
+ this.requireAuth();
46
+ const { args, flags } = await this.parse(CommitmentsHandover);
47
+ // ── Target agent: id or name, resolved like `code handover --to`. ──────
48
+ let agents = [];
49
+ try {
50
+ const resp = await data_provider_1.dataService.listAgents({ requiredPermission: data_provider_1.PermissionBits.VIEW });
51
+ agents = (resp?.data ?? []).filter(Boolean);
52
+ }
53
+ catch (err) {
54
+ this.handleApiError(err);
55
+ }
56
+ const matched = (0, code_handover_1.matchAgent)(flags.to, agents);
57
+ if (matched.kind === 'none') {
58
+ this.error(`No agent matches "${flags.to}". Run \`${this.config.bin} agents list\` to see the agents you can use.`);
59
+ }
60
+ if (matched.kind === 'ambiguous') {
61
+ this.error(`"${flags.to}" matches ${matched.candidates.length} agents. Pass the agent id instead:\n` +
62
+ matched.candidates.map((a) => ` ${a.id} ${a.name ?? ''}`).join('\n'));
63
+ }
64
+ const target = matched.agent;
65
+ let result;
66
+ try {
67
+ result = await commitments_1.commitmentApi.setExecutor(args.id, {
68
+ agentId: target.id,
69
+ ...(flags.reason ? { reason: flags.reason } : {}),
70
+ });
71
+ }
72
+ catch (err) {
73
+ this.handleApiError(err, { subject: `Commitment ${args.id}` });
74
+ }
75
+ if (flags.json) {
76
+ this.log(JSON.stringify(result, null, 2));
77
+ return;
78
+ }
79
+ const agentLabel = target.name ? `${target.name} (${target.id})` : target.id;
80
+ this.log(`Handed commitment ${args.id} over to ${agentLabel}`);
81
+ // The audit entry the server appended — echo it so "from whom" is answered
82
+ // without a second call. Absent on a server that predates the trail.
83
+ const commitment = result?.commitment ??
84
+ result;
85
+ const handovers = commitment?.executorHandovers;
86
+ if (Array.isArray(handovers) && handovers.length > 0) {
87
+ const last = handovers[handovers.length - 1];
88
+ if (typeof last?.fromAgentId === 'string' && typeof last?.toAgentId === 'string') {
89
+ this.log(` executor ${last.fromAgentId} → ${last.toAgentId}`);
90
+ }
91
+ }
92
+ if (commitment) {
93
+ const withUrl = (0, web_url_1.withCommitmentUrl)(this.cliConfig.baseURL, commitment);
94
+ if (withUrl.url)
95
+ this.log(` ${withUrl.url}`);
96
+ }
97
+ }
98
+ }
99
+ exports.default = CommitmentsHandover;
@@ -24,6 +24,7 @@ export default class ConvosHandover extends BaseCommand {
24
24
  to: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
25
25
  'up-to': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
26
26
  reason: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
27
+ 'allow-recall': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
27
28
  };
28
29
  run(): Promise<void>;
29
30
  }
@@ -44,6 +44,9 @@ class ConvosHandover extends base_command_1.BaseCommand {
44
44
  reason: core_1.Flags.string({
45
45
  description: 'Why the work is moving — recorded in provenance and the forward link',
46
46
  }),
47
+ 'allow-recall': core_1.Flags.boolean({
48
+ description: 'Grant the receiving agent a time-boxed (7d) re-read of this thread after handover',
49
+ }),
47
50
  };
48
51
  async run() {
49
52
  this.requireAuth();
@@ -74,6 +77,7 @@ class ConvosHandover extends base_command_1.BaseCommand {
74
77
  targetAgentId: flags.to,
75
78
  upTo: flags['up-to'],
76
79
  reason: flags.reason,
80
+ allowRecall: flags['allow-recall'] === true ? true : undefined,
77
81
  });
78
82
  }
79
83
  catch (err) {
@@ -26,6 +26,7 @@ class Daemon extends core_1.Command {
26
26
  '<%= config.bin %> daemon status',
27
27
  '<%= config.bin %> daemon start',
28
28
  '<%= config.bin %> daemon stop',
29
+ '<%= config.bin %> daemon update',
29
30
  '<%= config.bin %> daemon logs',
30
31
  '<%= config.bin %> daemon recover-offline-queue --apply',
31
32
  '<%= config.bin %> daemon usage --refresh',
@@ -44,6 +45,7 @@ class Daemon extends core_1.Command {
44
45
  this.log(' skrr daemon uninstall — remove that service (local state is left alone)');
45
46
  this.log(' skrr daemon start — start the daemon');
46
47
  this.log(' skrr daemon stop — stop the daemon');
48
+ this.log(' skrr daemon update — update the daemon binary to the latest signed release');
47
49
  this.log(' skrr daemon status — show running state and config');
48
50
  this.log(' skrr daemon logs — tail recent daemon logs');
49
51
  this.log(' skrr daemon recover-offline-queue — restore recoverable offline WALs');
@@ -0,0 +1,19 @@
1
+ import { Command } from '@oclif/core';
2
+ /**
3
+ * `skrr daemon update` — update the runtime binary on this computer.
4
+ *
5
+ * Proxied like every other lifecycle verb. The earlier exclusion reason — that
6
+ * a self-update "must not run from inside a wrapper it may replace" — did not
7
+ * hold: `skrr` is a separate Node process that spawns `skrrd update`; the
8
+ * verified swap targets the daemon BINARY on disk, never the running CLI. The
9
+ * running daemon process is likewise untouched — the self-upgrade monitor
10
+ * restarts it afterwards, exactly as it does for the background poller.
11
+ */
12
+ export default class DaemonUpdate extends Command {
13
+ static aliases: string[];
14
+ static summary: string;
15
+ static description: string;
16
+ static strict: boolean;
17
+ static examples: string[];
18
+ run(): Promise<void>;
19
+ }
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const exec_runtime_binary_1 = require("../../lib/exec-runtime-binary");
5
+ /**
6
+ * `skrr daemon update` — update the runtime binary on this computer.
7
+ *
8
+ * Proxied like every other lifecycle verb. The earlier exclusion reason — that
9
+ * a self-update "must not run from inside a wrapper it may replace" — did not
10
+ * hold: `skrr` is a separate Node process that spawns `skrrd update`; the
11
+ * verified swap targets the daemon BINARY on disk, never the running CLI. The
12
+ * running daemon process is likewise untouched — the self-upgrade monitor
13
+ * restarts it afterwards, exactly as it does for the background poller.
14
+ */
15
+ class DaemonUpdate extends core_1.Command {
16
+ // `skrr daemons <sub>` — the plural is advertised as an alias for the
17
+ // whole topic, and without a per-command alias only the bare topic
18
+ // resolved: every subcommand under it failed.
19
+ static aliases = ['daemons:update'];
20
+ static summary = 'Update the skrr daemon binary on this computer to the latest signed release';
21
+ static description = (0, exec_runtime_binary_1.forwardedFlagsNote)('update');
22
+ static strict = false; // forward all args verbatim — see run()
23
+ static examples = [
24
+ '<%= config.bin %> daemon update',
25
+ '<%= config.bin %> daemon update --check',
26
+ '<%= config.bin %> daemon update --target-version 0.8.54',
27
+ '<%= config.bin %> daemon update --restart',
28
+ ];
29
+ async run() {
30
+ // `this.argv`, NOT `this.parse()`. `strict = false` relaxes POSITIONAL
31
+ // arity and oclif still rejects any flag this command has not declared —
32
+ // so every `skrrd` flag would be unreachable through the wrapper that
33
+ // exists to reach it (OSK-296).
34
+ const argv = this.argv;
35
+ try {
36
+ const code = await (0, exec_runtime_binary_1.execRuntimeBinary)(['update', ...argv]);
37
+ this.exit(code);
38
+ }
39
+ catch (err) {
40
+ if (err.message === 'runtime-binary-not-installed') {
41
+ this.log((0, exec_runtime_binary_1.notInstalledMessage)());
42
+ this.exit(127);
43
+ }
44
+ throw err;
45
+ }
46
+ }
47
+ }
48
+ exports.default = DaemonUpdate;
@@ -30,6 +30,7 @@ class FollowupsWatch extends base_command_1.BaseCommand {
30
30
  '<%= config.bin %> followups watch --subject task:OSK-42 --why "deploy still running" --url https://ci.example.com/api/build/9 --until \'{"path":"status","op":"equals","value":"done"}\' --deadline 6h',
31
31
  '<%= config.bin %> followups watch --subject task:OSK-42 --why "pr review" --check "gh pr view 42 --json state" --success "state == MERGED" --abort "state == CLOSED" --wait event --wait-entity github --wait-event pull_request_merged --wait-fallback 2h',
32
32
  '<%= config.bin %> followups watch --subject conversation:abc --why "vendor reply" --check "check inbox" --success "reply arrived" --abort "3 days" --wait clock --wait-at 2026-09-20T09:00:00Z',
33
+ '<%= config.bin %> followups watch --subject task:OSK-42 --why "deploy tail" --check "release-status says live" --success "live" --abort "6h" --wait clock --wait-at 45m',
33
34
  ];
34
35
  static flags = {
35
36
  subject: core_1.Flags.string({ description: 'Subject ref: type:externalKey', required: true }),
@@ -41,9 +42,15 @@ class FollowupsWatch extends base_command_1.BaseCommand {
41
42
  description: 'Predicate tree as JSON (predicate form — the server evaluates it; no agent turn per check): a leaf {"path","op","value"} or {"all"|"any": [...]}. op is equals, not_equals, contains, starts_with, in, gt, lt, exists, … — an unknown op is refused with the full list',
42
43
  }),
43
44
  grant: core_1.Flags.string({ description: 'Grant id supplying the watch credentials' }),
44
- deadline: core_1.Flags.string({ description: 'When the watch must stop (predicate form, required)' }),
45
- 'check-every': core_1.Flags.string({ description: 'Observation interval (predicate form)' }),
46
- 'max-checks': core_1.Flags.integer({ description: 'Observation cap (predicate form)' }),
45
+ deadline: core_1.Flags.string({
46
+ description: 'When the watch must stop (predicate form, required — refused on a recipe watch, which ends through its --wait)',
47
+ }),
48
+ 'check-every': core_1.Flags.string({
49
+ description: 'Observation interval (predicate form — refused on a recipe watch)',
50
+ }),
51
+ 'max-checks': core_1.Flags.integer({
52
+ description: 'Observation cap (predicate form — refused on a recipe watch)',
53
+ }),
47
54
  'root-path': core_1.Flags.string({ description: 'JSON root path inside the response' }),
48
55
  'resume-prompt': core_1.Flags.string({ description: 'What to tell the settling turn' }),
49
56
  // recipe form
@@ -53,10 +60,15 @@ class FollowupsWatch extends base_command_1.BaseCommand {
53
60
  success: core_1.Flags.string({ description: 'What state means the wait resolved (recipe form)' }),
54
61
  abort: core_1.Flags.string({ description: 'When to stop and escalate (recipe form)' }),
55
62
  wait: core_1.Flags.string({
56
- description: 'Wait kind (recipe form)',
63
+ description: 'Wait kind (recipe form, required)',
57
64
  options: [...followups_1.WATCH_WAIT_KINDS],
58
65
  }),
59
- 'wait-at': core_1.Flags.string({ description: 'Instant/duration for --wait clock' }),
66
+ 'wait-at': core_1.Flags.string({
67
+ // Naming both shapes here and in the server's rejection, because the
68
+ // duration grammar is a shorthand and not ISO-8601: `PT45M` is refused
69
+ // (OSK-10392).
70
+ description: 'When --wait clock fires: an ISO-8601 instant, or a duration from now (45m, 2h, 3d, 1w)',
71
+ }),
60
72
  'wait-entity': core_1.Flags.string({ description: 'Entity type for --wait event' }),
61
73
  'wait-event': core_1.Flags.string({ description: 'Event type for --wait event' }),
62
74
  'wait-filter-field': core_1.Flags.string({ description: 'Payload field for an event filter' }),
@@ -161,6 +173,19 @@ class FollowupsWatch extends base_command_1.BaseCommand {
161
173
  });
162
174
  }
163
175
  }
176
+ const boundsRefusal = (0, followups_1.recipeWatchBoundsRefusal)(flags.wait, {
177
+ deadline: flags.deadline,
178
+ checkEvery: flags['check-every'],
179
+ maxChecks: flags['max-checks'],
180
+ });
181
+ if (boundsRefusal) {
182
+ this.failWithCliError({
183
+ message: boundsRefusal,
184
+ code: 'FOLLOWUP_INVALID_FIELD',
185
+ exit: 2,
186
+ retryable: false,
187
+ });
188
+ }
164
189
  const wait = { kind: flags.wait };
165
190
  if (flags.wait === 'clock') {
166
191
  wait.at = flags['wait-at'];
@@ -188,6 +213,16 @@ class FollowupsWatch extends base_command_1.BaseCommand {
188
213
  success: flags.success,
189
214
  abort: flags.abort,
190
215
  wait,
216
+ // Forwarded so the SERVER refuses them, rather than dropped here so
217
+ // nobody does (OSK-10407). These are predicate-only bounds; this branch
218
+ // used to assemble the five fields above and silently discard the rest,
219
+ // so `--deadline 7d` armed a watch and reported success having stored
220
+ // no deadline. The rule that answers now lives once, in
221
+ // `normalizeFollowUpInput`, so the CLI and the agent-facing paths
222
+ // cannot drift into disagreeing about which form owns a flag.
223
+ ...(flags.deadline !== undefined ? { deadline: flags.deadline } : {}),
224
+ ...(flags['check-every'] !== undefined ? { checkEvery: flags['check-every'] } : {}),
225
+ ...(flags['max-checks'] !== undefined ? { maxChecks: flags['max-checks'] } : {}),
191
226
  });
192
227
  }
193
228
  let response;
@@ -38,9 +38,17 @@ class SpacesBriefsGet extends base_command_1.BaseCommand {
38
38
  return;
39
39
  }
40
40
  const b = result;
41
+ const expertise = Array.isArray(b.expertise) ? b.expertise : [];
42
+ const dismissed = Array.isArray(b.dismissedExpertiseSuggestions)
43
+ ? b.dismissedExpertiseSuggestions
44
+ : [];
41
45
  this.log(`Principal Type: ${b.principalType ?? flags.principal}`);
42
46
  this.log(`Principal ID: ${b.principalId ?? flags.id}`);
43
47
  this.log(`Updated: ${b.updatedAt ? (0, format_1.localTime)(b.updatedAt) : '-'}`);
48
+ this.log(`Expertise: ${expertise.length > 0 ? expertise.join(', ') : '-'}`);
49
+ if (dismissed.length > 0) {
50
+ this.log(`Dismissed: ${dismissed.join(', ')}`);
51
+ }
44
52
  this.log('');
45
53
  this.log('Content:');
46
54
  const content = typeof b.content === 'string' ? b.content : '';
@@ -39,12 +39,14 @@ class SpacesBriefsList extends base_command_1.BaseCommand {
39
39
  const rows = items.map((b) => ({
40
40
  principalType: String(b.principalType ?? '-'),
41
41
  principalId: String(b.principalId ?? '-'),
42
+ expertise: Array.isArray(b.expertise) ? b.expertise.join(', ') : '-',
42
43
  length: String(typeof b.content === 'string' ? b.content.length : 0),
43
44
  updated: b.updatedAt ? (0, format_1.localTime)(b.updatedAt) : '-',
44
45
  }));
45
46
  (0, format_1.renderTable)(rows, [
46
47
  { key: 'principalType', header: 'PRINCIPAL TYPE' },
47
48
  { key: 'principalId', header: 'PRINCIPAL ID' },
49
+ { key: 'expertise', header: 'EXPERTISE', maxWidth: 40 },
48
50
  { key: 'length', header: 'LENGTH' },
49
51
  { key: 'updated', header: 'UPDATED' },
50
52
  ], (line) => this.log(line));
@@ -11,6 +11,9 @@ export default class SpacesBriefsSet extends BaseCommand {
11
11
  id: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
12
12
  content: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
13
13
  'content-file': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
14
+ expertise: import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
15
+ 'add-expertise': import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
16
+ 'dismiss-expertise': import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
14
17
  'from-json': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
15
18
  };
16
19
  run(): Promise<void>;
@@ -44,6 +44,9 @@ class SpacesBriefsSet extends base_command_1.BaseCommand {
44
44
  static examples = [
45
45
  '<%= config.bin %> spaces briefs set <space-id> --principal user --id <user-id> --content "You are..."',
46
46
  '<%= config.bin %> spaces briefs set <space-id> --principal agent --id <agent-id> --content-file brief.md',
47
+ '<%= config.bin %> spaces briefs set <space-id> --principal agent --id <agent-id> --expertise browser --expertise frontend',
48
+ '<%= config.bin %> spaces briefs set <space-id> --principal agent --id <agent-id> --add-expertise reliability',
49
+ '<%= config.bin %> spaces briefs set <space-id> --principal agent --id <agent-id> --dismiss-expertise browser',
47
50
  '<%= config.bin %> spaces briefs set <space-id> --principal user --id <user-id> --from-json brief.json',
48
51
  ];
49
52
  static args = {
@@ -59,15 +62,36 @@ class SpacesBriefsSet extends base_command_1.BaseCommand {
59
62
  id: core_1.Flags.string({ description: 'Principal ID', required: true }),
60
63
  content: core_1.Flags.string({ description: 'Brief content string' }),
61
64
  'content-file': core_1.Flags.string({ description: 'Path to a file whose content becomes the brief' }),
65
+ expertise: core_1.Flags.string({
66
+ description: 'Domain label this member is best suited for in the space (repeatable, ≤20 items, ≤40 chars each). Replaces the full expertise list.',
67
+ multiple: true,
68
+ }),
69
+ 'add-expertise': core_1.Flags.string({
70
+ description: 'Add a domain label to the existing expertise list (repeatable, merged — does not replace)',
71
+ multiple: true,
72
+ }),
73
+ 'dismiss-expertise': core_1.Flags.string({
74
+ description: 'Dismiss an expertise suggestion so it stops appearing (repeatable, merged into the dismissed list)',
75
+ multiple: true,
76
+ }),
62
77
  'from-json': core_1.Flags.string({
63
- description: 'Path to a JSON file (or "-" for stdin) containing { content: string }',
78
+ description: 'Path to a JSON file (or "-" for stdin) containing { content?: string, expertise?: string[], dismissedExpertiseSuggestions?: string[] }',
64
79
  }),
65
80
  };
66
81
  async run() {
67
82
  this.requireAuth();
68
83
  const { args, flags } = await this.parse(SpacesBriefsSet);
69
84
  let content;
85
+ let expertise;
86
+ let dismissed;
87
+ const addExpertise = flags['add-expertise'] ?? [];
88
+ const dismissExpertise = flags['dismiss-expertise'] ?? [];
70
89
  if (flags['from-json']) {
90
+ if (addExpertise.length > 0 || dismissExpertise.length > 0) {
91
+ this.error('--add-expertise / --dismiss-expertise cannot be combined with --from-json.', {
92
+ exit: 1,
93
+ });
94
+ }
71
95
  let body = {};
72
96
  try {
73
97
  body = await (0, prompt_1.readJsonBody)(flags['from-json']);
@@ -75,24 +99,56 @@ class SpacesBriefsSet extends base_command_1.BaseCommand {
75
99
  catch (err) {
76
100
  this.error(err.message, { exit: 1 });
77
101
  }
78
- if (typeof body.content !== 'string') {
79
- this.error('JSON body must contain a "content" string field.', { exit: 1 });
102
+ if (body.content !== undefined && typeof body.content !== 'string') {
103
+ this.error('JSON body "content" must be a string when provided.', { exit: 1 });
104
+ }
105
+ for (const key of ['expertise', 'dismissedExpertiseSuggestions']) {
106
+ if (body[key] !== undefined && !Array.isArray(body[key])) {
107
+ this.error(`JSON body "${key}" must be an array of strings when provided.`, { exit: 1 });
108
+ }
80
109
  }
81
110
  content = body.content;
111
+ expertise = body.expertise;
112
+ dismissed = body.dismissedExpertiseSuggestions;
82
113
  }
83
- else if (flags['content-file']) {
84
- try {
85
- content = fs.readFileSync(flags['content-file'], 'utf8');
114
+ else {
115
+ if (flags['content-file']) {
116
+ try {
117
+ content = fs.readFileSync(flags['content-file'], 'utf8');
118
+ }
119
+ catch (err) {
120
+ this.error(`Could not read content file: ${err.message}`, { exit: 1 });
121
+ }
86
122
  }
87
- catch (err) {
88
- this.error(`Could not read content file: ${err.message}`, { exit: 1 });
123
+ else if (flags.content !== undefined) {
124
+ content = flags.content;
125
+ }
126
+ if (flags.expertise !== undefined) {
127
+ expertise = flags.expertise;
89
128
  }
90
129
  }
91
- else if (flags.content !== undefined) {
92
- content = flags.content;
130
+ const needsMerge = addExpertise.length > 0 || dismissExpertise.length > 0;
131
+ if (needsMerge) {
132
+ let existing = {};
133
+ try {
134
+ existing = (await data_provider_1.dataService.getSpaceBrief(args.id, flags.principal, flags.id));
135
+ }
136
+ catch {
137
+ existing = {};
138
+ }
139
+ if (addExpertise.length > 0) {
140
+ const base = expertise ?? (Array.isArray(existing.expertise) ? existing.expertise : []);
141
+ expertise = [...new Set([...base, ...addExpertise])];
142
+ }
143
+ if (dismissExpertise.length > 0) {
144
+ const current = Array.isArray(existing.dismissedExpertiseSuggestions)
145
+ ? existing.dismissedExpertiseSuggestions
146
+ : [];
147
+ dismissed = [...new Set([...current, ...dismissExpertise])];
148
+ }
93
149
  }
94
- else {
95
- this.error('Provide content via --content, --content-file, or --from-json.', { exit: 1 });
150
+ if (content === undefined && expertise === undefined && dismissed === undefined) {
151
+ this.error('Provide --content, --content-file, --expertise, --add-expertise, --dismiss-expertise, or --from-json.', { exit: 1 });
96
152
  }
97
153
  let result;
98
154
  try {
@@ -101,6 +157,8 @@ class SpacesBriefsSet extends base_command_1.BaseCommand {
101
157
  principalType: flags.principal,
102
158
  principalId: flags.id,
103
159
  content,
160
+ expertise,
161
+ dismissedExpertiseSuggestions: dismissed,
104
162
  });
105
163
  }
106
164
  catch (err) {
@@ -110,7 +168,14 @@ class SpacesBriefsSet extends base_command_1.BaseCommand {
110
168
  this.log(JSON.stringify((0, space_output_1.withSpacePerspectiveUrl)(this.cliConfig.baseURL, args.id, 'members', result), null, 2));
111
169
  return;
112
170
  }
113
- this.log(`Saved brief for ${flags.principal} ${flags.id} in space ${args.id} (${content.length} chars)`);
171
+ const changed = [
172
+ content !== undefined ? `content (${content.length} chars)` : null,
173
+ expertise !== undefined ? `expertise=[${expertise.join(', ')}]` : null,
174
+ dismissed !== undefined ? `dismissed=[${dismissed.join(', ')}]` : null,
175
+ ]
176
+ .filter(Boolean)
177
+ .join(', ');
178
+ this.log(`Saved brief for ${flags.principal} ${flags.id} in space ${args.id}: ${changed}`);
114
179
  }
115
180
  }
116
181
  exports.default = SpacesBriefsSet;
@@ -0,0 +1,12 @@
1
+ import { BaseCommand } from '../../base-command';
2
+ export default class SpacesExpertiseSuggestions extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
10
+ };
11
+ run(): Promise<void>;
12
+ }
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const data_provider_1 = require("@skrr-ai/data-provider");
5
+ const base_command_1 = require("../../base-command");
6
+ const format_1 = require("../../lib/format");
7
+ const space_output_1 = require("../../lib/space-output");
8
+ class SpacesExpertiseSuggestions extends base_command_1.BaseCommand {
9
+ static description = 'Advisory domain expertise inferred from settled-task history (computed on read, never persisted)';
10
+ static examples = [
11
+ '<%= config.bin %> spaces expertise-suggestions <space-id>',
12
+ '<%= config.bin %> spaces expertise-suggestions <space-id> --json',
13
+ ];
14
+ static args = {
15
+ id: core_1.Args.string({ description: 'Space ID', required: true, ignoreStdin: true }),
16
+ };
17
+ static flags = {
18
+ json: core_1.Flags.boolean({ description: 'Output as JSON' }),
19
+ };
20
+ async run() {
21
+ this.requireAuth();
22
+ const { args, flags } = await this.parse(SpacesExpertiseSuggestions);
23
+ let response;
24
+ let members;
25
+ try {
26
+ response = await data_provider_1.dataService.getSpaceExpertiseSuggestions(args.id);
27
+ members = await data_provider_1.dataService.getSpaceMembers(args.id).catch(() => undefined);
28
+ }
29
+ catch (err) {
30
+ this.handleApiError(err);
31
+ }
32
+ if (flags.json) {
33
+ this.log(JSON.stringify((0, space_output_1.withSpacePerspectiveUrl)(this.cliConfig.baseURL, args.id, 'members', response), null, 2));
34
+ return;
35
+ }
36
+ const suggestions = response.suggestions ?? [];
37
+ if (suggestions.length === 0) {
38
+ this.log('No expertise suggestions.');
39
+ return;
40
+ }
41
+ const nameByAgentId = new Map();
42
+ for (const agent of members?.agents ?? []) {
43
+ if (agent.id)
44
+ nameByAgentId.set(agent.id, agent.name || agent.id);
45
+ if (agent._id)
46
+ nameByAgentId.set(agent._id, agent.name || agent.id);
47
+ }
48
+ const rows = suggestions.map((s) => {
49
+ const evidence = (s.evidence ?? {});
50
+ const agentId = String(s.agentId ?? '-');
51
+ return {
52
+ agent: nameByAgentId.get(agentId) ?? agentId,
53
+ suggested: String(s.suggestedExpertise ?? '-'),
54
+ evidence: evidence.tasks !== undefined && evidence.succeeded !== undefined
55
+ ? `${evidence.succeeded}/${evidence.tasks} succeeded`
56
+ : '-',
57
+ samples: (evidence.sampleIdentifiers ?? []).join(', ') || '-',
58
+ };
59
+ });
60
+ (0, format_1.renderTable)(rows, [
61
+ { key: 'agent', header: 'AGENT', maxWidth: 28 },
62
+ { key: 'suggested', header: 'SUGGESTED EXPERTISE', maxWidth: 32 },
63
+ { key: 'evidence', header: 'EVIDENCE' },
64
+ { key: 'samples', header: 'SAMPLE TASKS', maxWidth: 40 },
65
+ ], (line) => this.log(line));
66
+ this.log('');
67
+ this.log('Accept: skrr spaces briefs set <space-id> --principal agent --id <agent-id> --expertise <value>');
68
+ this.log('Dismiss: skrr spaces briefs set <space-id> --principal agent --id <agent-id> --dismiss-expertise <value>');
69
+ }
70
+ }
71
+ exports.default = SpacesExpertiseSuggestions;
@@ -12,6 +12,7 @@ export default class SpacesMembersAdd extends BaseCommand {
12
12
  role: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
13
13
  brief: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
14
14
  'brief-file': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
15
+ expertise: import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
15
16
  };
16
17
  run(): Promise<void>;
17
18
  }