@skrr-ai/cli 0.1.126 → 0.1.128

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 (123) hide show
  1. package/dist/base-command.d.ts +23 -0
  2. package/dist/base-command.js +72 -0
  3. package/dist/commands/agents/chat.js +7 -0
  4. package/dist/commands/agents/goals/list.d.ts +2 -2
  5. package/dist/commands/agents/goals/list.js +2 -2
  6. package/dist/commands/commitments/action-history.js +1 -1
  7. package/dist/commands/commitments/cases.js +3 -3
  8. package/dist/commands/commitments/check-now.d.ts +10 -2
  9. package/dist/commands/commitments/check-now.js +53 -3
  10. package/dist/commands/commitments/create.d.ts +6 -14
  11. package/dist/commands/commitments/create.js +61 -6
  12. package/dist/commands/commitments/doctor.js +3 -1
  13. package/dist/commands/commitments/effective-policy.d.ts +11 -1
  14. package/dist/commands/commitments/effective-policy.js +84 -4
  15. package/dist/commands/commitments/hypotheses.js +12 -12
  16. package/dist/commands/commitments/next-tick.js +5 -0
  17. package/dist/commands/commitments/observations.js +18 -17
  18. package/dist/commands/commitments/pack/list.js +2 -1
  19. package/dist/commands/commitments/pack/plan.d.ts +6 -0
  20. package/dist/commands/commitments/pack/plan.js +19 -0
  21. package/dist/commands/commitments/preflight.d.ts +19 -0
  22. package/dist/commands/commitments/preflight.js +108 -10
  23. package/dist/commands/commitments/trigger-status.js +8 -0
  24. package/dist/commands/commitments/update.d.ts +1 -0
  25. package/dist/commands/commitments/update.js +45 -4
  26. package/dist/commands/commitments/wake-capabilities.js +13 -0
  27. package/dist/commands/compass/archive.js +4 -0
  28. package/dist/commands/compass/commitments.js +15 -1
  29. package/dist/commands/compass/create.js +11 -3
  30. package/dist/commands/compass/effective-policy.d.ts +1 -0
  31. package/dist/commands/compass/effective-policy.js +8 -7
  32. package/dist/commands/compass/revert.d.ts +8 -0
  33. package/dist/commands/compass/revert.js +25 -1
  34. package/dist/commands/compass/revisions.d.ts +2 -0
  35. package/dist/commands/compass/revisions.js +18 -2
  36. package/dist/commands/compass/tactic.d.ts +5 -3
  37. package/dist/commands/compass/tactic.js +8 -7
  38. package/dist/commands/compass/update.js +19 -3
  39. package/dist/commands/followups/act.js +1 -1
  40. package/dist/commands/followups/cancel.js +13 -0
  41. package/dist/commands/followups/observability.js +20 -1
  42. package/dist/commands/followups/plans/create.js +12 -1
  43. package/dist/commands/followups/show.js +13 -0
  44. package/dist/commands/followups/watch.js +1 -1
  45. package/dist/commands/goals/create.d.ts +1 -0
  46. package/dist/commands/goals/create.js +4 -1
  47. package/dist/commands/goals/planner-config.d.ts +11 -0
  48. package/dist/commands/goals/planner-config.js +16 -0
  49. package/dist/commands/goals/revisions.js +3 -19
  50. package/dist/commands/machines/update.d.ts +5 -3
  51. package/dist/commands/machines/update.js +70 -9
  52. package/dist/commands/northstars/create.js +3 -1
  53. package/dist/commands/northstars/governance/configure.d.ts +4 -0
  54. package/dist/commands/northstars/governance/configure.js +75 -9
  55. package/dist/commands/northstars/intents/decide.d.ts +6 -0
  56. package/dist/commands/northstars/intents/decide.js +69 -15
  57. package/dist/commands/northstars/intents/list.d.ts +2 -1
  58. package/dist/commands/northstars/intents/list.js +15 -5
  59. package/dist/commands/northstars/intents.d.ts +26 -0
  60. package/dist/commands/northstars/intents.js +49 -0
  61. package/dist/commands/northstars/list.d.ts +1 -0
  62. package/dist/commands/northstars/list.js +24 -3
  63. package/dist/commands/northstars/recommendations/list.d.ts +1 -1
  64. package/dist/commands/northstars/recommendations/list.js +3 -3
  65. package/dist/commands/northstars/recommendations.d.ts +19 -0
  66. package/dist/commands/northstars/recommendations.js +42 -0
  67. package/dist/commands/northstars/review-now.d.ts +1 -0
  68. package/dist/commands/northstars/review-now.js +10 -4
  69. package/dist/commands/northstars/reviews.d.ts +1 -0
  70. package/dist/commands/northstars/reviews.js +19 -3
  71. package/dist/commands/northstars/update.js +8 -1
  72. package/dist/commands/proposals/list.js +11 -2
  73. package/dist/commands/proposals/show.js +9 -1
  74. package/dist/commands/spaces/auto-executor/preview.d.ts +14 -1
  75. package/dist/commands/spaces/auto-executor/preview.js +35 -1
  76. package/dist/commands/spaces/auto-executor/run.d.ts +17 -0
  77. package/dist/commands/spaces/auto-executor/run.js +60 -2
  78. package/dist/commands/spaces/files/upload.js +1 -2
  79. package/dist/commands/spaces/index.js +23 -0
  80. package/dist/commands/spaces/revisions.js +3 -2
  81. package/dist/commands/tasks/revisions.js +3 -2
  82. package/dist/commands/triggers/fire-detail.js +2 -0
  83. package/dist/generated/agent-guides.json +69 -69
  84. package/dist/lib/agentic-stream.d.ts +9 -0
  85. package/dist/lib/agentic-stream.js +14 -0
  86. package/dist/lib/command-miss.js +11 -2
  87. package/dist/lib/commitment-attention-copy.d.ts +96 -0
  88. package/dist/lib/commitment-attention-copy.js +209 -0
  89. package/dist/lib/commitment-loop-status.d.ts +6 -0
  90. package/dist/lib/commitment-loop-status.js +16 -6
  91. package/dist/lib/commitments.d.ts +88 -0
  92. package/dist/lib/commitments.js +188 -0
  93. package/dist/lib/compass-body-help.d.ts +20 -0
  94. package/dist/lib/compass-body-help.js +61 -0
  95. package/dist/lib/compass-refusals.d.ts +35 -0
  96. package/dist/lib/compass-refusals.js +50 -0
  97. package/dist/lib/compasses.d.ts +80 -4
  98. package/dist/lib/compasses.js +232 -12
  99. package/dist/lib/followups.d.ts +22 -1
  100. package/dist/lib/followups.js +21 -1
  101. package/dist/lib/format.d.ts +11 -0
  102. package/dist/lib/format.js +29 -0
  103. package/dist/lib/initiatives.d.ts +9 -2
  104. package/dist/lib/initiatives.js +13 -2
  105. package/dist/lib/machines.d.ts +14 -1
  106. package/dist/lib/machines.js +24 -0
  107. package/dist/lib/northstars.d.ts +158 -24
  108. package/dist/lib/northstars.js +538 -26
  109. package/dist/lib/proposal-decision-command.js +21 -1
  110. package/dist/lib/proposals.d.ts +26 -1
  111. package/dist/lib/proposals.js +163 -0
  112. package/dist/lib/rate-limit-retry.d.ts +5 -0
  113. package/dist/lib/rate-limit-retry.js +19 -0
  114. package/dist/lib/revision-actor.d.ts +2 -0
  115. package/dist/lib/revision-actor.js +16 -0
  116. package/dist/lib/roles.d.ts +1 -1
  117. package/dist/lib/space-charter.js +9 -0
  118. package/dist/lib/task-asset-upload.js +1 -2
  119. package/dist/lib/triggers.d.ts +33 -1
  120. package/dist/lib/triggers.js +77 -3
  121. package/dist/node_modules/@skrr-ai/data-provider/index.js +7537 -7474
  122. package/oclif.manifest.json +357 -34
  123. package/package.json +4 -4
@@ -82,7 +82,27 @@ class ProposalDecisionCommand extends base_command_1.BaseCommand {
82
82
  }
83
83
  this.handleApiError(error);
84
84
  }
85
- this.log(flags.json ? JSON.stringify(result, null, 2) : `Proposal ${decision} decision recorded.`);
85
+ // A retry whose first response was lost lands on the decision that already took effect; the
86
+ // server marks it, so it is not worded like a first success (OSK-21191).
87
+ if (flags.json) {
88
+ this.log(JSON.stringify(result, null, 2));
89
+ }
90
+ else if (result.replayed) {
91
+ // The receipt repeats the decision that was ASKED for; what the intent's outcome actually is
92
+ // (applied, still waiting to be applied, rejected) is on the proposal, so read it (OSK-21250).
93
+ // Best effort: when that read fails the message names the decision, as before.
94
+ let view;
95
+ try {
96
+ view = await proposals_1.proposalApi.show(ref.kind, ref.id);
97
+ }
98
+ catch {
99
+ view = undefined;
100
+ }
101
+ this.log((0, proposals_1.describeReplay)(decision, result, view));
102
+ }
103
+ else {
104
+ this.log(`Proposal ${decision} decision recorded.`);
105
+ }
86
106
  }
87
107
  }
88
108
  exports.ProposalDecisionCommand = ProposalDecisionCommand;
@@ -1,4 +1,4 @@
1
- import type { ProposalDecisionParams, ProposalDecisionResponse, ProposalKind, ProposalListParams, ProposalListResponse, ProposalView } from '@skrr-ai/data-provider';
1
+ import type { ProposalDecision, ProposalDecisionParams, ProposalDecisionResponse, ProposalKind, ProposalListParams, ProposalListResponse, ProposalView } from '@skrr-ai/data-provider';
2
2
  /** The unified, read-time proposal inbox. Decisions are routed by the server to
3
3
  * each family's own writer; the CLI never writes a source row. */
4
4
  export declare const proposalApi: {
@@ -23,3 +23,28 @@ export declare function resolveProposalRef(kindArg: string | undefined, idArg: s
23
23
  };
24
24
  /** A 404 here means no such proposal under THIS kind: the id is looked up under the kind given. */
25
25
  export declare function describeProposalNotFound(kind: string, id: string, bin?: string): string;
26
+ /**
27
+ * The intent's title in words: what it would DO, to what. Any other kind of proposal, and an
28
+ * intent the server sent no context for, keeps the view's own title.
29
+ */
30
+ export declare function proposalTitleFor(view: ProposalView): string;
31
+ /**
32
+ * The inbox files a rejection nobody on the owner's side made under `discarded`. For a Northstar
33
+ * intent the verdict, and who made it, is said in the NOTE column, so a policy refusal is not
34
+ * read as something the person threw away.
35
+ */
36
+ export declare function northstarIntentNote(view: ProposalView): string;
37
+ /**
38
+ * `proposals show` for a Northstar intent, as lines a person can read. Null for every other kind,
39
+ * which keeps printing the full JSON as before (a script reading it does not change).
40
+ */
41
+ export declare function renderNorthstarIntentView(view: ProposalView): string[] | null;
42
+ /**
43
+ * What a replayed decision says. The replay receipt names the decision that was asked for ("keep"), but a
44
+ * Northstar intent that was kept has gone on: it may be applied, or approved and not applied yet, and a
45
+ * discard lands as `rejected`. "Already kept" for an intent that was in fact applied understated what
46
+ * had happened (OSK-21250), so the intent's own verdict is named when the proposal could be read.
47
+ */
48
+ export declare function describeReplay(decision: ProposalDecision, receipt: {
49
+ decidedAt?: string;
50
+ }, view?: ProposalView): string;
@@ -3,8 +3,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.proposalApi = void 0;
4
4
  exports.resolveProposalRef = resolveProposalRef;
5
5
  exports.describeProposalNotFound = describeProposalNotFound;
6
+ exports.proposalTitleFor = proposalTitleFor;
7
+ exports.northstarIntentNote = northstarIntentNote;
8
+ exports.renderNorthstarIntentView = renderNorthstarIntentView;
9
+ exports.describeReplay = describeReplay;
6
10
  const data_provider_1 = require("@skrr-ai/data-provider");
7
11
  const data_provider_2 = require("@skrr-ai/data-provider");
12
+ const northstars_1 = require("./northstars");
8
13
  const triggers_1 = require("./triggers");
9
14
  const MOUNT = '/api/proposals';
10
15
  /** The unified, read-time proposal inbox. Decisions are routed by the server to
@@ -61,3 +66,161 @@ function describeProposalNotFound(kind, id, bin = 'skrr') {
61
66
  return (`No ${kind} proposal ${id} is visible to you. A proposal is looked up under its kind, so an id ` +
62
67
  `that exists under another kind is not found here; \`${bin} proposals list\` prints each as KIND:ID.`);
63
68
  }
69
+ /* --------------------------------------------------------------------------
70
+ * A Northstar intent, in words
71
+ *
72
+ * `proposals list` showed the intent's TITLE as its kind code (`pause_commitment`),
73
+ * `proposals show` printed the raw JSON, `rejected` read as `discarded` (the inbox's
74
+ * own vocabulary, which says a person discarded something), and the sentence the
75
+ * governor wrote lived only inside the JSON. Everything below renders fields the
76
+ * view already carries; no new data is needed.
77
+ * ------------------------------------------------------------------------ */
78
+ /** What each kind of intent does, as a phrase a target can follow. A new kind fails the build. */
79
+ const INTENT_PHRASE = {
80
+ pause_commitment: 'Pause',
81
+ resume_commitment: 'Resume',
82
+ reprioritize_commitment: 'Reprioritize',
83
+ request_commitment_check: 'Check now:',
84
+ set_execution_bounds: 'Change the run bounds of',
85
+ create_commitment_proposal: 'Start a new commitment',
86
+ reassign_commitment_proposal: 'Reassign',
87
+ };
88
+ /** The intent's own verdict, which the inbox flattens into proposed/kept/discarded/expired. */
89
+ const VERDICT_WORDS = {
90
+ advisory: 'advisory: recorded for you to read, nothing to decide',
91
+ awaiting_approval: 'waiting for your decision',
92
+ approved: 'approved, not applied yet',
93
+ applying: 'being applied',
94
+ applied: 'applied',
95
+ rejected: 'rejected',
96
+ invalidated: 'invalidated: it went stale before it was applied',
97
+ };
98
+ function isNorthstarIntentView(view) {
99
+ return view.kind === 'northstar_intent' && Boolean(view.decisionContext?.snapshot);
100
+ }
101
+ function intentTarget(view) {
102
+ const id = view.decisionContext?.snapshot.targetCommitmentId ?? view.commitmentId;
103
+ if (view.commitmentTitle)
104
+ return id ? `${view.commitmentTitle} (${id})` : view.commitmentTitle;
105
+ return id ?? 'a commitment';
106
+ }
107
+ /**
108
+ * The intent's title in words: what it would DO, to what. Any other kind of proposal, and an
109
+ * intent the server sent no context for, keeps the view's own title.
110
+ */
111
+ function proposalTitleFor(view) {
112
+ if (!isNorthstarIntentView(view))
113
+ return view.title;
114
+ const snapshot = view.decisionContext.snapshot;
115
+ const phrase = INTENT_PHRASE[snapshot.kind] ?? snapshot.kind;
116
+ if (snapshot.kind === 'create_commitment_proposal') {
117
+ const title = snapshot.payload?.title;
118
+ return typeof title === 'string' && title.trim() ? `${phrase}: ${title.trim()}` : phrase;
119
+ }
120
+ if (snapshot.kind === 'reassign_commitment_proposal') {
121
+ const agent = snapshot.payload?.agentId;
122
+ const to = typeof agent === 'string' && agent ? ` to agent ${agent}` : '';
123
+ return `${phrase} ${intentTarget(view)}${to}`;
124
+ }
125
+ return `${phrase} ${intentTarget(view)}`;
126
+ }
127
+ /**
128
+ * The inbox files a rejection nobody on the owner's side made under `discarded`. For a Northstar
129
+ * intent the verdict, and who made it, is said in the NOTE column, so a policy refusal is not
130
+ * read as something the person threw away.
131
+ */
132
+ function northstarIntentNote(view) {
133
+ if (!isNorthstarIntentView(view))
134
+ return '';
135
+ const verdict = view.sourceStatus;
136
+ const reason = view.decision?.reason ? `: ${view.decision.reason}` : '';
137
+ if (verdict === 'rejected') {
138
+ return view.decision?.by ? `rejected by you${reason}` : `rejected by policy${reason}`;
139
+ }
140
+ if (verdict === 'invalidated') {
141
+ return `invalidated: it went stale, nothing was discarded${reason}`;
142
+ }
143
+ if (verdict === 'advisory')
144
+ return 'advisory: nothing to decide';
145
+ return '';
146
+ }
147
+ /**
148
+ * `proposals show` for a Northstar intent, as lines a person can read. Null for every other kind,
149
+ * which keeps printing the full JSON as before (a script reading it does not change).
150
+ */
151
+ function renderNorthstarIntentView(view) {
152
+ if (!isNorthstarIntentView(view))
153
+ return null;
154
+ const context = view.decisionContext;
155
+ const verdict = view.sourceStatus;
156
+ const risk = context.riskLevel ?? context.snapshot.riskLevel ?? 'unknown';
157
+ const hasTarget = Boolean(context.snapshot.targetCommitmentId || view.commitmentId);
158
+ const lines = [proposalTitleFor(view), ''];
159
+ lines.push(`Proposal ${view.id}`);
160
+ lines.push(`Kind ${context.snapshot.kind} · risk ${risk}`);
161
+ lines.push(`Target ${hasTarget ? intentTarget(view) : 'none (a new commitment)'}`);
162
+ if (context.reason)
163
+ lines.push(`Why ${context.reason}`);
164
+ lines.push(`Status ${VERDICT_WORDS[verdict] ?? verdict} (the inbox files it under "${view.disposition}")`);
165
+ const decision = view.decision;
166
+ if (verdict === 'awaiting_approval' || verdict === 'advisory') {
167
+ // Nobody has decided: `decision.reason` here is the policy that HELD it (or kept it advisory),
168
+ // and printing it as "Decided … by policy" read as a recorded decision (OSK-21251).
169
+ if (decision?.reason) {
170
+ const words = (0, northstars_1.describeNorthstarReason)(decision.reason);
171
+ lines.push(verdict === 'awaiting_approval' ? `Waiting for you: ${words}` : `Not decided: ${words}`);
172
+ }
173
+ }
174
+ else if (decision && (decision.at || decision.by || decision.reason)) {
175
+ const at = decision.at ? `${(0, northstars_1.shortDate)(decision.at)} UTC` : 'at an unrecorded time';
176
+ const by = decision.by ? ` by user ${decision.by}` : ' by policy';
177
+ const via = decision.via ? ` via ${decision.via}` : '';
178
+ const reason = decision.reason ? `: ${decision.reason}` : '';
179
+ lines.push(`Decided ${at}${by}${via}${reason}`);
180
+ }
181
+ if (view.canDecide) {
182
+ lines.push(`Can decide ${view.availableDecisions.join(', ')}`);
183
+ }
184
+ else {
185
+ const why = view.undecidableReason
186
+ ? data_provider_2.PROPOSAL_UNDECIDABLE_TEXT[view.undecidableReason]
187
+ : 'not now';
188
+ lines.push(`Can decide no: ${why}`);
189
+ }
190
+ const continuation = view.continuation;
191
+ if (continuation) {
192
+ const bits = [continuation.status, continuation.commitmentId, continuation.reason].filter(Boolean);
193
+ lines.push(`Continued ${bits.join(' · ')}`);
194
+ }
195
+ lines.push(`Revision ${JSON.stringify(view.revision)}`);
196
+ // The hint is for a proposal that can still be decided: beside "Can decide no" it contradicts
197
+ // the line above it (OSK-21255).
198
+ if (view.canDecide) {
199
+ lines.push(' (pass this exact JSON as --expected to keep, discard or snooze)');
200
+ }
201
+ lines.push(`Web ${view.links.web}`);
202
+ return lines;
203
+ }
204
+ /**
205
+ * What a replayed decision says. The replay receipt names the decision that was asked for ("keep"), but a
206
+ * Northstar intent that was kept has gone on: it may be applied, or approved and not applied yet, and a
207
+ * discard lands as `rejected`. "Already kept" for an intent that was in fact applied understated what
208
+ * had happened (OSK-21250), so the intent's own verdict is named when the proposal could be read.
209
+ */
210
+ function describeReplay(decision, receipt, view) {
211
+ let outcome = { keep: 'kept', discard: 'discarded', snooze: 'snoozed' }[decision];
212
+ if (view && isNorthstarIntentView(view)) {
213
+ const settled = INTENT_REPLAY_WORDS[view.sourceStatus];
214
+ if (settled)
215
+ outcome = settled;
216
+ }
217
+ const when = receipt.decidedAt ? ` at ${(0, northstars_1.shortDate)(receipt.decidedAt)} UTC` : '';
218
+ return `Already ${outcome}${when} — nothing changed.`;
219
+ }
220
+ /** The verdicts a replayed keep or discard can find an intent in. */
221
+ const INTENT_REPLAY_WORDS = {
222
+ applied: 'applied',
223
+ approved: 'approved (not applied yet)',
224
+ applying: 'approved and being applied',
225
+ rejected: 'rejected',
226
+ };
@@ -10,6 +10,11 @@
10
10
  * `X-Idempotency-Key` (every `request.ts` / adapter write does). A 429 means the
11
11
  * request was refused before it ran, so the key is belt and braces.
12
12
  *
13
+ * Never retried when the body says it cannot help: a 429 that declares
14
+ * `retryable: false` (a follow-up budget refusal, which says the day resets at
15
+ * 00:00 UTC) is a refusal to keep, not a limiter to wait out. Retrying it sent the
16
+ * same refused request three times and told the server about three intents (OSK-21266).
17
+ *
13
18
  * Bounded twice: at most `maxRetries` extra attempts and a per-wait ceiling, so
14
19
  * a stuck limiter costs a script seconds, not minutes. `SKRR_RATE_LIMIT_RETRIES=0`
15
20
  * turns it off.
@@ -11,6 +11,11 @@
11
11
  * `X-Idempotency-Key` (every `request.ts` / adapter write does). A 429 means the
12
12
  * request was refused before it ran, so the key is belt and braces.
13
13
  *
14
+ * Never retried when the body says it cannot help: a 429 that declares
15
+ * `retryable: false` (a follow-up budget refusal, which says the day resets at
16
+ * 00:00 UTC) is a refusal to keep, not a limiter to wait out. Retrying it sent the
17
+ * same refused request three times and told the server about three intents (OSK-21266).
18
+ *
14
19
  * Bounded twice: at most `maxRetries` extra attempts and a per-wait ceiling, so
15
20
  * a stuck limiter costs a script seconds, not minutes. `SKRR_RATE_LIMIT_RETRIES=0`
16
21
  * turns it off.
@@ -65,6 +70,18 @@ function retryAfterOf(response) {
65
70
  ? parseRetryAfterMs(response.headers.get('retry-after'))
66
71
  : undefined;
67
72
  }
73
+ /** A 429 whose body declares `retryable: false`. Reads a clone, so the caller still gets the body. */
74
+ async function declaresNotRetryable(response) {
75
+ if (response.status !== 429)
76
+ return false;
77
+ try {
78
+ const body = (await response.clone().json());
79
+ return body?.retryable === false;
80
+ }
81
+ catch {
82
+ return false;
83
+ }
84
+ }
68
85
  /**
69
86
  * Run `attemptFetch` and, on a 429 for a replay-safe request, wait and run it
70
87
  * again. Returns the last response, with `retryAfterMs` of the final 429 (so the
@@ -80,6 +97,8 @@ async function fetchWithRateLimitRetry(attemptFetch, options) {
80
97
  for (let attempt = 0; attempt < max && response.status === 429; attempt++) {
81
98
  if (options.signal?.aborted)
82
99
  break;
100
+ if (await declaresNotRetryable(response))
101
+ break;
83
102
  await sleep(rateLimitWaitMs(attempt, retryAfterMs, options.random));
84
103
  response = await attemptFetch();
85
104
  retryAfterMs = retryAfterOf(response);
@@ -0,0 +1,2 @@
1
+ /** Render the actual actor without presenting a system or Agent as a user. */
2
+ export declare function revisionActorLabel(revision: Record<string, unknown>): string;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.revisionActorLabel = revisionActorLabel;
4
+ /** Render the actual actor without presenting a system or Agent as a user. */
5
+ function revisionActorLabel(revision) {
6
+ if (revision.userId != null)
7
+ return String(revision.userId);
8
+ const actor = revision.actor;
9
+ if (actor && typeof actor === 'object' && !Array.isArray(actor)) {
10
+ const { kind, id } = actor;
11
+ if ((kind === 'system' || kind === 'agent') && typeof id === 'string' && id) {
12
+ return `${kind}: ${id}`;
13
+ }
14
+ }
15
+ return '-';
16
+ }
@@ -53,7 +53,7 @@ export declare const roleApi: {
53
53
  config?: Record<string, unknown> | undefined;
54
54
  }[];
55
55
  sources: {
56
- source: "space" | "tasks" | "memory" | "daemon" | "email" | "goal" | "files" | "local" | "browser" | "conversations" | "slack" | "github" | "messages" | "calendar" | "agent_run" | "docs" | "metrics" | "agent_initiatives" | "documents" | "product_metrics" | "business_metrics" | "state_monitor" | "monitor";
56
+ source: "space" | "tasks" | "memory" | "daemon" | "email" | "goal" | "files" | "local" | "browser" | "conversations" | "slack" | "github" | "messages" | "calendar" | "docs" | "metrics" | "agent_run" | "agent_initiatives" | "documents" | "product_metrics" | "business_metrics" | "state_monitor" | "monitor";
57
57
  enabled: boolean;
58
58
  config?: Record<string, unknown> | undefined;
59
59
  }[];
@@ -51,6 +51,15 @@ function charterLines(charter, { binName, spaceId, full = false }) {
51
51
  if (lead) {
52
52
  for (const line of lead.split('\n'))
53
53
  lines.push(` ${line}`);
54
+ // Agents are handed about the first `leadContextChars` characters of the lead, then a pointer
55
+ // to the rest. The page printed all of it under "read by every agent", so a long lead looked
56
+ // fully delivered (the web card warns; the CLI said nothing). Measured like the server does.
57
+ const cap = charter.leadContextChars;
58
+ if (typeof cap === 'number' && cap > 0 && lead.length > cap) {
59
+ lines.push('');
60
+ lines.push(` Over the cap: this lead is ${lead.length} characters and agents get about the first ${cap}, ` +
61
+ 'then a pointer to the rest. Put the essentials first, or move detail below a ## heading.');
62
+ }
54
63
  }
55
64
  else {
56
65
  lines.push(' (empty: the page opens with a ## heading, so agents receive no charter lead. ' +
@@ -68,7 +68,6 @@ const fs = __importStar(require("node:fs"));
68
68
  const http = __importStar(require("node:http"));
69
69
  const https = __importStar(require("node:https"));
70
70
  const path = __importStar(require("node:path"));
71
- const node_buffer_1 = require("node:buffer");
72
71
  const node_crypto_1 = require("node:crypto");
73
72
  const data_provider_1 = require("@skrr-ai/data-provider");
74
73
  const base_command_1 = require("../base-command");
@@ -223,7 +222,7 @@ function taskAssetMarkdownRef(asset) {
223
222
  */
224
223
  function fileFormData(filePath, filename, mimeType, role, ledger) {
225
224
  const fd = new FormData();
226
- fd.append('file', new node_buffer_1.Blob([fs.readFileSync(filePath)], { type: mimeType }), filename);
225
+ fd.append('file', new Blob([fs.readFileSync(filePath)], { type: mimeType }), filename);
227
226
  if (role)
228
227
  fd.append('role', role);
229
228
  if (ledger?.identity) {
@@ -158,6 +158,15 @@ export interface TriggerFireRecord {
158
158
  replayBlockedReason?: string;
159
159
  replayRequirements?: string[];
160
160
  replayContext?: unknown;
161
+ /** Tool calls refused during a turn that was delivered (OSK-20776). */
162
+ permissionRefusals?: {
163
+ count?: number;
164
+ tools?: string[];
165
+ unattended?: boolean;
166
+ layer?: string;
167
+ };
168
+ /** The refusal record could not be read for this fire, which is not "none were refused". */
169
+ permissionRefusalsUnread?: boolean;
161
170
  };
162
171
  }
163
172
  export interface TriggerListResponse {
@@ -499,7 +508,7 @@ export declare function formatDuplicateSubscriptionWarning(trigger: unknown): {
499
508
  * "armed" when it will fire and "inert" until enabled — so a freshly-created
500
509
  * trigger did not plainly confirm it is live.
501
510
  */
502
- export declare function armedStateLabel(enabled: boolean | undefined): string;
511
+ export declare function armedStateLabel(enabled: boolean | undefined, lastResult?: string | null): string;
503
512
  /**
504
513
  * Human phrasing for an effect trigger's write mode (OSK-4886).
505
514
  *
@@ -516,6 +525,29 @@ export declare function writeModeLabel(writeMode: string): string;
516
525
  * the configured base URL rather than guessed.
517
526
  */
518
527
  export declare function webhookIngestUrl(baseURL: string, triggerId: string): string;
528
+ /**
529
+ * What `triggers show` adds when the last fire DELIVERED but had tool calls
530
+ * refused (OSK-20776). The outcome of such a fire is `succeeded`, so the
531
+ * `Fires:` line reads as success; this says what the string after `last` means
532
+ * and what a person can do about it.
533
+ *
534
+ * It states the mechanism and not this fire's cause, because `lastResult` names
535
+ * the refused kinds only. Whether a fire was unattended, and which layer
536
+ * refused, is on the fire record (`fire-detail`), and this points there.
537
+ *
538
+ * It does not recommend a mode. `bypassPermissions` is the only mode under which
539
+ * an unattended shell call runs, and it runs EVERY call without asking, which is
540
+ * a decision about what this agent may do on its own (a release job included),
541
+ * not a hint to paste.
542
+ */
543
+ export declare function toolsRefusedHintLines(trigger: TriggerDefinition, kinds: readonly string[], bin?: string): string[];
544
+ /**
545
+ * The refusal facts a fire record holds, as `fire-detail` lines. Nothing when
546
+ * the fire recorded none. A fire whose record could not be read says so: that
547
+ * is not the same as no refusal, and a reader must not be able to mistake it
548
+ * for one.
549
+ */
550
+ export declare function fireRefusalLines(metadata: unknown): string[];
519
551
  export declare function renderTriggerSummary(trigger: TriggerDefinition, log: (line: string) => void, options?: {
520
552
  baseURL?: string;
521
553
  }): void;
@@ -48,6 +48,8 @@ exports.formatDuplicateSubscriptionWarning = formatDuplicateSubscriptionWarning;
48
48
  exports.armedStateLabel = armedStateLabel;
49
49
  exports.writeModeLabel = writeModeLabel;
50
50
  exports.webhookIngestUrl = webhookIngestUrl;
51
+ exports.toolsRefusedHintLines = toolsRefusedHintLines;
52
+ exports.fireRefusalLines = fireRefusalLines;
51
53
  exports.renderTriggerSummary = renderTriggerSummary;
52
54
  exports.renderLegacyRuns = renderLegacyRuns;
53
55
  exports.renderFireAudit = renderFireAudit;
@@ -1239,8 +1241,16 @@ function formatDuplicateSubscriptionWarning(trigger) {
1239
1241
  * "armed" when it will fire and "inert" until enabled — so a freshly-created
1240
1242
  * trigger did not plainly confirm it is live.
1241
1243
  */
1242
- function armedStateLabel(enabled) {
1243
- return enabled === false ? 'no — inert until enabled' : 'yes — will fire';
1244
+ function armedStateLabel(enabled, lastResult) {
1245
+ if (enabled === false)
1246
+ return 'no — inert until enabled';
1247
+ // "Will fire" is the row's intent. A platform-managed row whose owner is fenced (Always-On off) ticks on
1248
+ // schedule and is skipped every time, and the same screen's `Fires:` line says so; the first line a reader
1249
+ // meets must not say the opposite (dogfood df-261031-commitment-authoring, OSK-21103).
1250
+ const skipped = typeof lastResult === 'string' ? /^skipped:(.+)$/.exec(lastResult) : null;
1251
+ return skipped
1252
+ ? `yes — but its last tick was skipped (${skipped[1]}), so it did not run`
1253
+ : 'yes — will fire';
1244
1254
  }
1245
1255
  /**
1246
1256
  * Human phrasing for an effect trigger's write mode (OSK-4886).
@@ -1276,10 +1286,69 @@ const WEBHOOK_INGEST_KINDS = new Set(['on_demand', 'form', 'chat_message']);
1276
1286
  function webhookIngestUrl(baseURL, triggerId) {
1277
1287
  return `${String(baseURL).replace(/\/+$/, '')}/api/triggers/webhook/${encodeURIComponent(triggerId)}`;
1278
1288
  }
1289
+ /** An id or token that is short and plain, so it is safe in a command a person will paste. */
1290
+ const PLAIN_TOKEN = /^[A-Za-z0-9_.-]{1,64}$/;
1291
+ /**
1292
+ * What `triggers show` adds when the last fire DELIVERED but had tool calls
1293
+ * refused (OSK-20776). The outcome of such a fire is `succeeded`, so the
1294
+ * `Fires:` line reads as success; this says what the string after `last` means
1295
+ * and what a person can do about it.
1296
+ *
1297
+ * It states the mechanism and not this fire's cause, because `lastResult` names
1298
+ * the refused kinds only. Whether a fire was unattended, and which layer
1299
+ * refused, is on the fire record (`fire-detail`), and this points there.
1300
+ *
1301
+ * It does not recommend a mode. `bypassPermissions` is the only mode under which
1302
+ * an unattended shell call runs, and it runs EVERY call without asking, which is
1303
+ * a decision about what this agent may do on its own (a release job included),
1304
+ * not a hint to paste.
1305
+ */
1306
+ function toolsRefusedHintLines(trigger, kinds, bin = 'skrr') {
1307
+ const scopedAgent = trigger.scope?.kind === 'agent' ? trigger.scope.refs?.agentId : undefined;
1308
+ const agent = typeof scopedAgent === 'string' && PLAIN_TOKEN.test(scopedAgent) ? scopedAgent : '<agent>';
1309
+ const triggerId = typeof trigger.id === 'string' && PLAIN_TOKEN.test(trigger.id) ? trigger.id : '<trigger-id>';
1310
+ const what = kinds.length > 0 ? kinds.join(', ') : 'some tool calls';
1311
+ return [
1312
+ `Refused: ${what} - the last fire delivered, but these tool calls were refused`,
1313
+ ' A scheduled fire runs unattended: a call that would need approval is refused, because no one is there to give it.',
1314
+ " To allow it, change the agent's permission mode (bypassPermissions runs every tool call without asking, so decide that on purpose):",
1315
+ ` ${bin} agents update ${agent} --default-permission-mode <mode> (modes: ${bin} agents update --help)`,
1316
+ ` What was refused, per fire: ${bin} triggers audit ${triggerId}, then ${bin} triggers fire-detail ${triggerId} <fire-id>`,
1317
+ ];
1318
+ }
1319
+ /**
1320
+ * The refusal facts a fire record holds, as `fire-detail` lines. Nothing when
1321
+ * the fire recorded none. A fire whose record could not be read says so: that
1322
+ * is not the same as no refusal, and a reader must not be able to mistake it
1323
+ * for one.
1324
+ */
1325
+ function fireRefusalLines(metadata) {
1326
+ if (!metadata || typeof metadata !== 'object')
1327
+ return [];
1328
+ const { permissionRefusals, permissionRefusalsUnread } = metadata;
1329
+ const lines = [];
1330
+ if (permissionRefusals && typeof permissionRefusals === 'object') {
1331
+ const { count, tools, unattended, layer } = permissionRefusals;
1332
+ const kinds = (Array.isArray(tools) ? tools : []).filter((tool) => typeof tool === 'string' && PLAIN_TOKEN.test(tool));
1333
+ const calls = typeof count === 'number' && Number.isFinite(count) && count > 0
1334
+ ? `${count} tool call${count === 1 ? '' : 's'}`
1335
+ : 'tool calls';
1336
+ const parts = [`${calls} refused${kinds.length > 0 ? ` (${kinds.join(', ')})` : ''}`];
1337
+ if (unattended === true)
1338
+ parts.push('unattended, so no one could grant them');
1339
+ if (typeof layer === 'string' && PLAIN_TOKEN.test(layer))
1340
+ parts.push(`refused by ${layer}`);
1341
+ lines.push(`Refused: ${parts.join('; ')}`);
1342
+ }
1343
+ if (permissionRefusalsUnread === true) {
1344
+ lines.push('Refused: unknown - the refusal record could not be read for this fire; that is not the same as none');
1345
+ }
1346
+ return lines;
1347
+ }
1279
1348
  function renderTriggerSummary(trigger, log, options = {}) {
1280
1349
  log(`ID: ${trigger.id ?? '-'}`);
1281
1350
  log(`Kind: ${trigger.kind ?? '-'}`);
1282
- log(`Armed: ${armedStateLabel(trigger.enabled)}`);
1351
+ log(`Armed: ${armedStateLabel(trigger.enabled, trigger.audit?.lastResult)}`);
1283
1352
  log(`Scope: ${scopeLabel(trigger)}`);
1284
1353
  /*
1285
1354
  * Say when the row is not yours to change.
@@ -1389,6 +1458,11 @@ function renderTriggerSummary(trigger, log, options = {}) {
1389
1458
  parts.push(`last ${audit.lastResult}`);
1390
1459
  log(`Fires: ${parts.join(', ')}`);
1391
1460
  }
1461
+ // `last tools_refused:...` above is a success that held a refusal. Say what that means.
1462
+ const refused = (0, data_provider_1.parseToolsRefusedResult)(audit.lastResult);
1463
+ if (refused)
1464
+ for (const line of toolsRefusedHintLines(trigger, refused))
1465
+ log(line);
1392
1466
  // The reason, at the surface a reader reaches for — not only in `--json`.
1393
1467
  if (audit.lastError)
1394
1468
  log(`Last error: ${audit.lastError}`);