@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.
- package/dist/base-command.js +24 -0
- package/dist/commands/agent/message/send.js +6 -2
- package/dist/commands/agents/triggers/create.js +7 -17
- package/dist/commands/agents/triggers/update.js +41 -28
- package/dist/commands/commitments/create.js +2 -0
- package/dist/commands/commitments/preflight.js +18 -1
- package/dist/commands/commitments/roster.d.ts +15 -0
- package/dist/commands/commitments/roster.js +39 -0
- package/dist/commands/commitments/show.js +1 -1
- package/dist/commands/commitments/status.d.ts +17 -0
- package/dist/commands/commitments/status.js +41 -0
- package/dist/commands/commitments/update.js +12 -0
- package/dist/commands/daemon/logs.d.ts +106 -1
- package/dist/commands/daemon/logs.js +311 -15
- package/dist/commands/followups/checkin.js +46 -3
- package/dist/commands/tasks/approvals/resolve.d.ts +12 -0
- package/dist/commands/tasks/approvals/resolve.js +21 -5
- package/dist/commands/triggers/create.js +8 -17
- package/dist/commands/triggers/fire.js +3 -0
- package/dist/commands/triggers/update.js +41 -27
- package/dist/commands/widgets/check.d.ts +39 -0
- package/dist/commands/widgets/check.js +179 -0
- package/dist/lib/agent-mailbox.d.ts +0 -1
- package/dist/lib/agent-mailbox.js +0 -1
- package/dist/lib/commitment-loop-status.d.ts +16 -0
- package/dist/lib/commitment-loop-status.js +164 -0
- package/dist/lib/commitments.d.ts +28 -1
- package/dist/lib/commitments.js +184 -5
- package/dist/lib/delegated-cli.d.ts +23 -1
- package/dist/lib/delegated-cli.js +61 -4
- package/dist/lib/first-party-harness.d.ts +1 -1
- package/dist/lib/first-party-harness.js +1 -1
- package/dist/lib/session-task-write.d.ts +13 -0
- package/dist/lib/session-task-write.js +63 -3
- package/dist/lib/triggers.d.ts +26 -0
- package/dist/lib/triggers.js +134 -1
- package/dist/lib/widgetCheck.d.ts +123 -0
- package/dist/lib/widgetCheck.js +998 -0
- package/dist/lib/widgetStaticScan.d.ts +111 -0
- package/dist/lib/widgetStaticScan.js +833 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.d.ts +9 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +12 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/telemetryRedaction.d.ts +61 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/telemetryRedaction.js +177 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.d.ts +9 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +12 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/telemetryRedaction.d.ts +61 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/telemetryRedaction.js +171 -0
- package/dist/node_modules/@skrr-ai/auth-core/package.json +11 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +11908 -8693
- package/oclif.manifest.json +219 -6
- package/package.json +2 -2
package/dist/lib/commitments.js
CHANGED
|
@@ -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 ||
|
|
516
|
-
|
|
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} —
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
274
|
-
//
|
|
275
|
-
|
|
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 `
|
|
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 `
|
|
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
|
-
|
|
30
|
-
|
|
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',
|
package/dist/lib/triggers.d.ts
CHANGED
|
@@ -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.
|