@skrr-ai/cli 0.1.100 → 0.1.102

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 (56) hide show
  1. package/dist/commands/commitments/analytics/detail.d.ts +2 -3
  2. package/dist/commands/commitments/analytics/detail.js +4 -5
  3. package/dist/commands/commitments/control/apply.d.ts +15 -0
  4. package/dist/commands/commitments/control/apply.js +36 -0
  5. package/dist/commands/commitments/control/preview.d.ts +17 -0
  6. package/dist/commands/commitments/control/preview.js +43 -0
  7. package/dist/commands/commitments/control/show.d.ts +12 -0
  8. package/dist/commands/commitments/control/show.js +29 -0
  9. package/dist/commands/commitments/create.js +5 -2
  10. package/dist/commands/commitments/observation/probe.d.ts +15 -0
  11. package/dist/commands/commitments/observation/probe.js +46 -0
  12. package/dist/commands/commitments/observation/summary.d.ts +9 -0
  13. package/dist/commands/commitments/observation/summary.js +31 -0
  14. package/dist/commands/commitments/readiness/show.d.ts +12 -0
  15. package/dist/commands/commitments/readiness/show.js +29 -0
  16. package/dist/commands/commitments/readiness/verify.d.ts +13 -0
  17. package/dist/commands/commitments/readiness/verify.js +33 -0
  18. package/dist/commands/commitments/update.js +11 -9
  19. package/dist/commands/commitments/work-plan.d.ts +11 -0
  20. package/dist/commands/commitments/work-plan.js +36 -0
  21. package/dist/commands/compass/effective-policy.d.ts +12 -0
  22. package/dist/commands/compass/effective-policy.js +34 -0
  23. package/dist/commands/compass/update.d.ts +12 -0
  24. package/dist/commands/compass/update.js +27 -0
  25. package/dist/commands/machines/dedicated/exec.js +3 -1
  26. package/dist/commands/proposals/discard.d.ts +4 -0
  27. package/dist/commands/proposals/discard.js +3 -0
  28. package/dist/commands/proposals/keep.d.ts +4 -0
  29. package/dist/commands/proposals/keep.js +3 -0
  30. package/dist/commands/proposals/snooze.d.ts +4 -0
  31. package/dist/commands/proposals/snooze.js +3 -0
  32. package/dist/commands/signals/monitors/validate.js +1 -1
  33. package/dist/commands/spaces/workflows/cancel-node.d.ts +16 -0
  34. package/dist/commands/spaces/workflows/cancel-node.js +44 -0
  35. package/dist/commands/spaces/workflows/cancel.d.ts +40 -0
  36. package/dist/commands/spaces/workflows/cancel.js +109 -3
  37. package/dist/commands/spaces/workflows/run.d.ts +9 -0
  38. package/dist/commands/spaces/workflows/run.js +38 -0
  39. package/dist/commands/spaces/workflows/update.js +1 -1
  40. package/dist/commands/tasks/schedule.js +1 -1
  41. package/dist/commands/triggers/capabilities.js +7 -0
  42. package/dist/commands/triggers/create.js +4 -1
  43. package/dist/lib/commitments.d.ts +19 -0
  44. package/dist/lib/commitments.js +36 -3
  45. package/dist/lib/compasses.d.ts +3 -0
  46. package/dist/lib/compasses.js +1 -0
  47. package/dist/lib/responsibility-controls.d.ts +11 -0
  48. package/dist/lib/responsibility-controls.js +119 -0
  49. package/dist/lib/stewardship-output.js +13 -2
  50. package/dist/lib/workflow-node-control.d.ts +10 -0
  51. package/dist/lib/workflow-node-control.js +76 -0
  52. package/dist/lib/workspaces.d.ts +2 -0
  53. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  54. package/dist/node_modules/@skrr-ai/data-provider/index.js +6369 -6355
  55. package/oclif.manifest.json +1037 -159
  56. package/package.json +11 -2
@@ -1,10 +1,26 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveExpectedVersion = resolveExpectedVersion;
3
4
  const core_1 = require("@oclif/core");
4
5
  const base_command_1 = require("../../base-command");
5
6
  const prompt_1 = require("../../lib/prompt");
6
7
  const compasses_1 = require("../../lib/compasses");
7
8
  const web_url_1 = require("../../lib/web-url");
9
+ /**
10
+ * The server fences every compass write on the version the caller last saw
11
+ * (`expectedVersion`, required), so a stale writer gets a conflict instead of
12
+ * silently overwriting a newer revision. Precedence: an explicit flag, then a
13
+ * value the caller put in `--json-body` (passed through untouched, so an invalid
14
+ * one is the server's to refuse), then the current version read just now.
15
+ */
16
+ async function resolveExpectedVersion(explicit, fileBody, fetchCurrentVersion) {
17
+ if (explicit !== undefined)
18
+ return { version: explicit, source: 'flag' };
19
+ if (fileBody.expectedVersion !== undefined) {
20
+ return { version: fileBody.expectedVersion, source: 'json-body' };
21
+ }
22
+ return { version: await fetchCurrentVersion(), source: 'current' };
23
+ }
8
24
  /**
9
25
  * `skrr compass update <compass-id> --agent <id>`
10
26
  *
@@ -22,6 +38,7 @@ class CompassUpdate extends base_command_1.BaseCommand {
22
38
  '<%= config.bin %> compass update agch_abc --agent agent_xyz --title "Reliability & cost"',
23
39
  '<%= config.bin %> compass update agch_abc --agent agent_xyz --invariant "p99 under 300ms" --reason "new SLO"',
24
40
  '<%= config.bin %> compass update agch_abc --agent agent_xyz --json-body ./compass.json',
41
+ '<%= config.bin %> compass update agch_abc --agent agent_xyz --title "New" --expected-version 4',
25
42
  ];
26
43
  static args = {
27
44
  compassId: core_1.Args.string({ ignoreStdin: true, description: 'Compass ID', required: true }),
@@ -37,6 +54,11 @@ class CompassUpdate extends base_command_1.BaseCommand {
37
54
  multiple: true,
38
55
  }),
39
56
  reason: core_1.Flags.string({ description: 'Why — recorded on the revision ledger' }),
57
+ 'expected-version': core_1.Flags.integer({
58
+ min: 1,
59
+ description: 'Compass version this change is based on; the update is refused if it moved. ' +
60
+ 'Defaults to the version read at the start of this command.',
61
+ }),
40
62
  'json-body': core_1.Flags.string({ description: 'Path to a JSON file with the full body' }),
41
63
  };
42
64
  async run() {
@@ -61,6 +83,11 @@ class CompassUpdate extends base_command_1.BaseCommand {
61
83
  }
62
84
  let compass;
63
85
  try {
86
+ const expected = await resolveExpectedVersion(flags['expected-version'], fileBody, async () => (await compasses_1.compassApi.get(args.compassId, { agentId: flags.agent })).version);
87
+ body.expectedVersion = expected.version;
88
+ if (expected.source === 'current' && !flags.json) {
89
+ this.logToStderr(`No --expected-version given; updating from the current version (v${String(expected.version)}).`);
90
+ }
64
91
  compass = await compasses_1.compassApi.update(args.compassId, body);
65
92
  }
66
93
  catch (err) {
@@ -104,7 +104,9 @@ class DedicatedMachinesExec extends base_command_1.BaseCommand {
104
104
  description: `How long to let it run, such as 30s or 5m (default 2m, max ${dedicated_machines_1.DEDICATED_EXEC_MAX_TIMEOUT_MS / 60000}m). The process group is killed at the deadline.`,
105
105
  }),
106
106
  'request-id': core_1.Flags.string({
107
- description: 'Idempotency key for this command. Repeating the invocation with the same id returns the run it already started (and its output, for 5 minutes after it ends) instead of running it again. Letters, digits and . _ : / - only.',
107
+ description: 'Idempotency key for this command. Repeating the invocation with the same id returns the run it already started instead of running it again. ' +
108
+ 'Output is kept for at most 5 minutes after it ends and at most 60 seconds after the first terminal read, whichever expires sooner. ' +
109
+ 'Letters, digits and . _ : / - only.',
108
110
  }),
109
111
  workspace: core_1.Flags.string({ description: 'Workspace scope' }),
110
112
  json: core_1.Flags.boolean({ description: 'Output the result as JSON' }),
@@ -1,5 +1,9 @@
1
1
  import { ProposalDecisionCommand } from '../../lib/proposal-decision-command';
2
2
  export default class ProposalsDiscard extends ProposalDecisionCommand {
3
3
  static description: string;
4
+ static args: {
5
+ kind: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
6
+ id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
4
8
  static decision: "discard";
5
9
  }
@@ -3,6 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const proposal_decision_command_1 = require("../../lib/proposal-decision-command");
4
4
  class ProposalsDiscard extends proposal_decision_command_1.ProposalDecisionCommand {
5
5
  static description = 'Discard one displayed proposal through its domain authority';
6
+ // Own, not inherited: oclif spreads the command class into the parser input, and a static a
7
+ // subclass only inherits is dropped, which left these commands with no declared KIND/ID (OSK-15346).
8
+ static args = proposal_decision_command_1.ProposalDecisionCommand.args;
6
9
  static decision = 'discard';
7
10
  }
8
11
  exports.default = ProposalsDiscard;
@@ -1,5 +1,9 @@
1
1
  import { ProposalDecisionCommand } from '../../lib/proposal-decision-command';
2
2
  export default class ProposalsKeep extends ProposalDecisionCommand {
3
3
  static description: string;
4
+ static args: {
5
+ kind: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
6
+ id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
4
8
  static decision: "keep";
5
9
  }
@@ -3,6 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const proposal_decision_command_1 = require("../../lib/proposal-decision-command");
4
4
  class ProposalsKeep extends proposal_decision_command_1.ProposalDecisionCommand {
5
5
  static description = 'Keep one displayed proposal through its domain authority';
6
+ // Own, not inherited: oclif spreads the command class into the parser input, and a static a
7
+ // subclass only inherits is dropped, which left these commands with no declared KIND/ID (OSK-15346).
8
+ static args = proposal_decision_command_1.ProposalDecisionCommand.args;
6
9
  static decision = 'keep';
7
10
  }
8
11
  exports.default = ProposalsKeep;
@@ -1,5 +1,9 @@
1
1
  import { ProposalDecisionCommand } from '../../lib/proposal-decision-command';
2
2
  export default class ProposalsSnooze extends ProposalDecisionCommand {
3
3
  static description: string;
4
+ static args: {
5
+ kind: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
6
+ id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
4
8
  static decision: "snooze";
5
9
  }
@@ -3,6 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const proposal_decision_command_1 = require("../../lib/proposal-decision-command");
4
4
  class ProposalsSnooze extends proposal_decision_command_1.ProposalDecisionCommand {
5
5
  static description = 'Snooze one displayed proposal through its domain authority';
6
+ // Own, not inherited: oclif spreads the command class into the parser input, and a static a
7
+ // subclass only inherits is dropped, which left these commands with no declared KIND/ID (OSK-15346).
8
+ static args = proposal_decision_command_1.ProposalDecisionCommand.args;
6
9
  static decision = 'snooze';
7
10
  }
8
11
  exports.default = ProposalsSnooze;
@@ -5,7 +5,7 @@ const signal_command_1 = require("../../../signal-command");
5
5
  const signals_1 = require("../../../lib/signals");
6
6
  const prompt_1 = require("../../../lib/prompt");
7
7
  class MonitorsValidate extends signal_command_1.SignalCommand {
8
- static description = 'Compile-only check of a Monitor definition; nothing is persisted';
8
+ static description = 'Compile-only check of a Monitor definition; nothing is persisted. Requires active Signal Sources owned by you in the selected workspace. Inspect each source with signals sources show before validating.';
9
9
  static flags = {
10
10
  json: core_1.Flags.boolean({ description: 'Output as JSON' }),
11
11
  'from-json': core_1.Flags.string({
@@ -0,0 +1,16 @@
1
+ import { BaseCommand } from '../../../base-command';
2
+ export default class SpacesWorkflowsCancelNode extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ spaceId: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
7
+ runId: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
8
+ nodeKey: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
9
+ };
10
+ static flags: {
11
+ 'from-json': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
12
+ reason: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
13
+ json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
14
+ };
15
+ run(): Promise<void>;
16
+ }
@@ -0,0 +1,44 @@
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 prompt_1 = require("../../../lib/prompt");
7
+ const workflow_node_control_1 = require("../../../lib/workflow-node-control");
8
+ class SpacesWorkflowsCancelNode extends base_command_1.BaseCommand {
9
+ static description = 'Request cancellation of one exact workflow node attempt and show the returned execution state';
10
+ static examples = [
11
+ '<%= config.bin %> spaces workflows run <space> <run> --json > reviewed-run.json',
12
+ '<%= config.bin %> spaces workflows cancel-node <space> <run> <node> --from-json reviewed-run.json',
13
+ ];
14
+ static args = {
15
+ spaceId: core_1.Args.string({ description: 'Owning Space ID', required: true, ignoreStdin: true }),
16
+ runId: core_1.Args.string({ description: 'Workflow run ID', required: true, ignoreStdin: true }),
17
+ nodeKey: core_1.Args.string({ description: 'Node key to stop', required: true, ignoreStdin: true }),
18
+ };
19
+ static flags = {
20
+ 'from-json': core_1.Flags.string({ description: 'Saved canonical workflow run snapshot, or - for stdin', required: true }),
21
+ reason: core_1.Flags.string({ description: 'Cancellation reason (at most 500 characters)' }),
22
+ json: core_1.Flags.boolean({ description: 'Output the updated workflow run as JSON' }),
23
+ };
24
+ async run() {
25
+ this.requireAuth();
26
+ const { args, flags } = await this.parse(SpacesWorkflowsCancelNode);
27
+ try {
28
+ const body = (0, workflow_node_control_1.nodeCancellationFromSnapshot)(args.spaceId, args.runId, args.nodeKey, await (0, prompt_1.readJsonBody)(flags['from-json']), flags.reason);
29
+ const run = await data_provider_1.dataService.cancelSpaceWorkflowNode(args.spaceId, args.runId, args.nodeKey, body);
30
+ if (flags.json) {
31
+ this.log(JSON.stringify(run, null, 2));
32
+ return;
33
+ }
34
+ const node = run.nodes?.find((item) => item.nodeKey === args.nodeKey);
35
+ this.log(`Cancellation for ${args.nodeKey}: ${node?.cancellation?.state ?? 'unknown'}.`);
36
+ this.log(`Returned node state: ${node?.status ?? 'unknown'}.`);
37
+ this.log(`Workflow run ${run.id}: ${run.status}.`);
38
+ }
39
+ catch (error) {
40
+ this.handleApiError(error);
41
+ }
42
+ }
43
+ }
44
+ exports.default = SpacesWorkflowsCancelNode;
@@ -1,4 +1,43 @@
1
1
  import { BaseCommand } from '../../../base-command';
2
+ /**
3
+ * Child workflow runs a cancelled run was waiting on. A cancelled node keeps its `waitingFor`,
4
+ * and every node kind that starts a child run (`workflow_call` = subworkflow, `loop` iteration,
5
+ * `task_plan`) records that run as `waitingFor.workflowRunId`. Reading the id rather than the
6
+ * kind means a new child-starting node kind is not silently missed.
7
+ */
8
+ export declare function childWorkflowRunIds(run: {
9
+ id?: string;
10
+ nodes?: Array<{
11
+ waitingFor?: unknown;
12
+ }>;
13
+ }): string[];
14
+ /**
15
+ * Lines for a cancel that has not finished converging. `cancellation.state: settled` with
16
+ * nothing pending is the only converged answer; anything else must not read as "stopped".
17
+ */
18
+ export declare function describeCancellationProgress(spaceId: string, run: {
19
+ id: string;
20
+ cancellation?: {
21
+ state?: string;
22
+ descendantsPending?: boolean;
23
+ executorsPending?: number;
24
+ };
25
+ }): string[];
26
+ /**
27
+ * The lines that tell a person which child runs a `preserve_children` cancel left running.
28
+ * "Cancelled" next to a child that is still waiting forever is the part nobody could see
29
+ * (OSK-15331), so say it and say how to stop them. Re-cancelling the parent with
30
+ * `--scope subtree` widens the stop to every descendant (the server persists the wider
31
+ * decision before touching any descendant). An unreadable child is reported as unreadable,
32
+ * never as stopped.
33
+ */
34
+ export declare function describePreservedChildren(spaceId: string, parentRunId: string, children: Array<{
35
+ id: string;
36
+ status?: string;
37
+ unreadable?: boolean;
38
+ }>, { reviewedScope }?: {
39
+ reviewedScope?: boolean;
40
+ }): string[];
2
41
  export default class SpacesWorkflowsCancel extends BaseCommand {
3
42
  static description: string;
4
43
  static args: {
@@ -7,6 +46,7 @@ export default class SpacesWorkflowsCancel extends BaseCommand {
7
46
  };
8
47
  static flags: {
9
48
  json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
49
+ scope: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
10
50
  yes: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
11
51
  };
12
52
  run(): Promise<void>;
@@ -1,10 +1,84 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.childWorkflowRunIds = childWorkflowRunIds;
4
+ exports.describeCancellationProgress = describeCancellationProgress;
5
+ exports.describePreservedChildren = describePreservedChildren;
3
6
  const core_1 = require("@oclif/core");
4
7
  const data_provider_1 = require("@skrr-ai/data-provider");
5
8
  const base_command_1 = require("../../../base-command");
6
9
  const prompt_1 = require("../../../lib/prompt");
7
10
  const space_output_1 = require("../../../lib/space-output");
11
+ const CANCELLATION_SCOPES = [
12
+ 'preserve_children',
13
+ 'subtree',
14
+ ];
15
+ /**
16
+ * Child workflow runs a cancelled run was waiting on. A cancelled node keeps its `waitingFor`,
17
+ * and every node kind that starts a child run (`workflow_call` = subworkflow, `loop` iteration,
18
+ * `task_plan`) records that run as `waitingFor.workflowRunId`. Reading the id rather than the
19
+ * kind means a new child-starting node kind is not silently missed.
20
+ */
21
+ function childWorkflowRunIds(run) {
22
+ const ids = new Set();
23
+ for (const node of run.nodes ?? []) {
24
+ const waitingFor = node.waitingFor;
25
+ const childId = waitingFor?.workflowRunId;
26
+ if (typeof childId === 'string' && childId && childId !== run.id)
27
+ ids.add(childId);
28
+ }
29
+ return [...ids];
30
+ }
31
+ /**
32
+ * Lines for a cancel that has not finished converging. `cancellation.state: settled` with
33
+ * nothing pending is the only converged answer; anything else must not read as "stopped".
34
+ */
35
+ function describeCancellationProgress(spaceId, run) {
36
+ const c = run.cancellation;
37
+ if (!c)
38
+ return [];
39
+ const pending = c.state !== 'settled' || c.descendantsPending || Number(c.executorsPending) > 0;
40
+ if (!pending)
41
+ return [];
42
+ const detail = `state ${c.state ?? 'unknown'}, descendant runs pending: ${c.descendantsPending ? 'yes' : 'no'}, executors pending: ${Number(c.executorsPending) || 0}`;
43
+ // `failed` means the cleanup gave up: reading again never converges it, only a reconcile retries.
44
+ if (c.state === 'failed') {
45
+ return [
46
+ `Cancellation cleanup gave up (${detail}).`,
47
+ ` Retry it with: skrr spaces workflows reconcile ${spaceId} ${run.id}`,
48
+ ];
49
+ }
50
+ return [
51
+ `Cancellation is still converging (${detail}).`,
52
+ ` Re-read it with: skrr spaces workflows run ${spaceId} ${run.id}`,
53
+ ];
54
+ }
55
+ /**
56
+ * The lines that tell a person which child runs a `preserve_children` cancel left running.
57
+ * "Cancelled" next to a child that is still waiting forever is the part nobody could see
58
+ * (OSK-15331), so say it and say how to stop them. Re-cancelling the parent with
59
+ * `--scope subtree` widens the stop to every descendant (the server persists the wider
60
+ * decision before touching any descendant). An unreadable child is reported as unreadable,
61
+ * never as stopped.
62
+ */
63
+ function describePreservedChildren(spaceId, parentRunId, children, { reviewedScope = false } = {}) {
64
+ const live = children.filter((child) => child.unreadable || (0, data_provider_1.isActiveWorkflowRunStatus)(child.status));
65
+ if (live.length === 0)
66
+ return [];
67
+ const lines = [
68
+ `Left running (scope preserve_children): ${live.length} child workflow run${live.length === 1 ? '' : 's'} started by this run.`,
69
+ ];
70
+ for (const child of live) {
71
+ lines.push(` ${child.id} (${child.unreadable ? 'status unreadable' : child.status})`);
72
+ }
73
+ if (reviewedScope) {
74
+ // A stop under a reviewed scope is frozen to that review, so cancelling the parent again
75
+ // does not widen it; each child is stopped on its own.
76
+ lines.push(`This run was stopped under a reviewed scope, so cancelling it again will not widen the stop.`, `Stop a child with: skrr spaces workflows cancel ${spaceId} <child-run-id> --scope subtree --yes`);
77
+ return lines;
78
+ }
79
+ lines.push(`Stop them now: skrr spaces workflows cancel ${spaceId} ${parentRunId} --scope subtree --yes`, ` (widens this cancel to every descendant run), or stop one: skrr spaces workflows cancel ${spaceId} <child-run-id> --scope subtree --yes`);
80
+ return lines;
81
+ }
8
82
  class SpacesWorkflowsCancel extends base_command_1.BaseCommand {
9
83
  static description = 'Cancel a workflow run and stop further graph coordination';
10
84
  static args = {
@@ -12,15 +86,25 @@ class SpacesWorkflowsCancel extends base_command_1.BaseCommand {
12
86
  runId: core_1.Args.string({ description: 'Workflow run ID', required: true, ignoreStdin: true }),
13
87
  };
14
88
  static flags = {
15
- json: core_1.Flags.boolean({ description: 'Output the cancelled run as JSON' }),
89
+ json: core_1.Flags.boolean({
90
+ description: 'Output the cancelled run as JSON (cancellation.scope and the waitingFor.workflowRunId of the cancelled nodes show which child runs were left running)',
91
+ }),
92
+ scope: core_1.Flags.string({
93
+ description: 'What happens to child workflow runs this run started: preserve_children (default) leaves them running, subtree stops every descendant run too',
94
+ options: [...CANCELLATION_SCOPES],
95
+ }),
16
96
  yes: core_1.Flags.boolean({ char: 'y', description: 'Skip confirmation prompt' }),
17
97
  };
18
98
  async run() {
19
99
  this.requireAuth();
20
100
  const { args, flags } = await this.parse(SpacesWorkflowsCancel);
101
+ const scope = flags.scope;
21
102
  let confirmed;
22
103
  try {
23
- confirmed = await (0, prompt_1.confirmDestructive)(`Cancel workflow run ${args.runId}? This stops workflow coordination; already-dispatched task execution may continue.`, flags.yes);
104
+ const scopeNote = scope === 'subtree'
105
+ ? ' Child workflow runs it started are cancelled too.'
106
+ : ' Child workflow runs it started keep running (--scope subtree stops them too).';
107
+ confirmed = await (0, prompt_1.confirmDestructive)(`Cancel workflow run ${args.runId}? This stops workflow coordination; already-dispatched task execution may continue.${scopeNote}`, flags.yes);
24
108
  }
25
109
  catch (error) {
26
110
  this.error(error.message, {
@@ -34,7 +118,7 @@ class SpacesWorkflowsCancel extends base_command_1.BaseCommand {
34
118
  }
35
119
  let run;
36
120
  try {
37
- run = await data_provider_1.dataService.cancelSpaceWorkflowRun(args.spaceId, args.runId);
121
+ run = await data_provider_1.dataService.cancelSpaceWorkflowRun(args.spaceId, args.runId, scope ? { scope } : {});
38
122
  }
39
123
  catch (error) {
40
124
  this.handleApiError(error);
@@ -45,6 +129,28 @@ class SpacesWorkflowsCancel extends base_command_1.BaseCommand {
45
129
  return;
46
130
  }
47
131
  this.log(`Cancelled workflow run ${run.id} (${run.status}).`);
132
+ for (const line of describeCancellationProgress(args.spaceId, run))
133
+ this.log(line);
134
+ if ((run.cancellation?.scope ?? run.cancellationScope ?? scope) === 'subtree')
135
+ return;
136
+ const childIds = childWorkflowRunIds(run);
137
+ if (childIds.length === 0)
138
+ return;
139
+ const children = await Promise.all(childIds.map(async (id) => {
140
+ try {
141
+ const child = await data_provider_1.dataService.getSpaceWorkflowRun(args.spaceId, id);
142
+ return { id, status: child.status };
143
+ }
144
+ catch {
145
+ return { id, unreadable: true };
146
+ }
147
+ }));
148
+ const reviewedScope = Boolean(run.cancellationExpectedScope);
149
+ for (const line of describePreservedChildren(args.spaceId, run.id, children, {
150
+ reviewedScope,
151
+ })) {
152
+ this.log(line);
153
+ }
48
154
  }
49
155
  }
50
156
  exports.default = SpacesWorkflowsCancel;
@@ -16,6 +16,15 @@ export declare function describeNodeAttention(node: {
16
16
  message?: string;
17
17
  } | null;
18
18
  }): string;
19
+ export declare function describeLoopItemsNote(node: {
20
+ nodeKey?: unknown;
21
+ nodeType?: unknown;
22
+ output?: {
23
+ itemsStatus?: unknown;
24
+ itemsKey?: unknown;
25
+ itemsFoundType?: unknown;
26
+ } | null;
27
+ }): string;
19
28
  export default class SpacesWorkflowsRun extends BaseCommand {
20
29
  static description: string;
21
30
  static args: {
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.describeNodeAttention = describeNodeAttention;
4
+ exports.describeLoopItemsNote = describeLoopItemsNote;
4
5
  const core_1 = require("@oclif/core");
5
6
  const data_provider_1 = require("@skrr-ai/data-provider");
6
7
  const base_command_1 = require("../../../base-command");
@@ -26,6 +27,38 @@ function describeNodeAttention(node) {
26
27
  return `${reason}: ${message}`;
27
28
  return reason || message || 'needs attention';
28
29
  }
30
+ /**
31
+ * A note for a loop that succeeded having iterated nothing because its
32
+ * collection never arrived, or '' when there is nothing to say (OSK-15324).
33
+ *
34
+ * A zero-iteration loop is `succeeded`, so the plain table reads as healthy
35
+ * whether the list was legitimately empty or the value the author meant never
36
+ * reached the loop. The server records which on the node output; say it. An
37
+ * empty list is the legitimate case and stays silent.
38
+ */
39
+ /** One printable line: control characters (an ESC sequence included) become spaces. */
40
+ function oneLine(value) {
41
+ // eslint-disable-next-line no-control-regex
42
+ return (0, format_1.collapseWhitespace)(value.replace(/[\u0000-\u001f\u007f-\u009f]/g, ' '));
43
+ }
44
+ function describeLoopItemsNote(node) {
45
+ if (node.nodeType !== 'loop')
46
+ return '';
47
+ const output = node.output;
48
+ // `itemsKey` is author-chosen free text, unlike a node key, so it is printed the way the
49
+ // table prints a cell: one line, no control characters.
50
+ const key = oneLine(typeof output?.itemsKey === 'string' && output.itemsKey ? output.itemsKey : 'items');
51
+ const name = oneLine(String(node.nodeKey ?? 'loop'));
52
+ if (output?.itemsStatus === 'missing') {
53
+ return `${name}: ran 0 iterations because its input "${key}" was missing or null`;
54
+ }
55
+ if (output?.itemsStatus === 'not_array') {
56
+ const found = typeof output.itemsFoundType === 'string' ? output.itemsFoundType : 'a value';
57
+ const article = /^[aeiou]/i.test(found) ? 'an' : 'a';
58
+ return `${name}: ran 0 iterations because its input "${key}" was ${article} ${oneLine(found)}, not a list`;
59
+ }
60
+ return '';
61
+ }
29
62
  class SpacesWorkflowsRun extends base_command_1.BaseCommand {
30
63
  static description = 'Inspect a workflow run and each durable node state';
31
64
  static args = {
@@ -84,6 +117,11 @@ class SpacesWorkflowsRun extends base_command_1.BaseCommand {
84
117
  ? [{ key: 'attention', header: 'ATTENTION' }]
85
118
  : []),
86
119
  ], (line) => this.log(line));
120
+ for (const node of nodes) {
121
+ const note = describeLoopItemsNote(node);
122
+ if (note)
123
+ this.log(`Note: ${note}`);
124
+ }
87
125
  }
88
126
  }
89
127
  }
@@ -61,7 +61,7 @@ class SpacesWorkflowsUpdate extends base_command_1.BaseCommand {
61
61
  description: 'Expected semantic revision; pass with --presentation-revision. For graph/draft writes, omit both to fetch the latest server revisions',
62
62
  }),
63
63
  'max-duration': core_1.Flags.string({
64
- description: 'Run duration limit, e.g. 90m, 4h or 2d. A run still open past it is cancelled with WORKFLOW_RUN_DEADLINE. Applies from the next publish; runs already started keep their deadline',
64
+ description: 'Run duration limit from 5m to 90d inclusive, e.g. 90m, 4h or 2d. A run still open past it is cancelled with WORKFLOW_RUN_DEADLINE. Applies from the next publish; runs already started keep their deadline',
65
65
  }),
66
66
  'clear-max-duration': core_1.Flags.boolean({ description: 'Remove the run duration limit' }),
67
67
  'presentation-revision': core_1.Flags.integer({
@@ -28,7 +28,7 @@ class TasksSchedule extends base_command_1.BaseCommand {
28
28
  dependsOn: ['cron'],
29
29
  }),
30
30
  'max-runs': core_1.Flags.integer({
31
- description: 'Cap total fires (recurring only)',
31
+ description: 'Cap total occurrences (recurring only): each scheduled fire counts, and so does an occurrence you complete by hand before the scheduler touches it',
32
32
  dependsOn: ['cron'],
33
33
  }),
34
34
  'end-date': core_1.Flags.string({
@@ -37,6 +37,13 @@ class TriggersCapabilities extends base_command_1.BaseCommand {
37
37
  ? capabilities.templateVariables.map((v) => typeof v === 'object' && v !== null ? String(v.name) : String(v))
38
38
  : capabilities.templateVariableNames;
39
39
  this.log(`Template vars: ${Array.isArray(vars) ? vars.join(', ') : '-'}`);
40
+ // Some scopes cannot create a trigger until something else is set up, and the refusal
41
+ // otherwise arrives only after submitting.
42
+ const needs = capabilities.prerequisites;
43
+ if (Array.isArray(needs)) {
44
+ for (const need of needs)
45
+ this.log(`Needs: ${String(need)}`);
46
+ }
40
47
  // Beside the modes, because `external_work_task` is a mode that cannot be
41
48
  // authored without one of these.
42
49
  const contracts = capabilities.effectContracts;
@@ -20,7 +20,10 @@ class TriggersCreate extends base_command_1.BaseCommand {
20
20
  'from-json': core_1.Flags.string({
21
21
  description: 'Path to a JSON file (or "-" for stdin) to use as the request body',
22
22
  }),
23
- scope: core_1.Flags.string({ description: 'Trigger scope kind', options: [...triggers_1.TRIGGER_SCOPE_KINDS] }),
23
+ scope: core_1.Flags.string({
24
+ description: 'Trigger scope kind. A scope can have prerequisites (a space_oracle Space needs a Leader agent): `triggers capabilities <scope>` lists them under "Needs"',
25
+ options: [...triggers_1.TRIGGER_SCOPE_KINDS],
26
+ }),
24
27
  ref: core_1.Flags.string({
25
28
  description: 'Scope ref as key=value (repeatable), for example --ref agentId=abc',
26
29
  multiple: true,
@@ -247,6 +247,23 @@ export declare const commitmentApi: {
247
247
  };
248
248
  }>;
249
249
  get: (id: string) => Promise<CommitmentStatusResponse>;
250
+ controlPreview: (id: string, query: Parameters<typeof withQuery>[1]) => Promise<Record<string, unknown>>;
251
+ applyControl: (id: string, body: Record<string, unknown>) => Promise<Record<string, unknown>>;
252
+ controlReceipt: (id: string, controlId: string) => Promise<Record<string, unknown>>;
253
+ workPlan: (id: string) => Promise<Record<string, unknown>>;
254
+ observationSummary: (query?: {
255
+ agentId?: string;
256
+ }) => Promise<Record<string, unknown>>;
257
+ draftObservationProbe: (body: Record<string, unknown>) => Promise<Record<string, unknown>>;
258
+ observationProbe: (id: string, body?: {
259
+ expectedVersion?: number;
260
+ timeoutMs?: number;
261
+ }) => Promise<Record<string, unknown>>;
262
+ verifyReadiness: (id: string, body: {
263
+ expectedVersion: number;
264
+ timeoutMs?: number;
265
+ }) => Promise<Record<string, unknown>>;
266
+ readinessReceipt: (id: string, receiptId: string) => Promise<Record<string, unknown>>;
250
267
  create: (body: Record<string, unknown>) => Promise<Commitment>;
251
268
  update: (id: string, body: Record<string, unknown>) => Promise<Commitment>;
252
269
  draftPreflight: (body: Record<string, unknown>) => Promise<CommitmentPreflightReport>;
@@ -629,6 +646,8 @@ export type WatchFlagInputs = {
629
646
  spaceIds?: string[];
630
647
  monitorIds?: string[];
631
648
  };
649
+ /** Keep the canonical one-binding-per-source contract without discarding conflicting intent. */
650
+ export declare function normalizeWatchedSourceDeclarations<T>(declarations: T[]): T[];
632
651
  export declare function buildWatchedSources(inputs: WatchFlagInputs): Array<Record<string, unknown>> | undefined;
633
652
  /** The evidence-health shape carried on a check's `inputs`. */
634
653
  export type EvidenceHealthView = {
@@ -9,6 +9,7 @@ exports.parseCommitmentConcurrencyLimit = parseCommitmentConcurrencyLimit;
9
9
  exports.commitmentConcurrencyEnvelopePatch = commitmentConcurrencyEnvelopePatch;
10
10
  exports.describeCommitmentConcurrency = describeCommitmentConcurrency;
11
11
  exports.parseIntervalToMs = parseIntervalToMs;
12
+ exports.normalizeWatchedSourceDeclarations = normalizeWatchedSourceDeclarations;
12
13
  exports.buildWatchedSources = buildWatchedSources;
13
14
  exports.describeEvidenceHealth = describeEvidenceHealth;
14
15
  exports.describeIntervalMs = describeIntervalMs;
@@ -33,6 +34,7 @@ exports.renderCommitmentCases = renderCommitmentCases;
33
34
  exports.renderCommitmentCaseDetail = renderCommitmentCaseDetail;
34
35
  exports.renderCommitmentRemediationProposals = renderCommitmentRemediationProposals;
35
36
  exports.renderCommitmentRemediationProposal = renderCommitmentRemediationProposal;
37
+ const node_util_1 = require("node:util");
36
38
  const data_provider_1 = require("@skrr-ai/data-provider");
37
39
  /**
38
40
  * commitments.ts — request-based client for the Commitments subsystem
@@ -138,6 +140,15 @@ exports.commitmentApi = {
138
140
  // GET /:id returns `{ commitment, latestCheck }` — NOT a bare contract.
139
141
  // Unwrap before rendering, or every field reads as undefined.
140
142
  get: (id) => data_provider_2.request.get(base(id)),
143
+ controlPreview: (id, query) => data_provider_2.request.get((0, triggers_1.withQuery)(`${base(id)}/control-preview`, query)),
144
+ applyControl: (id, body) => data_provider_2.request.post(`${base(id)}/control`, body),
145
+ controlReceipt: (id, controlId) => data_provider_2.request.get(`${base(id)}/control/${encodeURIComponent(controlId)}`),
146
+ workPlan: (id) => data_provider_2.request.get(`${base(id)}/work-plan`),
147
+ observationSummary: (query = {}) => data_provider_2.request.get((0, triggers_1.withQuery)('/api/commitments/observation-summary', query)),
148
+ draftObservationProbe: (body) => data_provider_2.request.post('/api/commitments/observation-probe', body),
149
+ observationProbe: (id, body = {}) => data_provider_2.request.post(`${base(id)}/observation-probe`, body),
150
+ verifyReadiness: (id, body) => data_provider_2.request.post(`${base(id)}/readiness-receipt`, body),
151
+ readinessReceipt: (id, receiptId) => data_provider_2.request.get(`${base(id)}/readiness-receipt/${encodeURIComponent(receiptId)}`),
141
152
  create: (body) => data_provider_2.request.post('/api/commitments', body),
142
153
  update: (id, body) => data_provider_2.request.patch(base(id), body),
143
154
  draftPreflight: (body) => data_provider_2.request.post('/api/commitments/preflight', body),
@@ -496,11 +507,33 @@ function parseIntervalToMs(input) {
496
507
  }
497
508
  return ms;
498
509
  }
510
+ /** Keep the canonical one-binding-per-source contract without discarding conflicting intent. */
511
+ function normalizeWatchedSourceDeclarations(declarations) {
512
+ const bySource = new Map();
513
+ const normalized = [];
514
+ for (const binding of declarations) {
515
+ if (!binding || typeof binding !== 'object' ||
516
+ !('source' in binding) || typeof binding.source !== 'string') {
517
+ normalized.push(binding); // The API owns validation of the remaining DTO.
518
+ continue;
519
+ }
520
+ if (bySource.has(binding.source)) {
521
+ if (!(0, node_util_1.isDeepStrictEqual)(bySource.get(binding.source), binding)) {
522
+ throw new Error(`Watched source "${binding.source}" has conflicting declarations. ` +
523
+ 'Choose one complete binding per source.');
524
+ }
525
+ continue;
526
+ }
527
+ bySource.set(binding.source, binding);
528
+ normalized.push(binding);
529
+ }
530
+ return normalized;
531
+ }
499
532
  function buildWatchedSources(inputs) {
500
533
  const watched = (inputs.watch ?? []).filter(Boolean);
501
534
  const metricKeys = (inputs.metricKey ?? []).filter(Boolean);
502
- const spaceIds = (inputs.spaceIds ?? []).filter(Boolean);
503
- const monitorIds = (inputs.monitorIds ?? []).filter(Boolean);
535
+ const spaceIds = [...new Set((inputs.spaceIds ?? []).filter(Boolean))];
536
+ const monitorIds = [...new Set((inputs.monitorIds ?? []).filter(Boolean))];
504
537
  const githubEvents = (inputs.githubEvents ?? []).filter(Boolean);
505
538
  const githubRepository = String(inputs.githubRepository || '')
506
539
  .trim()
@@ -584,7 +617,7 @@ function buildWatchedSources(inputs) {
584
617
  rows.push({ source, enabled: true });
585
618
  }
586
619
  }
587
- return rows;
620
+ return normalizeWatchedSourceDeclarations(rows);
588
621
  }
589
622
  /**
590
623
  * One line an author can act on, or nothing.