@skrr-ai/cli 0.1.128 → 0.1.129

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 (79) hide show
  1. package/dist/base-command.d.ts +12 -0
  2. package/dist/base-command.js +35 -0
  3. package/dist/commands/agenda/trigger/list.d.ts +2 -1
  4. package/dist/commands/agenda/trigger/list.js +55 -6
  5. package/dist/commands/agents/create.js +3 -1
  6. package/dist/commands/browser/persistent-profile.d.ts +22 -0
  7. package/dist/commands/browser/persistent-profile.js +46 -0
  8. package/dist/commands/commitments/create.js +3 -1
  9. package/dist/commands/commitments/effective-policy.js +6 -0
  10. package/dist/commands/commitments/explain.js +10 -1
  11. package/dist/commands/commitments/plan.d.ts +14 -0
  12. package/dist/commands/commitments/plan.js +32 -0
  13. package/dist/commands/commitments/preflight.d.ts +1 -1
  14. package/dist/commands/commitments/preflight.js +12 -4
  15. package/dist/commands/commitments/show.d.ts +9 -0
  16. package/dist/commands/commitments/show.js +14 -1
  17. package/dist/commands/compass/revisions.d.ts +6 -2
  18. package/dist/commands/compass/revisions.js +13 -5
  19. package/dist/commands/compass/tactic.d.ts +5 -0
  20. package/dist/commands/compass/tactic.js +14 -0
  21. package/dist/commands/followups/plans/create.js +1 -1
  22. package/dist/commands/harnesses/list.js +1 -1
  23. package/dist/commands/northstars/governance/configure.js +1 -1
  24. package/dist/commands/northstars/intents/decide.js +2 -3
  25. package/dist/commands/northstars/review-now.js +5 -0
  26. package/dist/commands/northstars/reviews.js +15 -3
  27. package/dist/commands/spaces/auto-executor/runs.d.ts +9 -1
  28. package/dist/commands/spaces/auto-executor/runs.js +18 -1
  29. package/dist/commands/spaces/heartbeats/create.js +7 -5
  30. package/dist/commands/spaces/heartbeats/update.js +5 -3
  31. package/dist/commands/store/artifacts/releases.d.ts +20 -0
  32. package/dist/commands/store/artifacts/releases.js +85 -0
  33. package/dist/commands/store/library.d.ts +14 -0
  34. package/dist/commands/store/library.js +71 -0
  35. package/dist/commands/tasks/list.d.ts +48 -0
  36. package/dist/commands/tasks/list.js +141 -9
  37. package/dist/commands/tasks/remind.js +23 -11
  38. package/dist/commands/tasks/retry-schedule.js +6 -3
  39. package/dist/commands/tasks/schedule.js +29 -7
  40. package/dist/commands/tasks/show.js +4 -0
  41. package/dist/lib/agent-trigger-listing.d.ts +26 -0
  42. package/dist/lib/agent-trigger-listing.js +63 -0
  43. package/dist/lib/agentic-stream.js +138 -41
  44. package/dist/lib/commitment-ledgers.js +3 -1
  45. package/dist/lib/commitments.d.ts +24 -0
  46. package/dist/lib/commitments.js +64 -4
  47. package/dist/lib/compasses.d.ts +2 -0
  48. package/dist/lib/iso-instant.d.ts +52 -0
  49. package/dist/lib/iso-instant.js +162 -0
  50. package/dist/lib/northstars.d.ts +43 -0
  51. package/dist/lib/northstars.js +122 -6
  52. package/dist/lib/proposals.js +6 -2
  53. package/dist/lib/store-artifact-releases.d.ts +39 -0
  54. package/dist/lib/store-artifact-releases.js +12 -0
  55. package/dist/lib/store-library.d.ts +37 -0
  56. package/dist/lib/store-library.js +12 -0
  57. package/dist/lib/task-calendar.d.ts +19 -0
  58. package/dist/lib/task-calendar.js +72 -0
  59. package/dist/lib/task-recurrence-outcome.js +3 -1
  60. package/dist/lib/task-recurrence-summary.d.ts +15 -0
  61. package/dist/lib/task-recurrence-summary.js +228 -0
  62. package/dist/lib/task-reminder.d.ts +28 -0
  63. package/dist/lib/task-reminder.js +50 -0
  64. package/dist/lib/triggers.d.ts +13 -2
  65. package/dist/lib/triggers.js +22 -12
  66. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonWire.js +11 -1
  67. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/executorStopReceipt.d.ts +123 -0
  68. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/executorStopReceipt.js +160 -0
  69. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -0
  70. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +17 -0
  71. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonWire.js +11 -1
  72. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/executorStopReceipt.d.ts +123 -0
  73. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/executorStopReceipt.js +173 -0
  74. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -0
  75. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +2 -0
  76. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  77. package/dist/node_modules/@skrr-ai/data-provider/index.js +6755 -6672
  78. package/oclif.manifest.json +263 -29
  79. package/package.json +1 -1
@@ -580,6 +580,18 @@ export declare function describeHarnessUnavailable(body: Record<string, unknown>
580
580
  * Exported for tests.
581
581
  */
582
582
  export declare function describeGitHubUnavailable(): string;
583
+ /**
584
+ * Frame a typed `TASK_LIST_TIMEOUT` failure as what it is — a list too broad for the database's
585
+ * time limit — rather than a control-plane outage.
586
+ *
587
+ * `GET /api/tasks` answers 503 with this code when its one aggregate is cut off at `maxTimeMS`,
588
+ * which an unscoped list over every accessible Space reaches because no index serves its sort
589
+ * across the access filter. Lead with the server's `cause`/`remedy`, falling back to the CLI's
590
+ * own sentence: the 5xx redaction boundary generalizes a bare message in production.
591
+ *
592
+ * Exported for tests.
593
+ */
594
+ export declare function describeTaskListTimeout(body: Record<string, unknown> | null, bin?: string): string;
583
595
  /**
584
596
  * Frame a typed `LABEL_SUGGESTION_UNAVAILABLE` failure as what it is — the
585
597
  * suggestion's hosted-inference call found no billing payer — rather than a
@@ -9,6 +9,7 @@ exports.commitmentActivationHint = commitmentActivationHint;
9
9
  exports.withSubject = withSubject;
10
10
  exports.describeHarnessUnavailable = describeHarnessUnavailable;
11
11
  exports.describeGitHubUnavailable = describeGitHubUnavailable;
12
+ exports.describeTaskListTimeout = describeTaskListTimeout;
12
13
  exports.describeLabelSuggestionUnavailable = describeLabelSuggestionUnavailable;
13
14
  exports.describeGitHubAppNotConfigured = describeGitHubAppNotConfigured;
14
15
  exports.describeResourceNotFound = describeResourceNotFound;
@@ -1573,6 +1574,20 @@ class BaseCommand extends core_1.Command {
1573
1574
  details: envelopeDetails(body),
1574
1575
  });
1575
1576
  }
1577
+ else if (bodyCode === 'TASK_LIST_TIMEOUT') {
1578
+ // The list was too broad for the database's time limit — a 503 the API sent on purpose,
1579
+ // with the narrower question to ask. The outage branch below would say skrr is down,
1580
+ // record an incident, and tell an agent to retry the very query that cannot finish
1581
+ // (OSK-21284).
1582
+ this.failWithCliError({
1583
+ message: say(describeTaskListTimeout(body, this.config.bin)),
1584
+ code: bodyCode,
1585
+ status: e.status,
1586
+ exit: 1,
1587
+ retryable: false,
1588
+ details: envelopeDetails(body),
1589
+ });
1590
+ }
1576
1591
  else if (bodyCode === 'LABEL_SUGGESTION_UNAVAILABLE') {
1577
1592
  // The label-suggestion route found no inference payer — a per-call
1578
1593
  // condition the API deliberately reported, not a platform outage,
@@ -2388,6 +2403,26 @@ function describeGitHubUnavailable() {
2388
2403
  return ('GitHub is temporarily unreachable, so this request could not be completed. ' +
2389
2404
  'skrr itself answered, so this is not a skrr outage — try again shortly.');
2390
2405
  }
2406
+ /**
2407
+ * Frame a typed `TASK_LIST_TIMEOUT` failure as what it is — a list too broad for the database's
2408
+ * time limit — rather than a control-plane outage.
2409
+ *
2410
+ * `GET /api/tasks` answers 503 with this code when its one aggregate is cut off at `maxTimeMS`,
2411
+ * which an unscoped list over every accessible Space reaches because no index serves its sort
2412
+ * across the access filter. Lead with the server's `cause`/`remedy`, falling back to the CLI's
2413
+ * own sentence: the 5xx redaction boundary generalizes a bare message in production.
2414
+ *
2415
+ * Exported for tests.
2416
+ */
2417
+ function describeTaskListTimeout(body, bin = 'skrr') {
2418
+ const cause = stringProp(body, 'cause') ||
2419
+ 'The task list took too long: it spans every task you can see across all your workspaces and ' +
2420
+ 'Spaces, and the database could not return the newest page within its time limit.';
2421
+ const remedy = stringProp(body, 'remedy') ||
2422
+ `Name where to look: a workspace (\`${bin} workspaces use <workspace-id>\`, or \`--workspace <workspace-id>\`) ` +
2423
+ `or a Space (\`${bin} tasks list --space <space-id>\`).`;
2424
+ return `${cause}\n → ${remedy}`;
2425
+ }
2391
2426
  /**
2392
2427
  * Frame a typed `LABEL_SUGGESTION_UNAVAILABLE` failure as what it is — the
2393
2428
  * suggestion's hosted-inference call found no billing payer — rather than a
@@ -12,7 +12,8 @@ import { BaseCommand } from '../../../base-command';
12
12
  export declare function isUnconfirmedTurnCapture(trigger: Record<string, unknown>): boolean;
13
13
  /**
14
14
  * `skrr agenda trigger list <agent-id>` — show all triggers registered for
15
- * an agent (any kind, not just agenda-created ones).
15
+ * an agent (any kind, not just agenda-created ones), including the ones the
16
+ * platform manages for its Commitments (the daily self-reflection check).
16
17
  */
17
18
  export default class AgendaTriggerList extends BaseCommand {
18
19
  static description: string;
@@ -7,6 +7,7 @@ const data_provider_1 = require("@skrr-ai/data-provider");
7
7
  const base_command_1 = require("../../../base-command");
8
8
  const format_1 = require("../../../lib/format");
9
9
  const triggers_1 = require("../../../lib/triggers");
10
+ const agent_trigger_listing_1 = require("../../../lib/agent-trigger-listing");
10
11
  /**
11
12
  * A schedule persisted while handling a conversation turn, for which no explicit
12
13
  * confirmation was recorded.
@@ -27,10 +28,11 @@ function isUnconfirmedTurnCapture(trigger) {
27
28
  }
28
29
  /**
29
30
  * `skrr agenda trigger list <agent-id>` — show all triggers registered for
30
- * an agent (any kind, not just agenda-created ones).
31
+ * an agent (any kind, not just agenda-created ones), including the ones the
32
+ * platform manages for its Commitments (the daily self-reflection check).
31
33
  */
32
34
  class AgendaTriggerList extends base_command_1.BaseCommand {
33
- static description = 'List every trigger registered against an agent.';
35
+ static description = "List every trigger registered against an agent, including the ones the platform manages for its Commitments (SOURCE 'system'). A passive agent (--initial-always-on off) still has a daily self-reflection check.";
34
36
  static examples = [
35
37
  '<%= config.bin %> agenda trigger list <agent-id>',
36
38
  '<%= config.bin %> agenda trigger list <agent-id> --unconfirmed',
@@ -55,17 +57,50 @@ class AgendaTriggerList extends base_command_1.BaseCommand {
55
57
  catch (err) {
56
58
  this.handleApiError(err);
57
59
  }
60
+ const r = response;
61
+ const listed = r?.data ?? [];
62
+ // The per-agent route lists `scope: 'agent'` rows only; the Commitment triggers a passive agent still owns
63
+ // live under `commitment_watcher`. `--unconfirmed` is about turn captures, which are agent-scoped, so it
64
+ // skips the extra read.
65
+ let extra = [];
66
+ let extraUnreadable = null;
67
+ if (!flags.unconfirmed) {
68
+ try {
69
+ const universal = await triggers_1.triggerApi.list((0, triggers_1.withQuery)('/api/triggers', { refEntity: [`agent:${args.agentId}`], limit: 200 }));
70
+ extra = (0, agent_trigger_listing_1.triggersMissingFromAgentList)(listed, (universal?.triggers ?? []));
71
+ }
72
+ catch (err) {
73
+ extraUnreadable = err instanceof Error ? err.message : String(err);
74
+ }
75
+ }
58
76
  if (flags.json) {
59
- this.printJsonResult((0, list_output_1.listResult)(response));
77
+ this.printJsonResult((0, list_output_1.listResult)(r && typeof r === 'object'
78
+ ? {
79
+ ...r,
80
+ data: [...listed, ...extra],
81
+ ...(extraUnreadable
82
+ ? {
83
+ warnings: [
84
+ {
85
+ code: 'system_triggers_unreadable',
86
+ value: extraUnreadable,
87
+ hint: systemTriggersUnreadable(args.agentId, extraUnreadable),
88
+ },
89
+ ],
90
+ }
91
+ : {}),
92
+ }
93
+ : response));
60
94
  return;
61
95
  }
62
- const r = response;
63
- let items = r?.data ?? [];
96
+ let items = [...listed, ...extra];
64
97
  if (flags.unconfirmed) {
65
98
  items = items.filter((t) => isUnconfirmedTurnCapture(t));
66
99
  }
67
100
  if (items.length === 0) {
68
101
  this.log(flags.unconfirmed ? 'No unconfirmed turn-captured schedules.' : 'No triggers.');
102
+ if (extraUnreadable)
103
+ this.log(systemTriggersUnreadable(args.agentId, extraUnreadable));
69
104
  return;
70
105
  }
71
106
  const rows = items.map((t) => {
@@ -81,7 +116,7 @@ class AgendaTriggerList extends base_command_1.BaseCommand {
81
116
  label: String(t.label ?? '-').slice(0, 40),
82
117
  next: String(next).slice(0, 25),
83
118
  fires: String(fires),
84
- source: isUnconfirmedTurnCapture(t) ? 'turn*' : '-',
119
+ source: (0, agent_trigger_listing_1.sourceLabel)(t, isUnconfirmedTurnCapture(t)),
85
120
  };
86
121
  });
87
122
  (0, format_1.renderTable)(rows, [
@@ -93,6 +128,15 @@ class AgendaTriggerList extends base_command_1.BaseCommand {
93
128
  { key: 'fires', header: 'FIRES' },
94
129
  { key: 'source', header: 'SOURCE' },
95
130
  ], (line) => this.log(line));
131
+ if (rows.some((row) => row.source.startsWith('system'))) {
132
+ this.log('');
133
+ for (const line of (0, agent_trigger_listing_1.systemManagedLegend)(args.agentId, this.config.bin))
134
+ this.log(line);
135
+ }
136
+ if (extraUnreadable) {
137
+ this.log('');
138
+ this.log(systemTriggersUnreadable(args.agentId, extraUnreadable));
139
+ }
96
140
  if (rows.some((row) => row.source === 'turn*')) {
97
141
  // Deliberately not "the agent scheduled this by itself". The capture layer
98
142
  // cannot tell "remind me at 9" from a nudge the agent invented, so the
@@ -105,3 +149,8 @@ class AgendaTriggerList extends base_command_1.BaseCommand {
105
149
  }
106
150
  }
107
151
  exports.default = AgendaTriggerList;
152
+ /** Said, not skipped: a missing read must not look like an agent with no Commitment triggers. */
153
+ function systemTriggersUnreadable(agentId, reason) {
154
+ return (`Platform-managed (Commitment) triggers could not be read (${reason}); this list may be incomplete. ` +
155
+ `Try: skrr triggers list --ref-entity agent:${agentId}`);
156
+ }
@@ -245,7 +245,9 @@ class AgentsCreate extends base_command_1.BaseCommand {
245
245
  description: 'Default working directory for local Harness sessions',
246
246
  }),
247
247
  'initial-always-on': core_1.Flags.string({
248
- description: 'Start autonomous work after creation (default: on). Use off for a passive agent; explicit chat still works. Relationship-loop seeding is configured separately.',
248
+ description: 'Start autonomous work after creation (default: on). Use off for a passive agent; explicit chat still works. ' +
249
+ 'A passive agent is still seeded with the system-managed daily self-reflection Commitment: `agenda trigger list <agent>` shows its trigger (SOURCE "system", marked "suspended" while the platform holds it for Always-On being off) ' +
250
+ 'and `commitments list --agent <agent> --include-system` finds the Commitment to pause. Relationship-loop seeding is configured separately.',
249
251
  options: ['on', 'off'],
250
252
  }),
251
253
  'no-relationship-loops': core_1.Flags.boolean({
@@ -0,0 +1,22 @@
1
+ import { Command } from '@oclif/core';
2
+ /**
3
+ * Whether browser sessions on THIS computer share one profile that keeps its
4
+ * logins and cache, instead of a fresh one each.
5
+ *
6
+ * The setting lives in the daemon's own config on this computer, so it is
7
+ * forwarded to the daemon rather than sent to the API: a browser that stays
8
+ * signed in is the person at the machine's decision, and a request from the
9
+ * cloud must not be able to make it (the same boundary as `browser
10
+ * cloud-agents`). The profile is the daemon's own, never the everyday Chrome.
11
+ */
12
+ export default class BrowserPersistentProfile extends Command {
13
+ static description: string;
14
+ static examples: string[];
15
+ static args: {
16
+ action: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
17
+ };
18
+ static flags: {
19
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
20
+ };
21
+ run(): Promise<void>;
22
+ }
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const browser_1 = require("../browser");
5
+ /**
6
+ * Whether browser sessions on THIS computer share one profile that keeps its
7
+ * logins and cache, instead of a fresh one each.
8
+ *
9
+ * The setting lives in the daemon's own config on this computer, so it is
10
+ * forwarded to the daemon rather than sent to the API: a browser that stays
11
+ * signed in is the person at the machine's decision, and a request from the
12
+ * cloud must not be able to make it (the same boundary as `browser
13
+ * cloud-agents`). The profile is the daemon's own, never the everyday Chrome.
14
+ */
15
+ class BrowserPersistentProfile extends core_1.Command {
16
+ static description = 'Keep one browser profile (logins, cookies, cache) across browser sessions on this computer (off by default)';
17
+ static examples = [
18
+ '<%= config.bin %> browser persistent-profile status',
19
+ '<%= config.bin %> browser persistent-profile on',
20
+ '<%= config.bin %> browser persistent-profile off',
21
+ '<%= config.bin %> browser persistent-profile reset',
22
+ ];
23
+ static args = {
24
+ action: core_1.Args.string({
25
+ description: 'on, off, status, or reset (sign out of everything and delete the profile)',
26
+ required: true,
27
+ options: ['on', 'off', 'status', 'reset'],
28
+ // Without this oclif reads stdin for a missing positional, so the command
29
+ // hangs when run from a pipe or a non-tty instead of refusing. Enforced
30
+ // by ARG_IMPLICIT_STDIN_ENABLED in the command-hygiene audit.
31
+ ignoreStdin: true,
32
+ }),
33
+ };
34
+ static flags = {
35
+ json: core_1.Flags.boolean({ description: 'Output JSON' }),
36
+ };
37
+ async run() {
38
+ const { args, flags } = await this.parse(BrowserPersistentProfile);
39
+ await (0, browser_1.forwardBrowserCommand)(this, [
40
+ 'persistent-profile',
41
+ args.action,
42
+ ...(flags.json ? ['--json'] : []),
43
+ ]);
44
+ }
45
+ }
46
+ exports.default = BrowserPersistentProfile;
@@ -260,7 +260,9 @@ JSON
260
260
  <%= config.bin %> compass commitments <compass-id> --agent <agent-id> # lists it once it is bound`,
261
261
  ];
262
262
  static flags = {
263
- json: core_1.Flags.boolean({ description: 'Output the created commitment as JSON' }),
263
+ json: core_1.Flags.boolean({
264
+ description: 'Output the created commitment as JSON: the commitment itself, not wrapped (commitments show --json puts it under .commitment)',
265
+ }),
264
266
  'from-json': core_1.Flags.string({
265
267
  description: `JSON file (or "-" for stdin) for fields not exposed as flags. See examples below for daily times, named agent_run keys, zero budgets and "compassId" (binds a Compass; fixed at creation). ${commitment_attention_copy_1.QUIET_HOURS_HELP} ${commitment_attention_copy_1.EVENT_SOURCES_HELP}`,
266
268
  }),
@@ -7,6 +7,7 @@ const base_command_1 = require("../../base-command");
7
7
  const commitments_1 = require("../../lib/commitments");
8
8
  const commitment_product_1 = require("../../lib/commitment-product");
9
9
  const commitment_attention_copy_1 = require("../../lib/commitment-attention-copy");
10
+ const northstars_1 = require("../../lib/northstars");
10
11
  /** Which limit binds the daily action runs — the fix lives on that object. */
11
12
  const RUN_LIMIT_BINDING_LABELS = {
12
13
  northstar_allocation: 'Northstar allocation',
@@ -71,6 +72,11 @@ function budgetDayLines(policy) {
71
72
  ? 'no ceiling'
72
73
  : `${value}/day`;
73
74
  lines.push(`Northstar shared pools: action runs ${pool(shared.actionRunsPerDay, shared.actionRunsPerDayState)} · observation runs ${pool(shared.observationRunsPerDay, shared.observationRunsPerDayState)}`);
75
+ // A pool below the number of linked active commitments is still handed out at the floor of
76
+ // 1/day each: the same sentence `northstars governance show` and `review-now` print (OSK-21334).
77
+ const floorNote = (0, northstars_1.runBudgetFloorNote)(policy.portfolio?.runBudgetFloor);
78
+ if (floorNote)
79
+ lines.push(`! ${floorNote}`);
74
80
  }
75
81
  return lines;
76
82
  }
@@ -24,13 +24,22 @@ class CommitmentsExplain extends base_command_1.BaseCommand {
24
24
  this.requireAuth();
25
25
  const { args, flags } = await this.parse(CommitmentsExplain);
26
26
  try {
27
- const explanation = await commitments_1.commitmentApi.explain(args.commitmentId);
27
+ // The explanation names neither the bound Compass nor its status, so the text view reads the
28
+ // Commitment beside it (the read `show` renders). `--json` stays the explanation as the
29
+ // server sent it, with no extra request. A failed read must not fail the explanation
30
+ // (OSK-21332): it becomes a "not shown" line instead.
31
+ const [explanation, read] = await Promise.all([
32
+ commitments_1.commitmentApi.explain(args.commitmentId),
33
+ flags.json ? null : commitments_1.commitmentApi.get(args.commitmentId).catch(() => null),
34
+ ]);
28
35
  if (flags.json) {
29
36
  this.log(JSON.stringify(explanation, null, 2));
30
37
  return;
31
38
  }
32
39
  for (const line of (0, commitments_1.formatCommitmentExplanation)(explanation))
33
40
  this.log(line);
41
+ for (const line of (0, commitments_1.explainCompassLines)(args.commitmentId, read?.commitment))
42
+ this.log(line);
34
43
  }
35
44
  catch (err) {
36
45
  this.handleApiError(err);
@@ -18,6 +18,20 @@ import { BaseCommand } from '../../base-command';
18
18
  * the command body so it can be unit-tested without the oclif harness.
19
19
  */
20
20
  export declare function formatDroppedFields(droppedFields: string[] | undefined): string[];
21
+ /**
22
+ * The zone a proposal's daily times will be read in, said before the person applies it (OSK-21336).
23
+ * The planner is not told a timezone unless `--timezone` is passed, and the proposal is printed as raw JSON,
24
+ * so "every day at 9am" came back as `times: ["09:00"]` with no zone; applied as it is, a daily cadence with
25
+ * no zone is read in UTC, while the web wizard writes the browser's zone for the same proposal and says so.
26
+ * Nothing for a proposal with no daily times.
27
+ */
28
+ export declare function planTimezoneLines(proposal: {
29
+ cadence?: {
30
+ mode?: unknown;
31
+ times?: unknown;
32
+ timezone?: unknown;
33
+ };
34
+ } | null | undefined, requestedTimezone?: string): string[];
21
35
  export default class CommitmentsPlan extends BaseCommand {
22
36
  static description: string;
23
37
  static examples: string[];
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.formatDroppedFields = formatDroppedFields;
4
+ exports.planTimezoneLines = planTimezoneLines;
4
5
  const core_1 = require("@oclif/core");
5
6
  const base_command_1 = require("../../base-command");
6
7
  const commitments_1 = require("../../lib/commitments");
@@ -36,6 +37,35 @@ function formatDroppedFields(droppedFields) {
36
37
  lines.push(` - ${field}`);
37
38
  return lines;
38
39
  }
40
+ /**
41
+ * The zone a proposal's daily times will be read in, said before the person applies it (OSK-21336).
42
+ * The planner is not told a timezone unless `--timezone` is passed, and the proposal is printed as raw JSON,
43
+ * so "every day at 9am" came back as `times: ["09:00"]` with no zone; applied as it is, a daily cadence with
44
+ * no zone is read in UTC, while the web wizard writes the browser's zone for the same proposal and says so.
45
+ * Nothing for a proposal with no daily times.
46
+ */
47
+ function planTimezoneLines(proposal, requestedTimezone) {
48
+ const cadence = proposal?.cadence;
49
+ if (!cadence || cadence.mode !== 'daily' || !Array.isArray(cadence.times) || !cadence.times.length) {
50
+ return [];
51
+ }
52
+ const times = cadence.times.filter((time) => typeof time === 'string');
53
+ if (!times.length)
54
+ return [];
55
+ const label = `daily time${times.length === 1 ? '' : 's'} ${times.join(', ')}`;
56
+ if (typeof cadence.timezone === 'string' && cadence.timezone) {
57
+ return ['', `The ${label} ${times.length === 1 ? 'is' : 'are'} read in ${cadence.timezone}.`];
58
+ }
59
+ return [
60
+ '',
61
+ `Note: the ${label} ${times.length === 1 ? 'carries' : 'carry'} no timezone, so applying this as it is reads ` +
62
+ `${times.length === 1 ? 'it' : 'them'} in UTC (the web wizard would use your browser's zone). ` +
63
+ (requestedTimezone
64
+ ? `You planned with --timezone ${requestedTimezone}, but the proposal did not keep it: add "timezone": ` +
65
+ `"${requestedTimezone}" to its cadence.`
66
+ : 'Add "timezone": "<IANA zone>" to its cadence, or plan again with --timezone <IANA zone>.'),
67
+ ];
68
+ }
39
69
  class CommitmentsPlan extends base_command_1.BaseCommand {
40
70
  static description = 'Draft a commitment contract from a sentence (read-only proposal — nothing is created)';
41
71
  static examples = [
@@ -115,6 +145,8 @@ class CommitmentsPlan extends base_command_1.BaseCommand {
115
145
  this.log('No proposal produced.');
116
146
  return;
117
147
  }
148
+ for (const line of planTimezoneLines(response.proposal, flags.timezone))
149
+ this.log(line);
118
150
  this.log('');
119
151
  this.log('Proposed contract:');
120
152
  this.log(JSON.stringify(response.proposal, null, 2));
@@ -43,7 +43,7 @@ export declare function nextRunsLines(report: {
43
43
  * observes. The headline carries the caveat instead of contradicting the gate below it.
44
44
  */
45
45
  export declare function preflightHeadline(report: Pick<CommitmentPreflightReport, 'verdict' | 'gates'>, persisted: boolean): string;
46
- export declare function preflightGateLines(gate: PreflightGate, verbose?: boolean): string[];
46
+ export declare function preflightGateLines(gate: PreflightGate, verbose?: boolean, refusesActivation?: boolean): string[];
47
47
  /**
48
48
  * The `Attention:` footer, and under it the settings behind the verdict (OSK-21120).
49
49
  *
@@ -63,7 +63,7 @@ function preflightHeadline(report, persisted) {
63
63
  ? `${label}: Ready to run, but its daily Run budget is 0, so no agent turn will start`
64
64
  : `${label}: Ready to run`;
65
65
  }
66
- function preflightGateLines(gate, verbose = false) {
66
+ function preflightGateLines(gate, verbose = false, refusesActivation = false) {
67
67
  // "Could it contact you now?" is not a Run gate. It answers whether a message
68
68
  // would reach the owner, and a limit the owner set themselves (zero nudges a day,
69
69
  // quiet hours) is a chosen quiet, not a defect. It carried the same BLOCK tag, a
@@ -94,7 +94,13 @@ function preflightGateLines(gate, verbose = false) {
94
94
  // A `block` the report itself marks advisory stops nothing: it is not a Run-admission gate, so it
95
95
  // never feeds the verdict or `pack activate`. Printed BLOCK, `pack preflight` read "ready" above
96
96
  // two BLOCK lines (dogfooded on a goal_advance draft); the tag has to mean what the report means.
97
- const advisoryBlock = gate.verdict === 'block' && gate.admission === 'advisory';
97
+ //
98
+ // An advisory gate the SERVER says refuses activation is a different case (OSK-21258): `resume`
99
+ // answers 409 for it, so it is tagged BLOCK and says what it stops. `info (advisory: does not stop a
100
+ // run)` beside an "Activation: would be refused" section naming the same gate was two answers to one
101
+ // question.
102
+ const advisoryBlock = gate.verdict === 'block' && gate.admission === 'advisory' && !refusesActivation;
103
+ const stopsActivationOnly = gate.verdict === 'block' && gate.admission === 'advisory' && refusesActivation;
98
104
  const mark = advisoryBlock
99
105
  ? 'info'
100
106
  : gate.verdict === 'pass'
@@ -119,7 +125,8 @@ function preflightGateLines(gate, verbose = false) {
119
125
  const showReason = verbose || (gate.verdict === 'block' && !hasNote);
120
126
  const lines = [
121
127
  ` ${mark.padEnd(5)} ${gate.label}${showReason && gate.reason ? ` — ${gate.reason}` : ''}` +
122
- (advisoryBlock ? ' (advisory: does not stop a run)' : ''),
128
+ (advisoryBlock ? ' (advisory: does not stop a run)' : '') +
129
+ (stopsActivationOnly ? ' (stops activation, not a run that is already live)' : ''),
123
130
  ];
124
131
  if (hasNote) {
125
132
  lines.push(` ${String(note).trim()}`);
@@ -280,7 +287,8 @@ class CommitmentsPreflight extends base_command_1.BaseCommand {
280
287
  }
281
288
  this.log('\nGates:');
282
289
  for (const gate of report.gates || []) {
283
- for (const line of preflightGateLines(gate, flags.verbose))
290
+ const refusesActivation = (0, commitments_1.gateRefusesActivation)(report.activation, gate);
291
+ for (const line of preflightGateLines(gate, flags.verbose, refusesActivation))
284
292
  this.log(line);
285
293
  }
286
294
  if (report.capabilities?.length) {
@@ -1,4 +1,13 @@
1
1
  import { BaseCommand } from '../../base-command';
2
+ /**
3
+ * What `show --json` prints, said where the flag is read (OSK-21334, from OSK-21205): the server's
4
+ * read envelope, with the commitment UNDER `.commitment`. `create --json` and `update --json` print
5
+ * the commitment itself, so a script written against one raised `KeyError: 'priority'` on the other.
6
+ * The shape is kept (moving the commitment up a level would break every reader of `show --json`) and
7
+ * no top-level copy is added; `docs/cli/SKRR_COMMITMENTS.md` says why, and
8
+ * `__tests__/json-shapes.spec.ts` runs both commands against this sentence.
9
+ */
10
+ export declare const SHOW_JSON_DESCRIPTION: string;
2
11
  /**
3
12
  * `skrr commitments show <id>` — the commitment's contract + live control-loop
4
13
  * state (health, last check, next check, unresolved-check streak), plus the most
@@ -1,10 +1,22 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SHOW_JSON_DESCRIPTION = void 0;
3
4
  const core_1 = require("@oclif/core");
4
5
  const base_command_1 = require("../../base-command");
5
6
  const commitments_1 = require("../../lib/commitments");
6
7
  const web_url_1 = require("../../lib/web-url");
7
8
  const commitment_product_1 = require("../../lib/commitment-product");
9
+ /**
10
+ * What `show --json` prints, said where the flag is read (OSK-21334, from OSK-21205): the server's
11
+ * read envelope, with the commitment UNDER `.commitment`. `create --json` and `update --json` print
12
+ * the commitment itself, so a script written against one raised `KeyError: 'priority'` on the other.
13
+ * The shape is kept (moving the commitment up a level would break every reader of `show --json`) and
14
+ * no top-level copy is added; `docs/cli/SKRR_COMMITMENTS.md` says why, and
15
+ * `__tests__/json-shapes.spec.ts` runs both commands against this sentence.
16
+ */
17
+ exports.SHOW_JSON_DESCRIPTION = 'Output the read envelope as JSON: { commitment, latestCheck, operatingState, abilities, url }. ' +
18
+ 'The commitment itself is under .commitment (so the priority is .commitment.priority); ' +
19
+ 'create --json and update --json print the commitment unwrapped.';
8
20
  /**
9
21
  * `skrr commitments show <id>` — the commitment's contract + live control-loop
10
22
  * state (health, last check, next check, unresolved-check streak), plus the most
@@ -18,12 +30,13 @@ class CommitmentsShow extends base_command_1.BaseCommand {
18
30
  static examples = [
19
31
  '<%= config.bin %> commitments show <commitment-id>',
20
32
  '<%= config.bin %> commitments show <commitment-id> --json',
33
+ "<%= config.bin %> commitments show <commitment-id> --json | jq '.commitment.priority'",
21
34
  ];
22
35
  static args = {
23
36
  id: core_1.Args.string({ description: 'Commitment ID', required: true, ignoreStdin: true }),
24
37
  };
25
38
  static flags = {
26
- json: core_1.Flags.boolean({ description: 'Output as JSON' }),
39
+ json: core_1.Flags.boolean({ description: exports.SHOW_JSON_DESCRIPTION }),
27
40
  verbose: core_1.Flags.boolean({
28
41
  description: 'Include raw compatibility policy and check diagnostics',
29
42
  }),
@@ -1,6 +1,10 @@
1
1
  import { BaseCommand } from '../../base-command';
2
- /** What to say under a list the ledger had more rows for. */
3
- export declare function revisionsFooter(shown: number): string;
2
+ /**
3
+ * What to say under a list the ledger had more rows for. `total` is the API's own count of the
4
+ * ledger; with it the footer is exact (`Showing 20 of 25`). A server that does not send it, or one
5
+ * whose count is not above what was shown, still gets the lookahead's true "at least" wording.
6
+ */
7
+ export declare function revisionsFooter(shown: number, total?: number): string;
4
8
  /**
5
9
  * `skrr compass revisions <compass-id> --agent <id>` — the append-only ledger.
6
10
  *
@@ -4,11 +4,18 @@ exports.revisionsFooter = revisionsFooter;
4
4
  const core_1 = require("@oclif/core");
5
5
  const base_command_1 = require("../../base-command");
6
6
  const compasses_1 = require("../../lib/compasses");
7
- /** What to say under a list the ledger had more rows for. */
8
- function revisionsFooter(shown) {
7
+ /**
8
+ * What to say under a list the ledger had more rows for. `total` is the API's own count of the
9
+ * ledger; with it the footer is exact (`Showing 20 of 25`). A server that does not send it, or one
10
+ * whose count is not above what was shown, still gets the lookahead's true "at least" wording.
11
+ */
12
+ function revisionsFooter(shown, total) {
13
+ const of = typeof total === 'number' && Number.isInteger(total) && total > shown
14
+ ? `${total}`
15
+ : `at least ${shown + 1}`;
9
16
  return shown >= 100
10
- ? `Showing ${shown} of at least ${shown + 1} revisions. --limit is capped at 100, so older ones cannot be listed yet.`
11
- : `Showing ${shown} of at least ${shown + 1} revisions — use --limit (up to 100) to see more.`;
17
+ ? `Showing ${shown} of ${of} revisions. --limit is capped at 100, so older ones cannot be listed yet.`
18
+ : `Showing ${shown} of ${of} revisions — use --limit (up to 100) to see more.`;
12
19
  }
13
20
  /**
14
21
  * `skrr compass revisions <compass-id> --agent <id>` — the append-only ledger.
@@ -55,8 +62,9 @@ class CompassRevisions extends base_command_1.BaseCommand {
55
62
  (0, compasses_1.renderRevisionList)(rows, (line) => this.log(line));
56
63
  // The API reads one row past the page, so `has_more` is true only when older rows exist. A
57
64
  // 22-revision ledger used to list 20 rows and stop, with v1 (the create) silently missing.
65
+ // `total` (the ledger's own count) makes the footer exact when the server sends it.
58
66
  if (response.has_more)
59
- this.log(revisionsFooter(rows.length));
67
+ this.log(revisionsFooter(rows.length, response.total));
60
68
  }
61
69
  }
62
70
  exports.default = CompassRevisions;
@@ -7,6 +7,10 @@ import { BaseCommand } from '../../base-command';
7
7
  * purpose: expressing an append as a full-object write would let two concurrent
8
8
  * learners clobber each other's entry. It says which of appended / updated /
9
9
  * unchanged happened, and names any tactic that rolled off the end.
10
+ *
11
+ * `--invariant <target-id>` chooses the invariant target the tactic is filed under. Without it
12
+ * the API files it under the general target (the one with no domain), else the first, and the
13
+ * output says which; a Compass whose targets all carry a domain used to leave no way to choose.
10
14
  */
11
15
  export default class CompassTactic extends BaseCommand {
12
16
  static description: string;
@@ -19,6 +23,7 @@ export default class CompassTactic extends BaseCommand {
19
23
  agent: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
20
24
  description: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
21
25
  id: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
26
+ invariant: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
22
27
  reason: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
23
28
  };
24
29
  run(): Promise<void>;
@@ -11,11 +11,16 @@ const compasses_1 = require("../../lib/compasses");
11
11
  * purpose: expressing an append as a full-object write would let two concurrent
12
12
  * learners clobber each other's entry. It says which of appended / updated /
13
13
  * unchanged happened, and names any tactic that rolled off the end.
14
+ *
15
+ * `--invariant <target-id>` chooses the invariant target the tactic is filed under. Without it
16
+ * the API files it under the general target (the one with no domain), else the first, and the
17
+ * output says which; a Compass whose targets all carry a domain used to leave no way to choose.
14
18
  */
15
19
  class CompassTactic extends base_command_1.BaseCommand {
16
20
  static description = 'Record a learned tactic on a compass (appends, or updates the tactic with the same --id)';
17
21
  static examples = [
18
22
  '<%= config.bin %> compass tactic agch_abc --agent agent_xyz --description "Roll back before paging on a deploy regression"',
23
+ '<%= config.bin %> compass tactic agch_abc --agent agent_xyz --invariant invariant_2 --description "Page the owner before restarting"',
19
24
  ];
20
25
  static args = {
21
26
  compassId: core_1.Args.string({ ignoreStdin: true, description: 'Compass ID', required: true }),
@@ -27,17 +32,26 @@ class CompassTactic extends base_command_1.BaseCommand {
27
32
  id: core_1.Flags.string({
28
33
  description: 'Stable tactic ID. A known ID with a different description updates that entry; with the same description nothing is written.',
29
34
  }),
35
+ invariant: core_1.Flags.string({
36
+ description: 'Invariant target ID to file the tactic under (the `id` that `compass show` prints for each target). ' +
37
+ 'Default: the general target (the one with no domain), else the first. An unknown ID is refused with the IDs the compass has; ' +
38
+ 'a tactic --id that already lives on another target is refused too, so name that target to update it.',
39
+ }),
30
40
  reason: core_1.Flags.string({ description: 'Why — recorded on the revision ledger' }),
31
41
  };
32
42
  async run() {
33
43
  this.requireAuth();
34
44
  const { args, flags } = await this.parse(CompassTactic);
45
+ if (flags.invariant !== undefined && !flags.invariant.trim()) {
46
+ this.error('--invariant needs an invariant target ID. `compass show` prints each target as `id <id>`; leave --invariant off to use the default target.');
47
+ }
35
48
  let compass;
36
49
  try {
37
50
  compass = await compasses_1.compassApi.appendTactic(args.compassId, {
38
51
  agentId: flags.agent,
39
52
  description: flags.description,
40
53
  ...(flags.id ? { id: flags.id } : {}),
54
+ ...(flags.invariant ? { invariantTargetId: flags.invariant.trim() } : {}),
41
55
  ...(flags.reason ? { reason: flags.reason } : {}),
42
56
  });
43
57
  }