@skrr-ai/cli 0.1.19 → 0.1.21

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 (53) 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/decide.js +22 -5
  4. package/dist/commands/commitments/action-proposals/execute.js +7 -3
  5. package/dist/commands/commitments/action-proposals/propose.d.ts +32 -0
  6. package/dist/commands/commitments/action-proposals/propose.js +111 -0
  7. package/dist/commands/commitments/action-proposals.js +6 -4
  8. package/dist/commands/commitments/create.d.ts +2 -0
  9. package/dist/commands/commitments/create.js +42 -2
  10. package/dist/commands/commitments/doctor.js +11 -0
  11. package/dist/commands/commitments/update.d.ts +2 -0
  12. package/dist/commands/commitments/update.js +40 -5
  13. package/dist/commands/store/browse.d.ts +14 -0
  14. package/dist/commands/store/browse.js +71 -0
  15. package/dist/commands/store/install.d.ts +16 -0
  16. package/dist/commands/store/install.js +54 -0
  17. package/dist/commands/store/releases.d.ts +19 -0
  18. package/dist/commands/store/releases.js +60 -0
  19. package/dist/commands/store/update.d.ts +15 -0
  20. package/dist/commands/store/update.js +69 -0
  21. package/dist/commands/store/updates.d.ts +24 -0
  22. package/dist/commands/store/updates.js +113 -0
  23. package/dist/commands/subscriptions/cancel.d.ts +14 -0
  24. package/dist/commands/subscriptions/cancel.js +47 -0
  25. package/dist/commands/subscriptions/health.d.ts +24 -0
  26. package/dist/commands/subscriptions/health.js +62 -0
  27. package/dist/commands/subscriptions/list.d.ts +9 -0
  28. package/dist/commands/subscriptions/list.js +52 -0
  29. package/dist/commands/subscriptions/status.d.ts +12 -0
  30. package/dist/commands/subscriptions/status.js +49 -0
  31. package/dist/commands/subscriptions/subscribe.d.ts +15 -0
  32. package/dist/commands/subscriptions/subscribe.js +59 -0
  33. package/dist/commands/tasks/complete.d.ts +10 -4
  34. package/dist/commands/tasks/complete.js +7 -5
  35. package/dist/commands/tasks/self-schedule.d.ts +30 -0
  36. package/dist/commands/tasks/self-schedule.js +114 -0
  37. package/dist/commands/triggers/disable.js +3 -1
  38. package/dist/commands/triggers/enable.js +3 -1
  39. package/dist/commands/triggers/rotate-secret.js +5 -0
  40. package/dist/commands/triggers/show.js +3 -1
  41. package/dist/lib/commitment-product.js +9 -0
  42. package/dist/lib/commitments.d.ts +21 -1
  43. package/dist/lib/commitments.js +62 -1
  44. package/dist/lib/node-adapter.js +4 -0
  45. package/dist/lib/task-extras.d.ts +18 -0
  46. package/dist/lib/task-extras.js +19 -1
  47. package/dist/lib/triggers.d.ts +21 -2
  48. package/dist/lib/triggers.js +48 -1
  49. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refreshClassification.js +9 -2
  50. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refreshClassification.js +9 -2
  51. package/dist/node_modules/@skrr-ai/data-provider/index.js +4073 -3904
  52. package/oclif.manifest.json +18424 -17362
  53. package/package.json +1 -1
@@ -371,18 +371,21 @@ export declare function describeResourceNotFound(serverMessage: string | undefin
371
371
  export declare function describeAssigneeNotInSpace(serverMessage: string | undefined, bin?: string): string;
372
372
  export declare function describeForbidden(bodyMessage?: string | null, method?: string | null): string;
373
373
  /**
374
- * True when a credential is unmistakably machine/daemon authority rather
375
- * than a human CLI session. This is a diagnostic guard only, never an
376
- * authorization decision: the server remains authoritative for every token.
374
+ * Identify an authority that is unmistakably machine-only rather than a human
375
+ * CLI session. This is a diagnostic guard only, never an authorization
376
+ * decision: the server remains authoritative for every token.
377
377
  *
378
378
  * Opaque daemon access tokens have a stable public prefix. Legacy daemon JWTs
379
- * expose `scope: "daemon"`; decoding the unsigned payload lets the CLI give a
380
- * repair instruction instead of calling a predictable 403 a transient edge
381
- * failure. Malformed and unknown token shapes intentionally return false so a
382
- * generic authentication failure remains honest.
379
+ * expose `scope: "daemon"`; bootstrap-only JWTs use `"daemon-install"`. Both
380
+ * scopes are incompatible with human CLI commands, so identifying either lets
381
+ * the CLI give a repair instruction instead of describing a predictable 403 as
382
+ * a transient edge failure. Malformed and unknown token shapes intentionally
383
+ * return false so a generic authentication failure remains honest.
383
384
  */
385
+ type IncompatibleCliCredentialScope = 'daemon' | 'daemon-install';
386
+ /** Compatibility export for callers specifically distinguishing daemon scope. */
384
387
  export declare function isDaemonScopedCredential(token?: string | null): boolean;
385
- /** The one repair for a daemon token passed to a human CLI command. */
386
- export declare function describeDaemonScopedCredential(bin: string): string;
388
+ /** The one repair for a machine-only token passed to a human CLI command. */
389
+ export declare function describeDaemonScopedCredential(bin: string, scope?: IncompatibleCliCredentialScope, source?: ResolvedCredential['source']): string;
387
390
  export declare function isRetryableFailure(status: number | undefined, body: Record<string, unknown> | null): boolean;
388
391
  export { Flags };
@@ -578,9 +578,10 @@ class BaseCommand extends core_1.Command {
578
578
  retryable: false,
579
579
  });
580
580
  }
581
- if (isDaemonScopedCredential(this.resolvedCredential.token)) {
581
+ const incompatibleScope = incompatibleCliCredentialScope(this.resolvedCredential.token);
582
+ if (incompatibleScope) {
582
583
  this.failWithCliError({
583
- message: describeDaemonScopedCredential(this.config.bin),
584
+ message: describeDaemonScopedCredential(this.config.bin, incompatibleScope, this.resolvedCredential.source),
584
585
  code: 'DAEMON_SCOPE_CREDENTIAL',
585
586
  exit: 2,
586
587
  retryable: false,
@@ -817,10 +818,10 @@ class BaseCommand extends core_1.Command {
817
818
  // request and a supplied `--token` can bypass their normal preflight.
818
819
  // A daemon credential reaching a human endpoint is deterministic, not
819
820
  // an edge outage; retrying only repeats the same 401/403.
820
- if ((e.status === 401 || e.status === 403) &&
821
- isDaemonScopedCredential(this.resolvedCredential?.token)) {
821
+ const incompatibleScope = incompatibleCliCredentialScope(this.resolvedCredential?.token);
822
+ if ((e.status === 401 || e.status === 403) && incompatibleScope) {
822
823
  this.failWithCliError({
823
- message: say(describeDaemonScopedCredential(this.config.bin)),
824
+ message: say(describeDaemonScopedCredential(this.config.bin, incompatibleScope, this.resolvedCredential?.source)),
824
825
  code: 'DAEMON_SCOPE_CREDENTIAL',
825
826
  status: e.status,
826
827
  exit: 2,
@@ -1416,38 +1417,38 @@ function describeForbidden(bodyMessage, method) {
1416
1417
  }
1417
1418
  return `${lead}.`;
1418
1419
  }
1419
- /**
1420
- * True when a credential is unmistakably machine/daemon authority rather
1421
- * than a human CLI session. This is a diagnostic guard only, never an
1422
- * authorization decision: the server remains authoritative for every token.
1423
- *
1424
- * Opaque daemon access tokens have a stable public prefix. Legacy daemon JWTs
1425
- * expose `scope: "daemon"`; decoding the unsigned payload lets the CLI give a
1426
- * repair instruction instead of calling a predictable 403 a transient edge
1427
- * failure. Malformed and unknown token shapes intentionally return false so a
1428
- * generic authentication failure remains honest.
1429
- */
1430
- function isDaemonScopedCredential(token) {
1420
+ function incompatibleCliCredentialScope(token) {
1431
1421
  if (typeof token !== 'string' || token.length === 0)
1432
- return false;
1422
+ return null;
1433
1423
  if (token.startsWith('osk_dmn_'))
1434
- return true;
1424
+ return 'daemon';
1435
1425
  const payload = token.split('.')[1];
1436
1426
  if (!payload)
1437
- return false;
1427
+ return null;
1438
1428
  try {
1439
1429
  const decoded = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
1440
- return (typeof decoded === 'object' &&
1430
+ const scope = typeof decoded === 'object' &&
1441
1431
  decoded !== null &&
1442
- decoded.scope === 'daemon');
1432
+ typeof decoded.scope === 'string'
1433
+ ? decoded.scope
1434
+ : null;
1435
+ return scope === 'daemon' || scope === 'daemon-install' ? scope : null;
1443
1436
  }
1444
1437
  catch {
1445
- return false;
1438
+ return null;
1446
1439
  }
1447
1440
  }
1448
- /** The one repair for a daemon token passed to a human CLI command. */
1449
- function describeDaemonScopedCredential(bin) {
1450
- return ('This command received a daemon-scoped credential, which cannot access user API commands. ' +
1441
+ /** Compatibility export for callers specifically distinguishing daemon scope. */
1442
+ function isDaemonScopedCredential(token) {
1443
+ return incompatibleCliCredentialScope(token) === 'daemon';
1444
+ }
1445
+ /** The one repair for a machine-only token passed to a human CLI command. */
1446
+ function describeDaemonScopedCredential(bin, scope = 'daemon', source) {
1447
+ const scopeLabel = scope === 'daemon-install' ? 'daemon-install-scoped' : 'daemon-scoped';
1448
+ const subject = source === 'keychain' || source === 'file'
1449
+ ? `The stored credential has ${scope === 'daemon-install' ? 'daemon-install' : 'daemon'} scope`
1450
+ : `This command received a ${scopeLabel} credential`;
1451
+ return (`${subject}, which cannot access user API commands. ` +
1451
1452
  `Run \`${bin} login\` to create a CLI credential. For the local runtime itself, use \`${bin} daemon login\`.`);
1452
1453
  }
1453
1454
  function isRetryableFailure(status, body) {
@@ -4,10 +4,14 @@ const core_1 = require("@oclif/core");
4
4
  const base_command_1 = require("../../../base-command");
5
5
  const commitments_1 = require("../../../lib/commitments");
6
6
  class CommitmentsActionProposalsDecide extends base_command_1.BaseCommand {
7
- static description = 'Approve & run, or reject, one frozen prepared action';
7
+ static description = 'Approve & run, or reject, one frozen Action Bundle';
8
8
  static args = {
9
9
  commitmentId: core_1.Args.string({ description: 'Commitment ID', required: true, ignoreStdin: true }),
10
- proposalId: core_1.Args.string({ description: 'Action Proposal ID', required: true, ignoreStdin: true }),
10
+ proposalId: core_1.Args.string({
11
+ description: 'Action Proposal ID',
12
+ required: true,
13
+ ignoreStdin: true,
14
+ }),
11
15
  };
12
16
  static flags = {
13
17
  json: core_1.Flags.boolean({ description: 'Output the decision and execution receipts as JSON' }),
@@ -19,7 +23,20 @@ class CommitmentsActionProposalsDecide extends base_command_1.BaseCommand {
19
23
  const decision = flags.decision;
20
24
  let result;
21
25
  try {
22
- const decided = await commitments_1.commitmentApi.decideActionProposal(args.commitmentId, args.proposalId, decision);
26
+ const listed = await commitments_1.commitmentApi.actionProposals(args.commitmentId, { limit: 200 });
27
+ const proposal = listed.data.find((item) => item.id === args.proposalId);
28
+ if (!proposal)
29
+ this.error('Action Bundle not found.');
30
+ if (Number(proposal.bundleVersion) >= 2 && !proposal.snapshotDigest) {
31
+ this.error('Action Bundle has no review digest and cannot be decided safely.');
32
+ }
33
+ if (!flags.json && proposal.snapshotDigest) {
34
+ this.log(`Action Bundle digest: ${proposal.snapshotDigest}`);
35
+ }
36
+ const decided = await commitments_1.commitmentApi.decideActionProposal(args.commitmentId, args.proposalId, decision, {
37
+ snapshotDigest: proposal.snapshotDigest,
38
+ bundleVersion: proposal.bundleVersion,
39
+ });
23
40
  const execution = decision === 'approve' && decided.status === 'approved'
24
41
  ? await commitments_1.commitmentApi.executeActionProposal(args.commitmentId, args.proposalId)
25
42
  : undefined;
@@ -31,10 +48,10 @@ class CommitmentsActionProposalsDecide extends base_command_1.BaseCommand {
31
48
  if (flags.json)
32
49
  this.log(JSON.stringify(result, null, 2));
33
50
  else if (decision === 'approve') {
34
- this.log('Prepared action approved and handed to the durable executor.');
51
+ this.log('Action Bundle approved and handed to the durable executor.');
35
52
  }
36
53
  else {
37
- this.log('Prepared action rejected.');
54
+ this.log('Action Bundle rejected.');
38
55
  }
39
56
  }
40
57
  }
@@ -4,10 +4,14 @@ const core_1 = require("@oclif/core");
4
4
  const base_command_1 = require("../../../base-command");
5
5
  const commitments_1 = require("../../../lib/commitments");
6
6
  class CommitmentsActionProposalsExecute extends base_command_1.BaseCommand {
7
- static description = 'Nudge an approved action now (recovery continues automatically)';
7
+ static description = 'Nudge an approved Action Bundle now (recovery continues automatically)';
8
8
  static args = {
9
9
  commitmentId: core_1.Args.string({ description: 'Commitment ID', required: true, ignoreStdin: true }),
10
- proposalId: core_1.Args.string({ description: 'Action Proposal ID', required: true, ignoreStdin: true }),
10
+ proposalId: core_1.Args.string({
11
+ description: 'Action Proposal ID',
12
+ required: true,
13
+ ignoreStdin: true,
14
+ }),
11
15
  };
12
16
  static flags = { json: core_1.Flags.boolean({ description: 'Output as JSON' }) };
13
17
  async run() {
@@ -23,7 +27,7 @@ class CommitmentsActionProposalsExecute extends base_command_1.BaseCommand {
23
27
  if (flags.json)
24
28
  this.log(JSON.stringify(result, null, 2));
25
29
  else
26
- this.log(`Prepared action is ${String(result.status || 'queued')}.`);
30
+ this.log(`Action Bundle is ${String(result.status || 'queued')}.`);
27
31
  }
28
32
  }
29
33
  exports.default = CommitmentsActionProposalsExecute;
@@ -0,0 +1,32 @@
1
+ import { BaseCommand } from '../../../base-command';
2
+ /**
3
+ * Propose a prepared action against a commitment check.
4
+ *
5
+ * `decide` and `execute` shipped without this, so a headless operator could only
6
+ * ever REACT to proposals the server made. An agent that has investigated a
7
+ * commitment and knows what should be done had no way to say so — the domain
8
+ * was half-reachable in exactly the way the parity rule exists to catch.
9
+ *
10
+ * Nothing here executes. A proposal is FROZEN and waits for `decide`, which is
11
+ * the whole point of the two-step: the thing that decides what to do and the
12
+ * thing that authorizes it are deliberately different acts.
13
+ */
14
+ export default class CommitmentsActionProposalsPropose extends BaseCommand {
15
+ static description: string;
16
+ static examples: string[];
17
+ static args: {
18
+ commitmentId: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
19
+ };
20
+ static flags: {
21
+ json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
22
+ check: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
23
+ type: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
24
+ config: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
25
+ file: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
26
+ diagnosis: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
27
+ changes: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
28
+ verification: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
29
+ run: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
30
+ };
31
+ run(): Promise<void>;
32
+ }
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const node_fs_1 = require("node:fs");
4
+ const core_1 = require("@oclif/core");
5
+ const data_provider_1 = require("@skrr-ai/data-provider");
6
+ const base_command_1 = require("../../../base-command");
7
+ const commitments_1 = require("../../../lib/commitments");
8
+ /**
9
+ * Propose a prepared action against a commitment check.
10
+ *
11
+ * `decide` and `execute` shipped without this, so a headless operator could only
12
+ * ever REACT to proposals the server made. An agent that has investigated a
13
+ * commitment and knows what should be done had no way to say so — the domain
14
+ * was half-reachable in exactly the way the parity rule exists to catch.
15
+ *
16
+ * Nothing here executes. A proposal is FROZEN and waits for `decide`, which is
17
+ * the whole point of the two-step: the thing that decides what to do and the
18
+ * thing that authorizes it are deliberately different acts.
19
+ */
20
+ class CommitmentsActionProposalsPropose extends base_command_1.BaseCommand {
21
+ static description = 'Propose a prepared action for a commitment check (frozen until approved)';
22
+ static examples = [
23
+ '<%= config.bin %> commitments action-proposals propose cmt_123 --check chk_456 --type create_task --config \'{"title":"Roll back the deploy"}\'',
24
+ '<%= config.bin %> commitments action-proposals propose cmt_123 --check chk_456 --file proposal.json',
25
+ ];
26
+ static args = {
27
+ commitmentId: core_1.Args.string({ description: 'Commitment ID', required: true, ignoreStdin: true }),
28
+ };
29
+ static flags = {
30
+ json: core_1.Flags.boolean({ description: 'Output the created proposal as JSON' }),
31
+ check: core_1.Flags.string({
32
+ description: 'The commitment check this responds to (see `commitments checks`)',
33
+ required: true,
34
+ }),
35
+ type: core_1.Flags.string({
36
+ description: 'Action type',
37
+ options: [...data_provider_1.COMMITMENT_PUBLIC_ACTION_BUNDLE_ACTION_TYPES],
38
+ exclusive: ['file'],
39
+ }),
40
+ config: core_1.Flags.string({ description: 'Action config as JSON', exclusive: ['file'] }),
41
+ file: core_1.Flags.string({
42
+ description: 'A JSON file holding the whole request body — for a multi-action bundle, or anything a flag cannot express',
43
+ exclusive: ['type', 'config'],
44
+ }),
45
+ diagnosis: core_1.Flags.string({ description: 'What you concluded is wrong' }),
46
+ changes: core_1.Flags.string({ description: 'What the action would change' }),
47
+ verification: core_1.Flags.string({ description: 'How to tell afterwards whether it worked' }),
48
+ run: core_1.Flags.string({ description: 'The autonomous run that produced this, if any' }),
49
+ };
50
+ async run() {
51
+ this.requireAuth();
52
+ const { args, flags } = await this.parse(CommitmentsActionProposalsPropose);
53
+ let body;
54
+ if (flags.file) {
55
+ try {
56
+ body = JSON.parse((0, node_fs_1.readFileSync)(flags.file, 'utf8'));
57
+ }
58
+ catch (error) {
59
+ this.error(`Could not read ${flags.file} as JSON: ${error.message}`, {
60
+ exit: 1,
61
+ });
62
+ }
63
+ }
64
+ else {
65
+ if (!flags.type) {
66
+ // Refused rather than defaulted: an action type the caller did not name
67
+ // is a decision nobody made, and this endpoint arms real work.
68
+ this.error('Supply --type (or --file for a multi-action bundle).', { exit: 1 });
69
+ }
70
+ let config;
71
+ if (flags.config) {
72
+ try {
73
+ config = JSON.parse(flags.config);
74
+ }
75
+ catch (error) {
76
+ this.error(`--config is not valid JSON: ${error.message}`, { exit: 1 });
77
+ }
78
+ }
79
+ body = { action: { type: flags.type, ...(config ? { config } : {}) } };
80
+ }
81
+ body.commitmentCheckId = flags.check;
82
+ if (flags.run) {
83
+ body.autonomousRunId = flags.run;
84
+ }
85
+ const bundle = {
86
+ ...(flags.diagnosis ? { diagnosis: flags.diagnosis } : {}),
87
+ ...(flags.changes ? { proposedChanges: flags.changes } : {}),
88
+ ...(flags.verification ? { verificationPlan: flags.verification } : {}),
89
+ };
90
+ if (Object.keys(bundle).length > 0) {
91
+ body.bundle = { ...(body.bundle ?? {}), ...bundle };
92
+ }
93
+ let result;
94
+ try {
95
+ result = await commitments_1.commitmentApi.proposeActionProposal(args.commitmentId, body);
96
+ }
97
+ catch (error) {
98
+ this.handleApiError(error);
99
+ }
100
+ if (flags.json) {
101
+ this.log(JSON.stringify(result, null, 2));
102
+ return;
103
+ }
104
+ const proposal = result
105
+ ?.proposal;
106
+ this.log(`Proposed ${proposal?.id ?? ''} (${proposal?.status ?? 'proposed'}).`);
107
+ // Say the two-step out loud: proposing is not doing.
108
+ this.log(`Nothing runs until it is approved: skrr commitments action-proposals decide ${args.commitmentId} ${proposal?.id ?? '<proposal>'} --decision approve`);
109
+ }
110
+ }
111
+ exports.default = CommitmentsActionProposalsPropose;
@@ -5,7 +5,7 @@ const base_command_1 = require("../../base-command");
5
5
  const commitments_1 = require("../../lib/commitments");
6
6
  const format_1 = require("../../lib/format");
7
7
  class CommitmentsActionProposals extends base_command_1.BaseCommand {
8
- static description = 'List frozen prepared actions and their recovery state';
8
+ static description = 'List frozen Action Bundles and their recovery state';
9
9
  static args = {
10
10
  id: core_1.Args.string({ description: 'Commitment ID', required: true, ignoreStdin: true }),
11
11
  };
@@ -35,18 +35,20 @@ class CommitmentsActionProposals extends base_command_1.BaseCommand {
35
35
  return;
36
36
  }
37
37
  if (!response.data.length) {
38
- this.log('No prepared actions.');
38
+ this.log('No prepared Action Bundles.');
39
39
  return;
40
40
  }
41
41
  (0, format_1.renderTable)(response.data.map((proposal) => ({
42
42
  id: proposal.id,
43
- action: String(proposal.actionSnapshot?.type || 'action').replace(/_/g, ' '),
43
+ actions: String(proposal.actionSnapshots?.length || 1),
44
+ digest: proposal.snapshotDigest || 'legacy',
44
45
  status: proposal.status,
45
46
  attempt: `${proposal.attempt || 0}/${proposal.maxAttempts || 3}`,
46
47
  expires: proposal.expiresAt || '—',
47
48
  })), [
48
49
  { key: 'id', header: 'PROPOSAL', verbatim: true },
49
- { key: 'action', header: 'ACTION', maxWidth: 28 },
50
+ { key: 'actions', header: 'ACTIONS' },
51
+ { key: 'digest', header: 'DIGEST', maxWidth: 64, verbatim: true },
50
52
  { key: 'status', header: 'STATUS' },
51
53
  { key: 'attempt', header: 'ATTEMPT' },
52
54
  { key: 'expires', header: 'EXPIRES', maxWidth: 28 },
@@ -76,6 +76,8 @@ export default class CommitmentsCreate extends BaseCommand {
76
76
  autonomy: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
77
77
  isolation: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
78
78
  'max-nudges': import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
79
+ 'max-concurrent-runs': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
80
+ 'concurrency-reason': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
79
81
  };
80
82
  run(): Promise<void>;
81
83
  }
@@ -288,6 +288,12 @@ class CommitmentsCreate extends base_command_1.BaseCommand {
288
288
  hidden: true,
289
289
  }),
290
290
  'max-nudges': core_1.Flags.integer({ description: 'Max user nudges per day (0..20)' }),
291
+ 'max-concurrent-runs': core_1.Flags.string({
292
+ description: 'Owner concurrency policy: "unlimited" (default) or 1..100. Finite values require --concurrency-reason and cannot be activated by an autonomous Agent session.',
293
+ }),
294
+ 'concurrency-reason': core_1.Flags.string({
295
+ description: 'Why the owner deliberately chose a finite Commitment concurrency cap',
296
+ }),
291
297
  };
292
298
  async run() {
293
299
  this.requireAuth();
@@ -366,7 +372,12 @@ class CommitmentsCreate extends base_command_1.BaseCommand {
366
372
  if (cadence)
367
373
  payload.cadence = cadence;
368
374
  assertEventDrivenWatchedSource(payload);
369
- const policy = {};
375
+ // `--from-json` may already carry a policy. Start from it so an explicit
376
+ // flag refines the submitted contract instead of replacing it.
377
+ const existingPolicy = payload.policy && typeof payload.policy === 'object' && !Array.isArray(payload.policy)
378
+ ? payload.policy
379
+ : {};
380
+ const policy = { ...existingPolicy };
370
381
  if (flags.autonomy)
371
382
  policy.autonomyMode = flags.autonomy;
372
383
  if (flags.mode && flags.autonomy)
@@ -381,6 +392,18 @@ class CommitmentsCreate extends base_command_1.BaseCommand {
381
392
  policy.executionIsolation = flags.isolation;
382
393
  if (flags['max-nudges'] !== undefined)
383
394
  policy.maxUserNudgesPerDay = flags['max-nudges'];
395
+ const concurrencyPatch = (0, commitments_1.commitmentConcurrencyEnvelopePatch)({
396
+ rawValue: flags['max-concurrent-runs'],
397
+ reason: flags['concurrency-reason'],
398
+ });
399
+ if (concurrencyPatch) {
400
+ const existingEnvelope = policy.autonomyEnvelope &&
401
+ typeof policy.autonomyEnvelope === 'object' &&
402
+ !Array.isArray(policy.autonomyEnvelope)
403
+ ? policy.autonomyEnvelope
404
+ : {};
405
+ policy.autonomyEnvelope = { ...existingEnvelope, ...concurrencyPatch };
406
+ }
384
407
  if (Object.keys(policy).length)
385
408
  payload.policy = policy;
386
409
  const payloadPolicy = payload.policy && typeof payload.policy === 'object' && !Array.isArray(payload.policy)
@@ -391,13 +414,30 @@ class CommitmentsCreate extends base_command_1.BaseCommand {
391
414
  : typeof payloadPolicy.autonomyMode === 'string'
392
415
  ? payloadPolicy.autonomyMode
393
416
  : 'monitor';
394
- if (payload.delivery && typeof payload.delivery === 'object' && !Array.isArray(payload.delivery)) {
417
+ if (payload.delivery &&
418
+ typeof payload.delivery === 'object' &&
419
+ !Array.isArray(payload.delivery)) {
395
420
  const declared = payload.delivery;
396
421
  const coherent = (0, commitment_product_1.deliveryForProductMode)(selectedMode, declared);
397
422
  deliveryNormalizedForMonitor =
398
423
  (0, commitment_product_1.productMode)(selectedMode) === 'monitor' && declared.mode !== 'report_only';
399
424
  payload.delivery = coherent;
400
425
  }
426
+ if (payload.createdVia === undefined) {
427
+ payload.createdVia = process.env.OVERSKY_AUTONOMOUS_SESSION === '1' ? 'agent_cli' : 'cli';
428
+ }
429
+ // `--from-json` is intentionally powerful, but it must not be an escape
430
+ // hatch around the same authoring decision the explicit flag enforces.
431
+ const envelope = policy.autonomyEnvelope;
432
+ if (envelope && Object.prototype.hasOwnProperty.call(envelope, 'maxConcurrentRuns')) {
433
+ const value = envelope.maxConcurrentRuns;
434
+ if (process.env.OVERSKY_AUTONOMOUS_SESSION === '1' && value != null) {
435
+ throw new Error('An autonomous Agent cannot activate a finite Commitment concurrency cap from --from-json. Omit it or set it to null; an owner may set it later.');
436
+ }
437
+ if (value != null && !String(envelope.maxConcurrentRunsReason || '').trim()) {
438
+ throw new Error('A finite policy.autonomyEnvelope.maxConcurrentRuns in --from-json requires maxConcurrentRunsReason.');
439
+ }
440
+ }
401
441
  }
402
442
  catch (err) {
403
443
  this.error(err.message, { exit: 1 });
@@ -18,6 +18,16 @@ function doctorVerdict(input) {
18
18
  if (operatingState.coverage && operatingState.coverage.complete === false) {
19
19
  return { verdict: 'degraded', next: 'Retry doctor; one or more operating-state projections are unavailable' };
20
20
  }
21
+ const autonomy = operatingState.autonomy;
22
+ const budget = autonomy?.budget;
23
+ const concurrentRuns = budget?.concurrentRuns;
24
+ const decision = concurrentRuns?.decision;
25
+ if (concurrentRuns?.mode === 'limited' && decision?.status === 'review_required') {
26
+ return {
27
+ verdict: 'policy_review_required',
28
+ next: 'Confirm the cap with --max-concurrent-runs <n> --concurrency-reason "...", or remove it with --max-concurrent-runs unlimited',
29
+ };
30
+ }
21
31
  if (operatingState.status && commitment.status === 'active') {
22
32
  const status = String(operatingState.status);
23
33
  return {
@@ -76,6 +86,7 @@ class CommitmentsDoctor extends base_command_1.BaseCommand {
76
86
  if (autonomy) {
77
87
  this.log(`Mode: ${(0, commitments_1.commitmentAutonomyLabel)(String(autonomy.declaredMode || ''))} → ${(0, commitments_1.commitmentAutonomyLabel)(String(autonomy.effectiveMode || ''))}`);
78
88
  }
89
+ this.log(`Concurrency: ${(0, commitments_1.describeCommitmentConcurrency)(commitment)}`);
79
90
  const approvals = operatingState.approvals;
80
91
  if (approvals?.pendingCount)
81
92
  this.log(`Waiting approval: ${String(approvals.pendingCount)}`);
@@ -47,6 +47,8 @@ export default class CommitmentsUpdate extends BaseCommand {
47
47
  'cadence-mode': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
48
48
  timezone: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
49
49
  isolation: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
50
+ 'max-concurrent-runs': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
51
+ 'concurrency-reason': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
50
52
  };
51
53
  run(): Promise<void>;
52
54
  }
@@ -105,6 +105,12 @@ class CommitmentsUpdate extends base_command_1.BaseCommand {
105
105
  'capability.',
106
106
  options: [...commitments_1.COMMITMENT_EXECUTION_ISOLATION_MODES],
107
107
  }),
108
+ 'max-concurrent-runs': core_1.Flags.string({
109
+ description: 'Set 1..100, or "unlimited" to remove the owner cap. Finite values require --concurrency-reason.',
110
+ }),
111
+ 'concurrency-reason': core_1.Flags.string({
112
+ description: 'Why the owner deliberately chose a finite Commitment concurrency cap',
113
+ }),
108
114
  };
109
115
  async run() {
110
116
  this.requireAuth();
@@ -220,11 +226,40 @@ class CommitmentsUpdate extends base_command_1.BaseCommand {
220
226
  cadence.timezone = flags.timezone;
221
227
  payload.cadence = cadence;
222
228
  }
223
- // A single key, deliberately. `mergePolicyPatch` on the server merges it
224
- // into the stored policy, so naming only this field is what keeps the
225
- // rest of the contract intact.
226
- if (flags.isolation)
227
- payload.policy = { executionIsolation: flags.isolation };
229
+ const concurrencyPatch = (0, commitments_1.commitmentConcurrencyEnvelopePatch)({
230
+ rawValue: flags['max-concurrent-runs'],
231
+ reason: flags['concurrency-reason'],
232
+ });
233
+ if (flags.isolation || concurrencyPatch) {
234
+ const existingPolicy = payload.policy && typeof payload.policy === 'object' && !Array.isArray(payload.policy)
235
+ ? payload.policy
236
+ : {};
237
+ const nextPolicy = { ...existingPolicy };
238
+ if (flags.isolation)
239
+ nextPolicy.executionIsolation = flags.isolation;
240
+ if (concurrencyPatch) {
241
+ const existingEnvelope = nextPolicy.autonomyEnvelope &&
242
+ typeof nextPolicy.autonomyEnvelope === 'object' &&
243
+ !Array.isArray(nextPolicy.autonomyEnvelope)
244
+ ? nextPolicy.autonomyEnvelope
245
+ : {};
246
+ nextPolicy.autonomyEnvelope = { ...existingEnvelope, ...concurrencyPatch };
247
+ }
248
+ payload.policy = nextPolicy;
249
+ }
250
+ const finalPolicy = payload.policy && typeof payload.policy === 'object' && !Array.isArray(payload.policy)
251
+ ? payload.policy
252
+ : undefined;
253
+ const finalEnvelope = finalPolicy?.autonomyEnvelope &&
254
+ typeof finalPolicy.autonomyEnvelope === 'object' &&
255
+ !Array.isArray(finalPolicy.autonomyEnvelope)
256
+ ? finalPolicy.autonomyEnvelope
257
+ : undefined;
258
+ if (process.env.OVERSKY_AUTONOMOUS_SESSION === '1' &&
259
+ finalEnvelope &&
260
+ Object.prototype.hasOwnProperty.call(finalEnvelope, 'maxConcurrentRuns')) {
261
+ throw new Error('An autonomous Agent cannot change owner Commitment concurrency policy. Ask the owner to run this update outside the Agent session.');
262
+ }
228
263
  }
229
264
  catch (err) {
230
265
  this.error(err.message, { exit: 1 });
@@ -0,0 +1,14 @@
1
+ import { BaseCommand } from '../../base-command';
2
+ export default class StoreBrowse extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
7
+ q: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
8
+ category: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
9
+ limit: import("@oclif/core/lib/interfaces").OptionFlag<number, import("@oclif/core/lib/interfaces").CustomOptions>;
10
+ cursor: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
11
+ sort: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
12
+ };
13
+ run(): Promise<void>;
14
+ }