@skrr-ai/cli 0.1.102 → 0.1.104

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 (52) hide show
  1. package/dist/base-command.js +24 -0
  2. package/dist/commands/agent/message/send.js +6 -2
  3. package/dist/commands/agents/triggers/create.js +7 -17
  4. package/dist/commands/agents/triggers/update.js +41 -28
  5. package/dist/commands/commitments/create.js +2 -0
  6. package/dist/commands/commitments/preflight.js +18 -1
  7. package/dist/commands/commitments/roster.d.ts +15 -0
  8. package/dist/commands/commitments/roster.js +39 -0
  9. package/dist/commands/commitments/show.js +1 -1
  10. package/dist/commands/commitments/status.d.ts +17 -0
  11. package/dist/commands/commitments/status.js +41 -0
  12. package/dist/commands/commitments/update.js +12 -0
  13. package/dist/commands/daemon/logs.d.ts +106 -1
  14. package/dist/commands/daemon/logs.js +311 -15
  15. package/dist/commands/followups/checkin.js +46 -3
  16. package/dist/commands/tasks/approvals/resolve.d.ts +12 -0
  17. package/dist/commands/tasks/approvals/resolve.js +21 -5
  18. package/dist/commands/triggers/create.js +8 -17
  19. package/dist/commands/triggers/fire.js +3 -0
  20. package/dist/commands/triggers/update.js +41 -27
  21. package/dist/commands/widgets/check.d.ts +39 -0
  22. package/dist/commands/widgets/check.js +179 -0
  23. package/dist/lib/agent-mailbox.d.ts +0 -1
  24. package/dist/lib/agent-mailbox.js +0 -1
  25. package/dist/lib/commitment-loop-status.d.ts +16 -0
  26. package/dist/lib/commitment-loop-status.js +164 -0
  27. package/dist/lib/commitments.d.ts +28 -1
  28. package/dist/lib/commitments.js +184 -5
  29. package/dist/lib/delegated-cli.d.ts +23 -1
  30. package/dist/lib/delegated-cli.js +61 -4
  31. package/dist/lib/first-party-harness.d.ts +1 -1
  32. package/dist/lib/first-party-harness.js +1 -1
  33. package/dist/lib/session-task-write.d.ts +13 -0
  34. package/dist/lib/session-task-write.js +63 -3
  35. package/dist/lib/triggers.d.ts +26 -0
  36. package/dist/lib/triggers.js +134 -1
  37. package/dist/lib/widgetCheck.d.ts +123 -0
  38. package/dist/lib/widgetCheck.js +998 -0
  39. package/dist/lib/widgetStaticScan.d.ts +111 -0
  40. package/dist/lib/widgetStaticScan.js +833 -0
  41. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.d.ts +9 -1
  42. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +12 -0
  43. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/telemetryRedaction.d.ts +61 -0
  44. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/telemetryRedaction.js +177 -0
  45. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.d.ts +9 -1
  46. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +12 -0
  47. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/telemetryRedaction.d.ts +61 -0
  48. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/telemetryRedaction.js +171 -0
  49. package/dist/node_modules/@skrr-ai/auth-core/package.json +11 -1
  50. package/dist/node_modules/@skrr-ai/data-provider/index.js +11908 -8693
  51. package/oclif.manifest.json +219 -6
  52. package/package.json +2 -2
@@ -18,14 +18,17 @@ exports.cadenceLabel = cadenceLabel;
18
18
  exports.renderCommitmentList = renderCommitmentList;
19
19
  exports.extractStandstillFromCheck = extractStandstillFromCheck;
20
20
  exports.describeExecutorBlocked = describeExecutorBlocked;
21
+ exports.withLiveAssessment = withLiveAssessment;
21
22
  exports.renderCommitmentSummary = renderCommitmentSummary;
22
23
  exports.describeWakeDecision = describeWakeDecision;
23
24
  exports.renderCheckList = renderCheckList;
24
25
  exports.renderCommitmentLedger = renderCommitmentLedger;
25
26
  exports.renderCommitmentTrace = renderCommitmentTrace;
27
+ exports.renderProactivenessScorecard = renderProactivenessScorecard;
26
28
  exports.formatCommitmentCycles = formatCommitmentCycles;
27
29
  exports.formatCommitmentExplanation = formatCommitmentExplanation;
28
30
  exports.renderListPageHint = renderListPageHint;
31
+ exports.describeObservationArtifacts = describeObservationArtifacts;
29
32
  exports.renderCommitmentObservations = renderCommitmentObservations;
30
33
  exports.renderCommitmentHypotheses = renderCommitmentHypotheses;
31
34
  exports.renderCommitmentLineage = renderCommitmentLineage;
@@ -134,6 +137,10 @@ exports.commitmentApi = {
134
137
  * swept, how stale that is, and the tally it last persisted. Aggregate only.
135
138
  */
136
139
  livenessSweep: () => data_provider_2.request.get('/api/commitments/liveness/sweep'),
140
+ /** One agent's Commitments, system roles included, each with its loop status. */
141
+ roster: (agentId) => data_provider_2.request.get((0, triggers_1.withQuery)('/api/commitments/roster', { agentId })),
142
+ /** Is this Commitment's loop installed and working. */
143
+ loopStatus: (id) => data_provider_2.request.get(`${base(id)}/loop-status`),
137
144
  manifest: (id) => data_provider_2.request.get(`${base(id)}/manifest`),
138
145
  import: (body) => data_provider_2.request.post('/api/commitments/import', body),
139
146
  fork: (id, body) => data_provider_2.request.post(`${base(id)}/fork`, body),
@@ -512,8 +519,10 @@ function normalizeWatchedSourceDeclarations(declarations) {
512
519
  const bySource = new Map();
513
520
  const normalized = [];
514
521
  for (const binding of declarations) {
515
- if (!binding || typeof binding !== 'object' ||
516
- !('source' in binding) || typeof binding.source !== 'string') {
522
+ if (!binding ||
523
+ typeof binding !== 'object' ||
524
+ !('source' in binding) ||
525
+ typeof binding.source !== 'string') {
517
526
  normalized.push(binding); // The API owns validation of the remaining DTO.
518
527
  continue;
519
528
  }
@@ -646,10 +655,33 @@ function describeEvidenceHealth(health, criteriaCount) {
646
655
  return `unevidenceable_criteria${detail} — the criteria below cannot be judged from what this commitment observes; rewrite them or attach a Measure`;
647
656
  }
648
657
  if (state === 'degraded_observability') {
649
- return `degraded_observability${detail} — an evaluator could not run; this is a fault, not a criteria problem`;
658
+ return `degraded_observability${detail} — ${describeDegradedObservability(reasons)}`;
650
659
  }
651
660
  return `${state}${detail}`;
652
661
  }
662
+ /**
663
+ * What a `degraded_observability` verdict actually says, from its reasons.
664
+ *
665
+ * The state is a catch-all on the server (anything that is not purely an
666
+ * authoring problem lands here), so the copy used to assert "an evaluator
667
+ * could not run; this is a fault" for every case — including a commitment
668
+ * whose first run had simply not reported yet (OSK-15989). Only the
669
+ * `*_evidence_unavailable` reasons say an evaluator could not run; the rest
670
+ * say which part of the reading was missing, and that is what this names.
671
+ */
672
+ function describeDegradedObservability(reasons) {
673
+ const has = (reason) => reasons.includes(reason);
674
+ if (has('state_evidence_unavailable') || has('outcome_evidence_unavailable')) {
675
+ return 'an evaluator could not run; this is a fault, not a criteria problem';
676
+ }
677
+ if (has('access_issues') || has('source_warning_or_error') || has('no_healthy_source')) {
678
+ return 'not every configured source could be read on the last check (`skrr commitments doctor <id>` names the next step)';
679
+ }
680
+ if (has('evidence_stale')) {
681
+ return "the freshest evidence is older than this commitment's freshness expectation";
682
+ }
683
+ return 'the last check could not see everything it needs to judge this commitment';
684
+ }
653
685
  /** Render an interval back as a compact human string. */
654
686
  function describeIntervalMs(ms) {
655
687
  if (!ms || !Number.isFinite(ms))
@@ -827,6 +859,20 @@ function describeExecutorBlocked(commitment) {
827
859
  : reason || 'blocked';
828
860
  return `${label}${until ? ` until ${until}` : ''}`;
829
861
  }
862
+ /**
863
+ * The commitment `show` should render: its stored contract, with the
864
+ * assessment replaced by the one the operating state recomputed for this read
865
+ * when the server sent one (OSK-15989). The stored `commitment.assessment` is
866
+ * what the LAST check wrote; while a run is in flight it can still say
867
+ * `blocked`, and printing it above "Agent is working" made the header
868
+ * contradict the rest of the page. `doctor` already reads the live one first.
869
+ */
870
+ function withLiveAssessment(commitment, operatingState) {
871
+ const live = operatingState?.assessment;
872
+ if (!live || typeof live !== 'object')
873
+ return commitment;
874
+ return { ...commitment, assessment: live };
875
+ }
830
876
  function renderCommitmentSummary(commitment, log,
831
877
  /**
832
878
  * The most recent check, when the caller has one. Optional because pause /
@@ -1195,10 +1241,103 @@ function renderCommitmentTrace(trace, log) {
1195
1241
  { key: 'stop', header: 'STOPPED AT', maxWidth: 48 },
1196
1242
  { key: 'summary', header: 'SUMMARY', maxWidth: 72 },
1197
1243
  ], 'No trace events recorded.', log);
1244
+ const scorecard = record(trace.proactiveness);
1245
+ if (scorecard)
1246
+ renderProactivenessScorecard(scorecard, log);
1198
1247
  if (trace.has_more === true && trace.next_cursor) {
1199
1248
  log(`More events are available; continue with --cursor ${text(trace.next_cursor)}.`);
1200
1249
  }
1201
1250
  }
1251
+ /*
1252
+ * Display order and words for `workClassification.counts`. The names are owned
1253
+ * by the API scorecard (`COMMITMENT_CHECK_WORK_CLASSIFICATIONS` in
1254
+ * CommitmentProactivenessMetrics); an unknown name still prints, by its key.
1255
+ */
1256
+ const WORK_CLASSIFICATION_LABELS = {
1257
+ justified_source_change: 'source changed',
1258
+ justified_time_boundary: 'review/deadline due',
1259
+ justified_evidence: 'evidence refresh',
1260
+ work_item: 'open work item',
1261
+ instruction: 'instruction',
1262
+ repeat_identical_evidence: 'REPEAT on identical evidence',
1263
+ unknown_basis: 'basis not recorded',
1264
+ };
1265
+ const SCORECARD_METRICS = [
1266
+ { key: 'justifiedDecisionRate', label: 'Justified work', kind: 'ratio' },
1267
+ { key: 'repeatDecisionRate', label: 'Repeat decisions', kind: 'ratio' },
1268
+ { key: 'duplicateInvestigationRate', label: 'Duplicate investigations', kind: 'ratio' },
1269
+ { key: 'usefulActionYield', label: 'Useful action yield', kind: 'ratio' },
1270
+ { key: 'notificationPrecision', label: 'Notification precision', kind: 'ratio' },
1271
+ { key: 'taskClosureRate', label: 'Task closure', kind: 'ratio' },
1272
+ { key: 'provenanceCompleteness', label: 'Provenance complete', kind: 'ratio' },
1273
+ { key: 'materialDetectionLatencyMs', label: 'Detection latency p50', kind: 'latency' },
1274
+ { key: 'evidenceFreshnessMs', label: 'Evidence age p50', kind: 'latency' },
1275
+ ];
1276
+ function scorecardValue(value, kind) {
1277
+ if (kind === 'ratio') {
1278
+ return typeof value === 'number' && Number.isFinite(value)
1279
+ ? `${Math.round(value * 100)}%`
1280
+ : '-';
1281
+ }
1282
+ const p50 = record(value)?.p50;
1283
+ if (typeof p50 !== 'number' || !Number.isFinite(p50))
1284
+ return '-';
1285
+ return p50 >= 60_000 ? `${Math.round(p50 / 60_000)}m` : `${Math.round(p50 / 1000)}s`;
1286
+ }
1287
+ /** The qualifier that keeps a number from claiming more than its samples can. */
1288
+ function scorecardQualifier(coverage) {
1289
+ if (!coverage)
1290
+ return '';
1291
+ const samples = typeof coverage.samples === 'number' ? coverage.samples : null;
1292
+ const parts = [];
1293
+ if (samples !== null)
1294
+ parts.push(`${samples} sample${samples === 1 ? '' : 's'}`);
1295
+ if (coverage.reliability === 'no_samples')
1296
+ return '(no samples)';
1297
+ if (coverage.reliability === 'mostly_unknown_basis') {
1298
+ parts.push(`ESTIMATE: ${text(coverage.unknownBasis, '?')} with no recorded basis`);
1299
+ }
1300
+ else if (typeof coverage.unknownBasis === 'number' && coverage.unknownBasis > 0) {
1301
+ parts.push(`${coverage.unknownBasis} with no recorded basis`);
1302
+ }
1303
+ if (typeof coverage.partialObservation === 'number' && coverage.partialObservation > 0) {
1304
+ parts.push(`${coverage.partialObservation} partial observation`);
1305
+ }
1306
+ if (coverage.partialWindow === true)
1307
+ parts.push('partial page');
1308
+ return parts.length ? `(${parts.join('; ')})` : '';
1309
+ }
1310
+ function renderProactivenessScorecard(scorecard, log) {
1311
+ const coverage = record(scorecard.coverage);
1312
+ const metrics = record(scorecard.metrics) || {};
1313
+ const metricCoverage = record(scorecard.metricCoverage) || {};
1314
+ log('');
1315
+ log(coverage?.partialWindow === true || record(scorecard.window)?.partial === true
1316
+ ? 'Proactiveness (this page only — not the whole window)'
1317
+ : 'Proactiveness');
1318
+ if (coverage) {
1319
+ log(` Checks: ${text(coverage.checks, '0')} read · ${text(coverage.decisioningChecks, '0')} decided work · ` +
1320
+ `${text(coverage.quietChecks, '0')} quiet · ${text(coverage.dedupeSuppressedChecks, '0')} suppressed as duplicate`);
1321
+ }
1322
+ const counts = record(record(scorecard.workClassification)?.counts);
1323
+ if (counts) {
1324
+ const parts = Object.entries(counts)
1325
+ .filter(([, value]) => typeof value === 'number' && value > 0)
1326
+ .map(([key, value]) => `${WORK_CLASSIFICATION_LABELS[key] || key} ${value}`);
1327
+ log(` Why work was decided: ${parts.length ? parts.join(' · ') : 'no decisioning Checks'}`);
1328
+ }
1329
+ for (const metric of SCORECARD_METRICS) {
1330
+ if (!(metric.key in metrics))
1331
+ continue;
1332
+ const qualifier = scorecardQualifier(record(metricCoverage[metric.key]));
1333
+ log(` ${metric.label.padEnd(26)} ${scorecardValue(metrics[metric.key], metric.kind).padStart(5)}` +
1334
+ (qualifier ? ` ${qualifier}` : ''));
1335
+ }
1336
+ for (const note of Array.isArray(coverage?.notes) ? coverage.notes : []) {
1337
+ if (typeof note === 'string' && note)
1338
+ log(` Note: ${note}`);
1339
+ }
1340
+ }
1202
1341
  const CYCLE_GLYPH = {
1203
1342
  advanced: '✓',
1204
1343
  stopped: '●',
@@ -1225,7 +1364,12 @@ function cycleWake(cycle) {
1225
1364
  function cycleSummary(cycle) {
1226
1365
  if (cycle.outcome === 'stopped' && cycle.stop) {
1227
1366
  const { question, gate, reason, fix, detail } = cycle.stop;
1228
- return `${question} ${reason || gate} — ${fix || detail || 'stopped'}`;
1367
+ // A stopped cycle can still have moved something (the Check's own read, a task it
1368
+ // made); say so, or the row reads as if nothing happened (OSK-15408).
1369
+ const moved = cycle.advancementKinds?.length
1370
+ ? ` (moved: ${cycle.advancementKinds.join(', ')})`
1371
+ : '';
1372
+ return `${question} ${reason || gate} — ${fix || detail || 'stopped'}${moved}`;
1229
1373
  }
1230
1374
  if (cycle.outcome === 'advanced') {
1231
1375
  return cycle.advancementKinds?.length
@@ -1321,6 +1465,8 @@ function formatCycleLatency(cycle) {
1321
1465
  ];
1322
1466
  }
1323
1467
  const ANSWER_GLYPH = { yes: '✓', no: '✗', 'n/a': '–' };
1468
+ /** The mark `cycles` already gives a chosen quiet: the loop looked and decided to say nothing. */
1469
+ const QUIET_ANSWER_GLYPH = CYCLE_GLYPH.quiet;
1324
1470
  /** The six questions for the latest Cycle, the first ✗ carrying its fix. */
1325
1471
  function formatCommitmentExplanation(explanation) {
1326
1472
  const cycle = explanation.cycle;
@@ -1332,7 +1478,14 @@ function formatCommitmentExplanation(explanation) {
1332
1478
  if (cycle)
1333
1479
  lines.push(...formatCycleLatency(cycle));
1334
1480
  for (const answer of explanation.answers || []) {
1335
- lines.push(` ${ANSWER_GLYPH[answer.answer] || '?'} ${answer.question} ${answer.because}`);
1481
+ // The answer a deliberate quiet stopped at is not a refusal: `cycles` marks
1482
+ // that cycle ○, and a ✗ here would read as a gate (OSK-15403).
1483
+ const glyph = explanation.verdict === 'quiet' &&
1484
+ answer.answer === 'no' &&
1485
+ answer.key === explanation.stoppedAt
1486
+ ? QUIET_ANSWER_GLYPH
1487
+ : ANSWER_GLYPH[answer.answer] || '?';
1488
+ lines.push(` ${glyph} ${answer.question} ${answer.because}`);
1336
1489
  if (answer.fix)
1337
1490
  lines.push(` Fix: ${answer.fix}`);
1338
1491
  }
@@ -1376,6 +1529,30 @@ function yesNo(value) {
1376
1529
  return 'no';
1377
1530
  return '-';
1378
1531
  }
1532
+ /**
1533
+ * Whether a row's retained evidence can still be reopened (OSK-16125).
1534
+ *
1535
+ * The count alone read as evidence even when every path pointed into a
1536
+ * worktree the daemon had already deleted. `retained` artifacts are git
1537
+ * locators on the run's delivery commit; `ephemeral` ones cannot be counted
1538
+ * on; `unconfirmed` are locators for a path the run did not change. Refs the
1539
+ * server classified as none of these (URLs, hashes, legacy rows) count as
1540
+ * `other`. `--json` carries each artifact's `ref`, `locator` and `originalRef`.
1541
+ */
1542
+ function describeObservationArtifacts(artifacts) {
1543
+ const list = Array.isArray(artifacts) ? artifacts : [];
1544
+ if (list.length === 0)
1545
+ return '-';
1546
+ const counts = new Map();
1547
+ for (const artifact of list) {
1548
+ const availability = text(record(artifact)?.availability, 'other');
1549
+ counts.set(availability, (counts.get(availability) || 0) + 1);
1550
+ }
1551
+ return ['retained', 'unconfirmed', 'ephemeral', 'other']
1552
+ .filter((name) => counts.has(name))
1553
+ .map((name) => `${counts.get(name)} ${name}`)
1554
+ .join(', ');
1555
+ }
1379
1556
  function renderCommitmentObservations(response, log, options = {}) {
1380
1557
  const data = records(response.data);
1381
1558
  renderRows(data.map((item) => ({
@@ -1404,6 +1581,7 @@ function renderCommitmentObservations(response, log, options = {}) {
1404
1581
  declared: yesNo(item.declared),
1405
1582
  access: text(item.accessStatus),
1406
1583
  confidence: (0, format_1.formatCell)(item.confidence),
1584
+ artifacts: describeObservationArtifacts(item.artifacts),
1407
1585
  })), [
1408
1586
  { key: 'at', header: 'OBSERVED', maxWidth: 24 },
1409
1587
  { key: 'recorded', header: 'RECORDED', maxWidth: 24 },
@@ -1419,6 +1597,7 @@ function renderCommitmentObservations(response, log, options = {}) {
1419
1597
  { key: 'declared', header: 'DECLARED', maxWidth: 9 },
1420
1598
  { key: 'access', header: 'ACCESS', maxWidth: 13 },
1421
1599
  { key: 'confidence', header: 'CONF', maxWidth: 6 },
1600
+ { key: 'artifacts', header: 'ARTIFACTS', maxWidth: 36 },
1422
1601
  ], options.emptyMessage || 'No observations recorded.', log);
1423
1602
  }
1424
1603
  /** Human view for causal hypotheses. */
@@ -2,7 +2,29 @@ import { type ResolvedCredential } from '@skrr-ai/auth-core';
2
2
  export declare function isDelegatedCliSession(env?: Record<string, string | undefined>): boolean;
3
3
  /** Drop the memoized credential. Exported for tests. */
4
4
  export declare function resetDelegatedCliCredentialCache(): void;
5
- /** Fetch a full-permission CLI access token from the local daemon. */
5
+ /**
6
+ * The daemon's delegated broker answered and could not mint, for a reason it
7
+ * named (OSK-12151). `retryable` says whether waiting can help: false when only
8
+ * a new daemon sign-in repairs it (DEVICE_PROOF_PIN_MISMATCH), and then
9
+ * `remedy` is the command to run.
10
+ */
11
+ export declare class DelegatedCliUnavailableError extends Error {
12
+ readonly retryable: boolean;
13
+ readonly upstreamStatus?: number;
14
+ readonly upstreamCode?: string;
15
+ readonly upstreamReason?: string;
16
+ /** ISO time before which the daemon will not ask the server again. */
17
+ readonly retryAt?: string;
18
+ readonly remedy?: string;
19
+ constructor(message: string, fields: {
20
+ retryable: boolean;
21
+ upstreamStatus?: number;
22
+ upstreamCode?: string;
23
+ upstreamReason?: string;
24
+ retryAt?: string;
25
+ remedy?: string;
26
+ });
27
+ }
6
28
  export declare function fetchDelegatedCliCredential(env?: Record<string, string | undefined>, options?: {
7
29
  forceRefresh?: boolean;
8
30
  }): Promise<ResolvedCredential | null>;
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.DelegatedCliUnavailableError = void 0;
36
37
  exports.isDelegatedCliSession = isDelegatedCliSession;
37
38
  exports.resetDelegatedCliCredentialCache = resetDelegatedCliCredentialCache;
38
39
  exports.fetchDelegatedCliCredential = fetchDelegatedCliCredential;
@@ -198,6 +199,50 @@ function composeUnreachableBrokerMessage(brokerUrl, err) {
198
199
  (code ? ` (${code})` : ''));
199
200
  }
200
201
  /** Fetch a full-permission CLI access token from the local daemon. */
202
+ /**
203
+ * Upstream codes the SAME daemon credential will keep getting, so retrying is
204
+ * pointless and the repair is signing the daemon in again (OSK-15990). Mirrors
205
+ * the daemon's `CREDENTIAL_BOUND_CODES`; it is the fallback for a daemon that
206
+ * predates the `retryable` field on its 503.
207
+ */
208
+ const SIGN_IN_BOUND_CODES = new Set([
209
+ 'DEVICE_PROOF_PIN_MISMATCH',
210
+ 'CI_TOKEN_REFUSED',
211
+ 'SESSION_REVOKED',
212
+ 'PRINCIPAL_UNRESOLVED',
213
+ 'USER_ID_UNRESOLVED',
214
+ ]);
215
+ function isSignInBoundCode(code) {
216
+ return typeof code === 'string' && SIGN_IN_BOUND_CODES.has(code);
217
+ }
218
+ /**
219
+ * The daemon's delegated broker answered and could not mint, for a reason it
220
+ * named (OSK-12151). `retryable` says whether waiting can help: false when only
221
+ * a new daemon sign-in repairs it (DEVICE_PROOF_PIN_MISMATCH), and then
222
+ * `remedy` is the command to run.
223
+ */
224
+ class DelegatedCliUnavailableError extends Error {
225
+ retryable;
226
+ upstreamStatus;
227
+ upstreamCode;
228
+ upstreamReason;
229
+ /** ISO time before which the daemon will not ask the server again. */
230
+ retryAt;
231
+ remedy;
232
+ constructor(message, fields) {
233
+ super(message);
234
+ this.name = 'DelegatedCliUnavailableError';
235
+ this.retryable = fields.retryable;
236
+ this.upstreamStatus = fields.upstreamStatus;
237
+ this.upstreamCode = fields.upstreamCode;
238
+ this.upstreamReason = fields.upstreamReason;
239
+ this.retryAt = fields.retryAt;
240
+ this.remedy =
241
+ fields.remedy ??
242
+ (fields.retryable ? undefined : 'run `skrr daemon login` on the machine running the daemon');
243
+ }
244
+ }
245
+ exports.DelegatedCliUnavailableError = DelegatedCliUnavailableError;
201
246
  async function fetchDelegatedCliCredential(env = process.env, options = {}) {
202
247
  if (!isDelegatedCliSession(env))
203
248
  return null;
@@ -270,11 +315,23 @@ async function fetchDelegatedCliCredential(env = process.env, options = {}) {
270
315
  ]
271
316
  .filter(Boolean)
272
317
  .join(' ');
273
- // The daemon's `message` already names the refusal; fall back to the
274
- // status and code when an older daemon sent none.
275
- throw new Error(typeof body?.message === 'string' && body.message
318
+ // The daemon's `message` already names the refusal (and, from a
319
+ // daemon with OSK-16117, when to retry or what to run); fall back to
320
+ // the status and code when an older daemon sent none.
321
+ throw new DelegatedCliUnavailableError(typeof body?.message === 'string' && body.message
276
322
  ? `the daemon's delegated CLI broker could not mint a credential: ${body.message}`
277
- : `the skrr server refused the daemon's request for this session's credential (${named}).`);
323
+ : `the skrr server refused the daemon's request for this session's credential (${named}).`, {
324
+ ...(typeof body?.upstreamStatus === 'number' ? { upstreamStatus: body.upstreamStatus } : {}),
325
+ ...(typeof body?.upstreamCode === 'string' ? { upstreamCode: body.upstreamCode } : {}),
326
+ ...(typeof body?.upstreamReason === 'string'
327
+ ? { upstreamReason: body.upstreamReason }
328
+ : {}),
329
+ ...(typeof body?.retryAt === 'string' ? { retryAt: body.retryAt } : {}),
330
+ ...(typeof body?.remedy === 'string' ? { remedy: body.remedy } : {}),
331
+ // Only an explicit `false` from the daemon makes this terminal; an
332
+ // older daemon sends nothing and keeps today's retryable answer.
333
+ retryable: !(body?.retryable === false || isSignInBoundCode(body?.upstreamCode)),
334
+ });
278
335
  }
279
336
  if (res.status === 503 || body?.code === 'DELEGATED_CLI_CREDENTIAL_UNAVAILABLE') {
280
337
  throw new Error(`the daemon's delegated CLI broker is alive but cannot mint a credential ` +
@@ -139,7 +139,7 @@ export declare function resolveEngine(env?: NodeJS.ProcessEnv): EngineResolution
139
139
  * Build the engine argv for a `skrr code` invocation.
140
140
  *
141
141
  * Exported and pure so the forwarding contract is unit-testable without an
142
- * oclif runtime or a real binary — the convention `buildOverskyLogsArgs` uses.
142
+ * oclif runtime or a real binary — the convention `buildDaemonLogsArgs` uses.
143
143
  *
144
144
  * `subcommand === null` means bare `skrr code`, which launches the interactive
145
145
  * TUI: the engine takes no subcommand for that, and passing one would put it
@@ -266,7 +266,7 @@ function resolveEngine(env = process.env) {
266
266
  * Build the engine argv for a `skrr code` invocation.
267
267
  *
268
268
  * Exported and pure so the forwarding contract is unit-testable without an
269
- * oclif runtime or a real binary — the convention `buildOverskyLogsArgs` uses.
269
+ * oclif runtime or a real binary — the convention `buildDaemonLogsArgs` uses.
270
270
  *
271
271
  * `subcommand === null` means bare `skrr code`, which launches the interactive
272
272
  * TUI: the engine takes no subcommand for that, and passing one would put it
@@ -1,4 +1,17 @@
1
1
  import type { SessionTaskOperation } from './session-task-endpoints';
2
+ /**
3
+ * The server's refusal when a session's run carries no Task at all — a
4
+ * Commitment, Routine or other autonomous run that is not a Task run. It is
5
+ * the ONE refusal that means "the session-scoped route does not apply here",
6
+ * not "you may not write": such a run legitimately writes the Tasks it created
7
+ * or was handed, through the generic `/api/tasks/:id/*` routes, where
8
+ * `resolveTaskActor` attributes the Agent from the run headers and the Task's
9
+ * own ACL decides. Every other refusal (`session_claim_*`, `task_edit_forbidden`,
10
+ * an unavailable claim check) still fails closed.
11
+ */
12
+ export declare const SESSION_HAS_NO_TASK = "session_has_no_task";
13
+ /** Test hook: forget what this process learned about bound Tasks. */
14
+ export declare function resetSessionTaskWriteCache(): void;
2
15
  /**
3
16
  * A canonical Task write must retain its authenticated session actor even if
4
17
  * the daemon also grants the CLI a wide credential. The wide credential names
@@ -1,9 +1,55 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SESSION_HAS_NO_TASK = void 0;
4
+ exports.resetSessionTaskWriteCache = resetSessionTaskWriteCache;
3
5
  exports.sessionTaskWrite = sessionTaskWrite;
4
6
  const api_fetch_1 = require("./api-fetch");
5
7
  const credential_resolver_1 = require("./credential-resolver");
6
8
  const session_task_endpoints_1 = require("./session-task-endpoints");
9
+ /**
10
+ * The server's refusal when a session's run carries no Task at all — a
11
+ * Commitment, Routine or other autonomous run that is not a Task run. It is
12
+ * the ONE refusal that means "the session-scoped route does not apply here",
13
+ * not "you may not write": such a run legitimately writes the Tasks it created
14
+ * or was handed, through the generic `/api/tasks/:id/*` routes, where
15
+ * `resolveTaskActor` attributes the Agent from the run headers and the Task's
16
+ * own ACL decides. Every other refusal (`session_claim_*`, `task_edit_forbidden`,
17
+ * an unavailable claim check) still fails closed.
18
+ */
19
+ exports.SESSION_HAS_NO_TASK = 'session_has_no_task';
20
+ /**
21
+ * Sessions this process has already learned have no bound Task. A run's Task
22
+ * binding is fixed when the run is admitted, so the answer cannot change for
23
+ * the life of the session; remembering it saves a refused GET on every write.
24
+ */
25
+ const sessionsWithoutBoundTask = new Set();
26
+ /** Test hook: forget what this process learned about bound Tasks. */
27
+ function resetSessionTaskWriteCache() {
28
+ sessionsWithoutBoundTask.clear();
29
+ }
30
+ /** The refusal reason of a 403 `ApiFetchError` (`{ ok:false, reason }` body). */
31
+ function refusalReason(err) {
32
+ if (!err || typeof err !== 'object')
33
+ return undefined;
34
+ const { status, code, body } = err;
35
+ if (status !== 403)
36
+ return undefined;
37
+ if (typeof code === 'string' && code)
38
+ return code;
39
+ if (typeof body !== 'string' || !body)
40
+ return undefined;
41
+ try {
42
+ const parsed = JSON.parse(body);
43
+ if (typeof parsed.reason === 'string')
44
+ return parsed.reason;
45
+ if (typeof parsed.code === 'string')
46
+ return parsed.code;
47
+ }
48
+ catch {
49
+ /* non-JSON body */
50
+ }
51
+ return undefined;
52
+ }
7
53
  /**
8
54
  * A canonical Task write must retain its authenticated session actor even if
9
55
  * the daemon also grants the CLI a wide credential. The wide credential names
@@ -21,14 +67,28 @@ async function sessionTaskWrite(operation, requestedTask, env = process.env) {
21
67
  code: 'SESSION_TASK_CREDENTIAL_MISSING',
22
68
  });
23
69
  }
70
+ // A run with no bound Task (e.g. a Commitment run) has no session-scoped
71
+ // Task surface; its writes take the generic routes (OSK-16124).
72
+ if (sessionsWithoutBoundTask.has(sessionId))
73
+ return null;
24
74
  const prefix = `/api/agentic/sessions/${encodeURIComponent(sessionId)}`;
25
75
  // A scoped endpoint derives its Task and ignores a caller's Task id. Check
26
76
  // the requested coordinate first, so naming a sibling cannot quietly finish
27
77
  // the bound Task instead. The server independently rechecks the live binding
28
78
  // and checkout when committing the write.
29
- const bound = await (0, api_fetch_1.apiFetch)(`${prefix}/task`, {
30
- credential,
31
- });
79
+ let bound;
80
+ try {
81
+ bound = await (0, api_fetch_1.apiFetch)(`${prefix}/task`, {
82
+ credential,
83
+ });
84
+ }
85
+ catch (err) {
86
+ if (refusalReason(err) === exports.SESSION_HAS_NO_TASK) {
87
+ sessionsWithoutBoundTask.add(sessionId);
88
+ return null;
89
+ }
90
+ throw err;
91
+ }
32
92
  if (!bound.id || !(0, session_task_endpoints_1.requestMatchesBoundTask)(bound, requestedTask)) {
33
93
  throw Object.assign(new Error(`This session is bound to task ${bound.identifier || bound.id || '?'} and cannot write the requested Task.`), {
34
94
  code: 'SESSION_SCOPE_TASK_MISMATCH',
@@ -244,6 +244,8 @@ export declare function buildOnceConfigFromFlags(flags: Record<string, unknown>,
244
244
  allowNextRunAtAlias?: boolean;
245
245
  }): Record<string, unknown>;
246
246
  export declare function buildSelfScheduledConfigFromFlags(flags: Record<string, unknown>): Record<string, unknown>;
247
+ export declare function buildEventConfigFromFlags(flags: Record<string, unknown>, filterValues: string[] | undefined): Record<string, unknown>;
248
+ export declare function buildStochasticConfigFromFlags(flags: Record<string, unknown>): Record<string, unknown>;
247
249
  export declare function buildFormConfigFromFlags(flags: Record<string, unknown>): Record<string, unknown>;
248
250
  export declare function buildChatMessageConfigFromFlags(flags: Record<string, unknown>): Record<string, unknown>;
249
251
  export declare function parseExecutionReferences(referenceValues: string[] | undefined, referencesJson: string | undefined): Array<Record<string, unknown>> | undefined;
@@ -269,6 +271,30 @@ export declare function setDefined(target: Record<string, unknown>, values: Reco
269
271
  export declare function mergeObjectField(target: Record<string, unknown>, key: string, values: Record<string, unknown> | undefined): void;
270
272
  /** Attach only the discriminator subdocument owned by `kind`. */
271
273
  export declare function mergeTriggerKindConfig(target: Record<string, unknown>, kind: unknown, configs: Record<string, Record<string, unknown> | undefined>): void;
274
+ /**
275
+ * The kinds whose config block each kind-specific flag fills. A flag that is not listed
276
+ * (--label, --mode, --prompt, --concurrency-key …) belongs to every kind.
277
+ *
278
+ * `mergeTriggerKindConfig` keeps only the block the chosen kind owns, so a flag for another
279
+ * kind used to be accepted and thrown away: `--max-fires-per-day 1` on an on_demand trigger
280
+ * stored no cap, `preflight` said `no_cap`, and the second fire still ran (OSK-15491).
281
+ * Declared beside the builders above, which are what read these flags.
282
+ */
283
+ export declare const FLAG_OWNER_KINDS: Readonly<Record<string, readonly string[]>>;
284
+ /**
285
+ * Refuse a kind-specific flag the trigger's kind cannot use, naming each one and the kinds that
286
+ * take it. Silence here is the defect: the caller believes a limit is in force.
287
+ */
288
+ export declare function assertKindFlagsApply(kind: unknown, flags: Record<string, unknown>, options?: {
289
+ kindIsFixed?: boolean;
290
+ }): void;
291
+ /**
292
+ * The sentence for a fire's `scheduledFire` report: what a MANUAL fire skipped that a scheduled
293
+ * one would not. A manual fire is exempt from the enabled gate, the working/quiet-hours overlay and
294
+ * the autonomy ladder, so on its own it certifies nothing about whether the trigger will ever fire
295
+ * by itself (OSK-15492). Absent when the server reported nothing to say.
296
+ */
297
+ export declare function describeScheduledFire(result: Record<string, unknown>): string | undefined;
272
298
  export declare function withQuery(path: string, params: Record<string, string | number | boolean | string[] | undefined>): string;
273
299
  /**
274
300
  * Build the universal-engine scope block for an agent-owned trigger.