@skrr-ai/cli 0.1.111 → 0.1.114
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/bin/update-npm-runner.cjs +4 -1
- package/bin/update-worker.cjs +6 -3
- package/dist/commands/agents/create.d.ts +3 -1
- package/dist/commands/agents/create.js +36 -3
- package/dist/commands/followups/plans/create.js +4 -2
- package/dist/commands/followups/plans/example.d.ts +6 -0
- package/dist/commands/followups/plans/example.js +12 -0
- package/dist/commands/followups/plans/revise.d.ts +1 -0
- package/dist/commands/followups/plans/revise.js +5 -2
- package/dist/commands/followups/remind.js +13 -5
- package/dist/commands/followups/watch.js +2 -2
- package/dist/lib/lookbackPlanHelp.d.ts +19 -0
- package/dist/lib/lookbackPlanHelp.js +27 -0
- package/dist/lib/lookbackPlans.d.ts +9 -0
- package/dist/lib/lookbackPlans.js +21 -1
- package/dist/lib/update-check.d.ts +11 -10
- package/dist/lib/update-check.js +41 -23
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.d.ts +3 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.js +2 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.d.ts +3 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.js +2 -2
- package/dist/node_modules/@skrr-ai/data-provider/index.js +1608 -1602
- package/docs/v1/README.md +1 -0
- package/docs/v1/lookback-plans.md +88 -0
- package/oclif.manifest.json +100 -25
- package/package.json +5 -2
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
// Park before npm executes. The parent publishes this process group's durable
|
|
4
4
|
// mutation ownership before authorizing execution, closing its crash window.
|
|
5
5
|
const fs = require('node:fs');
|
|
6
|
+
const path = require('node:path');
|
|
6
7
|
const { spawn } = require('node:child_process');
|
|
7
8
|
const requestFile = process.argv[2];
|
|
8
9
|
const request = JSON.parse(fs.readFileSync(requestFile, 'utf8'));
|
|
10
|
+
if (request.npmPrefix != null && !path.isAbsolute(request.npmPrefix)) throw new Error('Invalid npm prefix');
|
|
9
11
|
const deadline = Date.now() + 30000;
|
|
10
12
|
const timer = setInterval(() => {
|
|
11
13
|
let authorization;
|
|
@@ -16,7 +18,8 @@ const timer = setInterval(() => {
|
|
|
16
18
|
}
|
|
17
19
|
if (authorization?.pid === process.pid && authorization?.token === request.token) {
|
|
18
20
|
clearInterval(timer);
|
|
19
|
-
const
|
|
21
|
+
const prefixArgs = request.npmPrefix ? ['--prefix', request.npmPrefix] : [];
|
|
22
|
+
const child = spawn(request.npmPath, ['install', '-g', ...prefixArgs, `@skrr-ai/cli@${request.version}`], {
|
|
20
23
|
stdio: 'inherit',
|
|
21
24
|
shell: process.platform === 'win32',
|
|
22
25
|
});
|
package/bin/update-worker.cjs
CHANGED
|
@@ -41,7 +41,8 @@ async function run(directory, options = {}) {
|
|
|
41
41
|
if (
|
|
42
42
|
!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(attempt.version) ||
|
|
43
43
|
!path.isAbsolute(attempt.packageRoot) ||
|
|
44
|
-
!path.isAbsolute(attempt.npmPath)
|
|
44
|
+
!path.isAbsolute(attempt.npmPath) ||
|
|
45
|
+
(attempt.npmPrefix != null && !path.isAbsolute(attempt.npmPrefix))
|
|
45
46
|
) {
|
|
46
47
|
throw new Error('Invalid persisted CLI update request');
|
|
47
48
|
}
|
|
@@ -86,7 +87,8 @@ async function run(directory, options = {}) {
|
|
|
86
87
|
});
|
|
87
88
|
return;
|
|
88
89
|
}
|
|
89
|
-
const
|
|
90
|
+
const prefixArgs = attempt.npmPrefix ? ['--prefix', attempt.npmPrefix] : [];
|
|
91
|
+
const rootResult = await execFileAsync(attempt.npmPath, ['root', '-g', ...prefixArgs], {
|
|
90
92
|
timeout: 1500,
|
|
91
93
|
maxBuffer: 64 * 1024,
|
|
92
94
|
shell: process.platform === 'win32',
|
|
@@ -117,7 +119,8 @@ async function run(directory, options = {}) {
|
|
|
117
119
|
await new Promise((resolve, reject) => {
|
|
118
120
|
const requestFile = path.join(directory, `runner-${crypto.randomUUID()}.json`);
|
|
119
121
|
const token = crypto.randomUUID();
|
|
120
|
-
runtime.writeJson(requestFile, { token, npmPath: attempt.npmPath, version: attempt.version
|
|
122
|
+
runtime.writeJson(requestFile, { token, npmPath: attempt.npmPath, version: attempt.version,
|
|
123
|
+
...(attempt.npmPrefix ? { npmPrefix: attempt.npmPrefix } : {}) });
|
|
121
124
|
const child = spawn(
|
|
122
125
|
process.execPath,
|
|
123
126
|
[path.join(__dirname, 'update-npm-runner.cjs'), requestFile],
|
|
@@ -71,7 +71,8 @@ export declare function unappliedPayloadKeys(sent: Record<string, unknown>, crea
|
|
|
71
71
|
* to claim a platform role that they had misspelled something.
|
|
72
72
|
*/
|
|
73
73
|
export declare function unappliedPayloadKeysWarning(keys: readonly string[]): string;
|
|
74
|
-
export declare function
|
|
74
|
+
export declare function initialAutonomyReceipt(created: unknown, requested: unknown): boolean | 'unconfirmed';
|
|
75
|
+
export declare function backgroundLoopLines(agentId: string, relationshipLoopsSkipped: boolean, initialAlwaysOn?: boolean | 'unconfirmed'): string[];
|
|
75
76
|
/**
|
|
76
77
|
* The flags `--from-spec` may be combined with: placement and identity for the
|
|
77
78
|
* NEW row, which a spec document deliberately cannot carry (where an agent runs
|
|
@@ -96,6 +97,7 @@ export default class AgentsCreate extends BaseCommand {
|
|
|
96
97
|
target: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
97
98
|
'next-target': import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
98
99
|
'working-directory': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
100
|
+
'initial-always-on': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
99
101
|
'no-relationship-loops': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
100
102
|
'default-permission-mode': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
101
103
|
tools: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
@@ -5,6 +5,7 @@ exports.agentCreateMissingFieldsMessage = agentCreateMissingFieldsMessage;
|
|
|
5
5
|
exports.agentCreateMissingFields = agentCreateMissingFields;
|
|
6
6
|
exports.unappliedPayloadKeys = unappliedPayloadKeys;
|
|
7
7
|
exports.unappliedPayloadKeysWarning = unappliedPayloadKeysWarning;
|
|
8
|
+
exports.initialAutonomyReceipt = initialAutonomyReceipt;
|
|
8
9
|
exports.backgroundLoopLines = backgroundLoopLines;
|
|
9
10
|
const core_1 = require("@oclif/core");
|
|
10
11
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
@@ -121,7 +122,25 @@ function unappliedPayloadKeysWarning(keys) {
|
|
|
121
122
|
return (`The server did not keep ${keys.join(', ')}: ${one ? 'it is not an agent field' : 'they are not agent fields'}, ` +
|
|
122
123
|
`or only the server sets ${one ? 'it' : 'them'}. \`skrr agents show <id> --json\` shows what the agent stores.`);
|
|
123
124
|
}
|
|
124
|
-
function
|
|
125
|
+
function initialAutonomyReceipt(created, requested) {
|
|
126
|
+
if (requested !== false)
|
|
127
|
+
return true;
|
|
128
|
+
const provisioning = created && typeof created === 'object'
|
|
129
|
+
? created.proactiveProvisioning : null;
|
|
130
|
+
return provisioning && typeof provisioning === 'object' &&
|
|
131
|
+
provisioning.initialAlwaysOn === false ? false : 'unconfirmed';
|
|
132
|
+
}
|
|
133
|
+
function backgroundLoopLines(agentId, relationshipLoopsSkipped, initialAlwaysOn = true) {
|
|
134
|
+
if (initialAlwaysOn === 'unconfirmed')
|
|
135
|
+
return [
|
|
136
|
+
'Passive creation was requested, but the server did not acknowledge it. Autonomous state is unconfirmed.',
|
|
137
|
+
` skrr agents show ${agentId} --json # inspect actual runtime state`,
|
|
138
|
+
];
|
|
139
|
+
if (!initialAlwaysOn)
|
|
140
|
+
return [
|
|
141
|
+
'Passive creation acknowledged. Legacy task/memory scans were omitted. Explicit chats still run; Always-On can enable the seeded managed portfolio later.',
|
|
142
|
+
` skrr agents always-on enable ${agentId} # start autonomous work later`,
|
|
143
|
+
];
|
|
125
144
|
if (relationshipLoopsSkipped) {
|
|
126
145
|
return [
|
|
127
146
|
'Background loops: relationship portfolio skipped (--no-relationship-loops). ' +
|
|
@@ -156,6 +175,7 @@ exports.FROM_SPEC_COMPATIBLE_FLAGS = Object.freeze([
|
|
|
156
175
|
'working-directory',
|
|
157
176
|
'default-permission-mode',
|
|
158
177
|
'no-relationship-loops',
|
|
178
|
+
'initial-always-on',
|
|
159
179
|
]);
|
|
160
180
|
class AgentsCreate extends base_command_1.BaseCommand {
|
|
161
181
|
static description = 'Create a new agent in the selected workspace, or in your default workspace when --workspace is omitted (`skrr workspaces list` names it)';
|
|
@@ -224,6 +244,10 @@ class AgentsCreate extends base_command_1.BaseCommand {
|
|
|
224
244
|
'working-directory': core_1.Flags.string({
|
|
225
245
|
description: 'Default working directory for local Harness sessions',
|
|
226
246
|
}),
|
|
247
|
+
'initial-always-on': core_1.Flags.string({
|
|
248
|
+
description: 'Start autonomous work after creation (default: on). Use off for a passive agent; explicit chat still works. Relationship-loop seeding is configured separately.',
|
|
249
|
+
options: ['on', 'off'],
|
|
250
|
+
}),
|
|
227
251
|
'no-relationship-loops': core_1.Flags.boolean({
|
|
228
252
|
description: 'Single-purpose agent: skip the system-managed relationship portfolio (conversation continuity, relationship pulse, commitment incubator, open-loop closer). Self-reflection is still seeded.',
|
|
229
253
|
}),
|
|
@@ -361,6 +385,7 @@ class AgentsCreate extends base_command_1.BaseCommand {
|
|
|
361
385
|
// so an ordinary create carries no `relationshipLoops` key and the
|
|
362
386
|
// server seeds the full portfolio exactly as it always has.
|
|
363
387
|
relationshipLoops: flags['no-relationship-loops'] ? false : undefined,
|
|
388
|
+
initialAlwaysOn: flags['initial-always-on'] === undefined ? undefined : flags['initial-always-on'] === 'on',
|
|
364
389
|
});
|
|
365
390
|
// `mergeAgentConfigFlags` parses the typed flags (avatar, scene background,
|
|
366
391
|
// working hours, every JSON flag). A parse failure is the caller's input, so
|
|
@@ -412,6 +437,7 @@ class AgentsCreate extends base_command_1.BaseCommand {
|
|
|
412
437
|
// definition-field round-trip warning.
|
|
413
438
|
const unapplied = unappliedPayloadKeys(payload, created, [
|
|
414
439
|
'relationshipLoops',
|
|
440
|
+
'initialAlwaysOn',
|
|
415
441
|
...(0, agent_config_1.deprecatedFieldNames)(created),
|
|
416
442
|
]);
|
|
417
443
|
for (const warning of (0, agent_config_1.deprecatedFieldsWarnings)(created))
|
|
@@ -426,6 +452,9 @@ class AgentsCreate extends base_command_1.BaseCommand {
|
|
|
426
452
|
bin: this.config.bin,
|
|
427
453
|
warn: (message) => this.warn(message),
|
|
428
454
|
});
|
|
455
|
+
if (initialAutonomyReceipt(created, payload.initialAlwaysOn) === 'unconfirmed') {
|
|
456
|
+
this.warn('Passive creation was requested but not acknowledged by the server. Autonomous state is unconfirmed; inspect agents show before relying on it.');
|
|
457
|
+
}
|
|
429
458
|
if (flags.json) {
|
|
430
459
|
this.log(JSON.stringify((0, web_url_1.withAgentUrl)(this.cliConfig.baseURL, created), null, 2));
|
|
431
460
|
return;
|
|
@@ -447,7 +476,7 @@ class AgentsCreate extends base_command_1.BaseCommand {
|
|
|
447
476
|
this.log(`Avatar: ${(0, agent_visual_refs_1.agentAvatarLine)(a.avatar)}`);
|
|
448
477
|
this.log(' Decided by your default-avatar policy — `skrr agents avatar default` shows or changes it.');
|
|
449
478
|
}
|
|
450
|
-
for (const line of backgroundLoopLines(agentId, flags['no-relationship-loops'] === true)) {
|
|
479
|
+
for (const line of backgroundLoopLines(agentId, flags['no-relationship-loops'] === true, initialAutonomyReceipt(created, payload.initialAlwaysOn))) {
|
|
451
480
|
this.log(line);
|
|
452
481
|
}
|
|
453
482
|
this.log(`URL: ${(0, web_url_1.withAgentUrl)(this.cliConfig.baseURL, a).url ?? '-'}`);
|
|
@@ -568,6 +597,7 @@ class AgentsCreate extends base_command_1.BaseCommand {
|
|
|
568
597
|
workingDirectory: flags['working-directory'],
|
|
569
598
|
defaultPermissionMode: flags['default-permission-mode'],
|
|
570
599
|
relationshipLoops: flags['no-relationship-loops'] ? false : undefined,
|
|
600
|
+
initialAlwaysOn: flags['initial-always-on'] === undefined ? undefined : flags['initial-always-on'] === 'on',
|
|
571
601
|
});
|
|
572
602
|
let created;
|
|
573
603
|
try {
|
|
@@ -605,6 +635,9 @@ class AgentsCreate extends base_command_1.BaseCommand {
|
|
|
605
635
|
details: { agentId, rolledBack: !rollback.includes('NOT'), issues: refusal?.issues },
|
|
606
636
|
});
|
|
607
637
|
}
|
|
638
|
+
if (initialAutonomyReceipt(created, payload.initialAlwaysOn) === 'unconfirmed') {
|
|
639
|
+
this.warn('Passive creation was requested but not acknowledged by the server. Autonomous state is unconfirmed; inspect agents show before relying on it.');
|
|
640
|
+
}
|
|
608
641
|
if (flags.json) {
|
|
609
642
|
this.log(JSON.stringify({
|
|
610
643
|
agent: (0, web_url_1.withAgentUrl)(this.cliConfig.baseURL, created),
|
|
@@ -628,7 +661,7 @@ class AgentsCreate extends base_command_1.BaseCommand {
|
|
|
628
661
|
this.log(`Not taken from the file, because the server derives it from other fields: ${derived.join(', ')}.`);
|
|
629
662
|
}
|
|
630
663
|
this.log(`Keep the file in step: \`skrr agents push ${agentId} --file ${source === '-' ? '<file>' : source}\`.`);
|
|
631
|
-
for (const line of backgroundLoopLines(agentId, flags['no-relationship-loops'] === true)) {
|
|
664
|
+
for (const line of backgroundLoopLines(agentId, flags['no-relationship-loops'] === true, initialAutonomyReceipt(created, payload.initialAlwaysOn))) {
|
|
632
665
|
this.log(line);
|
|
633
666
|
}
|
|
634
667
|
this.log(`URL: ${(0, web_url_1.withAgentUrl)(this.cliConfig.baseURL, created).url ?? '-'}`);
|
|
@@ -4,8 +4,10 @@ const core_1 = require("@oclif/core");
|
|
|
4
4
|
const base_command_1 = require("../../../base-command");
|
|
5
5
|
const prompt_1 = require("../../../lib/prompt");
|
|
6
6
|
const lookbackPlans_1 = require("../../../lib/lookbackPlans");
|
|
7
|
+
const lookbackPlanHelp_1 = require("../../../lib/lookbackPlanHelp");
|
|
7
8
|
class LookbackPlansCreate extends base_command_1.BaseCommand {
|
|
8
|
-
static description = 'Schedule a bounded plan of 1–10 lookbacks, each free to stay silent'
|
|
9
|
+
static description = 'Schedule a bounded plan of 1–10 lookbacks, each free to stay silent\n\n' + lookbackPlanHelp_1.LOOKBACK_PLAN_INPUT_HELP +
|
|
10
|
+
'\n\nCreate also accepts {"agentId":"...","pinnedConversationId":"...","plan":{...}}. Source and delivery destination are separate; pin the original conversation to return there.';
|
|
9
11
|
static examples = ['<%= config.bin %> followups plans create --from-json plan.json --agent agent_1 --json'];
|
|
10
12
|
static flags = {
|
|
11
13
|
'from-json': core_1.Flags.string({ description: 'Plan or create envelope JSON file; - reads stdin', required: true }),
|
|
@@ -20,7 +22,7 @@ class LookbackPlansCreate extends base_command_1.BaseCommand {
|
|
|
20
22
|
body = (0, lookbackPlans_1.lookbackCreateBody)(await (0, prompt_1.readJsonBody)(flags['from-json']), flags.agent);
|
|
21
23
|
}
|
|
22
24
|
catch (err) {
|
|
23
|
-
this.failWithCliError(
|
|
25
|
+
this.failWithCliError((0, lookbackPlans_1.lookbackInputFailure)(err));
|
|
24
26
|
return;
|
|
25
27
|
}
|
|
26
28
|
try {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const base_command_1 = require("../../../base-command");
|
|
4
|
+
const lookbackPlanHelp_1 = require("../../../lib/lookbackPlanHelp");
|
|
5
|
+
class LookbackPlanExample extends base_command_1.BaseCommand {
|
|
6
|
+
static description = 'Print an editable Plan JSON template without signing in\n\nReplace the source placeholders with a real completed assistant reply before creating a plan. This command schedules nothing.';
|
|
7
|
+
static examples = ['<%= config.bin %> followups plans example > plan.json'];
|
|
8
|
+
async run() {
|
|
9
|
+
this.log(JSON.stringify(lookbackPlanHelp_1.LOOKBACK_PLAN_EXAMPLE, null, 2));
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
exports.default = LookbackPlanExample;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BaseCommand } from '../../../base-command';
|
|
2
2
|
export default class LookbackPlansRevise extends BaseCommand {
|
|
3
3
|
static description: string;
|
|
4
|
+
static examples: string[];
|
|
4
5
|
static args: {
|
|
5
6
|
id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
|
|
6
7
|
};
|
|
@@ -4,8 +4,11 @@ const core_1 = require("@oclif/core");
|
|
|
4
4
|
const base_command_1 = require("../../../base-command");
|
|
5
5
|
const prompt_1 = require("../../../lib/prompt");
|
|
6
6
|
const lookbackPlans_1 = require("../../../lib/lookbackPlans");
|
|
7
|
+
const lookbackPlanHelp_1 = require("../../../lib/lookbackPlanHelp");
|
|
7
8
|
class LookbackPlansRevise extends base_command_1.BaseCommand {
|
|
8
|
-
static description = 'Revise pending lookbacks using the original source and step IDs'
|
|
9
|
+
static description = 'Revise pending lookbacks using the original source and step IDs\n\n' + lookbackPlanHelp_1.LOOKBACK_PLAN_INPUT_HELP +
|
|
10
|
+
'\n\nRevise accepts the plan itself, not a create envelope. Use the current --revision from show; a stale revision returns LOOKBACK_CONFLICT (409).';
|
|
11
|
+
static examples = ['<%= config.bin %> followups plans revise PLAN_ID --revision 1 --from-json revised-plan.json --json'];
|
|
9
12
|
static args = { id: lookbackPlans_1.lookbackPlanIdArg };
|
|
10
13
|
static flags = {
|
|
11
14
|
revision: core_1.Flags.integer({ description: 'Expected current revision; stale edits are refused', min: 1, required: true }),
|
|
@@ -20,7 +23,7 @@ class LookbackPlansRevise extends base_command_1.BaseCommand {
|
|
|
20
23
|
plan = (0, lookbackPlans_1.lookbackInput)(await (0, prompt_1.readJsonBody)(flags['from-json']), true);
|
|
21
24
|
}
|
|
22
25
|
catch (err) {
|
|
23
|
-
this.failWithCliError(
|
|
26
|
+
this.failWithCliError((0, lookbackPlans_1.lookbackInputFailure)(err));
|
|
24
27
|
return;
|
|
25
28
|
}
|
|
26
29
|
try {
|
|
@@ -12,7 +12,7 @@ const agent_resolver_1 = require("../../lib/agent-resolver");
|
|
|
12
12
|
* needs an agent context the passive form does not.
|
|
13
13
|
*/
|
|
14
14
|
class FollowupsRemind extends base_command_1.BaseCommand {
|
|
15
|
-
static description = 'Remind the owner at a time
|
|
15
|
+
static description = 'Remind the owner at a time. By default, deliver the literal content through owner notifications without an agent turn. Use --active for agent-written delivery.';
|
|
16
16
|
static examples = [
|
|
17
17
|
'<%= config.bin %> followups remind --subject task:OSK-42 --content "pick the deploy up" --in 2h',
|
|
18
18
|
'<%= config.bin %> followups remind --subject conversation:conv_9 --content "ask how the move went" --at 2026-09-20T09:00:00Z --active --agent agent_1',
|
|
@@ -34,15 +34,23 @@ class FollowupsRemind extends base_command_1.BaseCommand {
|
|
|
34
34
|
description: 'Wake the agent to word the reminder (needs an agent context)',
|
|
35
35
|
default: false,
|
|
36
36
|
}),
|
|
37
|
-
agent: core_1.Flags.string({ description: 'Agent ID (default: bound session agent)' }),
|
|
38
|
-
endpoint: core_1.Flags.string({ description: '
|
|
39
|
-
'endpoint-target': core_1.Flags.string({ description: '
|
|
40
|
-
pin: core_1.Flags.string({ description: '
|
|
37
|
+
agent: core_1.Flags.string({ description: 'Agent ID (default: bound session agent). Attribution only unless --active is set' }),
|
|
38
|
+
endpoint: core_1.Flags.string({ description: 'Active only: delivery endpoint id; requires --active' }),
|
|
39
|
+
'endpoint-target': core_1.Flags.string({ description: 'Active only: channel target for a channel endpoint; requires --active' }),
|
|
40
|
+
pin: core_1.Flags.string({ description: 'Active only: deliver into this conversation; requires --active. Do not combine with --endpoint other than pinned' }),
|
|
41
41
|
json: core_1.Flags.boolean({ description: 'Output as JSON' }),
|
|
42
42
|
};
|
|
43
43
|
async run() {
|
|
44
44
|
this.requireAuth();
|
|
45
45
|
const { flags } = await this.parse(FollowupsRemind);
|
|
46
|
+
if (!flags.active && (flags.endpoint || flags['endpoint-target'] || flags.pin)) {
|
|
47
|
+
this.failWithCliError({
|
|
48
|
+
message: '--endpoint, --endpoint-target and --pin require --active. For a passive reminder, remove these flags; the literal content is delivered through owner notifications without an agent turn. --agent alone records attribution.',
|
|
49
|
+
code: 'FOLLOWUP_INVALID_ENDPOINT',
|
|
50
|
+
exit: 2,
|
|
51
|
+
retryable: false,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
46
54
|
const at = flags.at ?? flags.in;
|
|
47
55
|
if (!at) {
|
|
48
56
|
this.failWithCliError({
|
|
@@ -83,9 +83,9 @@ class FollowupsWatch extends base_command_1.BaseCommand {
|
|
|
83
83
|
description: 'SLA backstop — required for --wait event (an event may never arrive), optional for poll_until',
|
|
84
84
|
}),
|
|
85
85
|
// delivery
|
|
86
|
-
endpoint: core_1.Flags.string({ description: 'Delivery endpoint id (
|
|
86
|
+
endpoint: core_1.Flags.string({ description: 'Delivery endpoint id (defaults to main). Use --pin to deliver into a specific conversation' }),
|
|
87
87
|
'endpoint-target': core_1.Flags.string({ description: 'Channel target for channel endpoints' }),
|
|
88
|
-
pin: core_1.Flags.string({ description: '
|
|
88
|
+
pin: core_1.Flags.string({ description: 'Deliver into this conversation. Do not combine with --endpoint other than pinned' }),
|
|
89
89
|
json: core_1.Flags.boolean({ description: 'Output as JSON' }),
|
|
90
90
|
};
|
|
91
91
|
async run() {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Authored input, not a copy of the server's normalized Plan projection. */
|
|
2
|
+
export declare const LOOKBACK_PLAN_EXAMPLE: {
|
|
3
|
+
objective: string;
|
|
4
|
+
stopCondition: string;
|
|
5
|
+
source: {
|
|
6
|
+
conversationId: string;
|
|
7
|
+
messageId: string;
|
|
8
|
+
replyCompletedAt: string;
|
|
9
|
+
};
|
|
10
|
+
steps: {
|
|
11
|
+
id: string;
|
|
12
|
+
instruction: string;
|
|
13
|
+
when: {
|
|
14
|
+
kind: string;
|
|
15
|
+
offset: string;
|
|
16
|
+
};
|
|
17
|
+
}[];
|
|
18
|
+
};
|
|
19
|
+
export declare const LOOKBACK_PLAN_INPUT_HELP: string;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LOOKBACK_PLAN_INPUT_HELP = exports.LOOKBACK_PLAN_EXAMPLE = void 0;
|
|
4
|
+
/** Authored input, not a copy of the server's normalized Plan projection. */
|
|
5
|
+
exports.LOOKBACK_PLAN_EXAMPLE = {
|
|
6
|
+
objective: 'Check deployment health',
|
|
7
|
+
stopCondition: 'Deployment is ready or the owner cancels',
|
|
8
|
+
source: {
|
|
9
|
+
conversationId: 'YOUR_CONVERSATION_ID',
|
|
10
|
+
messageId: 'YOUR_COMPLETED_ASSISTANT_MESSAGE_ID',
|
|
11
|
+
replyCompletedAt: 'ACTUAL_REPLY_COMPLETION_ISO_TIMESTAMP',
|
|
12
|
+
},
|
|
13
|
+
steps: [{
|
|
14
|
+
id: 'health-check',
|
|
15
|
+
instruction: 'Read the latest original conversation. Report readiness once; otherwise stay silent.',
|
|
16
|
+
when: { kind: 'relative', offset: '10m' },
|
|
17
|
+
}],
|
|
18
|
+
};
|
|
19
|
+
exports.LOOKBACK_PLAN_INPUT_HELP = [
|
|
20
|
+
'Input is authored Plan JSON with objective, stopCondition, source and 1–10 steps. Generate an editable JSON file:',
|
|
21
|
+
'skrr followups plans example > plan.json',
|
|
22
|
+
'Replace the three source placeholders with a real completed assistant reply belonging to this agent. `skrr convos messages <conversation-id> --json` reads its messageId and completion updatedAt. Never use the current clock as a replacement anchor.',
|
|
23
|
+
'Relative time is measured from source.replyCompletedAt, not command invocation. Choose an offset whose resulting time is still in the future.',
|
|
24
|
+
'Each when supports {"kind":"relative","offset":"10m"}, {"kind":"absolute","at":"2026-10-21T12:00:00Z"}, or {"kind":"local","dateTime":"2026-10-21T09:00:00","timezone":"America/Los_Angeles"}. Replace example dates with future dates. Absolute time requires Z or a UTC offset; ambiguous/nonexistent local DST times are refused.',
|
|
25
|
+
'Optional fields: title, timezone; per step: id, label, expiresAfter (for example "1h"). Revision retains the exact source and existing step IDs. Do not submit the show response unchanged: remove lifecycle fields such as state, runAt and expiresAt.',
|
|
26
|
+
'A check may remain silent. Only armed:true confirms timer registration; saved/pending is not armed. Full guide is shipped at docs/v1/lookback-plans.md in the installed CLI package.',
|
|
27
|
+
].join('\n\n');
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { lookbackPlansApi, type CreateLookbackPlanRequest, type LookbackPlan, type LookbackPlanInput } from '@skrr-ai/data-provider';
|
|
2
2
|
export { lookbackPlansApi };
|
|
3
|
+
export declare function lookbackInputFailure(error: unknown): {
|
|
4
|
+
exit: number;
|
|
5
|
+
retryable: boolean;
|
|
6
|
+
details?: {
|
|
7
|
+
field: string;
|
|
8
|
+
} | undefined;
|
|
9
|
+
code: "LOOKBACK_INVALID_FIELD" | "LOOKBACK_INVALID_TIME" | "LOOKBACK_INVALID_TIMEZONE" | "LOOKBACK_AMBIGUOUS_TIME" | "LOOKBACK_NONEXISTENT_TIME" | "LOOKBACK_LIMIT_EXCEEDED";
|
|
10
|
+
message: string;
|
|
11
|
+
};
|
|
3
12
|
export declare const lookbackPlanIdArg: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
|
|
4
13
|
/** JSON can be the plan itself or the canonical create envelope. Never copy identity fields. */
|
|
5
14
|
export declare function lookbackCreateBody(input: Record<string, unknown>, explicitAgentId?: string): CreateLookbackPlanRequest;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.lookbackPlanIdArg = exports.lookbackPlansApi = void 0;
|
|
4
|
+
exports.lookbackInputFailure = lookbackInputFailure;
|
|
4
5
|
exports.lookbackCreateBody = lookbackCreateBody;
|
|
5
6
|
exports.lookbackInput = lookbackInput;
|
|
6
7
|
exports.lookbackPlanSummary = lookbackPlanSummary;
|
|
@@ -9,6 +10,22 @@ const core_1 = require("@oclif/core");
|
|
|
9
10
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
10
11
|
Object.defineProperty(exports, "lookbackPlansApi", { enumerable: true, get: function () { return data_provider_1.lookbackPlansApi; } });
|
|
11
12
|
const followups_1 = require("./followups");
|
|
13
|
+
class LookbackValidationError extends Error {
|
|
14
|
+
issue;
|
|
15
|
+
constructor(issue) {
|
|
16
|
+
super(`${issue.code}: ${issue.field}: ${issue.message}`);
|
|
17
|
+
this.issue = issue;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function lookbackInputFailure(error) {
|
|
21
|
+
return {
|
|
22
|
+
code: error instanceof LookbackValidationError ? error.issue.code : 'LOOKBACK_INVALID_FIELD',
|
|
23
|
+
message: error instanceof Error ? error.message : String(error),
|
|
24
|
+
...(error instanceof LookbackValidationError ? { details: { field: error.issue.field } } : {}),
|
|
25
|
+
exit: 2,
|
|
26
|
+
retryable: false,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
12
29
|
exports.lookbackPlanIdArg = core_1.Args.string({
|
|
13
30
|
description: 'Lookback plan id', required: true, ignoreStdin: true,
|
|
14
31
|
parse: async (value) => {
|
|
@@ -52,7 +69,7 @@ function lookbackCreateBody(input, explicitAgentId) {
|
|
|
52
69
|
function lookbackInput(raw, allowPast = false) {
|
|
53
70
|
const parsed = (0, data_provider_1.normalizeLookbackPlanInput)(raw, { allowPast });
|
|
54
71
|
if (!parsed.ok)
|
|
55
|
-
throw new
|
|
72
|
+
throw new LookbackValidationError(parsed);
|
|
56
73
|
// Send only authored fields. The server fixes normalized times, generated
|
|
57
74
|
// step IDs, lifecycle and ownership; CLI must not forge those projections.
|
|
58
75
|
return raw;
|
|
@@ -63,6 +80,9 @@ function lookbackPlanSummary(plan) {
|
|
|
63
80
|
` ${plan.steps.length} lookbacks${next ? ` next:${next.runAt}` : ''}`;
|
|
64
81
|
}
|
|
65
82
|
function lookbackArmingLine(result) {
|
|
83
|
+
if (['cancelled', 'completed', 'failed'].includes(result.plan.state)) {
|
|
84
|
+
return `Lookback plan ${result.plan.id}: ${result.plan.state} (r${result.plan.revision})${result.reused ? ' (existing plan)' : ''}.`;
|
|
85
|
+
}
|
|
66
86
|
const status = result.armed ? 'armed' : 'saved; scheduling is pending';
|
|
67
87
|
return `Lookback plan ${result.plan.id} ${status}${result.reused ? ' (existing plan)' : ''}.`;
|
|
68
88
|
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
export type CliInstallKind = 'npm-global' | 'npm-local' | 'dev' | 'bundled' | 'image' | 'homebrew' | 'other';
|
|
2
|
+
export interface CliInstallDescriptor {
|
|
3
|
+
kind: CliInstallKind;
|
|
4
|
+
writable?: boolean;
|
|
5
|
+
npmPrefix?: string;
|
|
6
|
+
}
|
|
2
7
|
export interface CliUpdateAttempt {
|
|
3
8
|
status: 'queued' | 'running' | 'succeeded' | 'failed';
|
|
4
9
|
version: string;
|
|
@@ -10,6 +15,7 @@ export interface CliUpdateAttempt {
|
|
|
10
15
|
message?: string;
|
|
11
16
|
packageRoot?: string;
|
|
12
17
|
npmPath?: string;
|
|
18
|
+
npmPrefix?: string;
|
|
13
19
|
coordinationVersion?: number;
|
|
14
20
|
workerPid?: number;
|
|
15
21
|
interrupted?: boolean;
|
|
@@ -19,10 +25,7 @@ export interface CheckCache {
|
|
|
19
25
|
latest: string | null;
|
|
20
26
|
npmPackage: string | null;
|
|
21
27
|
baseURL: string;
|
|
22
|
-
install?:
|
|
23
|
-
kind: CliInstallKind;
|
|
24
|
-
writable?: boolean;
|
|
25
|
-
};
|
|
28
|
+
install?: CliInstallDescriptor;
|
|
26
29
|
packageRoot?: string;
|
|
27
30
|
npmGlobalRoot?: string | null;
|
|
28
31
|
autoUpdate?: {
|
|
@@ -45,6 +48,7 @@ export interface CliUpdateStatus {
|
|
|
45
48
|
installedVersion: string | null;
|
|
46
49
|
offeredVersion: string | null;
|
|
47
50
|
owner: CliInstallKind;
|
|
51
|
+
npmPrefix?: string;
|
|
48
52
|
state: 'up-to-date' | 'update-available' | 'pending' | 'restart-pending' | 'failed' | 'managed' | 'unknown' | 'check-failed';
|
|
49
53
|
autoUpdateAllowed: boolean;
|
|
50
54
|
autoUpdateReason: string | null;
|
|
@@ -76,12 +80,9 @@ export declare function resolveCliPackageDir(argv?: readonly string[]): string |
|
|
|
76
80
|
* dir beside `dist` exists only in a checkout — published packages ship
|
|
77
81
|
* `dist` and no `src`.
|
|
78
82
|
*/
|
|
79
|
-
export declare function classifyCliInstall(env: NodeJS.ProcessEnv, argv: readonly string[], npmGlobalRoot: string | null, probeWritable?: boolean):
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
};
|
|
83
|
-
export declare function cliUpdateCommand(owner: CliInstallKind): string;
|
|
84
|
-
export declare function buildNudgeLine(currentVersion: string, latest: string | null, _npmPackage: string | null, installKind?: CliInstallKind): string | null;
|
|
83
|
+
export declare function classifyCliInstall(env: NodeJS.ProcessEnv, argv: readonly string[], npmGlobalRoot: string | null, probeWritable?: boolean): CliInstallDescriptor;
|
|
84
|
+
export declare function cliUpdateCommand(owner: CliInstallKind, npmPrefix?: string): string;
|
|
85
|
+
export declare function buildNudgeLine(currentVersion: string, latest: string | null, _npmPackage: string | null, installKind?: CliInstallKind, npmPrefix?: string): string | null;
|
|
85
86
|
export type CliUpdateAction = {
|
|
86
87
|
type: 'nudge';
|
|
87
88
|
line: string;
|
package/dist/lib/update-check.js
CHANGED
|
@@ -207,6 +207,20 @@ function classifyCliInstall(env, argv, npmGlobalRoot, probeWritable = true) {
|
|
|
207
207
|
return { kind: 'other' };
|
|
208
208
|
}
|
|
209
209
|
const nodeModulesDir = path.dirname(path.dirname(packageDir)); // …/node_modules
|
|
210
|
+
const npmPrefix = process.platform !== 'win32' && path.basename(path.dirname(nodeModulesDir)) === 'lib'
|
|
211
|
+
? path.dirname(path.dirname(nodeModulesDir)) : undefined;
|
|
212
|
+
// npm's current default may differ from the prefix that installed this
|
|
213
|
+
// executable. A project directory named `lib` is insufficient evidence:
|
|
214
|
+
// the prefix's public bin must resolve to this exact installed entry.
|
|
215
|
+
let linkedGlobalPrefix = false;
|
|
216
|
+
if (npmPrefix) {
|
|
217
|
+
try {
|
|
218
|
+
linkedGlobalPrefix = !fs.existsSync(path.join(npmPrefix, 'package.json')) &&
|
|
219
|
+
fs.realpathSync(path.join(npmPrefix, 'bin', 'skrr')) ===
|
|
220
|
+
fs.realpathSync(path.join(packageDir, 'bin', 'run.js'));
|
|
221
|
+
}
|
|
222
|
+
catch { /* no executable ownership proof */ }
|
|
223
|
+
}
|
|
210
224
|
if (npmGlobalRoot) {
|
|
211
225
|
let resolvedGlobal = npmGlobalRoot;
|
|
212
226
|
try {
|
|
@@ -222,15 +236,13 @@ function classifyCliInstall(env, argv, npmGlobalRoot, probeWritable = true) {
|
|
|
222
236
|
catch {
|
|
223
237
|
/* compare the raw form */
|
|
224
238
|
}
|
|
225
|
-
if (resolvedNodeModules !== resolvedGlobal)
|
|
239
|
+
if (resolvedNodeModules !== resolvedGlobal && !linkedGlobalPrefix)
|
|
226
240
|
return { kind: 'npm-local' };
|
|
227
241
|
}
|
|
228
|
-
else if (
|
|
242
|
+
else if (!linkedGlobalPrefix) {
|
|
229
243
|
// `npm root -g` gave no answer, so prove the global shape from the path
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
// project-local `<project>/node_modules` never matches either — without
|
|
233
|
-
// proof we must not treat it as ours to auto-update.
|
|
244
|
+
// and matching public executable instead. Without ownership proof we
|
|
245
|
+
// must not treat a project's dependency as ours to auto-update.
|
|
234
246
|
return { kind: 'npm-local' };
|
|
235
247
|
}
|
|
236
248
|
let writable = false;
|
|
@@ -245,7 +257,7 @@ function classifyCliInstall(env, argv, npmGlobalRoot, probeWritable = true) {
|
|
|
245
257
|
/* read-only */
|
|
246
258
|
}
|
|
247
259
|
}
|
|
248
|
-
return { kind: 'npm-global', writable };
|
|
260
|
+
return { kind: 'npm-global', writable, ...(npmPrefix ? { npmPrefix } : {}) };
|
|
249
261
|
}
|
|
250
262
|
/**
|
|
251
263
|
* Resolve `npm root -g` for install classification. Runs ONLY inside the
|
|
@@ -269,12 +281,14 @@ async function resolveNpmGlobalRoot(env = process.env) {
|
|
|
269
281
|
return null;
|
|
270
282
|
}
|
|
271
283
|
}
|
|
272
|
-
function cliUpdateCommand(owner) {
|
|
284
|
+
function cliUpdateCommand(owner, npmPrefix) {
|
|
273
285
|
switch (owner) {
|
|
274
286
|
case 'npm-global':
|
|
275
|
-
return
|
|
287
|
+
return npmPrefix
|
|
288
|
+
? `npm install --global --prefix '${npmPrefix.replace(/'/g, "'\\''")}' ${CLI_PACKAGE}@latest`
|
|
289
|
+
: `npm install -g ${CLI_PACKAGE}@latest`;
|
|
276
290
|
case 'npm-local':
|
|
277
|
-
return `npm
|
|
291
|
+
return `Use npm in the project that owns ${CLI_PACKAGE}`;
|
|
278
292
|
case 'dev':
|
|
279
293
|
return 'npm --prefix cli run build';
|
|
280
294
|
case 'bundled':
|
|
@@ -287,16 +301,18 @@ function cliUpdateCommand(owner) {
|
|
|
287
301
|
return 'Update via your install method — see https://skrr.ai';
|
|
288
302
|
}
|
|
289
303
|
}
|
|
290
|
-
function buildNudgeLine(currentVersion, latest, _npmPackage, installKind = 'npm-global') {
|
|
304
|
+
function buildNudgeLine(currentVersion, latest, _npmPackage, installKind = 'npm-global', npmPrefix) {
|
|
291
305
|
if (!latest || !isOlderVersion(currentVersion, latest))
|
|
292
306
|
return null;
|
|
293
|
-
const command = installKind === 'npm-global'
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
307
|
+
const command = installKind === 'npm-global' && !npmPrefix
|
|
308
|
+
? `npm update -g ${CLI_PACKAGE}` : cliUpdateCommand(installKind, npmPrefix);
|
|
309
|
+
const context = installKind === 'dev'
|
|
310
|
+
? ' to rebuild your checkout'
|
|
311
|
+
: '';
|
|
312
|
+
const advice = installKind === 'npm-local'
|
|
313
|
+
? `Update ${CLI_PACKAGE} using the project that owns ${CLI_PACKAGE}.`
|
|
314
|
+
: `Run \`${command}\`${context}.`;
|
|
315
|
+
return `[skrr] Update available: ${currentVersion} → ${latest}. ${advice} Silence with OVERSKY_NO_UPDATE_NUDGE=1.\n`;
|
|
300
316
|
}
|
|
301
317
|
function decideCliUpdateAction(args) {
|
|
302
318
|
const { currentVersion, cache, env, isTTY } = args;
|
|
@@ -327,10 +343,10 @@ function decideCliUpdateAction(args) {
|
|
|
327
343
|
if (attempt?.status === 'failed')
|
|
328
344
|
return {
|
|
329
345
|
type: 'nudge',
|
|
330
|
-
line: `[skrr] CLI update to ${cache.latest} failed. Inspect \`skrr update --status\` or run \`${cliUpdateCommand(install.kind)}\`.\n`,
|
|
346
|
+
line: `[skrr] CLI update to ${cache.latest} failed. Inspect \`skrr update --status\` or run \`${cliUpdateCommand(install.kind, install.npmPrefix)}\`.\n`,
|
|
331
347
|
};
|
|
332
348
|
}
|
|
333
|
-
const line = buildNudgeLine(currentVersion, cache.latest, CLI_PACKAGE, install.kind);
|
|
349
|
+
const line = buildNudgeLine(currentVersion, cache.latest, CLI_PACKAGE, install.kind, install.npmPrefix);
|
|
334
350
|
return line ? { type: 'nudge', line } : null;
|
|
335
351
|
}
|
|
336
352
|
function readCache(env = process.env) {
|
|
@@ -462,10 +478,11 @@ function readCliUpdateStatus(opts) {
|
|
|
462
478
|
installedVersion,
|
|
463
479
|
offeredVersion,
|
|
464
480
|
owner: install.kind,
|
|
481
|
+
...(install.npmPrefix ? { npmPrefix: install.npmPrefix } : {}),
|
|
465
482
|
state,
|
|
466
483
|
autoUpdateAllowed: autoUpdateReason === null,
|
|
467
484
|
autoUpdateReason,
|
|
468
|
-
command: cliUpdateCommand(install.kind),
|
|
485
|
+
command: cliUpdateCommand(install.kind, install.npmPrefix),
|
|
469
486
|
lastCheckAt: matchingCache?.lastCheckAt ?? null,
|
|
470
487
|
lastAttempt,
|
|
471
488
|
logPath: path.join(directory, 'update.log'),
|
|
@@ -591,7 +608,7 @@ async function performCliUpdate(opts) {
|
|
|
591
608
|
env.SKRR_CLI_UPDATE_SESSION_UNAVAILABLE === '1') {
|
|
592
609
|
return {
|
|
593
610
|
...status,
|
|
594
|
-
message: `This CLI is owned by ${install.kind}${install.kind === 'npm-global' ? ' in a shared or external prefix' : ''}. ${cliUpdateCommand(install.kind)}.`,
|
|
611
|
+
message: `This CLI is owned by ${install.kind}${install.kind === 'npm-global' ? ' in a shared or external prefix' : ''}. ${cliUpdateCommand(install.kind, install.npmPrefix)}.`,
|
|
595
612
|
};
|
|
596
613
|
}
|
|
597
614
|
const recoverableAttempt = status.lastAttempt && ['queued', 'running'].includes(status.lastAttempt.status);
|
|
@@ -620,6 +637,7 @@ async function performCliUpdate(opts) {
|
|
|
620
637
|
attemptedAt: Date.now(),
|
|
621
638
|
packageRoot: packageRoot(argv),
|
|
622
639
|
npmPath,
|
|
640
|
+
...(install.npmPrefix ? { npmPrefix: install.npmPrefix } : {}),
|
|
623
641
|
coordinationVersion: api.COORDINATION_VERSION,
|
|
624
642
|
message: 'Waiting for active CLI sessions to exit; the next invocation uses the new version.',
|
|
625
643
|
});
|
|
@@ -649,7 +667,7 @@ async function maybeWarnOutdatedCli(opts) {
|
|
|
649
667
|
currentVersion: opts.currentVersion,
|
|
650
668
|
cache: {
|
|
651
669
|
...cache,
|
|
652
|
-
install: { kind: status.owner, writable: status.autoUpdateAllowed },
|
|
670
|
+
install: { kind: status.owner, writable: status.autoUpdateAllowed, npmPrefix: status.npmPrefix },
|
|
653
671
|
autoUpdate: status.lastAttempt
|
|
654
672
|
? {
|
|
655
673
|
attemptedAt: status.lastAttempt.attemptedAt,
|