@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.
@@ -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 child = spawn(request.npmPath, ['install', '-g', `@skrr-ai/cli@${request.version}`], {
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
  });
@@ -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 rootResult = await execFileAsync(attempt.npmPath, ['root', '-g'], {
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 backgroundLoopLines(agentId: string, relationshipLoopsSkipped: boolean): string[];
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 backgroundLoopLines(agentId, relationshipLoopsSkipped) {
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({ message: err instanceof Error ? err.message : String(err), code: 'LOOKBACK_INVALID_FIELD', exit: 2, retryable: false });
25
+ this.failWithCliError((0, lookbackPlans_1.lookbackInputFailure)(err));
24
26
  return;
25
27
  }
26
28
  try {
@@ -0,0 +1,6 @@
1
+ import { BaseCommand } from '../../../base-command';
2
+ export default class LookbackPlanExample extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ run(): Promise<void>;
6
+ }
@@ -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({ message: err instanceof Error ? err.message : String(err), code: 'LOOKBACK_INVALID_FIELD', exit: 2, retryable: false });
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 — the reminder follow-up';
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: 'Delivery endpoint id (EndpointRegistry)' }),
39
- 'endpoint-target': core_1.Flags.string({ description: 'Channel target for channel endpoints' }),
40
- pin: core_1.Flags.string({ description: 'Conversation id the reminder was authored in' }),
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 (EndpointRegistry)' }),
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: 'Conversation id the watch was authored in' }),
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 Error(`${parsed.code}: ${parsed.message}`);
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
- kind: CliInstallKind;
81
- writable?: boolean;
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;
@@ -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 (!/[/\\](lib|npm)[/\\]node_modules$/.test(nodeModulesDir)) {
242
+ else if (!linkedGlobalPrefix) {
229
243
  // `npm root -g` gave no answer, so prove the global shape from the path
230
- // instead: npm's unix global root is always `<prefix>/lib/node_modules`
231
- // (incl. nvm/volta) and on Windows `%APPDATA%\npm\node_modules`. A
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 `npm install -g ${CLI_PACKAGE}@latest`;
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 update ${CLI_PACKAGE}`;
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' ? `npm update -g ${CLI_PACKAGE}` : cliUpdateCommand(installKind);
294
- const context = installKind === 'npm-local'
295
- ? ' in the owning project'
296
- : installKind === 'dev'
297
- ? ' to rebuild your checkout'
298
- : '';
299
- return `[skrr] Update available: ${currentVersion} → ${latest}. Run \`${command}\`${context}. Silence with OVERSKY_NO_UPDATE_NUDGE=1.\n`;
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,