@skrr-ai/cli 0.1.109 → 0.1.110
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.d.ts +15 -0
- package/dist/base-command.js +64 -17
- package/dist/commands/agents/triggers/create.js +1 -1
- package/dist/commands/agents/triggers/update.js +1 -1
- package/dist/commands/agents/update.d.ts +11 -0
- package/dist/commands/agents/update.js +58 -2
- package/dist/commands/daemon/install.js +17 -1
- package/dist/commands/daemon/login.js +14 -0
- package/dist/commands/harnesses/prune.d.ts +26 -0
- package/dist/commands/harnesses/prune.js +172 -0
- package/dist/commands/login.js +11 -0
- package/dist/commands/spaces/workflows/start.js +3 -1
- package/dist/commands/spaces/workflows/validate.js +12 -2
- package/dist/commands/tasks/activity.js +11 -1
- package/dist/commands/tasks/list.js +6 -2
- package/dist/commands/tasks/rearm-recurrence.js +22 -3
- package/dist/commands/tasks/show.d.ts +8 -0
- package/dist/commands/tasks/show.js +64 -32
- package/dist/commands/tasks/unschedule.js +5 -1
- package/dist/commands/tasks/update.js +11 -1
- package/dist/commands/triggers/audit.js +12 -3
- package/dist/commands/triggers/create.js +1 -1
- package/dist/commands/triggers/fire-detail.d.ts +7 -0
- package/dist/commands/triggers/fire-detail.js +30 -12
- package/dist/commands/triggers/fire.js +13 -3
- package/dist/commands/triggers/parse-intent.js +1 -1
- package/dist/commands/triggers/update.js +1 -1
- package/dist/commands/workspaces/grants.js +1 -4
- package/dist/commands/workspaces/members.js +6 -6
- package/dist/lib/commitment-product.js +5 -2
- package/dist/lib/commitments.d.ts +2 -1
- package/dist/lib/commitments.js +40 -40
- package/dist/lib/daemon-machine-identity-copy.d.ts +14 -0
- package/dist/lib/daemon-machine-identity-copy.js +27 -0
- package/dist/lib/daemon-machine-identity.d.ts +27 -0
- package/dist/lib/daemon-machine-identity.js +61 -0
- package/dist/lib/daemonHandoff.d.ts +19 -1
- package/dist/lib/daemonHandoff.js +59 -0
- package/dist/lib/harnesses.d.ts +71 -0
- package/dist/lib/harnesses.js +70 -0
- package/dist/lib/list-output.d.ts +1 -1
- package/dist/lib/list-output.js +2 -0
- package/dist/lib/profile-machine-identity-guard.js +11 -1
- package/dist/lib/task-economics.d.ts +10 -0
- package/dist/lib/task-economics.js +24 -2
- package/dist/lib/triggers.d.ts +11 -0
- package/dist/lib/triggers.js +43 -1
- package/dist/lib/workflow-run-view.d.ts +4 -0
- package/dist/lib/workflow-run-view.js +5 -0
- package/dist/lib/workspaces.d.ts +24 -7
- package/dist/lib/workspaces.js +14 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +5 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.d.ts +60 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +130 -6
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.d.ts +60 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +128 -6
- package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +4003 -3968
- package/oclif.manifest.json +99 -9
- package/package.json +10 -1
package/dist/lib/commitments.js
CHANGED
|
@@ -958,40 +958,38 @@ function describeOperation(operation, reason) {
|
|
|
958
958
|
};
|
|
959
959
|
return `not running — ${meaning[status] ?? status}`;
|
|
960
960
|
}
|
|
961
|
-
/**
|
|
962
|
-
* What to do about an operation that is not running, from the operating state's
|
|
963
|
-
* first actionable blocker (`withLiveAssessment` carries it onto the view).
|
|
964
|
-
*
|
|
965
|
-
* `show` printed `Operation: blocked — typed_external_blocker` and nothing else,
|
|
966
|
-
* so the one line a reader acts on was a code; the remediation the server
|
|
967
|
-
* already composed sat only in `--json` and `doctor` (OSK-16212). Nothing is
|
|
968
|
-
* printed while the operation is ready or working, where a fix would contradict
|
|
969
|
-
* the line above it.
|
|
970
|
-
*
|
|
971
|
-
* Returns one or two lines. `Fix:` only when the blocker carries an action; a
|
|
972
|
-
* typed blocker an agent returned carries only what happened, which printed
|
|
973
|
-
* under "Fix:" read as a remedy nobody wrote (OSK-16230). That narrative is
|
|
974
|
-
* labelled `Blocker:` instead, and no remedy is made up for it.
|
|
975
|
-
*/
|
|
976
961
|
function describeOperationFix(commitment, operation) {
|
|
977
962
|
if (operation === 'ready' || operation === 'working')
|
|
978
963
|
return [];
|
|
979
964
|
const blocker = commitment.firstActionableBlocker;
|
|
980
|
-
if (!blocker)
|
|
981
|
-
return [];
|
|
982
|
-
const remediation = typeof blocker.remediation === 'string' ? blocker.remediation.trim() : '';
|
|
983
|
-
const detail = typeof blocker.detail === 'string' ? blocker.detail.trim() : '';
|
|
984
|
-
const who = typeof blocker.ownerLabel === 'string' && blocker.ownerLabel
|
|
985
|
-
? blocker.ownerLabel
|
|
986
|
-
: typeof blocker.owner === 'string' && blocker.owner
|
|
987
|
-
? blocker.owner
|
|
988
|
-
: '';
|
|
989
965
|
const lines = [];
|
|
990
|
-
|
|
991
|
-
if (
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
966
|
+
let remediation = '';
|
|
967
|
+
if (blocker) {
|
|
968
|
+
remediation = typeof blocker.remediation === 'string' ? blocker.remediation.trim() : '';
|
|
969
|
+
const detail = typeof blocker.detail === 'string' ? blocker.detail.trim() : '';
|
|
970
|
+
const who = typeof blocker.ownerLabel === 'string' && blocker.ownerLabel
|
|
971
|
+
? blocker.ownerLabel
|
|
972
|
+
: typeof blocker.owner === 'string' && blocker.owner
|
|
973
|
+
? blocker.owner
|
|
974
|
+
: '';
|
|
975
|
+
// The detail is shown only when the fix does not already say it.
|
|
976
|
+
if (detail && detail !== remediation)
|
|
977
|
+
lines.push({ label: 'Blocker', text: `${detail}${!remediation && who ? ` (${who})` : ''}` });
|
|
978
|
+
if (remediation)
|
|
979
|
+
lines.push({ label: 'Fix', text: `${remediation}${who ? ` (${who})` : ''}` });
|
|
980
|
+
}
|
|
981
|
+
// What refused the last Run is a second fact, not an alternative to the
|
|
982
|
+
// blocker: a paused Commitment whose Run was refused needs both a resume and
|
|
983
|
+
// the fix, or resuming only fails again (OSK-15389). The sentence is the
|
|
984
|
+
// server's, derived on read from the failure code; it is printed whenever it
|
|
985
|
+
// exists and says something the blocker's fix does not.
|
|
986
|
+
const liveness = commitment.state?.liveness;
|
|
987
|
+
const lastRunFix = liveness && liveness.current !== false && typeof liveness.diagnosisFix === 'string'
|
|
988
|
+
? liveness.diagnosisFix.trim()
|
|
989
|
+
: '';
|
|
990
|
+
if (lastRunFix && lastRunFix !== remediation) {
|
|
991
|
+
lines.push({ label: lines.length ? 'Last run' : 'Fix', text: lastRunFix });
|
|
992
|
+
}
|
|
995
993
|
return lines;
|
|
996
994
|
}
|
|
997
995
|
/**
|
|
@@ -1293,9 +1291,16 @@ function renderCheckList(checks, log) {
|
|
|
1293
1291
|
// meant re-running the command with `--json` (OSK-6175).
|
|
1294
1292
|
{ key: 'id', header: 'ID', verbatim: true },
|
|
1295
1293
|
], log);
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1294
|
+
// The legend states each distinct reason the server recorded, in the same
|
|
1295
|
+
// words explain and cycles use; it never guesses "compacted" for a Check
|
|
1296
|
+
// whose history is merely missing (OSK-15148).
|
|
1297
|
+
const reasons = [
|
|
1298
|
+
...new Set(checks
|
|
1299
|
+
.map((c) => (0, data_provider_2.describeCommitmentHistoryDetail)(c.historyDetail))
|
|
1300
|
+
.filter((sentence) => Boolean(sentence))),
|
|
1301
|
+
];
|
|
1302
|
+
if (reasons.length > 0)
|
|
1303
|
+
log(`'-' = not retained. ${reasons.join(' ')}`);
|
|
1299
1304
|
}
|
|
1300
1305
|
function records(value) {
|
|
1301
1306
|
return Array.isArray(value)
|
|
@@ -1663,14 +1668,9 @@ function formatCommitmentCycles(page) {
|
|
|
1663
1668
|
*/
|
|
1664
1669
|
function formatCycleLatency(cycle) {
|
|
1665
1670
|
const latency = cycle.latency;
|
|
1666
|
-
|
|
1667
|
-
const
|
|
1668
|
-
|
|
1669
|
-
` Detailed history was compacted${history.at ? ` at ${history.at}` : ''}; available reason and source timestamps were retained.`,
|
|
1670
|
-
]
|
|
1671
|
-
: history?.state === 'unavailable'
|
|
1672
|
-
? [' Detailed history is unavailable; why it is missing was not recorded.']
|
|
1673
|
-
: [];
|
|
1671
|
+
// One sentence for every surface, from the server's own record (OSK-15148).
|
|
1672
|
+
const historySentence = (0, data_provider_2.describeCommitmentHistoryDetail)(cycle.historyDetail);
|
|
1673
|
+
const disclosure = historySentence ? [` ${historySentence}`] : [];
|
|
1674
1674
|
// A hop whose inputs were compacted away without a retained projection is
|
|
1675
1675
|
// "not retained": it may have been measured and shown before (OSK-15148).
|
|
1676
1676
|
const unretained = new Set(latency?.unretainedStages || []);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DaemonMachineIdentitySkew } from './daemon-machine-identity';
|
|
2
|
+
/**
|
|
3
|
+
* What to tell someone whose installed background service would register their
|
|
4
|
+
* profile on the default profile's machine row (OSK-S-440).
|
|
5
|
+
*
|
|
6
|
+
* The consequence is stated before the remedy because the remedy costs something
|
|
7
|
+
* (updating a service) and the consequence is what justifies it: a second
|
|
8
|
+
* profile on the default's row replaces its Daemon and its device-key pin, and
|
|
9
|
+
* the default profile's next refresh is refused until it signs in again.
|
|
10
|
+
*
|
|
11
|
+
* Lives here, apart from the comparison, because this is copy and reads
|
|
12
|
+
* `BRAND.displayName`; the comparison is pure and imports nothing from it.
|
|
13
|
+
*/
|
|
14
|
+
export declare function describeDaemonMachineIdentitySkew(skew: DaemonMachineIdentitySkew): string[];
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.describeDaemonMachineIdentitySkew = describeDaemonMachineIdentitySkew;
|
|
4
|
+
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
5
|
+
/**
|
|
6
|
+
* What to tell someone whose installed background service would register their
|
|
7
|
+
* profile on the default profile's machine row (OSK-S-440).
|
|
8
|
+
*
|
|
9
|
+
* The consequence is stated before the remedy because the remedy costs something
|
|
10
|
+
* (updating a service) and the consequence is what justifies it: a second
|
|
11
|
+
* profile on the default's row replaces its Daemon and its device-key pin, and
|
|
12
|
+
* the default profile's next refresh is refused until it signs in again.
|
|
13
|
+
*
|
|
14
|
+
* Lives here, apart from the comparison, because this is copy and reads
|
|
15
|
+
* `BRAND.displayName`; the comparison is pure and imports nothing from it.
|
|
16
|
+
*/
|
|
17
|
+
function describeDaemonMachineIdentitySkew(skew) {
|
|
18
|
+
const product = data_provider_1.BRAND.displayName;
|
|
19
|
+
const said = skew.reported
|
|
20
|
+
? `it reports machine identity ${skew.reported}, where profile '${skew.profile}' has ${skew.expected}`
|
|
21
|
+
: `it does not report a machine identity at all, so ${product} cannot tell which one it would use`;
|
|
22
|
+
return [
|
|
23
|
+
`The ${product} background service at ${skew.binary} does not support a separate machine identity per profile: ${said}.`,
|
|
24
|
+
`Using it for '${skew.profile}' would register that profile on the default profile's machine and could replace the default profile's device-key pin. Nothing was changed.`,
|
|
25
|
+
`Update the background service (skrr daemon update), or put a current skrrd first on your PATH, then run: skrr login --profile ${skew.profile}`,
|
|
26
|
+
];
|
|
27
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface DaemonMachineIdentitySkew {
|
|
2
|
+
/** The profile being signed in. */
|
|
3
|
+
profile: string;
|
|
4
|
+
/** The machine identity this profile has, as decided by the profile's own record. */
|
|
5
|
+
expected: string;
|
|
6
|
+
/** What the daemon binary says it will send; null when it said nothing. */
|
|
7
|
+
reported: string | null;
|
|
8
|
+
/** The `skrrd` that was asked. */
|
|
9
|
+
binary: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Null when the daemon will register this profile under the identity it has;
|
|
13
|
+
* otherwise what disagrees.
|
|
14
|
+
*
|
|
15
|
+
* Not applicable to the default profile (its identity is the hardware value in
|
|
16
|
+
* every version), to a hosted runtime (the control plane issues it), or when no
|
|
17
|
+
* profile is named.
|
|
18
|
+
*
|
|
19
|
+
* A daemon that reports NOTHING is a skew, not a pass: this check exists because
|
|
20
|
+
* an older binary cannot say, and "could not confirm" must not read as "fine" on
|
|
21
|
+
* the step that binds a credential to a machine row.
|
|
22
|
+
*/
|
|
23
|
+
export declare function daemonMachineIdentitySkew(opts: {
|
|
24
|
+
profile: string | null | undefined;
|
|
25
|
+
binary: string;
|
|
26
|
+
reported: string | null | undefined;
|
|
27
|
+
}): DaemonMachineIdentitySkew | null;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.daemonMachineIdentitySkew = daemonMachineIdentitySkew;
|
|
4
|
+
/**
|
|
5
|
+
* daemon-machine-identity.ts — does the installed daemon give this profile the
|
|
6
|
+
* machine identity the CLI decided it has?
|
|
7
|
+
*
|
|
8
|
+
* Two programs compute one identity: the CLI (and the profile's persisted
|
|
9
|
+
* `machine-identity.json`) say a profile that started new has its own machine
|
|
10
|
+
* row, and the DAEMON is what registers. A daemon that predates per-profile
|
|
11
|
+
* identities (OSK-13845) does not read that file; it sends the hardware value
|
|
12
|
+
* whatever the profile is. Nothing noticed. The CLI minted the daemon's credential
|
|
13
|
+
* for what the daemon REPORTED (`accept-handoff --print-id`), so the credential
|
|
14
|
+
* was bound to the default machine's row, the service registered there, and the
|
|
15
|
+
* failure OSK-13845 exists to prevent happened anyway: two profiles on one Daemon
|
|
16
|
+
* row and one device-key pin, the second profile's interactive login re-pinning
|
|
17
|
+
* the first one's key (OSK-S-440, profile `aapexp`).
|
|
18
|
+
*
|
|
19
|
+
* Measured, not inferred: the same new profile, in an empty home, answered by
|
|
20
|
+
* two `skrrd` binaries. The current one reports `<hardware>:p-<hash>` and writes
|
|
21
|
+
* `machine-identity.json` and `daemon-identity.json`; the build that was first on
|
|
22
|
+
* the dogfooder's PATH (a checkout `dist` from before OSK-13845, whose version
|
|
23
|
+
* string still reads the checkout's package.json) reports the bare hardware value
|
|
24
|
+
* and writes neither. The profile it left behind had a `machine-identity.json`
|
|
25
|
+
* (the CLI's) and no `daemon-identity.json` after four daemon starts.
|
|
26
|
+
*
|
|
27
|
+
* The fix is to compare, not to guess a version: ask the binary what it will
|
|
28
|
+
* send, compare with what this profile's decided identity is, and refuse BEFORE
|
|
29
|
+
* anything is minted or installed when they differ. Comparing the value is the
|
|
30
|
+
* only test that cannot be wrong about a build that is older than its own
|
|
31
|
+
* version string says.
|
|
32
|
+
*
|
|
33
|
+
* Pure: the I/O (spawning the binary) is in `daemonHandoff.ts`, the copy in
|
|
34
|
+
* `daemon-machine-identity-copy.ts`.
|
|
35
|
+
*/
|
|
36
|
+
const machine_uuid_1 = require("@skrr-ai/auth-core/machine-uuid");
|
|
37
|
+
const keychain_1 = require("./keychain");
|
|
38
|
+
/**
|
|
39
|
+
* Null when the daemon will register this profile under the identity it has;
|
|
40
|
+
* otherwise what disagrees.
|
|
41
|
+
*
|
|
42
|
+
* Not applicable to the default profile (its identity is the hardware value in
|
|
43
|
+
* every version), to a hosted runtime (the control plane issues it), or when no
|
|
44
|
+
* profile is named.
|
|
45
|
+
*
|
|
46
|
+
* A daemon that reports NOTHING is a skew, not a pass: this check exists because
|
|
47
|
+
* an older binary cannot say, and "could not confirm" must not read as "fine" on
|
|
48
|
+
* the step that binds a credential to a machine row.
|
|
49
|
+
*/
|
|
50
|
+
function daemonMachineIdentitySkew(opts) {
|
|
51
|
+
const profile = opts.profile?.trim();
|
|
52
|
+
if (!profile || profile === keychain_1.DEFAULT_PROFILE)
|
|
53
|
+
return null;
|
|
54
|
+
if ((0, machine_uuid_1.hostedRuntimeIdOverride)('OVERSKY_MACHINE_UUID'))
|
|
55
|
+
return null;
|
|
56
|
+
const expected = (0, machine_uuid_1.getMachineUuidForProfile)(profile);
|
|
57
|
+
const reported = opts.reported?.trim() || null;
|
|
58
|
+
if (reported === expected)
|
|
59
|
+
return null;
|
|
60
|
+
return { profile, expected, reported, binary: opts.binary };
|
|
61
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type DaemonMachineIdentitySkew } from './daemon-machine-identity';
|
|
1
2
|
/**
|
|
2
3
|
* Hand the local daemon a credential of its own, right after `skrr login`.
|
|
3
4
|
*
|
|
@@ -40,8 +41,25 @@ export interface DaemonHandoffOutcome {
|
|
|
40
41
|
* The two absent cases are separated because they need different work — one
|
|
41
42
|
* needs a binary fetched, the other only needs the service registered.
|
|
42
43
|
*/
|
|
43
|
-
reason?: 'opted-out' | 'brokered' | 'no-binary' | 'legacy-binary' | 'unsupported-daemon' | 'no-service' | 'foreign-root' | 'mint-refused' | 'platform-managed';
|
|
44
|
+
reason?: 'opted-out' | 'brokered' | 'no-binary' | 'legacy-binary' | 'unsupported-daemon' | 'no-service' | 'foreign-root' | 'mint-refused' | 'platform-managed' | 'machine-identity-skew';
|
|
45
|
+
/** Set with `reason: 'machine-identity-skew'`; the caller words it. */
|
|
46
|
+
skew?: DaemonMachineIdentitySkew;
|
|
44
47
|
}
|
|
45
48
|
export declare function handOffToLocalDaemon(env?: NodeJS.ProcessEnv, opts?: {
|
|
46
49
|
loginFlow?: string;
|
|
47
50
|
}): Promise<DaemonHandoffOutcome>;
|
|
51
|
+
/**
|
|
52
|
+
* Ask the installed daemon which machine identity it will register `profile`
|
|
53
|
+
* under, and say what disagrees with the profile's own (OSK-S-440).
|
|
54
|
+
*
|
|
55
|
+
* For the commands that install or sign in a daemon WITHOUT going through the
|
|
56
|
+
* credential hand-off (`skrr daemon install`, `skrr daemon login`): the same
|
|
57
|
+
* question, asked of the binary they are about to run. Null when the daemon
|
|
58
|
+
* agrees, when the profile is the default one, or when there is no binary to ask
|
|
59
|
+
* (the caller has its own answer for that).
|
|
60
|
+
*
|
|
61
|
+
* A binary that cannot be asked at all - too old to have `accept-handoff`, or
|
|
62
|
+
* one that fails the probe - is reported as a skew with no identity, because the
|
|
63
|
+
* point of asking is not to bind a profile to a machine row on a guess.
|
|
64
|
+
*/
|
|
65
|
+
export declare function checkLocalDaemonMachineIdentity(profile: string | null | undefined, binary?: string | null): Promise<DaemonMachineIdentitySkew | null>;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.handOffToLocalDaemon = handOffToLocalDaemon;
|
|
4
|
+
exports.checkLocalDaemonMachineIdentity = checkLocalDaemonMachineIdentity;
|
|
4
5
|
const node_child_process_1 = require("node:child_process");
|
|
5
6
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
6
7
|
const config_1 = require("./config");
|
|
7
8
|
const daemon_handoff_opt_out_1 = require("./daemon-handoff-opt-out");
|
|
9
|
+
const daemon_machine_identity_1 = require("./daemon-machine-identity");
|
|
8
10
|
const dedicated_service_1 = require("./dedicated-service");
|
|
9
11
|
const exec_runtime_binary_1 = require("./exec-runtime-binary");
|
|
10
12
|
const keychain_1 = require("./keychain");
|
|
@@ -275,6 +277,26 @@ async function handOffToLocalDaemon(env = process.env, opts = {}) {
|
|
|
275
277
|
};
|
|
276
278
|
}
|
|
277
279
|
const { idSource, machineUuid, daemonServer } = parseIdProvenance(printed.stderr);
|
|
280
|
+
// Before anything is minted or installed: does THIS binary register the profile
|
|
281
|
+
// under the machine identity the profile has? The mint below binds the
|
|
282
|
+
// credential to the machine uuid the daemon just REPORTED, so a binary that
|
|
283
|
+
// reports the hardware value for a profile that has its own would put it on the
|
|
284
|
+
// default profile's row - and the install that follows would run that same
|
|
285
|
+
// binary as a service. Nothing about the id or the provenance would look wrong.
|
|
286
|
+
const skew = (0, daemon_machine_identity_1.daemonMachineIdentitySkew)({
|
|
287
|
+
profile: (0, keychain_1.getActiveProfile)(),
|
|
288
|
+
binary,
|
|
289
|
+
reported: machineUuid,
|
|
290
|
+
});
|
|
291
|
+
if (skew) {
|
|
292
|
+
return {
|
|
293
|
+
status: 'failed',
|
|
294
|
+
reason: 'machine-identity-skew',
|
|
295
|
+
skew,
|
|
296
|
+
detail: `the installed daemon reports machine identity ${skew.reported ?? '(none)'} ` +
|
|
297
|
+
`where profile '${skew.profile}' has ${skew.expected}`,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
278
300
|
// A well-formed id is not the same as a real daemon. With no service installed
|
|
279
301
|
// the daemon derives one from whatever directory this process is in, so the id
|
|
280
302
|
// above names a daemon that does not exist and will not exist under that id if
|
|
@@ -354,3 +376,40 @@ async function handOffToLocalDaemon(env = process.env, opts = {}) {
|
|
|
354
376
|
}),
|
|
355
377
|
};
|
|
356
378
|
}
|
|
379
|
+
/**
|
|
380
|
+
* Ask the installed daemon which machine identity it will register `profile`
|
|
381
|
+
* under, and say what disagrees with the profile's own (OSK-S-440).
|
|
382
|
+
*
|
|
383
|
+
* For the commands that install or sign in a daemon WITHOUT going through the
|
|
384
|
+
* credential hand-off (`skrr daemon install`, `skrr daemon login`): the same
|
|
385
|
+
* question, asked of the binary they are about to run. Null when the daemon
|
|
386
|
+
* agrees, when the profile is the default one, or when there is no binary to ask
|
|
387
|
+
* (the caller has its own answer for that).
|
|
388
|
+
*
|
|
389
|
+
* A binary that cannot be asked at all - too old to have `accept-handoff`, or
|
|
390
|
+
* one that fails the probe - is reported as a skew with no identity, because the
|
|
391
|
+
* point of asking is not to bind a profile to a machine row on a guess.
|
|
392
|
+
*/
|
|
393
|
+
async function checkLocalDaemonMachineIdentity(profile, binary = (0, exec_runtime_binary_1.findRuntimeBinary)()) {
|
|
394
|
+
if (!profile || profile === keychain_1.DEFAULT_PROFILE || !binary)
|
|
395
|
+
return null;
|
|
396
|
+
const printed = await runOversky(binary, ['accept-handoff', '--print-id', '--profile', profile], {
|
|
397
|
+
timeoutMs: PROBE_TIMEOUT_MS,
|
|
398
|
+
});
|
|
399
|
+
if (!printed.ok) {
|
|
400
|
+
// A daemon too old to have `accept-handoff` answers "unknown command": it is
|
|
401
|
+
// older than per-profile identities too, which is the skew itself.
|
|
402
|
+
if (/unknown command/i.test(printed.stderr)) {
|
|
403
|
+
return (0, daemon_machine_identity_1.daemonMachineIdentitySkew)({ profile, binary, reported: null });
|
|
404
|
+
}
|
|
405
|
+
// Any other failed probe says nothing about the daemon's identity, and
|
|
406
|
+
// refusing a sign-in on a flaky probe would be a worse failure than the one
|
|
407
|
+
// this guards: let the command run and meet whatever the daemon itself says.
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
return (0, daemon_machine_identity_1.daemonMachineIdentitySkew)({
|
|
411
|
+
profile,
|
|
412
|
+
binary,
|
|
413
|
+
reported: parseIdProvenance(printed.stderr).machineUuid,
|
|
414
|
+
});
|
|
415
|
+
}
|
package/dist/lib/harnesses.d.ts
CHANGED
|
@@ -238,6 +238,77 @@ export declare function updateHarness(id: string, patch: Record<string, unknown>
|
|
|
238
238
|
harness?: Harness;
|
|
239
239
|
}>;
|
|
240
240
|
export declare function forgetHarness(id: string): Promise<unknown>;
|
|
241
|
+
/**
|
|
242
|
+
* Why a superseded-looking row is KEPT by `harnesses prune`. Declared by the
|
|
243
|
+
* server (`pruneSupersededHarnesses.js`, `REASONS`) and worded once, in
|
|
244
|
+
* `harnessPruneReasonSentence`, so the table and `--json` never disagree.
|
|
245
|
+
*/
|
|
246
|
+
export type HarnessPruneReason = 'recent' | 'latest_registration' | 'no_live_sibling' | 'pinned' | 'granted' | 'default' | 'aliased' | 'changed_since_plan';
|
|
247
|
+
export interface HarnessPruneRow {
|
|
248
|
+
id: string;
|
|
249
|
+
daemonId: string | null;
|
|
250
|
+
provider: string | null;
|
|
251
|
+
name: string | null;
|
|
252
|
+
lastSeenAt: string | null;
|
|
253
|
+
machineUuid: string | null;
|
|
254
|
+
/** The live sibling that replaced a row this call removes (or would). */
|
|
255
|
+
supersededBy?: {
|
|
256
|
+
id: string;
|
|
257
|
+
daemonId: string | null;
|
|
258
|
+
lastSeenAt: string | null;
|
|
259
|
+
};
|
|
260
|
+
/** Set on kept rows. */
|
|
261
|
+
reason?: HarnessPruneReason;
|
|
262
|
+
/** For `pinned`: who holds the pin. Other people's agents are a count, never named. */
|
|
263
|
+
detail?: {
|
|
264
|
+
agents?: Array<{
|
|
265
|
+
id: string;
|
|
266
|
+
name: string | null;
|
|
267
|
+
}>;
|
|
268
|
+
moreAgents?: number;
|
|
269
|
+
otherAuthors?: number;
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
export interface HarnessPruneResult {
|
|
273
|
+
dryRun: boolean;
|
|
274
|
+
limit: number;
|
|
275
|
+
minUnseenMs: number;
|
|
276
|
+
scan: {
|
|
277
|
+
rows: number;
|
|
278
|
+
complete: boolean;
|
|
279
|
+
};
|
|
280
|
+
/** Rows this call removes (dry run: would remove). */
|
|
281
|
+
remove: HarnessPruneRow[];
|
|
282
|
+
/** Rows this call actually removed; 0 on a dry run. */
|
|
283
|
+
removedCount: number;
|
|
284
|
+
/** Removable rows beyond `limit`; the next call takes them. */
|
|
285
|
+
remaining: number;
|
|
286
|
+
kept: HarnessPruneRow[];
|
|
287
|
+
keptTruncated: boolean;
|
|
288
|
+
counts: Record<string, number>;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Remove the caller's superseded harness rows - or, by default, say which would go.
|
|
292
|
+
*
|
|
293
|
+
* ONE route for both: the server makes the decision once and this only asks for it
|
|
294
|
+
* to be shown (`dryRun: true`, also its own default) or carried out. The caller
|
|
295
|
+
* never lists rows and picks, so there is no client-side rule to drift from the
|
|
296
|
+
* server's. A write, so `apiFetch` gives it an idempotency key and a retried
|
|
297
|
+
* request replays the recorded answer instead of running again.
|
|
298
|
+
*/
|
|
299
|
+
export declare function pruneHarnesses(options?: {
|
|
300
|
+
dryRun: boolean;
|
|
301
|
+
limit?: number;
|
|
302
|
+
minUnseenMs?: number;
|
|
303
|
+
}): Promise<HarnessPruneResult>;
|
|
304
|
+
/**
|
|
305
|
+
* `--older-than` takes a whole number with a unit: `90m`, `36h`, `7d`. A bare
|
|
306
|
+
* number is hours. Returns milliseconds. The server never goes below its offline
|
|
307
|
+
* window whatever this says.
|
|
308
|
+
*/
|
|
309
|
+
export declare function parseHarnessPruneAge(value: string): number;
|
|
310
|
+
/** One sentence per reason a row was kept. The only place they are worded. */
|
|
311
|
+
export declare function harnessPruneReasonSentence(row: HarnessPruneRow): string;
|
|
241
312
|
/**
|
|
242
313
|
* A per-USER display name. Distinct from `updateHarness({name})`, which renames
|
|
243
314
|
* the harness for everyone it is shared with — this only changes what YOU see,
|
package/dist/lib/harnesses.js
CHANGED
|
@@ -13,6 +13,9 @@ exports.listQuotaAccounts = listQuotaAccounts;
|
|
|
13
13
|
exports.getQuotaHistory = getQuotaHistory;
|
|
14
14
|
exports.updateHarness = updateHarness;
|
|
15
15
|
exports.forgetHarness = forgetHarness;
|
|
16
|
+
exports.pruneHarnesses = pruneHarnesses;
|
|
17
|
+
exports.parseHarnessPruneAge = parseHarnessPruneAge;
|
|
18
|
+
exports.harnessPruneReasonSentence = harnessPruneReasonSentence;
|
|
16
19
|
exports.setHarnessAlias = setHarnessAlias;
|
|
17
20
|
exports.setDefaultHarness = setDefaultHarness;
|
|
18
21
|
exports.mintInstallToken = mintInstallToken;
|
|
@@ -173,6 +176,73 @@ async function updateHarness(id, patch) {
|
|
|
173
176
|
async function forgetHarness(id) {
|
|
174
177
|
return (0, api_fetch_1.apiFetch)(`/api/harnesses/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
175
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Remove the caller's superseded harness rows - or, by default, say which would go.
|
|
181
|
+
*
|
|
182
|
+
* ONE route for both: the server makes the decision once and this only asks for it
|
|
183
|
+
* to be shown (`dryRun: true`, also its own default) or carried out. The caller
|
|
184
|
+
* never lists rows and picks, so there is no client-side rule to drift from the
|
|
185
|
+
* server's. A write, so `apiFetch` gives it an idempotency key and a retried
|
|
186
|
+
* request replays the recorded answer instead of running again.
|
|
187
|
+
*/
|
|
188
|
+
async function pruneHarnesses(options = { dryRun: true }) {
|
|
189
|
+
return (0, api_fetch_1.apiFetch)('/api/harnesses/prune', {
|
|
190
|
+
method: 'POST',
|
|
191
|
+
body: {
|
|
192
|
+
dryRun: options.dryRun,
|
|
193
|
+
...(options.limit !== undefined ? { limit: options.limit } : {}),
|
|
194
|
+
...(options.minUnseenMs !== undefined ? { minUnseenMs: options.minUnseenMs } : {}),
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* `--older-than` takes a whole number with a unit: `90m`, `36h`, `7d`. A bare
|
|
200
|
+
* number is hours. Returns milliseconds. The server never goes below its offline
|
|
201
|
+
* window whatever this says.
|
|
202
|
+
*/
|
|
203
|
+
function parseHarnessPruneAge(value) {
|
|
204
|
+
const match = value.trim().match(/^(\d+)\s*(m|h|d)?$/i);
|
|
205
|
+
if (!match) {
|
|
206
|
+
throw new Error(`Invalid --older-than value "${value}". Use a number with m, h or d, such as 90m, 36h or 7d.`);
|
|
207
|
+
}
|
|
208
|
+
const amount = Number(match[1]);
|
|
209
|
+
const unit = (match[2] || 'h').toLowerCase();
|
|
210
|
+
const perUnit = unit === 'd' ? 86_400_000 : unit === 'h' ? 3_600_000 : 60_000;
|
|
211
|
+
return amount * perUnit;
|
|
212
|
+
}
|
|
213
|
+
/** One sentence per reason a row was kept. The only place they are worded. */
|
|
214
|
+
function harnessPruneReasonSentence(row) {
|
|
215
|
+
switch (row.reason) {
|
|
216
|
+
case 'recent':
|
|
217
|
+
return 'seen too recently to be sure it is gone';
|
|
218
|
+
case 'latest_registration':
|
|
219
|
+
return 'the newest registration of its provider on that machine';
|
|
220
|
+
case 'no_live_sibling':
|
|
221
|
+
return 'nothing has replaced it - no daemon on that machine is online for this provider';
|
|
222
|
+
case 'pinned': {
|
|
223
|
+
const names = (row.detail?.agents ?? []).map((a) => a.name ?? a.id);
|
|
224
|
+
const more = row.detail?.moreAgents ?? 0;
|
|
225
|
+
const others = row.detail?.otherAuthors ?? 0;
|
|
226
|
+
const parts = [];
|
|
227
|
+
if (names.length > 0) {
|
|
228
|
+
parts.push(`${names.join(', ')}${more > 0 ? ` and ${more} more` : ''}`);
|
|
229
|
+
}
|
|
230
|
+
if (others > 0)
|
|
231
|
+
parts.push(`${others} agent${others === 1 ? '' : 's'} of other people`);
|
|
232
|
+
return `agents are pinned to it${parts.length > 0 ? ` (${parts.join('; ')})` : ''}`;
|
|
233
|
+
}
|
|
234
|
+
case 'granted':
|
|
235
|
+
return 'it is lent to a workspace';
|
|
236
|
+
case 'default':
|
|
237
|
+
return 'it is your default harness';
|
|
238
|
+
case 'aliased':
|
|
239
|
+
return 'you gave it a name';
|
|
240
|
+
case 'changed_since_plan':
|
|
241
|
+
return 'it changed while this ran (came back online, or was pinned)';
|
|
242
|
+
default:
|
|
243
|
+
return String(row.reason ?? 'kept');
|
|
244
|
+
}
|
|
245
|
+
}
|
|
176
246
|
/**
|
|
177
247
|
* A per-USER display name. Distinct from `updateHarness({name})`, which renames
|
|
178
248
|
* the harness for everyone it is shared with — this only changes what YOU see,
|
|
@@ -18,7 +18,7 @@ export type ListEnvelope = {
|
|
|
18
18
|
* Documented in docs/cli/SKRR_TASKS.md (List output for agents).
|
|
19
19
|
*/
|
|
20
20
|
export declare const COMPACT_FIELDS: {
|
|
21
|
-
readonly task: readonly ["id", "identifier", "ref", "title", "status", "statusType", "priority", "estimate", "assigneeAgentIds", "assigneeUserIds", "parentId", "spaceId", "projectId", "goalId", "dueDate", "scheduledFor", "labelIds", "tags", "externalKey", "version", "executionMode", "nextAction", "createdAt", "updatedAt", "url"];
|
|
21
|
+
readonly task: readonly ["id", "identifier", "ref", "title", "status", "statusType", "priority", "estimate", "assigneeAgentIds", "assigneeUserIds", "parentId", "spaceId", "projectId", "goalId", "dueDate", "scheduledFor", "labelIds", "tags", "externalKey", "version", "executionMode", "nextAction", "economicsRank", "createdAt", "updatedAt", "url"];
|
|
22
22
|
readonly space: readonly ["id", "identifier", "title", "status", "parentSpaceId", "leadAgentId", "labelIds", "archivedAt", "createdAt", "updatedAt", "url"];
|
|
23
23
|
readonly project: readonly ["id", "identifier", "title", "spaceId", "externalKey", "taskCounts", "archivedAt", "createdAt", "updatedAt", "url"];
|
|
24
24
|
};
|
package/dist/lib/list-output.js
CHANGED
|
@@ -32,9 +32,19 @@ function legacyProfileSharesDefaultMachine() {
|
|
|
32
32
|
*/
|
|
33
33
|
function describeLegacySharedProfile(profile) {
|
|
34
34
|
const next = `${profile}-2`;
|
|
35
|
+
// WHY this profile is on the shared row, from the record that decided it: which
|
|
36
|
+
// file made it "already hold credentials", and when. Absent rather than guessed
|
|
37
|
+
// when the record cannot be read.
|
|
38
|
+
let why = '';
|
|
39
|
+
try {
|
|
40
|
+
why = `${(0, machine_uuid_1.describeMachineIdentity)().sentence} `;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
why = '';
|
|
44
|
+
}
|
|
35
45
|
return [
|
|
36
46
|
`Profile '${profile}' was set up by an older ${data_provider_1.BRAND.displayName} version that gave every profile on this ` +
|
|
37
|
-
|
|
47
|
+
`computer the same machine identity, and it still has it. ${why}Signing in here would replace the default ` +
|
|
38
48
|
"profile's device-key pin and sign the default daemon out. Nothing was changed.",
|
|
39
49
|
`Use a new profile name instead: skrr login --profile ${next}`,
|
|
40
50
|
`(For the background service alone: skrrd --profile ${next} login)`,
|
|
@@ -29,6 +29,16 @@ export declare function describeEstimate(economics: unknown): string;
|
|
|
29
29
|
* scored and sorts after every row that is.
|
|
30
30
|
*/
|
|
31
31
|
export declare function economicsScoreText(economics: unknown): string;
|
|
32
|
+
/** A rank's score as the table prints it: rounded, or `—` when the row is not scored. */
|
|
33
|
+
export declare function scoreText(score: number | null | undefined): string;
|
|
34
|
+
/**
|
|
35
|
+
* What `tasks update` should say about the estimate, read from the server's `economicsChange`
|
|
36
|
+
* (OSK-16476): `null` means the write changed nothing (an identical re-send, or a retraction of
|
|
37
|
+
* an estimate that is not there). `undefined` (an older server, or no estimate in the request) says
|
|
38
|
+
* nothing, so the CLI never guesses. When the estimate was the whole request the caller prints this
|
|
39
|
+
* line INSTEAD of "Updated task"; otherwise it follows it.
|
|
40
|
+
*/
|
|
41
|
+
export declare function estimateUnchangedLine(sent: unknown, updated: Record<string, unknown> | undefined): string | null;
|
|
32
42
|
/**
|
|
33
43
|
* One line for a `task.economics.*` Activity event (OSK-15365): the revision, the claim after the
|
|
34
44
|
* change and the claim it replaced, so `tasks activity` shows from-what-to-what without `--json`.
|
|
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.economicsFromFlags = economicsFromFlags;
|
|
4
4
|
exports.describeEstimate = describeEstimate;
|
|
5
5
|
exports.economicsScoreText = economicsScoreText;
|
|
6
|
+
exports.scoreText = scoreText;
|
|
7
|
+
exports.estimateUnchangedLine = estimateUnchangedLine;
|
|
6
8
|
exports.describeEconomicsChange = describeEconomicsChange;
|
|
7
9
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
8
10
|
/**
|
|
@@ -83,8 +85,28 @@ function describeEstimate(economics) {
|
|
|
83
85
|
* scored and sorts after every row that is.
|
|
84
86
|
*/
|
|
85
87
|
function economicsScoreText(economics) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
return scoreText((0, data_provider_1.proposalEconomicsRank)(economics).score);
|
|
89
|
+
}
|
|
90
|
+
/** A rank's score as the table prints it: rounded, or `—` when the row is not scored. */
|
|
91
|
+
function scoreText(score) {
|
|
92
|
+
return score === null || score === undefined || !Number.isFinite(score)
|
|
93
|
+
? '—'
|
|
94
|
+
: String(Math.round(score));
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* What `tasks update` should say about the estimate, read from the server's `economicsChange`
|
|
98
|
+
* (OSK-16476): `null` means the write changed nothing (an identical re-send, or a retraction of
|
|
99
|
+
* an estimate that is not there). `undefined` (an older server, or no estimate in the request) says
|
|
100
|
+
* nothing, so the CLI never guesses. When the estimate was the whole request the caller prints this
|
|
101
|
+
* line INSTEAD of "Updated task"; otherwise it follows it.
|
|
102
|
+
*/
|
|
103
|
+
function estimateUnchangedLine(sent, updated) {
|
|
104
|
+
if (sent === undefined || !updated || updated.economicsChange !== null)
|
|
105
|
+
return null;
|
|
106
|
+
if (sent === null)
|
|
107
|
+
return 'No estimate to retract; nothing changed.';
|
|
108
|
+
const revision = updated.economics?.revision;
|
|
109
|
+
return `Estimate unchanged${revision ? ` (revision ${revision})` : ''}: it already reads ${describeEstimate(updated.economics)}.`;
|
|
88
110
|
}
|
|
89
111
|
/**
|
|
90
112
|
* One line for a `task.economics.*` Activity event (OSK-15365): the revision, the claim after the
|
package/dist/lib/triggers.d.ts
CHANGED
|
@@ -232,6 +232,10 @@ export interface SpaceAssistantResponse {
|
|
|
232
232
|
export declare const SCOPE_IMPLIED_REF_KEY: Partial<Record<TriggerScopeKind, string>>;
|
|
233
233
|
export declare function parseKeyValueFlags(values: string[] | undefined, flagName: string, scopeKind?: string): Record<string, string>;
|
|
234
234
|
export declare function parseJsonObject(raw: string | undefined, flagName: string): Record<string, unknown> | undefined;
|
|
235
|
+
/**
|
|
236
|
+
* A JSON-array flag value: inline JSON, `@path` (a file), or a bare path to an existing file. A
|
|
237
|
+
* path used to reach `JSON.parse` and fail with "Unexpected token '/'" (OSK-15402).
|
|
238
|
+
*/
|
|
235
239
|
export declare function parseJsonArray(raw: string | undefined, flagName: string): unknown[] | undefined;
|
|
236
240
|
export declare function buildBuiltinActionPayload(input: {
|
|
237
241
|
builtinAction?: string;
|
|
@@ -332,6 +336,13 @@ export declare function describeScheduledFire(result: Record<string, unknown>):
|
|
|
332
336
|
* about `in_progress` once). Only these are success for `triggers fire --strict`.
|
|
333
337
|
*/
|
|
334
338
|
export declare function fireWasAccepted(result: Record<string, unknown>): boolean;
|
|
339
|
+
/**
|
|
340
|
+
* Why `triggers fire --strict` failed. A strict failure used to print exactly what the exit-0 run
|
|
341
|
+
* printed and then exit 1, so nothing said what the flag had judged (OSK-16474). It names the
|
|
342
|
+
* outcome it refused, the reason, the server's sentence when there is one, and what --strict
|
|
343
|
+
* accepts.
|
|
344
|
+
*/
|
|
345
|
+
export declare function describeStrictFireRefusal(result: Record<string, unknown>): string;
|
|
335
346
|
/**
|
|
336
347
|
* The lines a fire or replay owes beyond `outcome=… reason=…` (OSK-15494): the sentence the
|
|
337
348
|
* server wrote ("Replayed fire … resolved a step that was not waiting when that fire ran:
|