@skrr-ai/cli 0.1.101 → 0.1.103

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 (91) 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/control/apply.d.ts +15 -0
  6. package/dist/commands/commitments/control/apply.js +36 -0
  7. package/dist/commands/commitments/control/preview.d.ts +17 -0
  8. package/dist/commands/commitments/control/preview.js +43 -0
  9. package/dist/commands/commitments/control/show.d.ts +12 -0
  10. package/dist/commands/commitments/control/show.js +29 -0
  11. package/dist/commands/commitments/create.js +5 -2
  12. package/dist/commands/commitments/observation/probe.d.ts +15 -0
  13. package/dist/commands/commitments/observation/probe.js +46 -0
  14. package/dist/commands/commitments/observation/summary.d.ts +9 -0
  15. package/dist/commands/commitments/observation/summary.js +31 -0
  16. package/dist/commands/commitments/preflight.js +18 -1
  17. package/dist/commands/commitments/readiness/show.d.ts +12 -0
  18. package/dist/commands/commitments/readiness/show.js +29 -0
  19. package/dist/commands/commitments/readiness/verify.d.ts +13 -0
  20. package/dist/commands/commitments/readiness/verify.js +33 -0
  21. package/dist/commands/commitments/roster.d.ts +15 -0
  22. package/dist/commands/commitments/roster.js +39 -0
  23. package/dist/commands/commitments/show.js +1 -1
  24. package/dist/commands/commitments/status.d.ts +17 -0
  25. package/dist/commands/commitments/status.js +41 -0
  26. package/dist/commands/commitments/update.js +23 -9
  27. package/dist/commands/commitments/work-plan.d.ts +11 -0
  28. package/dist/commands/commitments/work-plan.js +36 -0
  29. package/dist/commands/compass/effective-policy.d.ts +12 -0
  30. package/dist/commands/compass/effective-policy.js +34 -0
  31. package/dist/commands/compass/update.d.ts +12 -0
  32. package/dist/commands/compass/update.js +27 -0
  33. package/dist/commands/daemon/logs.d.ts +39 -1
  34. package/dist/commands/daemon/logs.js +140 -13
  35. package/dist/commands/followups/checkin.js +46 -3
  36. package/dist/commands/machines/dedicated/exec.js +3 -1
  37. package/dist/commands/proposals/discard.d.ts +4 -0
  38. package/dist/commands/proposals/discard.js +3 -0
  39. package/dist/commands/proposals/keep.d.ts +4 -0
  40. package/dist/commands/proposals/keep.js +3 -0
  41. package/dist/commands/proposals/snooze.d.ts +4 -0
  42. package/dist/commands/proposals/snooze.js +3 -0
  43. package/dist/commands/spaces/workflows/cancel-node.d.ts +16 -0
  44. package/dist/commands/spaces/workflows/cancel-node.js +44 -0
  45. package/dist/commands/spaces/workflows/cancel.d.ts +40 -0
  46. package/dist/commands/spaces/workflows/cancel.js +109 -3
  47. package/dist/commands/spaces/workflows/run.d.ts +9 -0
  48. package/dist/commands/spaces/workflows/run.js +38 -0
  49. package/dist/commands/tasks/approvals/resolve.d.ts +12 -0
  50. package/dist/commands/tasks/approvals/resolve.js +21 -5
  51. package/dist/commands/tasks/schedule.js +1 -1
  52. package/dist/commands/triggers/capabilities.js +7 -0
  53. package/dist/commands/triggers/create.js +12 -18
  54. package/dist/commands/triggers/fire.js +3 -0
  55. package/dist/commands/triggers/update.js +41 -27
  56. package/dist/commands/widgets/check.d.ts +39 -0
  57. package/dist/commands/widgets/check.js +179 -0
  58. package/dist/lib/agent-mailbox.d.ts +0 -1
  59. package/dist/lib/agent-mailbox.js +0 -1
  60. package/dist/lib/commitment-loop-status.d.ts +16 -0
  61. package/dist/lib/commitment-loop-status.js +164 -0
  62. package/dist/lib/commitments.d.ts +47 -1
  63. package/dist/lib/commitments.js +218 -6
  64. package/dist/lib/compasses.d.ts +3 -0
  65. package/dist/lib/compasses.js +1 -0
  66. package/dist/lib/delegated-cli.d.ts +23 -1
  67. package/dist/lib/delegated-cli.js +61 -4
  68. package/dist/lib/first-party-harness.d.ts +1 -1
  69. package/dist/lib/first-party-harness.js +1 -1
  70. package/dist/lib/responsibility-controls.d.ts +11 -0
  71. package/dist/lib/responsibility-controls.js +119 -0
  72. package/dist/lib/session-task-write.d.ts +13 -0
  73. package/dist/lib/session-task-write.js +63 -3
  74. package/dist/lib/stewardship-output.js +13 -2
  75. package/dist/lib/triggers.d.ts +26 -0
  76. package/dist/lib/triggers.js +134 -1
  77. package/dist/lib/widgetCheck.d.ts +123 -0
  78. package/dist/lib/widgetCheck.js +998 -0
  79. package/dist/lib/widgetStaticScan.d.ts +111 -0
  80. package/dist/lib/widgetStaticScan.js +833 -0
  81. package/dist/lib/workflow-node-control.d.ts +10 -0
  82. package/dist/lib/workflow-node-control.js +76 -0
  83. package/dist/lib/workspaces.d.ts +2 -0
  84. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.d.ts +9 -1
  85. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +12 -0
  86. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.d.ts +9 -1
  87. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +12 -0
  88. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  89. package/dist/node_modules/@skrr-ai/data-provider/index.js +11924 -8704
  90. package/oclif.manifest.json +1487 -396
  91. package/package.json +11 -2
@@ -9,6 +9,7 @@ exports.parseCommitmentConcurrencyLimit = parseCommitmentConcurrencyLimit;
9
9
  exports.commitmentConcurrencyEnvelopePatch = commitmentConcurrencyEnvelopePatch;
10
10
  exports.describeCommitmentConcurrency = describeCommitmentConcurrency;
11
11
  exports.parseIntervalToMs = parseIntervalToMs;
12
+ exports.normalizeWatchedSourceDeclarations = normalizeWatchedSourceDeclarations;
12
13
  exports.buildWatchedSources = buildWatchedSources;
13
14
  exports.describeEvidenceHealth = describeEvidenceHealth;
14
15
  exports.describeIntervalMs = describeIntervalMs;
@@ -17,14 +18,17 @@ exports.cadenceLabel = cadenceLabel;
17
18
  exports.renderCommitmentList = renderCommitmentList;
18
19
  exports.extractStandstillFromCheck = extractStandstillFromCheck;
19
20
  exports.describeExecutorBlocked = describeExecutorBlocked;
21
+ exports.withLiveAssessment = withLiveAssessment;
20
22
  exports.renderCommitmentSummary = renderCommitmentSummary;
21
23
  exports.describeWakeDecision = describeWakeDecision;
22
24
  exports.renderCheckList = renderCheckList;
23
25
  exports.renderCommitmentLedger = renderCommitmentLedger;
24
26
  exports.renderCommitmentTrace = renderCommitmentTrace;
27
+ exports.renderProactivenessScorecard = renderProactivenessScorecard;
25
28
  exports.formatCommitmentCycles = formatCommitmentCycles;
26
29
  exports.formatCommitmentExplanation = formatCommitmentExplanation;
27
30
  exports.renderListPageHint = renderListPageHint;
31
+ exports.describeObservationArtifacts = describeObservationArtifacts;
28
32
  exports.renderCommitmentObservations = renderCommitmentObservations;
29
33
  exports.renderCommitmentHypotheses = renderCommitmentHypotheses;
30
34
  exports.renderCommitmentLineage = renderCommitmentLineage;
@@ -33,6 +37,7 @@ exports.renderCommitmentCases = renderCommitmentCases;
33
37
  exports.renderCommitmentCaseDetail = renderCommitmentCaseDetail;
34
38
  exports.renderCommitmentRemediationProposals = renderCommitmentRemediationProposals;
35
39
  exports.renderCommitmentRemediationProposal = renderCommitmentRemediationProposal;
40
+ const node_util_1 = require("node:util");
36
41
  const data_provider_1 = require("@skrr-ai/data-provider");
37
42
  /**
38
43
  * commitments.ts — request-based client for the Commitments subsystem
@@ -132,12 +137,25 @@ exports.commitmentApi = {
132
137
  * swept, how stale that is, and the tally it last persisted. Aggregate only.
133
138
  */
134
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`),
135
144
  manifest: (id) => data_provider_2.request.get(`${base(id)}/manifest`),
136
145
  import: (body) => data_provider_2.request.post('/api/commitments/import', body),
137
146
  fork: (id, body) => data_provider_2.request.post(`${base(id)}/fork`, body),
138
147
  // GET /:id returns `{ commitment, latestCheck }` — NOT a bare contract.
139
148
  // Unwrap before rendering, or every field reads as undefined.
140
149
  get: (id) => data_provider_2.request.get(base(id)),
150
+ controlPreview: (id, query) => data_provider_2.request.get((0, triggers_1.withQuery)(`${base(id)}/control-preview`, query)),
151
+ applyControl: (id, body) => data_provider_2.request.post(`${base(id)}/control`, body),
152
+ controlReceipt: (id, controlId) => data_provider_2.request.get(`${base(id)}/control/${encodeURIComponent(controlId)}`),
153
+ workPlan: (id) => data_provider_2.request.get(`${base(id)}/work-plan`),
154
+ observationSummary: (query = {}) => data_provider_2.request.get((0, triggers_1.withQuery)('/api/commitments/observation-summary', query)),
155
+ draftObservationProbe: (body) => data_provider_2.request.post('/api/commitments/observation-probe', body),
156
+ observationProbe: (id, body = {}) => data_provider_2.request.post(`${base(id)}/observation-probe`, body),
157
+ verifyReadiness: (id, body) => data_provider_2.request.post(`${base(id)}/readiness-receipt`, body),
158
+ readinessReceipt: (id, receiptId) => data_provider_2.request.get(`${base(id)}/readiness-receipt/${encodeURIComponent(receiptId)}`),
141
159
  create: (body) => data_provider_2.request.post('/api/commitments', body),
142
160
  update: (id, body) => data_provider_2.request.patch(base(id), body),
143
161
  draftPreflight: (body) => data_provider_2.request.post('/api/commitments/preflight', body),
@@ -496,11 +514,35 @@ function parseIntervalToMs(input) {
496
514
  }
497
515
  return ms;
498
516
  }
517
+ /** Keep the canonical one-binding-per-source contract without discarding conflicting intent. */
518
+ function normalizeWatchedSourceDeclarations(declarations) {
519
+ const bySource = new Map();
520
+ const normalized = [];
521
+ for (const binding of declarations) {
522
+ if (!binding ||
523
+ typeof binding !== 'object' ||
524
+ !('source' in binding) ||
525
+ typeof binding.source !== 'string') {
526
+ normalized.push(binding); // The API owns validation of the remaining DTO.
527
+ continue;
528
+ }
529
+ if (bySource.has(binding.source)) {
530
+ if (!(0, node_util_1.isDeepStrictEqual)(bySource.get(binding.source), binding)) {
531
+ throw new Error(`Watched source "${binding.source}" has conflicting declarations. ` +
532
+ 'Choose one complete binding per source.');
533
+ }
534
+ continue;
535
+ }
536
+ bySource.set(binding.source, binding);
537
+ normalized.push(binding);
538
+ }
539
+ return normalized;
540
+ }
499
541
  function buildWatchedSources(inputs) {
500
542
  const watched = (inputs.watch ?? []).filter(Boolean);
501
543
  const metricKeys = (inputs.metricKey ?? []).filter(Boolean);
502
- const spaceIds = (inputs.spaceIds ?? []).filter(Boolean);
503
- const monitorIds = (inputs.monitorIds ?? []).filter(Boolean);
544
+ const spaceIds = [...new Set((inputs.spaceIds ?? []).filter(Boolean))];
545
+ const monitorIds = [...new Set((inputs.monitorIds ?? []).filter(Boolean))];
504
546
  const githubEvents = (inputs.githubEvents ?? []).filter(Boolean);
505
547
  const githubRepository = String(inputs.githubRepository || '')
506
548
  .trim()
@@ -584,7 +626,7 @@ function buildWatchedSources(inputs) {
584
626
  rows.push({ source, enabled: true });
585
627
  }
586
628
  }
587
- return rows;
629
+ return normalizeWatchedSourceDeclarations(rows);
588
630
  }
589
631
  /**
590
632
  * One line an author can act on, or nothing.
@@ -613,10 +655,33 @@ function describeEvidenceHealth(health, criteriaCount) {
613
655
  return `unevidenceable_criteria${detail} — the criteria below cannot be judged from what this commitment observes; rewrite them or attach a Measure`;
614
656
  }
615
657
  if (state === 'degraded_observability') {
616
- return `degraded_observability${detail} — an evaluator could not run; this is a fault, not a criteria problem`;
658
+ return `degraded_observability${detail} — ${describeDegradedObservability(reasons)}`;
617
659
  }
618
660
  return `${state}${detail}`;
619
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
+ }
620
685
  /** Render an interval back as a compact human string. */
621
686
  function describeIntervalMs(ms) {
622
687
  if (!ms || !Number.isFinite(ms))
@@ -794,6 +859,20 @@ function describeExecutorBlocked(commitment) {
794
859
  : reason || 'blocked';
795
860
  return `${label}${until ? ` until ${until}` : ''}`;
796
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
+ }
797
876
  function renderCommitmentSummary(commitment, log,
798
877
  /**
799
878
  * The most recent check, when the caller has one. Optional because pause /
@@ -1162,10 +1241,103 @@ function renderCommitmentTrace(trace, log) {
1162
1241
  { key: 'stop', header: 'STOPPED AT', maxWidth: 48 },
1163
1242
  { key: 'summary', header: 'SUMMARY', maxWidth: 72 },
1164
1243
  ], 'No trace events recorded.', log);
1244
+ const scorecard = record(trace.proactiveness);
1245
+ if (scorecard)
1246
+ renderProactivenessScorecard(scorecard, log);
1165
1247
  if (trace.has_more === true && trace.next_cursor) {
1166
1248
  log(`More events are available; continue with --cursor ${text(trace.next_cursor)}.`);
1167
1249
  }
1168
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
+ }
1169
1341
  const CYCLE_GLYPH = {
1170
1342
  advanced: '✓',
1171
1343
  stopped: '●',
@@ -1192,7 +1364,12 @@ function cycleWake(cycle) {
1192
1364
  function cycleSummary(cycle) {
1193
1365
  if (cycle.outcome === 'stopped' && cycle.stop) {
1194
1366
  const { question, gate, reason, fix, detail } = cycle.stop;
1195
- 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}`;
1196
1373
  }
1197
1374
  if (cycle.outcome === 'advanced') {
1198
1375
  return cycle.advancementKinds?.length
@@ -1288,6 +1465,8 @@ function formatCycleLatency(cycle) {
1288
1465
  ];
1289
1466
  }
1290
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;
1291
1470
  /** The six questions for the latest Cycle, the first ✗ carrying its fix. */
1292
1471
  function formatCommitmentExplanation(explanation) {
1293
1472
  const cycle = explanation.cycle;
@@ -1299,7 +1478,14 @@ function formatCommitmentExplanation(explanation) {
1299
1478
  if (cycle)
1300
1479
  lines.push(...formatCycleLatency(cycle));
1301
1480
  for (const answer of explanation.answers || []) {
1302
- 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}`);
1303
1489
  if (answer.fix)
1304
1490
  lines.push(` Fix: ${answer.fix}`);
1305
1491
  }
@@ -1343,6 +1529,30 @@ function yesNo(value) {
1343
1529
  return 'no';
1344
1530
  return '-';
1345
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
+ }
1346
1556
  function renderCommitmentObservations(response, log, options = {}) {
1347
1557
  const data = records(response.data);
1348
1558
  renderRows(data.map((item) => ({
@@ -1371,6 +1581,7 @@ function renderCommitmentObservations(response, log, options = {}) {
1371
1581
  declared: yesNo(item.declared),
1372
1582
  access: text(item.accessStatus),
1373
1583
  confidence: (0, format_1.formatCell)(item.confidence),
1584
+ artifacts: describeObservationArtifacts(item.artifacts),
1374
1585
  })), [
1375
1586
  { key: 'at', header: 'OBSERVED', maxWidth: 24 },
1376
1587
  { key: 'recorded', header: 'RECORDED', maxWidth: 24 },
@@ -1386,6 +1597,7 @@ function renderCommitmentObservations(response, log, options = {}) {
1386
1597
  { key: 'declared', header: 'DECLARED', maxWidth: 9 },
1387
1598
  { key: 'access', header: 'ACCESS', maxWidth: 13 },
1388
1599
  { key: 'confidence', header: 'CONF', maxWidth: 6 },
1600
+ { key: 'artifacts', header: 'ARTIFACTS', maxWidth: 36 },
1389
1601
  ], options.emptyMessage || 'No observations recorded.', log);
1390
1602
  }
1391
1603
  /** Human view for causal hypotheses. */
@@ -88,6 +88,9 @@ export declare const compassApi: {
88
88
  }>;
89
89
  create: (body: Record<string, unknown>) => Promise<Compass>;
90
90
  get: (id: string, query: Parameters<typeof withQuery>[1]) => Promise<Compass>;
91
+ effectivePolicy: (id: string, query: {
92
+ agentId: string;
93
+ }) => Promise<Record<string, unknown>>;
91
94
  update: (id: string, body: Record<string, unknown>) => Promise<Compass>;
92
95
  archive: (id: string, query: Parameters<typeof withQuery>[1]) => Promise<{
93
96
  ok: boolean;
@@ -35,6 +35,7 @@ exports.compassApi = {
35
35
  list: (query) => data_provider_2.request.get((0, triggers_1.withQuery)('/api/compasses', query)),
36
36
  create: (body) => data_provider_2.request.post('/api/compasses', body),
37
37
  get: (id, query) => data_provider_2.request.get((0, triggers_1.withQuery)(base(id), query)),
38
+ effectivePolicy: (id, query) => data_provider_2.request.get((0, triggers_1.withQuery)(`${base(id)}/effective-policy`, query)),
38
39
  update: (id, body) => data_provider_2.request.patch(base(id), body),
39
40
  archive: (id, query) => data_provider_2.request.delete((0, triggers_1.withQuery)(base(id), query)),
40
41
  revisions: (id, query) => data_provider_2.request.get((0, triggers_1.withQuery)(`${base(id)}/revisions`, query)),
@@ -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
@@ -0,0 +1,11 @@
1
+ /** Scope/version coordinates are intent. The server re-authorizes every read and write. */
2
+ export declare const RESPONSIBILITY_CONTROL_SCOPES: readonly ["mute_updates", "pause_observation", "cancel_worker", "cancel_episode", "cancel_subtree", "retire"];
3
+ /**
4
+ * Apply only the saved preview, never a newly fetched version or live target set.
5
+ * Presentation and authority fields cannot become request fields. The canonical
6
+ * service validates every attempt pin and rejects stale or enlarged selections.
7
+ */
8
+ export declare function controlFromPreview(commitmentId: string, preview: Record<string, unknown>, idempotencyKey: string, reason?: string): Record<string, unknown>;
9
+ export declare function renderControl(value: Record<string, unknown>, log: (line: string) => void): void;
10
+ export declare function renderReadiness(value: Record<string, unknown>, log: (line: string) => void): void;
11
+ export declare function renderProbe(value: Record<string, unknown>, log: (line: string) => void): void;
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RESPONSIBILITY_CONTROL_SCOPES = void 0;
4
+ exports.controlFromPreview = controlFromPreview;
5
+ exports.renderControl = renderControl;
6
+ exports.renderReadiness = renderReadiness;
7
+ exports.renderProbe = renderProbe;
8
+ /** Scope/version coordinates are intent. The server re-authorizes every read and write. */
9
+ exports.RESPONSIBILITY_CONTROL_SCOPES = [
10
+ 'mute_updates', 'pause_observation', 'cancel_worker', 'cancel_episode', 'cancel_subtree', 'retire',
11
+ ];
12
+ function record(value) {
13
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
14
+ }
15
+ /**
16
+ * Apply only the saved preview, never a newly fetched version or live target set.
17
+ * Presentation and authority fields cannot become request fields. The canonical
18
+ * service validates every attempt pin and rejects stale or enlarged selections.
19
+ */
20
+ function controlFromPreview(commitmentId, preview, idempotencyKey, reason) {
21
+ if (preview.schemaVersion !== 1 || preview.commitmentId !== commitmentId) {
22
+ throw new Error('Use a control preview for this exact Commitment.');
23
+ }
24
+ if (preview.available !== true)
25
+ throw new Error('The reviewed control is unavailable; refresh its preview.');
26
+ if (!exports.RESPONSIBILITY_CONTROL_SCOPES.some((scope) => scope === preview.scope)) {
27
+ throw new Error('The preview has an unsupported control scope.');
28
+ }
29
+ if (typeof preview.expectedVersion !== 'number' || !Number.isSafeInteger(preview.expectedVersion)
30
+ || preview.expectedVersion < 1)
31
+ throw new Error('The preview must pin its Commitment version.');
32
+ if (!idempotencyKey.trim() || idempotencyKey.length > 200)
33
+ throw new Error('Use an idempotency key of 1..200 characters.');
34
+ if (reason !== undefined && reason.length > 1000)
35
+ throw new Error('The control reason must be at most 1000 characters.');
36
+ if (!Array.isArray(preview.targetIds) || !Array.isArray(preview.expectedTargets)
37
+ || preview.targetIds.length > 500 || preview.expectedTargets.length > 500) {
38
+ throw new Error('The preview must contain a bounded, frozen target set.');
39
+ }
40
+ const pinKeys = new Set(['id', 'type', 'generation', 'attempt', 'taskWorkflowGeneration',
41
+ 'workflowPlanGeneration', 'workflowNodeAttempt', 'workflowTaskRunId', 'bindingDigest',
42
+ 'executorAttemptRef', 'taskRunId']);
43
+ const executorKeys = new Set(['kind', 'runId', 'generation', 'attempt']);
44
+ const targetIds = preview.targetIds;
45
+ const pins = preview.expectedTargets;
46
+ if (targetIds.some((id) => typeof id !== 'string' || !id || id.length > 200)
47
+ || new Set(targetIds).size !== targetIds.length
48
+ || pins.some((pin) => !record(pin) || typeof pin.id !== 'string'
49
+ || typeof pin.generation !== 'number' || !Number.isSafeInteger(pin.generation) || pin.generation < 0
50
+ || Object.keys(pin).some((key) => !pinKeys.has(key))
51
+ || (pin.executorAttemptRef != null && (!record(pin.executorAttemptRef)
52
+ || Object.keys(pin.executorAttemptRef).some((key) => !executorKeys.has(key)))))
53
+ || pins.length !== targetIds.length
54
+ || new Set(pins.map((pin) => record(pin) ? pin.id : null)).size !== pins.length
55
+ || pins.some((pin) => !record(pin) || !targetIds.includes(pin.id))) {
56
+ throw new Error('Use the exact target identities and generation pins returned by control preview.');
57
+ }
58
+ if (String(preview.scope).startsWith('cancel_') && pins.length === 0) {
59
+ throw new Error('A cancellation preview must identify the exact worker or episode attempts.');
60
+ }
61
+ return {
62
+ scope: preview.scope, expectedVersion: preview.expectedVersion,
63
+ targetIds, expectedTargets: pins, idempotencyKey,
64
+ ...(reason !== undefined ? { reason } : {}),
65
+ };
66
+ }
67
+ function renderControl(value, log) {
68
+ log(`${value.scope}: ${value.state ?? (value.available === true ? 'available' : 'unavailable')}`);
69
+ if (value.controlId)
70
+ log(`Control receipt: ${value.controlId}`);
71
+ if (value.expectedVersion !== undefined)
72
+ log(`Commitment version: ${value.expectedVersion}`);
73
+ if (value.unavailableReason)
74
+ log(`Reason: ${value.unavailableReason}`);
75
+ for (const key of ['affected', 'remaining']) {
76
+ const rows = value[key];
77
+ if (!Array.isArray(rows))
78
+ continue;
79
+ log(`${key === 'affected' ? 'Affected' : 'Remaining'} work: ${rows.length}`);
80
+ for (const row of rows)
81
+ if (record(row))
82
+ log(` ${row.id}: ${row.status ?? 'unknown'}${row.acknowledgement ? ` (${row.acknowledgement})` : ''}`);
83
+ }
84
+ if (Array.isArray(value.warnings))
85
+ for (const warning of value.warnings)
86
+ log(String(warning));
87
+ }
88
+ function renderReadiness(value, log) {
89
+ const activation = value.object === 'commitment_readiness_activation_receipt';
90
+ log(`${activation ? 'Activation' : 'Readiness'} receipt: ${value.id ?? 'not recorded'}`);
91
+ if (activation) {
92
+ log(`Validated version: ${value.validatedVersion ?? 'unknown'}`);
93
+ log(`Activated version: ${value.activatedVersion ?? 'unknown'}`);
94
+ if (record(value.materialization))
95
+ log(`Trigger materialization: ${value.materialization.status ?? 'unknown'}`);
96
+ log(`Worker observation: ${value.workerObservationConfirmed === true ? 'confirmed' : 'unconfirmed'}`);
97
+ }
98
+ if (record(value.preflight))
99
+ log(`Preflight: ${value.preflight.verdict ?? 'unknown'}`);
100
+ if (record(value.observation)) {
101
+ log(`Observation: ${value.observation.status ?? 'unknown'}`);
102
+ log(`Required source coverage: ${value.observation.requiredCoverageComplete === true ? 'complete' : 'incomplete'}`);
103
+ log(`Worker observation: ${value.observation.workerObservationConfirmed === true ? 'confirmed' : 'unconfirmed'}`);
104
+ }
105
+ if (value.isCurrent !== undefined)
106
+ log(`Current evidence: ${value.isCurrent === true ? 'yes' : 'no'}`);
107
+ if (value.validUntil)
108
+ log(`Evidence valid until: ${value.validUntil}`);
109
+ }
110
+ function renderProbe(value, log) {
111
+ log(`Source observation: ${value.status ?? 'unknown'}`);
112
+ log(`Coverage: ${value.coverageComplete === true ? 'complete' : 'incomplete'}`);
113
+ if (Array.isArray(value.rows))
114
+ for (const row of value.rows) {
115
+ if (record(row))
116
+ log(` ${row.source ?? row.key ?? 'source'}: ${row.status ?? 'unknown'}${row.reason ? ` (${row.reason})` : ''}`);
117
+ }
118
+ log(`Worker observation: ${value.workerObservationConfirmed === true ? 'confirmed' : 'unconfirmed'}`);
119
+ }