@skrr-ai/cli 0.1.44 → 0.1.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/base-command.d.ts +13 -1
  2. package/dist/base-command.js +37 -3
  3. package/dist/commands/agents/actions/create.js +11 -4
  4. package/dist/commands/agents/chat.js +3 -4
  5. package/dist/commands/browser/cloud-agents.d.ts +20 -0
  6. package/dist/commands/browser/cloud-agents.js +43 -0
  7. package/dist/commands/code/handover.d.ts +11 -0
  8. package/dist/commands/code/handover.js +105 -1
  9. package/dist/commands/commitments/handover.d.ts +29 -0
  10. package/dist/commands/commitments/handover.js +99 -0
  11. package/dist/commands/followups/list.js +1 -1
  12. package/dist/commands/followups/watch.js +40 -5
  13. package/dist/commands/initiatives/list.d.ts +1 -0
  14. package/dist/commands/initiatives/list.js +11 -2
  15. package/dist/commands/labels/list.d.ts +28 -0
  16. package/dist/commands/labels/list.js +50 -32
  17. package/dist/commands/tasks/fork.d.ts +32 -0
  18. package/dist/commands/tasks/fork.js +171 -0
  19. package/dist/commands/tasks/handover.d.ts +23 -0
  20. package/dist/commands/tasks/handover.js +205 -0
  21. package/dist/commands/tasks/promote.d.ts +23 -0
  22. package/dist/commands/tasks/promote.js +82 -0
  23. package/dist/commands/tasks/runs.d.ts +16 -0
  24. package/dist/commands/tasks/runs.js +46 -1
  25. package/dist/commands/tasks/show.d.ts +23 -0
  26. package/dist/commands/tasks/show.js +59 -0
  27. package/dist/lib/agentic-stream.js +41 -2
  28. package/dist/lib/api-fetch.d.ts +7 -1
  29. package/dist/lib/api-fetch.js +9 -2
  30. package/dist/lib/auth-core-init.js +11 -4
  31. package/dist/lib/code-handover.d.ts +60 -0
  32. package/dist/lib/code-handover.js +120 -0
  33. package/dist/lib/commitments.d.ts +22 -0
  34. package/dist/lib/commitments.js +42 -0
  35. package/dist/lib/dedicated-wait.js +22 -1
  36. package/dist/lib/followups.d.ts +18 -0
  37. package/dist/lib/followups.js +44 -0
  38. package/dist/lib/initiatives.d.ts +15 -3
  39. package/dist/lib/label-ref.d.ts +2 -0
  40. package/dist/lib/node-adapter.js +6 -0
  41. package/dist/lib/task-extras.d.ts +44 -0
  42. package/dist/lib/task-extras.js +73 -0
  43. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.d.ts +29 -0
  44. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +169 -23
  45. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +91 -5
  46. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.d.ts +8 -1
  47. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.js +7 -0
  48. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessHome.js +45 -0
  49. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessSkillLayout.d.ts +65 -0
  50. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessSkillLayout.js +99 -0
  51. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +3 -1
  52. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +26 -2
  53. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loginLocalhost.js +177 -26
  54. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/profileStateDir.d.ts +119 -0
  55. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/profileStateDir.js +346 -0
  56. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.d.ts +29 -0
  57. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +169 -24
  58. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +93 -7
  59. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.d.ts +8 -1
  60. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.js +7 -0
  61. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessHome.js +45 -0
  62. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessSkillLayout.d.ts +65 -0
  63. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessSkillLayout.js +94 -0
  64. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +3 -1
  65. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +10 -1
  66. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loginLocalhost.js +177 -26
  67. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/profileStateDir.d.ts +119 -0
  68. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/profileStateDir.js +333 -0
  69. package/dist/node_modules/@skrr-ai/auth-core/package.json +11 -1
  70. package/dist/node_modules/@skrr-ai/data-provider/index.js +2746 -2711
  71. package/dist/node_modules/@skrr-ai/inference-broker/package.json +1 -1
  72. package/oclif.manifest.json +34300 -33843
  73. package/package.json +2 -2
@@ -12,6 +12,9 @@ exports.parseGitHubRepo = parseGitHubRepo;
12
12
  exports.evaluateHandoverSafety = evaluateHandoverSafety;
13
13
  exports.formatUnsafeHandover = formatUnsafeHandover;
14
14
  exports.composeHandoverPrompt = composeHandoverPrompt;
15
+ exports.parseSessionExportEnvelope = parseSessionExportEnvelope;
16
+ exports.classifySessionExportError = classifySessionExportError;
17
+ exports.renderSessionTranscript = renderSessionTranscript;
15
18
  exports.matchAgent = matchAgent;
16
19
  const auth_core_1 = require("@skrr-ai/auth-core");
17
20
  /** `owner/name` from any GitHub remote URL shape. */
@@ -72,6 +75,7 @@ function composeHandoverPrompt(input) {
72
75
  '---',
73
76
  `Handed over from a local ${auth_core_1.FIRST_PARTY_HARNESS.displayName} session in ${input.repoFullName} on ${input.branch}${at}.`,
74
77
  input.recentCommits ? `Recent commits:\n${input.recentCommits}` : null,
78
+ input.sessionTranscript ?? null,
75
79
  input.localWorkNotCarried
76
80
  ? 'NOTE: local uncommitted or unpushed work was NOT carried over — you are working from the pushed state of this branch.'
77
81
  : null,
@@ -79,6 +83,122 @@ function composeHandoverPrompt(input) {
79
83
  .filter(Boolean)
80
84
  .join('\n');
81
85
  }
86
+ /**
87
+ * Parse the export envelope, or null when the output is not the contract —
88
+ * an engine that predates `session export` cannot produce `{version, messages[]}`.
89
+ */
90
+ function parseSessionExportEnvelope(stdout) {
91
+ let parsed;
92
+ try {
93
+ parsed = JSON.parse(stdout);
94
+ }
95
+ catch {
96
+ return null;
97
+ }
98
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
99
+ return null;
100
+ const env = parsed;
101
+ if (env.version === undefined || env.version === null)
102
+ return null;
103
+ if (!Array.isArray(env.messages))
104
+ return null;
105
+ return env;
106
+ }
107
+ /**
108
+ * Classify a failed `session export` spawn (an `execFileSync` throw).
109
+ *
110
+ * C6 makes an unknown session id a distinct outcome from a broken engine, and
111
+ * the distinction matters to the operator: "the engine predates the contract"
112
+ * is an upgrade, "that session is not there" is a different id. The contract
113
+ * does not pin exit codes, so the signal is the engine's own stderr — which is
114
+ * also what the refusal relays verbatim either way.
115
+ */
116
+ function classifySessionExportError(err) {
117
+ const stderr = (err.stderr ?? '').trim();
118
+ // "session <id> not found" is an unknown session; "unknown command: session"
119
+ // is an engine that predates the contract. Both contain the words 'session'
120
+ // and 'unknown', so the command-not-understood phrasing is excluded FIRST.
121
+ const commandNotUnderstood = /unknown (command|subcommand)|unrecognized|not a valid|did you mean|usage:/i.test(stderr);
122
+ const sessionMissing = /(no such|cannot find|could not find|not found|does not exist|unknown)[\s\S]{0,60}session/i.test(stderr) || /session[\s\S]{0,60}(no such|not found|does not exist|missing|unknown)/i.test(stderr);
123
+ if (!commandNotUnderstood && sessionMissing) {
124
+ return { kind: 'unknown-session', detail: stderr || (err.message ?? '') };
125
+ }
126
+ return { kind: 'unsupported', detail: stderr || err.message || 'session export failed' };
127
+ }
128
+ /** Best-effort text of one exported message, tolerant of the fork's shape. */
129
+ function sessionMessageText(message) {
130
+ const m = (message ?? {});
131
+ const role = typeof m.role === 'string' && m.role ? m.role : 'message';
132
+ const content = m.content ?? m.text;
133
+ if (typeof content === 'string')
134
+ return { role, text: content };
135
+ if (Array.isArray(content)) {
136
+ const parts = content
137
+ .map((p) => p && typeof p === 'object' && typeof p.text === 'string'
138
+ ? p.text
139
+ : '')
140
+ .filter(Boolean);
141
+ if (parts.length)
142
+ return { role, text: parts.join('\n') };
143
+ }
144
+ // A message with no readable text is still a fact of the transcript; keep a
145
+ // marker rather than silently dropping it (the C4 honesty rule).
146
+ return { role, text: '[non-text content]' };
147
+ }
148
+ /**
149
+ * Render the exported session into the handover prompt, bounded.
150
+ *
151
+ * `POST /api/cloud-coding/tasks` has no transcript field — `prompt` is the only
152
+ * channel that reaches the receiving agent (the zod schema strips unknown keys
153
+ * and caps prompt at 50000 chars). So the transcript rides inside the prompt,
154
+ * clipped from the FRONT: the recent tail is what continuation needs, and the
155
+ * clip is declared in the block itself, never inferred (C3's rule applied to
156
+ * our own truncation too).
157
+ */
158
+ function renderSessionTranscript(exported, budget) {
159
+ const messages = exported.messages ?? [];
160
+ const rendered = messages.map((m) => {
161
+ const { role, text } = sessionMessageText(m);
162
+ return `${role}:\n${text}`;
163
+ });
164
+ const sid = exported.sessionId ? ` ${exported.sessionId}` : '';
165
+ const version = exported.version !== undefined ? `, export v${String(exported.version)}` : '';
166
+ const header = `Carried session transcript (engine session${sid}${version}):`;
167
+ const notes = [];
168
+ if (exported.truncated) {
169
+ notes.push('The engine marked this export TRUNCATED — earlier session content is not in it.');
170
+ }
171
+ if (exported.redactions) {
172
+ notes.push(`The engine declared redactions: ${JSON.stringify(exported.redactions)}`);
173
+ }
174
+ let carried = rendered.length;
175
+ let clipped = false;
176
+ const assemble = () => {
177
+ const clipNote = clipped
178
+ ? `(${rendered.length - carried} earlier message(s) clipped to fit the handover prompt budget.)\n`
179
+ : '';
180
+ const body = [
181
+ header,
182
+ ...notes,
183
+ clipNote + rendered.slice(rendered.length - carried).join('\n\n'),
184
+ ]
185
+ .filter(Boolean)
186
+ .join('\n');
187
+ return body;
188
+ };
189
+ // Drop oldest first — the tail is what a continuation needs.
190
+ while (carried > 0 && assemble().length > budget) {
191
+ carried -= 1;
192
+ clipped = true;
193
+ }
194
+ return {
195
+ text: assemble(),
196
+ carriedCount: carried,
197
+ totalCount: rendered.length,
198
+ engineTruncated: exported.truncated === true,
199
+ clipped,
200
+ };
201
+ }
82
202
  /** Resolve `--to` against the agents this user can see: exact id wins, then name. */
83
203
  function matchAgent(needle, agents) {
84
204
  const query = needle.trim();
@@ -277,6 +277,18 @@ export declare const commitmentApi: {
277
277
  executeActionProposal: (id: string, proposalId: string) => Promise<Record<string, unknown>>;
278
278
  preflight: (id: string, nextRunCount?: number) => Promise<CommitmentPreflightReport>;
279
279
  effectivePolicy: (id: string) => Promise<Record<string, unknown>>;
280
+ /**
281
+ * Executor rebind — `POST /api/commitments/:id/executor` (impl contract B4,
282
+ * docs/architecture/execution-handover-2026-09-17.md P4).
283
+ *
284
+ * `commitment.agentId` is single-valued by design, so moving it is an
285
+ * explicit recorded act: the server appends an `executorHandovers` entry
286
+ * rather than letting a bare PATCH rewrite who owns the loop without a trail.
287
+ */
288
+ setExecutor: (id: string, body: {
289
+ agentId: string;
290
+ reason?: string;
291
+ }) => Promise<Record<string, unknown>>;
280
292
  pause: (id: string) => Promise<Commitment>;
281
293
  resume: (id: string) => Promise<Commitment>;
282
294
  complete: (id: string) => Promise<Commitment>;
@@ -557,6 +569,16 @@ export declare function describeAutonomyLine(commitment: CommitmentView, latestC
557
569
  policyTrace?: Array<Record<string, unknown>>;
558
570
  } | null;
559
571
  } | null): string;
572
+ /**
573
+ * One-line rendering of `commitment.executorBlocked` — the B4/A7 signal that
574
+ * the executor's targets are all unusable: `{reason:'quota'|'offline', until?,
575
+ * provider?}`.
576
+ *
577
+ * Defensive by contract: the field is being added server-side, so its absence
578
+ * is the normal case and means "no information", not "not blocked". Returns
579
+ * null rather than printing a guess.
580
+ */
581
+ export declare function describeExecutorBlocked(commitment: CommitmentView): string | null;
560
582
  export declare function renderCommitmentSummary(commitment: CommitmentView, log: (line: string) => void,
561
583
  /**
562
584
  * The most recent check, when the caller has one. Optional because pause /
@@ -19,6 +19,7 @@ exports.renderCommitmentList = renderCommitmentList;
19
19
  exports.effectiveAutonomyMode = effectiveAutonomyMode;
20
20
  exports.extractStandstillFromCheck = extractStandstillFromCheck;
21
21
  exports.describeAutonomyLine = describeAutonomyLine;
22
+ exports.describeExecutorBlocked = describeExecutorBlocked;
22
23
  exports.renderCommitmentSummary = renderCommitmentSummary;
23
24
  exports.describeWakeDecision = describeWakeDecision;
24
25
  exports.renderCheckList = renderCheckList;
@@ -176,6 +177,15 @@ exports.commitmentApi = {
176
177
  ...(typeof nextRunCount === 'number' ? { nextRunCount } : {}),
177
178
  }),
178
179
  effectivePolicy: (id) => data_provider_1.request.get(`${base(id)}/effective-policy`),
180
+ /**
181
+ * Executor rebind — `POST /api/commitments/:id/executor` (impl contract B4,
182
+ * docs/architecture/execution-handover-2026-09-17.md P4).
183
+ *
184
+ * `commitment.agentId` is single-valued by design, so moving it is an
185
+ * explicit recorded act: the server appends an `executorHandovers` entry
186
+ * rather than letting a bare PATCH rewrite who owns the loop without a trail.
187
+ */
188
+ setExecutor: (id, body) => data_provider_1.request.post(`${base(id)}/executor`, body),
179
189
  pause: (id) => data_provider_1.request.post(`${base(id)}/pause`, {}),
180
190
  resume: (id) => data_provider_1.request.post(`${base(id)}/resume`, {}),
181
191
  complete: (id) => data_provider_1.request.post(`${base(id)}/complete`, {}),
@@ -738,6 +748,30 @@ function describeAutonomyLine(commitment, latestCheck) {
738
748
  }
739
749
  return (0, commitment_product_1.productModeLabel)(stated);
740
750
  }
751
+ /**
752
+ * One-line rendering of `commitment.executorBlocked` — the B4/A7 signal that
753
+ * the executor's targets are all unusable: `{reason:'quota'|'offline', until?,
754
+ * provider?}`.
755
+ *
756
+ * Defensive by contract: the field is being added server-side, so its absence
757
+ * is the normal case and means "no information", not "not blocked". Returns
758
+ * null rather than printing a guess.
759
+ */
760
+ function describeExecutorBlocked(commitment) {
761
+ const blocked = commitment.executorBlocked;
762
+ if (!blocked || typeof blocked !== 'object')
763
+ return null;
764
+ const b = blocked;
765
+ const reason = typeof b.reason === 'string' ? b.reason : '';
766
+ const provider = typeof b.provider === 'string' ? b.provider : '';
767
+ const until = typeof b.until === 'string' ? b.until : '';
768
+ const label = reason === 'quota'
769
+ ? `quota exhausted${provider ? ` on ${provider}` : ''}`
770
+ : reason === 'offline'
771
+ ? 'executor runtime offline'
772
+ : reason || 'blocked';
773
+ return `${label}${until ? ` until ${until}` : ''}`;
774
+ }
741
775
  function renderCommitmentSummary(commitment, log,
742
776
  /**
743
777
  * The most recent check, when the caller has one. Optional because pause /
@@ -750,6 +784,14 @@ latestCheck) {
750
784
  log(`Kind: ${commitment.kind ?? 'achieve'}`);
751
785
  log(`Status: ${commitment.status ?? '-'}`);
752
786
  log(`Agent: ${commitment.agentId ?? '-'}`);
787
+ // A commitment whose executor cannot run anywhere should say so on the
788
+ // header, not as a silent skip series — "executor cannot run until <date> —
789
+ // reassign or add a fallback target" (design doc P4).
790
+ const executorBlocked = describeExecutorBlocked(commitment);
791
+ if (executorBlocked) {
792
+ log(`Executor: blocked — ${executorBlocked}`);
793
+ log(` Fix: reassign with \`skrr commitments handover ${commitment.id ?? '<id>'} --to <agent>\` or add a fallback target`);
794
+ }
753
795
  if (commitment.goalId)
754
796
  log(`Goal: ${commitment.goalId}`);
755
797
  if (commitment.northstarId)
@@ -27,9 +27,30 @@ const PLANNED_REPLACEMENT_PROGRESS = {
27
27
  dedicated_runtime_image_update: 'replacing the machine',
28
28
  dedicated_runtime_image_update_rolling_back: 'replacing the machine again',
29
29
  };
30
+ /**
31
+ * The same two labels, keyed by the image move's OWN status.
32
+ *
33
+ * `stateReason` describes the state a lease is in, so anything that moves it
34
+ * again during the move can leave it behind: a watched `update-image --wait`
35
+ * still printed the unplanned-recovery hint through the `recovering` phase it
36
+ * had itself asked for (OSK-9933, live 2026-09-17, after a provision attempt
37
+ * lost its fence and was retried). `health.image.lastUpdate.status` says the
38
+ * move is under way for as long as it is, which is the question being asked.
39
+ */
40
+ const IN_FLIGHT_IMAGE_UPDATE_PROGRESS = {
41
+ applying: 'replacing the machine',
42
+ rolling_back: 'replacing the machine again',
43
+ };
44
+ function plannedReplacementProgress(lease) {
45
+ const byReason = lease.stateReason ? PLANNED_REPLACEMENT_PROGRESS[lease.stateReason] : undefined;
46
+ if (byReason)
47
+ return byReason;
48
+ const imageStatus = lease.health?.image?.lastUpdate?.status;
49
+ return imageStatus ? IN_FLIGHT_IMAGE_UPDATE_PROGRESS[imageStatus] : undefined;
50
+ }
30
51
  function progressOf(lease) {
31
52
  const state = (0, dedicated_machines_1.dedicatedLeaseState)(lease);
32
- const planned = lease.stateReason ? PLANNED_REPLACEMENT_PROGRESS[lease.stateReason] : undefined;
53
+ const planned = plannedReplacementProgress(lease);
33
54
  if (state === 'recovering' && planned)
34
55
  return planned;
35
56
  if (state === 'recovering') {
@@ -20,6 +20,24 @@ export declare function settledLine(id: string, result: Record<string, unknown>,
20
20
  export declare const followUpIdArg: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
21
21
  export { FOLLOWUP_KINDS, FOLLOWUP_STATES, FOLLOWUP_RESOLVE_OUTCOMES, type FollowUpKind, } from '@skrr-ai/data-provider';
22
22
  export declare const WATCH_WAIT_KINDS: readonly ["clock", "event", "poll_until"];
23
+ export type WatchWaitKind = (typeof WATCH_WAIT_KINDS)[number];
24
+ /**
25
+ * OSK-10407 — `--deadline`, `--check-every` and `--max-checks` are predicate-form
26
+ * flags. `skrr followups watch` accepted them on a recipe watch and dropped
27
+ * them, so a caller who wrote `--deadline 3h` was told the watch was armed and
28
+ * held a stopping condition that was never stored.
29
+ *
30
+ * Dropping them is right; dropping them SILENTLY is the defect. Refuse instead,
31
+ * and name the flag that bounds THIS wait kind — a refusal that only says "not
32
+ * here" leaves the caller with no way to express the thing they wanted.
33
+ *
34
+ * @returns the refusal message, or null when nothing predicate-shaped was passed.
35
+ */
36
+ export declare function recipeWatchBoundsRefusal(waitKind: string | undefined, passed: {
37
+ deadline?: unknown;
38
+ checkEvery?: unknown;
39
+ maxChecks?: unknown;
40
+ }): string | null;
23
41
  export interface FollowUpRow {
24
42
  id: string;
25
43
  kind?: string;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.followupsApi = exports.WATCH_WAIT_KINDS = exports.FOLLOWUP_RESOLVE_OUTCOMES = exports.FOLLOWUP_STATES = exports.FOLLOWUP_KINDS = exports.followUpIdArg = void 0;
4
4
  exports.settledLine = settledLine;
5
+ exports.recipeWatchBoundsRefusal = recipeWatchBoundsRefusal;
5
6
  exports.resolveFollowUpAgent = resolveFollowUpAgent;
6
7
  exports.parseSubjectRef = parseSubjectRef;
7
8
  exports.followupSummaryLine = followupSummaryLine;
@@ -71,6 +72,49 @@ Object.defineProperty(exports, "FOLLOWUP_KINDS", { enumerable: true, get: functi
71
72
  Object.defineProperty(exports, "FOLLOWUP_STATES", { enumerable: true, get: function () { return data_provider_2.FOLLOWUP_STATES; } });
72
73
  Object.defineProperty(exports, "FOLLOWUP_RESOLVE_OUTCOMES", { enumerable: true, get: function () { return data_provider_2.FOLLOWUP_RESOLVE_OUTCOMES; } });
73
74
  exports.WATCH_WAIT_KINDS = ['clock', 'event', 'poll_until'];
75
+ /**
76
+ * What actually bounds a recipe watch, per wait kind. A recipe watch ends
77
+ * through its `--wait`, never through the predicate form's `--deadline` /
78
+ * `--check-every` / `--max-checks`: the create route stores none of those for
79
+ * `form: 'recipe'`, so the row comes back `deadlineAt: null`.
80
+ */
81
+ const RECIPE_WATCH_BOUND_BY_WAIT = Object.freeze({
82
+ clock: '--wait-at, the instant it fires',
83
+ event: '--wait-fallback, the SLA backstop for an event that never arrives',
84
+ poll_until: '--wait-max-attempts and --wait-fallback',
85
+ });
86
+ /**
87
+ * OSK-10407 — `--deadline`, `--check-every` and `--max-checks` are predicate-form
88
+ * flags. `skrr followups watch` accepted them on a recipe watch and dropped
89
+ * them, so a caller who wrote `--deadline 3h` was told the watch was armed and
90
+ * held a stopping condition that was never stored.
91
+ *
92
+ * Dropping them is right; dropping them SILENTLY is the defect. Refuse instead,
93
+ * and name the flag that bounds THIS wait kind — a refusal that only says "not
94
+ * here" leaves the caller with no way to express the thing they wanted.
95
+ *
96
+ * @returns the refusal message, or null when nothing predicate-shaped was passed.
97
+ */
98
+ function recipeWatchBoundsRefusal(waitKind, passed) {
99
+ const offenders = [
100
+ ['--deadline', passed.deadline],
101
+ ['--check-every', passed.checkEvery],
102
+ ['--max-checks', passed.maxChecks],
103
+ ]
104
+ .filter(([, value]) => value !== undefined)
105
+ .map(([flag]) => flag);
106
+ if (offenders.length === 0)
107
+ return null;
108
+ const bound = RECIPE_WATCH_BOUND_BY_WAIT[waitKind];
109
+ const list = offenders.join(offenders.length === 2 ? ' and ' : ', ');
110
+ const verb = offenders.length === 1 ? 'bounds' : 'bound';
111
+ return [
112
+ `${list} ${verb} a predicate watch (--url + --until) and ${offenders.length === 1 ? 'is' : 'are'} not stored on a recipe watch.`,
113
+ bound
114
+ ? `A recipe watch ends through its --wait ${waitKind}: ${bound}.`
115
+ : 'A recipe watch ends through its --wait.',
116
+ ].join(' ');
117
+ }
74
118
  /**
75
119
  * Which agent a follow-up belongs to.
76
120
  *
@@ -29,10 +29,22 @@ export type InitiativeView = {
29
29
  label?: string;
30
30
  };
31
31
  };
32
+ /**
33
+ * The shape `GET /api/initiatives` actually returns. It is NOT the
34
+ * `{data, has_more, next_cursor}` envelope the Task routes use, and typing it
35
+ * that way is how `skrr initiatives list` came to print "Nothing is waiting on
36
+ * you." against 411 pending proposals — `response.data` was always undefined.
37
+ *
38
+ * `nextCursor` is an opaque keyset token; hand it straight back as `cursor`.
39
+ */
32
40
  export type InitiativeListResponse = {
33
- data?: InitiativeView[];
34
- has_more?: boolean;
35
- next_cursor?: string | null;
41
+ initiatives?: InitiativeView[];
42
+ counts?: {
43
+ total?: number;
44
+ active?: number;
45
+ byState?: Record<string, number>;
46
+ };
47
+ nextCursor?: string | null;
36
48
  };
37
49
  export declare const initiativeApi: {
38
50
  list: (query?: Parameters<typeof withQuery>[1]) => Promise<InitiativeListResponse>;
@@ -47,6 +47,8 @@ export interface LabelRow {
47
47
  color?: string;
48
48
  applicableTo?: string[];
49
49
  usageCount?: number;
50
+ /** The vocabulary's own order — the tie-break when two labels are used equally. */
51
+ position?: number;
50
52
  archivedAt?: string | null;
51
53
  [key: string]: unknown;
52
54
  }
@@ -332,6 +332,12 @@ function createNodeAdapter(opts = {}) {
332
332
  err.status = res.status;
333
333
  err.body = text;
334
334
  err.method = method;
335
+ // The server stamps every response with the id it logs against
336
+ // (`requestLogger`). Carrying it means a refused call can be found in
337
+ // the logs instead of guessed at: with several clients on one account,
338
+ // "which of these requests was mine" is otherwise unanswerable, and a
339
+ // 409 that belonged to another session was nearly filed as a CLI bug.
340
+ err.requestId = res.headers.get('x-request-id') || undefined;
335
341
  throw err;
336
342
  }
337
343
  if (options?.responseType === 'arraybuffer') {
@@ -57,6 +57,50 @@ export declare const taskSelfScheduleApi: {
57
57
  * parity check.
58
58
  */
59
59
  export declare function taskCompletePath(taskId: string): string;
60
+ /**
61
+ * `POST /api/tasks/:taskId/runs/:runId/handover` — the B1 admission path
62
+ * (docs/architecture/execution-handover-impl-contracts.md): mint a new run on a
63
+ * different agent/target, resumed from the source run's Resume Point.
64
+ *
65
+ * `apiFetch` rather than `request.post`: a handover ADMITS a run, so the write
66
+ * needs the shared `X-Idempotency-Key` stamping apiFetch applies — a retried
67
+ * request must not double-admit. Kept in the lib so `domain-api-parity` sees
68
+ * the endpoint — a command that builds its own `/api/tasks/...` string is
69
+ * invisible to the coverage check.
70
+ */
71
+ export declare function taskRunHandover(taskId: string, runId: string, body: Record<string, unknown>): Promise<Record<string, unknown>>;
72
+ /**
73
+ * `POST /api/tasks/:taskId/runs/:runId/fork` — B8. Admits a SIBLING run from
74
+ * the same Resume Point, so both attempts are live and neither is canonical
75
+ * until `promote`. Distinct from handover, which continues the work once.
76
+ */
77
+ export declare function taskRunFork(taskId: string, runId: string, body: Record<string, unknown>): Promise<Record<string, unknown>>;
78
+ /**
79
+ * `POST /api/tasks/:taskId/runs/:runId/promote` — B8. Fixes one branch of a
80
+ * parallel group as canonical and CANCELS the live losers, which is why it has
81
+ * a surface of its own rather than riding a flag on fork.
82
+ */
83
+ export declare function taskRunPromote(taskId: string, runId: string, body: Record<string, unknown>): Promise<Record<string, unknown>>;
84
+ /** The wire shape of an execution target: `{kind, ref?}`, never a bare string. */
85
+ export interface ExecutionTargetRef {
86
+ kind: 'harness' | 'hosted-machine' | 'agent-worker';
87
+ ref?: string;
88
+ }
89
+ /**
90
+ * Turn a `--harness` value into the `executionTargetRef` the API parses.
91
+ *
92
+ * The route validates an OBJECT (`{kind, ref?}`); passing the flag through as a
93
+ * string failed its zod parse, so every `--harness` invocation returned
94
+ * `400 Invalid request data` — the harness axis, which is half of what handover
95
+ * is for, was unreachable from the CLI and untested.
96
+ *
97
+ * The kind is INFERRED rather than asked for, because an operator naming a
98
+ * machine should not also have to name its category: the two managed selectors
99
+ * are reserved words, a `hosted-machine:<sessionId>` form addresses one
100
+ * session, and everything else is a harness id — which is what the flag is
101
+ * called.
102
+ */
103
+ export declare function parseExecutionTargetRef(value: string): ExecutionTargetRef;
60
104
  export declare const taskChatApi: {
61
105
  transcriptSources: (id: string, query?: Query) => Promise<Json>;
62
106
  reindexTranscript: (id: string) => Promise<Json>;
@@ -2,6 +2,10 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.taskChatApi = exports.taskSelfScheduleApi = exports.taskLookupApi = exports.taskWorkflowApi = void 0;
4
4
  exports.taskCompletePath = taskCompletePath;
5
+ exports.taskRunHandover = taskRunHandover;
6
+ exports.taskRunFork = taskRunFork;
7
+ exports.taskRunPromote = taskRunPromote;
8
+ exports.parseExecutionTargetRef = parseExecutionTargetRef;
5
9
  /**
6
10
  * task-extras.ts — the `/api/tasks` endpoints that have no `dataService.*`
7
11
  * wrapper, gathered so every one of them is reachable from `skrr`.
@@ -18,6 +22,7 @@ exports.taskCompletePath = taskCompletePath;
18
22
  */
19
23
  const data_provider_1 = require("@skrr-ai/data-provider");
20
24
  const triggers_1 = require("./triggers");
25
+ const api_fetch_1 = require("./api-fetch");
21
26
  const MOUNT = '/api/tasks';
22
27
  const base = (id) => `${MOUNT}/${encodeURIComponent(id)}`;
23
28
  exports.taskWorkflowApi = {
@@ -78,6 +83,74 @@ exports.taskSelfScheduleApi = {
78
83
  function taskCompletePath(taskId) {
79
84
  return `${base(taskId)}/complete`;
80
85
  }
86
+ /**
87
+ * `POST /api/tasks/:taskId/runs/:runId/handover` — the B1 admission path
88
+ * (docs/architecture/execution-handover-impl-contracts.md): mint a new run on a
89
+ * different agent/target, resumed from the source run's Resume Point.
90
+ *
91
+ * `apiFetch` rather than `request.post`: a handover ADMITS a run, so the write
92
+ * needs the shared `X-Idempotency-Key` stamping apiFetch applies — a retried
93
+ * request must not double-admit. Kept in the lib so `domain-api-parity` sees
94
+ * the endpoint — a command that builds its own `/api/tasks/...` string is
95
+ * invisible to the coverage check.
96
+ */
97
+ function taskRunHandover(taskId, runId, body) {
98
+ return (0, api_fetch_1.apiFetch)(`${base(taskId)}/runs/${encodeURIComponent(runId)}/handover`, {
99
+ method: 'POST',
100
+ body,
101
+ });
102
+ }
103
+ /**
104
+ * `POST /api/tasks/:taskId/runs/:runId/fork` — B8. Admits a SIBLING run from
105
+ * the same Resume Point, so both attempts are live and neither is canonical
106
+ * until `promote`. Distinct from handover, which continues the work once.
107
+ */
108
+ function taskRunFork(taskId, runId, body) {
109
+ return (0, api_fetch_1.apiFetch)(`${base(taskId)}/runs/${encodeURIComponent(runId)}/fork`, {
110
+ method: 'POST',
111
+ body,
112
+ });
113
+ }
114
+ /**
115
+ * `POST /api/tasks/:taskId/runs/:runId/promote` — B8. Fixes one branch of a
116
+ * parallel group as canonical and CANCELS the live losers, which is why it has
117
+ * a surface of its own rather than riding a flag on fork.
118
+ */
119
+ function taskRunPromote(taskId, runId, body) {
120
+ return (0, api_fetch_1.apiFetch)(`${base(taskId)}/runs/${encodeURIComponent(runId)}/promote`, {
121
+ method: 'POST',
122
+ body,
123
+ });
124
+ }
125
+ /** Selectors the platform reserves for its own managed targets. */
126
+ const AGENT_WORKER_SELECTORS = new Set(['agent-worker', '__oversky_worker__']);
127
+ const HOSTED_MACHINE_SELECTORS = new Set(['hosted-machine', '__oversky_hosted_machine__']);
128
+ /**
129
+ * Turn a `--harness` value into the `executionTargetRef` the API parses.
130
+ *
131
+ * The route validates an OBJECT (`{kind, ref?}`); passing the flag through as a
132
+ * string failed its zod parse, so every `--harness` invocation returned
133
+ * `400 Invalid request data` — the harness axis, which is half of what handover
134
+ * is for, was unreachable from the CLI and untested.
135
+ *
136
+ * The kind is INFERRED rather than asked for, because an operator naming a
137
+ * machine should not also have to name its category: the two managed selectors
138
+ * are reserved words, a `hosted-machine:<sessionId>` form addresses one
139
+ * session, and everything else is a harness id — which is what the flag is
140
+ * called.
141
+ */
142
+ function parseExecutionTargetRef(value) {
143
+ const trimmed = value.trim();
144
+ if (AGENT_WORKER_SELECTORS.has(trimmed))
145
+ return { kind: 'agent-worker' };
146
+ if (HOSTED_MACHINE_SELECTORS.has(trimmed))
147
+ return { kind: 'hosted-machine' };
148
+ const hosted = /^hosted-machine:(.+)$/.exec(trimmed);
149
+ if (hosted)
150
+ return { kind: 'hosted-machine', ref: hosted[1] };
151
+ const harness = /^harness:(.+)$/.exec(trimmed);
152
+ return { kind: 'harness', ref: harness ? harness[1] : trimmed };
153
+ }
81
154
  exports.taskChatApi = {
82
155
  transcriptSources: (id, query = {}) => data_provider_1.request.get((0, triggers_1.withQuery)(`${base(id)}/transcript/sources`, query)),
83
156
  reindexTranscript: (id) => data_provider_1.request.post(`${base(id)}/transcript/reindex`, {}),
@@ -36,6 +36,14 @@ export declare function initCredEnvelope(profile: string): Promise<void>;
36
36
  * Reset state. With `clearOnDisk` deletes the wrapped DEK file too —
37
37
  * used by `oversky logout` / `sky logout`. Zeroes the in-memory DEK
38
38
  * before dropping state for forward secrecy on the active credential.
39
+ *
40
+ * It deletes EVERY location this profile's key can occupy, plus the
41
+ * consolidation marker. `profileStateDir.ts` never deletes anything and says so;
42
+ * this is the one caller that must, and the reason is not symmetry — a logout
43
+ * that removed only the canonical copy would leave the pre-consolidation copy
44
+ * behind, and the next init would adopt it straight back. That is a deleted
45
+ * credential key resurrecting itself, which is a worse bug than the one this
46
+ * whole change is fixing.
39
47
  */
40
48
  export declare function resetCredEnvelope(opts?: {
41
49
  profile?: string;
@@ -49,6 +57,17 @@ export declare function __resetShutdownGuardForTest(): void;
49
57
  * predicate used by sync transforms; never throws.
50
58
  */
51
59
  export declare function isCredEnvelopeActive(): boolean;
60
+ /**
61
+ * True while this profile holds two usable and different wrapped DEKs.
62
+ *
63
+ * Read by `deviceIdentityBridge` before it migrates anything of its own. The
64
+ * device private key is sealed with whichever DEK is active, so moving it to the
65
+ * canonical directory while the two binaries are still on DIFFERENT DEKs hands
66
+ * the other one a key it cannot open — it regenerates, overwrites, and the two
67
+ * processes then destroy each other's device identity on every start. Unresolved
68
+ * conflicts freeze the whole profile's layout, not just the DEK's.
69
+ */
70
+ export declare function hasCredEnvelopePathConflict(): boolean;
52
71
  /**
53
72
  * Operator-readable summary of envelope state. Used by `oversky status`
54
73
  * and CLI debug commands. Never includes the DEK or any key material.
@@ -65,6 +84,16 @@ export declare function describeCredEnvelopeState(): {
65
84
  kekKind?: string | null;
66
85
  profile?: string;
67
86
  kekRotated?: boolean;
87
+ /**
88
+ * Set when this profile holds two usable and DIFFERENT wrapped DEKs and the
89
+ * consolidation refused to pick one (OSK-10314). The envelope is fully
90
+ * operational — this process reads the key it has always read — but the other
91
+ * binary is holding a different one and cannot read what this one seals.
92
+ */
93
+ pathConflict?: {
94
+ canonicalPath: string;
95
+ legacyPath: string;
96
+ };
68
97
  };
69
98
  /**
70
99
  * Sync write transform. Returns the wrapped serialized form when active.