@skrr-ai/cli 0.1.133 → 0.1.135

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.
@@ -479,6 +479,7 @@ export declare abstract class BaseCommand extends Command {
479
479
  * bug (2026-09-18).
480
480
  */
481
481
  private lastApiRequestId?;
482
+ private jsonErrorWritten;
482
483
  /** E6: the server's `Retry-After` for the refused call, when it was a 429. */
483
484
  private lastRetryAfterMs?;
484
485
  private writeJsonCliError;
@@ -1352,6 +1352,13 @@ class BaseCommand extends core_1.Command {
1352
1352
  }
1353
1353
  async catch(err) {
1354
1354
  if (isOclifExitError(err)) {
1355
+ // oclif's default ExitError handler calls process.exit immediately.
1356
+ // A JSON error already written to a pipe may still be buffered, so let
1357
+ // Node drain stdout naturally while preserving the requested exit code.
1358
+ if (this.jsonErrorWritten) {
1359
+ process.exitCode = err.oclif?.exit ?? 1;
1360
+ return;
1361
+ }
1355
1362
  return super.catch(err);
1356
1363
  }
1357
1364
  // Canonical Task Model v1 removed several `tasks complete` flags outright,
@@ -2155,6 +2162,7 @@ class BaseCommand extends core_1.Command {
2155
2162
  * bug (2026-09-18).
2156
2163
  */
2157
2164
  lastApiRequestId;
2165
+ jsonErrorWritten = false;
2158
2166
  /** E6: the server's `Retry-After` for the refused call, when it was a 429. */
2159
2167
  lastRetryAfterMs;
2160
2168
  writeJsonCliError({ message, code, status, exit, retryable, details, requestId, }) {
@@ -2182,6 +2190,7 @@ class BaseCommand extends core_1.Command {
2182
2190
  ...(details ? { details } : {}),
2183
2191
  },
2184
2192
  }), null, 2));
2193
+ this.jsonErrorWritten = true;
2185
2194
  this.exit(exit);
2186
2195
  }
2187
2196
  /**
@@ -133,7 +133,7 @@ class AgentMessageSend extends base_command_1.BaseCommand {
133
133
  description: 'Path to a canonical intent JSON payload, or "-" for stdin',
134
134
  }),
135
135
  'idempotency-key': core_1.Flags.string({
136
- description: 'Stable retry key for this intent; auto-derived when omitted',
136
+ description: 'Stable retry key; overrides the key in --from-json, or is auto-derived when neither is supplied',
137
137
  }),
138
138
  kind: core_1.Flags.string({
139
139
  description: 'Semantic kind; prefer milestone, status, decision, blocker, or result (legacy progress/question accepted)',
@@ -113,6 +113,7 @@ class AgentsScheduleTrace extends base_command_1.BaseCommand {
113
113
  { key: 'status', header: 'STATUS' },
114
114
  { key: 'summary', header: 'SUMMARY', maxWidth: 48 },
115
115
  ], (line) => this.log(line));
116
+ (0, schedule_1.renderTodoPolicyTraceDetails)(events, (line) => this.log(line));
116
117
  // Which collection each column came from — the trace reconciles four
117
118
  // sources and the reader should be able to go check one.
118
119
  if (trace.sourcePolicy) {
@@ -1,5 +1,7 @@
1
1
  import { BaseCommand } from '../../base-command';
2
2
  import { type AgentGuidePointer } from '../../lib/guide-pointer';
3
+ /** Do not silently discard a JSON source's scope or event filters. */
4
+ export declare function applyWatchedSourceFlags(payload: Record<string, unknown>, built: Array<Record<string, unknown>> | undefined): void;
3
5
  /**
4
6
  * True only when the authored create body can materialize at least one event
5
7
  * binding. A polling-only source must not make `--event-driven` look successful
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.applyWatchedSourceFlags = applyWatchedSourceFlags;
3
4
  exports.hasEventCapableWatchedSource = hasEventCapableWatchedSource;
4
5
  exports.assertEventDrivenWatchedSource = assertEventDrivenWatchedSource;
5
6
  exports.createCadenceFromFlags = createCadenceFromFlags;
@@ -36,6 +37,17 @@ function configuredId(config, ...keys) {
36
37
  return typeof value === 'string' && value.trim().length > 0;
37
38
  });
38
39
  }
40
+ /** Do not silently discard a JSON source's scope or event filters. */
41
+ function applyWatchedSourceFlags(payload, built) {
42
+ if (!built)
43
+ return;
44
+ if (payload.watchedSources !== undefined) {
45
+ throw new Error('Cannot combine watchedSources in --from-json with watched-source flags: ' +
46
+ 'the flags would replace the entire JSON list, including its filters. ' +
47
+ 'Remove watchedSources from the JSON to use flags, or remove the watched-source flags to keep the JSON.');
48
+ }
49
+ payload.watchedSources = built;
50
+ }
39
51
  function hasStructurallyValidExplicitEvent(config) {
40
52
  const events = config?.events;
41
53
  if (!Array.isArray(events))
@@ -456,8 +468,7 @@ JSON
456
468
  githubEvents: (0, triggers_1.splitRepeated)(flags['github-event']),
457
469
  githubRepository: flags.repo,
458
470
  });
459
- if (built)
460
- payload.watchedSources = built;
471
+ applyWatchedSourceFlags(payload, built);
461
472
  const cadence = createCadenceFromFlags({
462
473
  eventDriven: flags['event-driven'],
463
474
  cadence: flags.cadence,
@@ -38,15 +38,31 @@ class MemoryMaintain extends base_command_1.BaseCommand {
38
38
  this.error('input must be a JSON object');
39
39
  }
40
40
  let result;
41
+ // Keep stdout machine-readable for --json; progress goes only to stderr.
42
+ const waiting = args.operation === 'remember' ? setTimeout(() => {
43
+ process.stderr.write('Still waiting for the memory save; the result is not confirmed yet.\n');
44
+ }, 4000) : null;
45
+ waiting?.unref();
41
46
  try {
42
47
  if (args.operation === 'remember')
43
- result = await data_provider_1.dataService.rememberAgentMemory(flags.agent, input);
48
+ result = await data_provider_1.dataService.rememberAgentMemory(flags.agent, input, { timeoutMs: 60000 });
44
49
  else
45
50
  result = await data_provider_1.dataService.maintainAgentMemory(flags.agent, args.operation, input);
46
51
  }
47
52
  catch (error) {
53
+ const e = error;
54
+ const status = e?.status ?? e?.response?.status;
55
+ const transportLost = ['ECONNABORTED', 'ETIMEDOUT', 'ECONNRESET', 'EPIPE', 'ERR_NETWORK', 'ECONNREFUSED', 'EAI_AGAIN'].includes(e?.code || '') ||
56
+ e?.name === 'TimeoutError' || /fetch failed|socket hang up|network error|timed out|timeout/i.test(e?.message || '');
57
+ if (args.operation === 'remember' && ((status !== undefined && status >= 500) || (!status && transportLost))) {
58
+ this.error('The memory save was not confirmed. It may have succeeded; check the Agent’s Memory or retry the exact same command.', { code: 'MEMORY_OUTCOME_UNKNOWN' });
59
+ }
48
60
  this.handleApiError(error);
49
61
  }
62
+ finally {
63
+ if (waiting)
64
+ clearTimeout(waiting);
65
+ }
50
66
  if (flags.json)
51
67
  this.log(this.agentJson(flags.agent, result));
52
68
  else {
@@ -6,7 +6,7 @@ const scoped_memory_command_1 = require("../../lib/scoped-memory-command");
6
6
  const format_1 = require("../../lib/format");
7
7
  const memory_scope_1 = require("../../lib/memory-scope");
8
8
  class MemorySearch extends scoped_memory_command_1.ScopedMemoryCommand {
9
- static description = 'Semantic (hybrid vector + text) search over a memory pool — the same ranking the agent sees';
9
+ static description = 'Search a compatibility memory pool, or specify --scope or --store for canonical Memory';
10
10
  static examples = [
11
11
  '<%= config.bin %> memory search "dark mode preference"',
12
12
  '<%= config.bin %> memory search "billing" --agent agent_abc123',
@@ -68,13 +68,18 @@ class MemorySearch extends scoped_memory_command_1.ScopedMemoryCommand {
68
68
  catch (err) {
69
69
  this.handleApiError(err);
70
70
  }
71
+ const canonicalHint = flags.agent
72
+ ? 'Canonical Agent Memory is separate. Search with the same --agent and --scope agent:<agent-id>.'
73
+ : 'This is a compatibility pool. Search canonical Memory with --agent and --scope agent:<agent-id> or space:<space-id>.';
71
74
  if (flags.json) {
72
- this.log(JSON.stringify(response, null, 2));
75
+ this.log(JSON.stringify({ ...response, memorySource: 'compatibility_pool', canonicalSearchHint: canonicalHint }, null, 2));
73
76
  return;
74
77
  }
75
78
  const results = response.results ?? [];
79
+ this.log('Compatibility-pool memory search:');
80
+ this.log(canonicalHint);
76
81
  if (results.length === 0) {
77
- this.log(`No memories matched "${args.query}".`);
82
+ this.log(`No compatibility-pool memories matched "${args.query}".`);
78
83
  return;
79
84
  }
80
85
  const rows = results.map((r) => ({
@@ -12,7 +12,9 @@ class MemoryVersionsShow extends scoped_memory_command_1.ScopedMemoryCommand {
12
12
  this.requireAuth();
13
13
  const { args, flags } = await this.parse(MemoryVersionsShow);
14
14
  const result = await this.memoryHistoryApi(flags, { versionId: args.id });
15
- this.print(result, flags.json, `${result.id} ${result.operation}\n${JSON.stringify(result.payloadSnapshot, null, 2)}`);
15
+ this.print(result, flags.json, `${result.id} ${result.operation}\n${JSON.stringify(result.payloadSnapshot, null, 2)}${result.projection?.kind === 'legacy_episode_omitted'
16
+ ? '\nAn invalid historical episode reference was omitted; the content hash identifies the original stored payload.'
17
+ : ''}`);
16
18
  }
17
19
  }
18
20
  exports.default = MemoryVersionsShow;
@@ -0,0 +1,14 @@
1
+ import { BaseCommand } from '../../base-command';
2
+ export default class TriggersAuthorization extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ revoke: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ restore: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ };
13
+ run(): Promise<void>;
14
+ }
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const base_command_1 = require("../../base-command");
5
+ const triggers_1 = require("../../lib/triggers");
6
+ class TriggersAuthorization extends base_command_1.BaseCommand {
7
+ static description = 'Revoke or restore a trigger’s user-directed authorization grant';
8
+ static examples = [
9
+ '<%= config.bin %> triggers authorization <trigger-id> --revoke',
10
+ '<%= config.bin %> triggers authorization <trigger-id> --restore --json',
11
+ ];
12
+ static args = {
13
+ id: core_1.Args.string({ description: 'Trigger ID', required: true, ignoreStdin: true }),
14
+ };
15
+ static flags = {
16
+ revoke: core_1.Flags.boolean({ description: 'Withdraw the existing grant', exclusive: ['restore'] }),
17
+ restore: core_1.Flags.boolean({ description: 'Restore the existing grant', exclusive: ['revoke'] }),
18
+ json: core_1.Flags.boolean({ description: 'Output as JSON' }),
19
+ };
20
+ async run() {
21
+ this.requireAuth();
22
+ const { args, flags } = await this.parse(TriggersAuthorization);
23
+ if (!flags.revoke && !flags.restore) {
24
+ this.failWithCliError({
25
+ message: 'Choose --revoke or --restore for the existing authorization grant.',
26
+ code: 'AUTHORIZATION_ACTION_REQUIRED',
27
+ exit: 2,
28
+ retryable: false,
29
+ });
30
+ }
31
+ let result;
32
+ try {
33
+ result = await triggers_1.triggerApi.setAuthorizationRevoked(args.id, flags.revoke === true);
34
+ }
35
+ catch (err) {
36
+ this.handleApiError(err);
37
+ }
38
+ if (flags.json) {
39
+ this.log(JSON.stringify(result, null, 2));
40
+ return;
41
+ }
42
+ this.log(flags.revoke ? 'Authorization revoked.' : 'Authorization restored.');
43
+ }
44
+ }
45
+ exports.default = TriggersAuthorization;
@@ -78,6 +78,7 @@ export default class TriggersCreate extends BaseCommand {
78
78
  'allow-unknown-event': import("@oclif/core/interfaces").BooleanFlag<boolean>;
79
79
  };
80
80
  run(): Promise<void>;
81
+ private warnMissingSelfSchedule;
81
82
  /**
82
83
  * The server creates the trigger and then says whether something already
83
84
  * watches the same thing. Printed AFTER the summary, and the sentence itself
@@ -324,6 +324,7 @@ class TriggersCreate extends base_command_1.BaseCommand {
324
324
  }
325
325
  if (trigger.dryRun) {
326
326
  this.log('Trigger accepted by dry run.');
327
+ this.warnMissingSelfSchedule(trigger);
327
328
  return;
328
329
  }
329
330
  this.log('Created trigger:');
@@ -331,6 +332,15 @@ class TriggersCreate extends base_command_1.BaseCommand {
331
332
  if (url)
332
333
  this.log(`View it at ${url}`);
333
334
  this.warnDuplicateSubscriptions(trigger);
335
+ this.warnMissingSelfSchedule(trigger);
336
+ }
337
+ warnMissingSelfSchedule(trigger) {
338
+ const warnings = trigger?.warnings;
339
+ if (!Array.isArray(warnings))
340
+ return;
341
+ const warning = warnings.find((item) => item && typeof item === 'object' && item.code === 'self_scheduled_without_next_run');
342
+ if (typeof warning?.message === 'string')
343
+ this.warn(warning.message);
334
344
  }
335
345
  /**
336
346
  * The server creates the trigger and then says whether something already
@@ -29,6 +29,23 @@ function describeFireAuditDetail(detail, fireId, triggerId) {
29
29
  lines.push(`Duration: ${String(detail.durationMs)}ms`);
30
30
  if (detail.conversationId)
31
31
  lines.push(`Conversation: ${String(detail.conversationId)}`);
32
+ const metadata = detail.metadata;
33
+ if (metadata && typeof metadata === 'object' && !Array.isArray(metadata)) {
34
+ const record = metadata;
35
+ if (record.recordKind === 'schedule_end_marker') {
36
+ lines.push('Note: Schedule ended after the prior fire; this is not another occurrence.');
37
+ if (typeof record.afterOccurrenceId === 'string')
38
+ lines.push(`After fire: ${record.afterOccurrenceId}`);
39
+ }
40
+ const debounce = record.debounce;
41
+ if (debounce && typeof debounce === 'object' && !Array.isArray(debounce)) {
42
+ const { retryAt, windowMs } = debounce;
43
+ if (typeof retryAt === 'string' && retryAt.trim())
44
+ lines.push(`Retry at: ${retryAt}`);
45
+ if (typeof windowMs === 'number' && Number.isFinite(windowMs))
46
+ lines.push(`Debounce: ${windowMs}ms`);
47
+ }
48
+ }
32
49
  // Tool calls refused during a turn that was delivered: `triggers show` points here for the facts.
33
50
  lines.push(...(0, triggers_1.fireRefusalLines)(detail.metadata));
34
51
  return lines;
@@ -80,8 +80,18 @@ class TriggersPrune extends base_command_1.BaseCommand {
80
80
  // number that sizes the sweep, and `perUser` the cap it frees.
81
81
  const reclaimable = ownerWide ? quota.perUserReclaimable : quota.reclaimable;
82
82
  if (flags['dry-run']) {
83
+ let preview;
84
+ try {
85
+ preview = await triggers_1.triggerApi.prunePreview({
86
+ ...target,
87
+ ...(flags.limit ? { limit: flags.limit } : {}),
88
+ });
89
+ }
90
+ catch (err) {
91
+ this.handleApiError(err);
92
+ }
83
93
  if (flags.json) {
84
- this.log(JSON.stringify({ dryRun: true, ownerWide, wouldDelete: reclaimable, quota }, null, 2));
94
+ this.log(JSON.stringify({ ...preview, quota }, null, 2));
85
95
  return;
86
96
  }
87
97
  if (ownerWide) {
@@ -92,9 +102,11 @@ class TriggersPrune extends base_command_1.BaseCommand {
92
102
  this.log(`Scope: ${quota.scopeKind}`);
93
103
  this.log(`Active: ${quota.active} / ${quota.limit} (${quota.remaining} free)`);
94
104
  }
95
- this.log(`Reclaimable: ${reclaimable} disabled trigger(s)`);
105
+ this.log(`Would delete: ${preview.wouldDelete} disabled trigger(s)`);
106
+ for (const id of preview.ids)
107
+ this.log(` ${id}`);
96
108
  this.log('');
97
- this.log('Dry run — nothing was deleted.');
109
+ this.log('Dry run — nothing was deleted. Candidates can change before pruning.');
98
110
  return;
99
111
  }
100
112
  if (reclaimable === 0) {
@@ -454,7 +454,7 @@
454
454
  "id": "speaking",
455
455
  "title": "A Commitment finds; its Agent speaks",
456
456
  "readWhen": "a Commitment run found something and you must decide whether the owner hears of it",
457
- "tokens": 301,
457
+ "tokens": 320,
458
458
  "aliases": [],
459
459
  "explains": []
460
460
  },
@@ -681,7 +681,7 @@
681
681
  "id": "idempotency-keys",
682
682
  "title": "Idempotency keys",
683
683
  "readWhen": "a send may be retried, or one answered `duplicate` or `idempotency_conflict`",
684
- "tokens": 202,
684
+ "tokens": 230,
685
685
  "aliases": [],
686
686
  "explains": [
687
687
  "idempotency_conflict"
@@ -709,7 +709,7 @@
709
709
  ]
710
710
  }
711
711
  ],
712
- "hash": "74f6b87c1bd1f851fbfe0b1ee5da04425d1b6fdafbf55445a39e121af9c4e65b",
712
+ "hash": "f49e16e088923137a5c6c8bd98891defefe0536246e03b3e5cc92ee4ea4e9eaf",
713
713
  "tokens": 545
714
714
  },
715
715
  "routedIndex": {
@@ -1167,7 +1167,7 @@
1167
1167
  "id": "speaking",
1168
1168
  "title": "A Commitment finds; its Agent speaks",
1169
1169
  "readWhen": "a Commitment run found something and you must decide whether the owner hears of it",
1170
- "tokens": 301,
1170
+ "tokens": 320,
1171
1171
  "aliases": [],
1172
1172
  "explains": []
1173
1173
  },
@@ -1353,7 +1353,7 @@
1353
1353
  ]
1354
1354
  }
1355
1355
  ],
1356
- "hash": "6960740319e60127c5a1007fd32e77afada0b14442b5f6fb810a7fee1407cb63",
1356
+ "hash": "4981bf36d26a2768a45f209069e94372c0da86cc8658b706c3c77915fe5b5356",
1357
1357
  "tokens": 494
1358
1358
  },
1359
1359
  "cards": {
@@ -1747,7 +1747,7 @@
1747
1747
  "routed": true,
1748
1748
  "aliases": [],
1749
1749
  "body": "## Concept model\n\n| Noun | Question it answers |\n| --- | --- |\n| **Compass** | What does good look like for this Agent, and what may it never trade away? |\n| **Northstar** | Which Commitments should exist, at what priority and on what budget? |\n| **Commitment** | Which outcome does this Agent keep true, and how does it keep checking? |\n| **Check** | What did one wake of a Commitment see, decide and do? |\n\nA Commitment is not a unit of work. A **Task** is one unit of work and a **Goal** is a dated\noutcome with key results; a Commitment watches both and may create Tasks, and is neither.\n\n## Decide\n\nFive mechanisms make something happen later, and the noun alone does not pick one:\n\n| Mechanism | What it is for | The signal that picks it |\n| --- | --- | --- |\n| **Follow-up** | Coming back to one thing, once or a bounded few times: a reminder, a watch, a check-in, an agreed action | The ask ends: \"look tomorrow\", \"tell me when CI is green\" |\n| **Trigger** | Running the Agent with a set prompt on a schedule, an event, once, or on demand | The person names when, and every run does the same work |\n| **Heartbeat** | Checking in on a conversation, group chat or Space at a frequency that backs off while unread | The person wants regular contact, not an outcome held |\n| **Commitment** | Keeping an outcome true: watched sources, a cadence and a loop that decides when to act | \"Keep X true\", \"watch this metric\": no end, and stale evidence matters |\n| **Workflow** | Running a durable graph of Tasks in order | Repeatable work whose steps depend on each other |\n\nA one-off \"look at it tomorrow\" is a follow-up, never a Commitment. Standing observation is a\nCommitment with a freshness contract, not a recurring trigger or a task board left running.\n\n## Workflow\n\n### Start a Commitment\n\n1. Read the Agent's Commitments first, and change one rather than add a near-duplicate.\n2. Create it and turn it on in one call: the outcome, what it watches, how often it checks, where\n its work goes. Turning it on runs Preflight; a refusal keeps the draft and reports the code,\n reason and fix.\n3. Leave it a draft only when the person asked to see it first.\n4. Read it back, and tell the person what it watches, how often, and whether it is on.\n\n## Authority\n\n| Actor | May do | Only a person may do |\n| --- | --- | --- |\n| Agent | Read the Commitments of an Agent it can view; create one, change it and turn it on; request a check; pause one; decide whether a finding is worth telling the owner | Lift a stop a person made; set or clear a concurrency limit; choose the Task review policy; approve an action proposal or a cadence change; configure a Northstar's governance or decide its intents |\n| Person | Everything in both columns | — |\n\nA Commitment grants no permission: its runs work under the Agent's own. A Check never writes an\nowner-facing message, and a Compass never creates, rewrites, pauses or archives a Commitment. The\nserver cannot always tell an Agent from the person whose login it holds, so an accepted call is\nnot permission: the right-hand column stays a person's.\n\n## Live state\n\n- `skrr commitments list --agent <agent-id>` — an Agent's Commitments, with status and assessment.\n- `skrr commitments show <commitment-id>` — the contract, its next check and the last one.\n- `skrr commitments roster --agent <agent-id>` — each loop: whether it is armed, its next wake, runs today.\n\n## Related guides\n\n- `tasks` — the Tasks a Commitment creates or watches.\n- `spaces` — Goals, and Workflows: durable graphs of Tasks.\n- `agents` — the Agent a Commitment belongs to, and where its runs execute.\n- `communication` — whether, what and how to tell the owner.",
1750
- "hash": "197d9b84d1a4367ecf4b7a51687604d13814ec9e9265fe662883278a7738d2fa",
1750
+ "hash": "d9bbb541c0b955254d9795d98c26c1e7f075c98c5169c16574c457fde87fcec3",
1751
1751
  "tokens": 1169,
1752
1752
  "sections": [
1753
1753
  {
@@ -1824,7 +1824,7 @@
1824
1824
  "id": "speaking",
1825
1825
  "title": "A Commitment finds; its Agent speaks",
1826
1826
  "readWhen": "a Commitment run found something and you must decide whether the owner hears of it",
1827
- "tokens": 301,
1827
+ "tokens": 320,
1828
1828
  "aliases": [],
1829
1829
  "explains": []
1830
1830
  },
@@ -2027,7 +2027,7 @@
2027
2027
  "routed": false,
2028
2028
  "aliases": [],
2029
2029
  "body": "## Concept model\n\nAn **Agent Update** is a deliberate owner-facing message. It is conversation, not\nrecord: what durably changed belongs to the work's own records, and the message\npoints at it.\n\n## Agent Updates\n\nProactively message the owner whenever it would improve their ability to steer the work, understand\na meaningful change, or stay confidently involved. Adapt to the task, uncertainty and stakes, the\nuser's engagement, what you have already communicated, and any per-agent style direction. Do not\nfollow a fixed schedule or manufacture status chatter.\n\n- Choose the semantic kind: `milestone` for a meaningful completed stage, `status` for a\n useful non-blocking checkpoint, `decision` for a concrete choice, `blocker` when user action is needed to\n continue, and one `result` for the terminal outcome (including failure).\n- Declare the attention intent with `--attention ambient|notify|interrupt`: `ambient` stays in\n the durable timeline by default, `notify` is worth seeing soon, and `interrupt` is reserved for\n work that is blocked or genuinely time-sensitive. The system and the user choose delivery channels and native\n presentation; attention never directly selects an Android channel, iOS interruption level, sound, or critical\n alert, and never bypasses notification preferences, quiet hours, Focus, or rate limits.\n- Say what is newly true and what it changes for the work. Include the next step, decision, or\n uncertainty when it would help the user; do not force every message into a fixed template.\n- Write like a capable teammate giving a busy owner a real, in-the-moment briefing — not like a\n system log, a customer-service script, or a task template. Open on the thing itself, never on a\n subject line (\"Status on the refactor:\", \"Quick update:\"). In the user's language, lead with the\n fresh fact, decision, or risk; then make clear why it matters and what happens next. Where the\n evidence is available, ground the update in one or two useful specifics: the affected thing, the\n evidence, the timing, or the trade-off.\n Use plain, varied sentences and only the confidence the evidence earns. Keep them short: where a\n dash would staple two clauses into one, use two sentences instead. Be warm and direct, but\n never manufacture familiarity, emotion, certainty, or colour that is not true. Avoid AI-shaped\n filler such as \"I am pleased to report\", \"completed / in progress / next step\" recaps,\n \"I will continue to monitor\", \"feel free to let me know\", ceremonial thanks, and vague claims\n such as \"the optimization is complete\". Say the thing instead: \"The migration is in; the\n slowest query fell from 2.8s to 420ms. I am running the full regression now and will flag any\n breakage before merge.\" Treat that as a standard of specificity, not a rigid script. Do not make\n a message longer merely to sound human.\n- Task context and card presentation are separate. A managed session's task context is derived\n server-side, but a message shows no Task Card unless you explicitly choose `--attach-task-card`\n (or `presentation.taskCard: true`). Use a card when it materially helps the user open or follow the\n live task; omit it for ordinary conversational updates.\n- In a hand-started session, use `skrr agent message context --agent <id>` when recent\n updates, read state, or a user's reply would help your judgment. It supplies observations, not a\n schedule or a command to send.\n- In a skrr-managed session, do not pass task, run, or session identity: skrr derives it.\n In a hand-started session, add `--task <task-id>` only when you know the correct task; it is\n declared context.\n- Prefer one message for one thought. For a conversational owner DM — such as a pitch, invitation,\n question, or progress update — when a draft exceeds roughly 320 characters or contains two independent thoughts,\n normally use a 2–4 message human-scale sequence with `--part \"<next thought>\"` and one explicit semantic\n `--idempotency-key`. Do not fragment a single thought merely to send more messages, and never split text\n mechanically: choose the semantic boundaries yourself. If one part does not reach the user, the remaining parts\n are deliberately not sent — the user would otherwise read your thoughts out of order.\n- An idempotency key names ONE update for this agent permanently, so make it specific to this run\n (for example `<task-id>:verify-complete`). Reuse it only to retry that same update: reused with\n the same text it is dropped as a duplicate, and with different text it is rejected.\n- A non-`delivered` status (`held`, `approval_required`, `duplicate`, `dropped`) is a policy\n outcome, not an error. It is never a reason to bypass this command with a direct DM or Slack\n call — this is the only sanctioned agent-to-user path.\n\n## Authority\n\n| Actor | May do | Only a person may do |\n| --- | --- | --- |\n| Agent | Decide to message; declare kind, attention intent, task context and a task card | Choose the delivery channel or native presentation; release a held or approval-gated intent |\n| Person | Everything an Agent may do | — |\n\n## Live state\n\n- `skrr agent message context --agent <id>` — recent updates, read state and owner engagement, as observations for the decision above.\n\n## Related guides\n\n- `tasks` — the records an update points at, and the durable version of what changed.\n- `agents` — which Agent a session speaks as; its `identity` section owns the agent-context refusals.",
2030
- "hash": "2ca0bfacfd5ac8edcfe5e36ca344d8698f02874e2786646c4f03bea28ca623d9",
2030
+ "hash": "d502e1cf74a97001200c86fb62c4eb43f9720a31ce99014376115db6202d198f",
2031
2031
  "tokens": 1359,
2032
2032
  "sections": [
2033
2033
  {
@@ -2063,7 +2063,7 @@
2063
2063
  "id": "idempotency-keys",
2064
2064
  "title": "Idempotency keys",
2065
2065
  "readWhen": "a send may be retried, or one answered `duplicate` or `idempotency_conflict`",
2066
- "tokens": 202,
2066
+ "tokens": 230,
2067
2067
  "aliases": [],
2068
2068
  "explains": [
2069
2069
  "idempotency_conflict"
@@ -5850,7 +5850,7 @@
5850
5850
  "id": "speaking",
5851
5851
  "title": "A Commitment finds; its Agent speaks",
5852
5852
  "readWhen": "a Commitment run found something and you must decide whether the owner hears of it",
5853
- "tokens": 301,
5853
+ "tokens": 320,
5854
5854
  "aliases": [],
5855
5855
  "explains": []
5856
5856
  },
@@ -5966,7 +5966,7 @@
5966
5966
  "id": "speaking",
5967
5967
  "title": "A Commitment finds; its Agent speaks",
5968
5968
  "readWhen": "a Commitment run found something and you must decide whether the owner hears of it",
5969
- "tokens": 301,
5969
+ "tokens": 320,
5970
5970
  "aliases": [],
5971
5971
  "explains": []
5972
5972
  },
@@ -6084,7 +6084,7 @@
6084
6084
  "id": "speaking",
6085
6085
  "title": "A Commitment finds; its Agent speaks",
6086
6086
  "readWhen": "a Commitment run found something and you must decide whether the owner hears of it",
6087
- "tokens": 301,
6087
+ "tokens": 320,
6088
6088
  "aliases": [],
6089
6089
  "explains": []
6090
6090
  },
@@ -6206,7 +6206,7 @@
6206
6206
  "id": "speaking",
6207
6207
  "title": "A Commitment finds; its Agent speaks",
6208
6208
  "readWhen": "a Commitment run found something and you must decide whether the owner hears of it",
6209
- "tokens": 301,
6209
+ "tokens": 320,
6210
6210
  "aliases": [],
6211
6211
  "explains": []
6212
6212
  },
@@ -6322,7 +6322,7 @@
6322
6322
  "id": "speaking",
6323
6323
  "title": "A Commitment finds; its Agent speaks",
6324
6324
  "readWhen": "a Commitment run found something and you must decide whether the owner hears of it",
6325
- "tokens": 301,
6325
+ "tokens": 320,
6326
6326
  "aliases": [],
6327
6327
  "explains": []
6328
6328
  },
@@ -6438,7 +6438,7 @@
6438
6438
  "id": "speaking",
6439
6439
  "title": "A Commitment finds; its Agent speaks",
6440
6440
  "readWhen": "a Commitment run found something and you must decide whether the owner hears of it",
6441
- "tokens": 301,
6441
+ "tokens": 320,
6442
6442
  "aliases": [],
6443
6443
  "explains": []
6444
6444
  },
@@ -6560,7 +6560,7 @@
6560
6560
  "id": "speaking",
6561
6561
  "title": "A Commitment finds; its Agent speaks",
6562
6562
  "readWhen": "a Commitment run found something and you must decide whether the owner hears of it",
6563
- "tokens": 301,
6563
+ "tokens": 320,
6564
6564
  "aliases": [],
6565
6565
  "explains": []
6566
6566
  },
@@ -6598,9 +6598,9 @@
6598
6598
  "version": 4,
6599
6599
  "aliases": [],
6600
6600
  "explains": [],
6601
- "body": "Read this when a Commitment run found something and you must decide whether the owner hears of it.\n\nA Check never writes to the owner. When something it found may matter, the run is handed a brief\nunder \"Owner communication — your call.\": what was found, the evidence, why it may matter and a\nsuggested next step. Those are facts to judge, not a message to forward.\n\n- Decide with everything in view: what you already said, whether the owner replied, what another\n surface already raised. One message, a different message or nothing are all valid outcomes, and\n silence is often the right one.\n- To speak from the run, send it with `skrr agent message send` where the run allows it: it is\n delivered under the owner's limits and counted for this Commitment. Otherwise return a\n `status_update` or `report` action intent with its `text` for the Commitment's report\n destinations. A `message` action intent is recorded but never delivered. Write the words yourself.\n- The owner's limits (mute, quiet hours, a daily nudge cap) decide whether a run is asked to speak\n at all, and whatever you deliver counts against them, whichever way you send it.\n\n`skrr commitments reports <commitment-id>` lists where its reports go and on what condition; the\ndefault sends only on a material change.",
6602
- "hash": "45fa02a0344903cc355dff0b66eee45b14dab79a8f62fba8620106b29e11e8a9",
6603
- "tokens": 301,
6601
+ "body": "Read this when a Commitment run found something and you must decide whether the owner hears of it.\n\nA Check never writes to the owner, and it does not hand you a prepared message or brief: whether\nthe owner hears of what a run found is your call, from what you saw and what you already said.\n\n- Decide with everything in view: what you already said, whether the owner replied, what another\n surface already raised. One message, a different message or nothing are all valid outcomes, and\n silence is often the right one.\n- To speak from the run, send it with `skrr agent message send` where the run allows it: it is\n delivered under the owner's limits and counted for this Commitment. Otherwise return a\n `status_update` or `report` action intent with its `text` for the Commitment's report\n destinations. A `message` action intent is recorded but never delivered. Write the words yourself.\n- The owner's limits (mute, quiet hours, a daily nudge cap) apply when you deliver, whichever way\n you send it: whatever is delivered counts against them, and a delivery they do not allow is\n suppressed: the cycle shows it as a stop at \"reach\" where the platform recorded it, and `skrr agent\n message send` answers with its status.\n\n`skrr commitments reports <commitment-id>` lists where its reports go and on what condition; the\ndefault sends only on a material change.",
6602
+ "hash": "0e16cd15a9b31d7f5fbd477d623213e8c30d735c028841491691dab809b12c14",
6603
+ "tokens": 320,
6604
6604
  "sections": [
6605
6605
  {
6606
6606
  "id": "model",
@@ -6676,7 +6676,7 @@
6676
6676
  "id": "speaking",
6677
6677
  "title": "A Commitment finds; its Agent speaks",
6678
6678
  "readWhen": "a Commitment run found something and you must decide whether the owner hears of it",
6679
- "tokens": 301,
6679
+ "tokens": 320,
6680
6680
  "aliases": [],
6681
6681
  "explains": []
6682
6682
  },
@@ -6796,7 +6796,7 @@
6796
6796
  "id": "speaking",
6797
6797
  "title": "A Commitment finds; its Agent speaks",
6798
6798
  "readWhen": "a Commitment run found something and you must decide whether the owner hears of it",
6799
- "tokens": 301,
6799
+ "tokens": 320,
6800
6800
  "aliases": [],
6801
6801
  "explains": []
6802
6802
  },
@@ -6914,7 +6914,7 @@
6914
6914
  "id": "speaking",
6915
6915
  "title": "A Commitment finds; its Agent speaks",
6916
6916
  "readWhen": "a Commitment run found something and you must decide whether the owner hears of it",
6917
- "tokens": 301,
6917
+ "tokens": 320,
6918
6918
  "aliases": [],
6919
6919
  "explains": []
6920
6920
  },
@@ -8043,7 +8043,7 @@
8043
8043
  "id": "idempotency-keys",
8044
8044
  "title": "Idempotency keys",
8045
8045
  "readWhen": "a send may be retried, or one answered `duplicate` or `idempotency_conflict`",
8046
- "tokens": 202,
8046
+ "tokens": 230,
8047
8047
  "aliases": [],
8048
8048
  "explains": [
8049
8049
  "idempotency_conflict"
@@ -8120,7 +8120,7 @@
8120
8120
  "id": "idempotency-keys",
8121
8121
  "title": "Idempotency keys",
8122
8122
  "readWhen": "a send may be retried, or one answered `duplicate` or `idempotency_conflict`",
8123
- "tokens": 202,
8123
+ "tokens": 230,
8124
8124
  "aliases": [],
8125
8125
  "explains": [
8126
8126
  "idempotency_conflict"
@@ -8159,9 +8159,9 @@
8159
8159
  "explains": [
8160
8160
  "idempotency_conflict"
8161
8161
  ],
8162
- "body": "Read this when a send may be retried, or one answered `duplicate` or `idempotency_conflict`.\n\nOmit `--idempotency-key` and the CLI derives one from the request — fine for a\none-shot send, but a retry that must dedup needs the explicit key the card\ndescribes.\n\n- The key is verbatim text, up to 240 characters.\n- `duplicate` in the result means the key already carried this identical update —\n the earlier send stands; do not resend.\n- `idempotency_conflict` means the key already carried different content or a\n different delivery policy — the update needs a new name.\n\n## Refusals\n\n| Code | Cause | Fix |\n| --- | --- | --- |\n| `idempotency_conflict` | The key was already used for a different message or delivery policy | Mint a key that names this run's update and retry |",
8163
- "hash": "5f09a95c12c779db800350c7e848cb3b87811a6e4fdc13c606d3bba20a36996d",
8164
- "tokens": 202,
8162
+ "body": "Read this when a send may be retried, or one answered `duplicate` or `idempotency_conflict`.\n\n`--idempotency-key` overrides an `idempotencyKey` (or `idempotency_key` /\n`idempotency-key`) in `--from-json`. When neither supplies a key, the CLI derives\none from the request. For a retry that must deduplicate, use the explicit key\nthe card describes.\n\n- The key is verbatim text, up to 240 characters.\n- `duplicate` in the result means the key already carried this identical update —\n the earlier send stands; do not resend.\n- `idempotency_conflict` means the key already carried different content or a\n different delivery policy — the update needs a new name.\n\n## Refusals\n\n| Code | Cause | Fix |\n| --- | --- | --- |\n| `idempotency_conflict` | The key was already used for a different message or delivery policy | Mint a key that names this run's update and retry |",
8163
+ "hash": "5ba623c4d77a8f3e7902ab455abd7e0413ef6351a4cc58a5e01a7b46cac58937",
8164
+ "tokens": 230,
8165
8165
  "sections": [
8166
8166
  {
8167
8167
  "id": "writing",
@@ -8196,7 +8196,7 @@
8196
8196
  "id": "idempotency-keys",
8197
8197
  "title": "Idempotency keys",
8198
8198
  "readWhen": "a send may be retried, or one answered `duplicate` or `idempotency_conflict`",
8199
- "tokens": 202,
8199
+ "tokens": 230,
8200
8200
  "aliases": [],
8201
8201
  "explains": [
8202
8202
  "idempotency_conflict"
@@ -8281,7 +8281,7 @@
8281
8281
  "id": "idempotency-keys",
8282
8282
  "title": "Idempotency keys",
8283
8283
  "readWhen": "a send may be retried, or one answered `duplicate` or `idempotency_conflict`",
8284
- "tokens": 202,
8284
+ "tokens": 230,
8285
8285
  "aliases": [],
8286
8286
  "explains": [
8287
8287
  "idempotency_conflict"
@@ -30,6 +30,11 @@ function proactiveBaselineLines(b) {
30
30
  ? `${quiet.start ?? '?'}-${quiet.end ?? '?'} ${quiet.timezone ?? '(no zone recorded)'}`
31
31
  : 'disabled'}`);
32
32
  }
33
+ const working = b.policy.workingHours;
34
+ if (working) {
35
+ const window = `${working.start ?? '?'}-${working.end ?? '?'} ${working.timezone ?? '(no zone recorded)'}`;
36
+ lines.push(`Working hours: ${working.enabled ? window : `disabled (${window})`}`);
37
+ }
33
38
  lines.push(`System loops: ${loops}`);
34
39
  lines.push(`Watchers: ${watchers}`);
35
40
  lines.push(`Model-run ceiling: ${b.dailyModelRunCeiling}/day (declared by the system loops)`);
@@ -181,7 +181,15 @@ function createNodeAdapter(opts = {}) {
181
181
  // (and per retry: proofs are single-use). No-op for every other token.
182
182
  Object.assign(headers, await (0, dpop_auth_1.dpopHeadersFor)(currentCredential().token, method, finalUrl));
183
183
  // E6: bounded retry on 429 for a GET or an idempotency-keyed write.
184
- const { response, retryAfterMs } = await (0, rate_limit_retry_1.fetchWithRateLimitRetry)(() => fetch(finalUrl, { method, headers, body: finalBody, ...(0, task_runtime_headers_1.taskRuntimeRedirectOptions)(headers) }), { method, headers });
184
+ // Callers that explicitly set a timeout need a real fetch deadline. The
185
+ // Node adapter otherwise silently drops the data-provider request option.
186
+ const timeoutMs = Number(options?.timeout);
187
+ const timeoutSignal = Number.isFinite(timeoutMs) && timeoutMs > 0 ? AbortSignal.timeout(timeoutMs) : undefined;
188
+ const callerSignal = options?.signal;
189
+ const signal = timeoutSignal && callerSignal
190
+ ? AbortSignal.any([timeoutSignal, callerSignal])
191
+ : timeoutSignal || callerSignal;
192
+ const { response, retryAfterMs } = await (0, rate_limit_retry_1.fetchWithRateLimitRetry)(() => fetch(finalUrl, { method, headers, body: finalBody, ...(signal ? { signal } : {}), ...(0, task_runtime_headers_1.taskRuntimeRedirectOptions)(headers) }), { method, headers });
185
193
  lastRetryAfterMs = retryAfterMs;
186
194
  return response;
187
195
  }