@skrr-ai/cli 0.1.50 → 0.1.51

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 (100) hide show
  1. package/dist/base-command.js +59 -2
  2. package/dist/commands/agents/chat.d.ts +14 -0
  3. package/dist/commands/agents/chat.js +37 -1
  4. package/dist/commands/agents/show.d.ts +9 -0
  5. package/dist/commands/agents/show.js +53 -0
  6. package/dist/commands/agents/update.d.ts +9 -0
  7. package/dist/commands/agents/update.js +27 -0
  8. package/dist/commands/balance/show.d.ts +34 -0
  9. package/dist/commands/balance/show.js +92 -2
  10. package/dist/commands/commitments/doctor.d.ts +1 -0
  11. package/dist/commands/commitments/doctor.js +19 -4
  12. package/dist/commands/commitments/effective-policy.js +15 -1
  13. package/dist/commands/commitments/next-tick.js +3 -1
  14. package/dist/commands/commitments/preflight.js +10 -0
  15. package/dist/commands/commitments/show.js +11 -1
  16. package/dist/commands/daemon/config.d.ts +15 -0
  17. package/dist/commands/daemon/config.js +38 -0
  18. package/dist/commands/daemon/index.js +1 -0
  19. package/dist/commands/daemon/status.js +15 -1
  20. package/dist/commands/harnesses/models.d.ts +54 -0
  21. package/dist/commands/harnesses/models.js +100 -7
  22. package/dist/commands/harnesses/show.d.ts +11 -0
  23. package/dist/commands/harnesses/show.js +35 -0
  24. package/dist/commands/harnesses/update.d.ts +28 -1
  25. package/dist/commands/harnesses/update.js +73 -10
  26. package/dist/commands/outbox/list.d.ts +12 -1
  27. package/dist/commands/outbox/list.js +31 -2
  28. package/dist/commands/revoke-daemon.js +9 -0
  29. package/dist/commands/spaces/create.d.ts +1 -0
  30. package/dist/commands/spaces/create.js +17 -0
  31. package/dist/commands/spaces/show.js +5 -0
  32. package/dist/commands/spaces/update.d.ts +1 -0
  33. package/dist/commands/spaces/update.js +16 -0
  34. package/dist/commands/tasks/create.d.ts +1 -0
  35. package/dist/commands/tasks/create.js +5 -0
  36. package/dist/commands/tasks/show.js +45 -0
  37. package/dist/commands/tasks/update.d.ts +1 -0
  38. package/dist/commands/tasks/update.js +21 -0
  39. package/dist/commands/tasks/upsert.d.ts +1 -0
  40. package/dist/commands/tasks/upsert.js +16 -0
  41. package/dist/lib/agentic-stream.d.ts +68 -1
  42. package/dist/lib/agentic-stream.js +294 -7
  43. package/dist/lib/auth-core-init.d.ts +2 -10
  44. package/dist/lib/auth-core-init.js +20 -67
  45. package/dist/lib/auth-failure.d.ts +33 -0
  46. package/dist/lib/auth-failure.js +65 -0
  47. package/dist/lib/commitments.d.ts +21 -1
  48. package/dist/lib/commitments.js +34 -0
  49. package/dist/lib/daemonBroker.d.ts +14 -0
  50. package/dist/lib/daemonBroker.js +64 -12
  51. package/dist/lib/delegated-cli.js +4 -1
  52. package/dist/lib/exec-runtime-binary.d.ts +3 -1
  53. package/dist/lib/exec-runtime-binary.js +4 -2
  54. package/dist/lib/first-party-harness-broker.js +4 -1
  55. package/dist/lib/harnesses.d.ts +10 -0
  56. package/dist/lib/live-runtime-binary.d.ts +40 -0
  57. package/dist/lib/live-runtime-binary.js +193 -0
  58. package/dist/lib/login.d.ts +23 -0
  59. package/dist/lib/login.js +44 -10
  60. package/dist/lib/message-intent.js +3 -1
  61. package/dist/lib/tasks.d.ts +20 -0
  62. package/dist/lib/tasks.js +54 -0
  63. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.d.ts +2 -0
  64. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +26 -1
  65. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.d.ts +28 -4
  66. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.js +23 -5
  67. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +41 -3
  68. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +3 -1
  69. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +25 -3
  70. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loopbackHttp.d.ts +32 -0
  71. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loopbackHttp.js +268 -0
  72. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.d.ts +2 -2
  73. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.js +11 -10
  74. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.d.ts +36 -0
  75. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +156 -0
  76. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.d.ts +8 -0
  77. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.js +8 -1
  78. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.d.ts +22 -0
  79. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.js +61 -2
  80. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.d.ts +2 -0
  81. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +25 -1
  82. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.d.ts +28 -4
  83. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.js +22 -4
  84. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +41 -3
  85. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +3 -1
  86. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +10 -1
  87. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loopbackHttp.d.ts +32 -0
  88. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loopbackHttp.js +257 -0
  89. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.d.ts +2 -2
  90. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.js +11 -10
  91. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.d.ts +36 -0
  92. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +147 -0
  93. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.d.ts +8 -0
  94. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.js +8 -1
  95. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.d.ts +22 -0
  96. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.js +58 -1
  97. package/dist/node_modules/@skrr-ai/auth-core/package.json +21 -1
  98. package/dist/node_modules/@skrr-ai/data-provider/index.js +1928 -1924
  99. package/oclif.manifest.json +23832 -23688
  100. package/package.json +1 -1
@@ -1,11 +1,29 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.queuedBecause = queuedBecause;
3
4
  exports.outboxListSnapshot = outboxListSnapshot;
4
5
  const core_1 = require("@oclif/core");
5
6
  const base_command_1 = require("../../base-command");
6
7
  const keychain_1 = require("../../lib/keychain");
7
8
  const format_1 = require("../../lib/format");
8
9
  const outbox_1 = require("../../lib/outbox");
10
+ /**
11
+ * Why one entry was queued, in words (OSK-12121). The queue holds more than
12
+ * outage casualties — a 401 is queued too, so authored content survives a lapsed
13
+ * sign-in — and a list that described every entry as "the API was unavailable"
14
+ * sent a signed-out reader to wait for an outage that was not happening.
15
+ */
16
+ function queuedBecause(entry) {
17
+ const status = entry.reason?.status;
18
+ if (status === 401)
19
+ return { label: 'sign-in refused (401)', kind: 'auth' };
20
+ if (status === 429)
21
+ return { label: 'throttled (429)', kind: 'throttled' };
22
+ if ((typeof status === 'number' && status >= 500) || (0, outbox_1.isOutageFailure)(entry.reason)) {
23
+ return { label: status ? `unavailable (${status})` : 'unreachable', kind: 'unavailable' };
24
+ }
25
+ return { label: status ? `HTTP ${status}` : 'unknown', kind: 'other' };
26
+ }
9
27
  function outboxListSnapshot(profile) {
10
28
  const queued = (0, outbox_1.listWrites)(profile);
11
29
  const incident = (0, outbox_1.readIncident)(profile);
@@ -24,7 +42,7 @@ function outboxListSnapshot(profile) {
24
42
  * where they find out.
25
43
  */
26
44
  class OutboxList extends base_command_1.BaseCommand {
27
- static description = 'Show writes queued locally because the API was unavailable';
45
+ static description = 'Show writes queued locally because the server could not take them: an outage, throttling, or a lapsed sign-in';
28
46
  static examples = [
29
47
  '<%= config.bin %> outbox list',
30
48
  '<%= config.bin %> outbox list --json',
@@ -46,7 +64,9 @@ class OutboxList extends base_command_1.BaseCommand {
46
64
  `${incident.consecutive} consecutive failure(s).`);
47
65
  this.log('');
48
66
  }
49
- else if (incident) {
67
+ else if (incident && entries.some((e) => queuedBecause(e).kind === 'unavailable')) {
68
+ // A past incident is context only for entries an outage put here. Beside
69
+ // a queue of sign-in refusals it reads as the explanation, and is not.
50
70
  this.log(`Last service incident: ${incident.baseURL} failed at ${incident.lastOutageAt}` +
51
71
  `${incident.status ? ` (HTTP ${incident.status})` : ''}. ` +
52
72
  'No current outage is confirmed.');
@@ -61,6 +81,7 @@ class OutboxList extends base_command_1.BaseCommand {
61
81
  queued: e.queuedAt,
62
82
  command: e.command,
63
83
  summary: e.summary,
84
+ why: queuedBecause(e).label,
64
85
  // Surface the attempt count only once it matters; a column of "0"s is
65
86
  // noise, but an entry nearing the cap needs a human.
66
87
  attempts: e.attempts > 0 ? `${e.attempts}/${outbox_1.MAX_REPLAY_ATTEMPTS}` : '-',
@@ -69,6 +90,7 @@ class OutboxList extends base_command_1.BaseCommand {
69
90
  { key: 'queued', header: 'QUEUED', maxWidth: 24 },
70
91
  { key: 'command', header: 'COMMAND', maxWidth: 24 },
71
92
  { key: 'summary', header: 'SUMMARY', maxWidth: 40 },
93
+ { key: 'why', header: 'WHY', maxWidth: 24 },
72
94
  { key: 'attempts', header: 'TRIES' },
73
95
  ], (line) => this.log(line));
74
96
  const exhausted = entries.filter((e) => e.attempts >= outbox_1.MAX_REPLAY_ATTEMPTS);
@@ -77,6 +99,13 @@ class OutboxList extends base_command_1.BaseCommand {
77
99
  this.log(`${exhausted.length} entry(ies) hit the ${outbox_1.MAX_REPLAY_ATTEMPTS}-attempt cap and will not be ` +
78
100
  'retried automatically. Inspect the failure, then retry or drop them explicitly.');
79
101
  }
102
+ const authQueued = entries.filter((e) => queuedBecause(e).kind === 'auth').length;
103
+ if (authQueued > 0) {
104
+ this.log('');
105
+ this.log(`${authQueued} entry(ies) were queued because the server refused this sign-in, not because ` +
106
+ `it was down. Sign in with \`${this.config.bin} login\` first; a replay before that is ` +
107
+ 'refused the same way.');
108
+ }
80
109
  this.log('');
81
110
  this.log(`Replay: ${this.config.bin} outbox retry`);
82
111
  this.log(`Discard: ${this.config.bin} outbox drop <id>`);
@@ -67,6 +67,8 @@ class RevokeDaemon extends base_command_1.BaseCommand {
67
67
  const host = resolvedDaemon?.machineUuid
68
68
  ? resolvedDaemon.machineUuid.slice(0, 12)
69
69
  : (resolvedDaemon?.os ?? 'unknown host');
70
+ this.log('Revoking a daemon also revokes every CLI sign-in it brokered — on its machine, ' +
71
+ `\`${this.config.bin} login\` without a browser signs in through the daemon.`);
70
72
  const confirmed = await this.confirm(`Revoke daemon "${name}" on ${host}? [y/N] `);
71
73
  if (!confirmed) {
72
74
  this.log('Aborted.');
@@ -99,6 +101,13 @@ class RevokeDaemon extends base_command_1.BaseCommand {
99
101
  const daemonId = (res.daemon?.id ?? resolvedId).slice(0, 12);
100
102
  if (res.revoked) {
101
103
  this.log(`Revoked daemon ${daemonName} (${daemonId}).`);
104
+ if (res.callerSessionRevoked === true) {
105
+ // Said now, not discovered later as a refresh failure: the sign-in this
106
+ // command used was brokered by that daemon and died with it.
107
+ this.warn(`This also signed out ${this.config.bin} on this machine: its sign-in was brokered ` +
108
+ `by that daemon and was revoked with it. Run \`${this.config.bin} daemon login\` to ` +
109
+ `re-enroll the daemon, then \`${this.config.bin} login\` to sign the CLI in again.`);
110
+ }
102
111
  // exit 0 (default)
103
112
  }
104
113
  else if (res.alreadyRevoked) {
@@ -19,6 +19,7 @@ export default class SpacesCreate extends BaseCommand {
19
19
  'group-chat': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
20
20
  parent: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
21
21
  'inherit-access': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
22
+ 'working-directory': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
22
23
  };
23
24
  run(): Promise<void>;
24
25
  }
@@ -9,6 +9,7 @@ const label_ref_1 = require("../../lib/label-ref");
9
9
  const web_url_1 = require("../../lib/web-url");
10
10
  const prompt_1 = require("../../lib/prompt");
11
11
  const created_line_1 = require("../../lib/created-line");
12
+ const tasks_1 = require("../../lib/tasks");
12
13
  class SpacesCreate extends base_command_1.BaseCommand {
13
14
  static description = 'Create a new space';
14
15
  static examples = [
@@ -51,6 +52,11 @@ class SpacesCreate extends base_command_1.BaseCommand {
51
52
  allowNo: true,
52
53
  description: 'Inherit members/permissions from the parent chain (default). Use --no-inherit-access to make a private subspace.',
53
54
  }),
55
+ 'working-directory': core_1.Flags.string({
56
+ description: "Space-level working directory for this space's local Harness runs — an absolute " +
57
+ "path on the executing daemon's filesystem. The middle layer of task → space → " +
58
+ 'agent: a per-task override wins, the agent default applies when neither is set.',
59
+ }),
54
60
  };
55
61
  async run() {
56
62
  this.requireAuth();
@@ -95,7 +101,18 @@ class SpacesCreate extends base_command_1.BaseCommand {
95
101
  groupChatId: flags['group-chat'],
96
102
  parentSpaceId: flags.parent,
97
103
  inheritAccess: flags['inherit-access'],
104
+ workingDirectory: (0, tasks_1.workingDirectoryFromFlag)(flags['working-directory']),
98
105
  });
106
+ // Post-merge, so --working-directory and --from-json are checked
107
+ // identically: a relative path is refused here rather than stored and
108
+ // silently fallen back from at run time on the daemon.
109
+ const workingDirectoryError = (0, tasks_1.workingDirectoryValidationError)(payload.workingDirectory);
110
+ if (workingDirectoryError) {
111
+ this.error(workingDirectoryError, { exit: 2 });
112
+ }
113
+ // 'none' on create means "no space default" — the field is absent anyway.
114
+ if (payload.workingDirectory === null)
115
+ delete payload.workingDirectory;
99
116
  if (!payload.title || typeof payload.title !== 'string') {
100
117
  this.error('A title is required. Pass --title "..." or include it in --from-json.', {
101
118
  exit: 1,
@@ -57,6 +57,11 @@ class SpacesShow extends base_command_1.BaseCommand {
57
57
  this.log(`Leader: ${s.leadAgentId ?? '-'}`);
58
58
  this.log(`Target Date: ${s.targetDate ?? '-'}`);
59
59
  this.log(`Goals: ${Array.isArray(s.goalIds) ? s.goalIds.length : 0}`);
60
+ // Only when set — the space default is the middle layer of the
61
+ // task → space → agent cascade, and most spaces leave it unset.
62
+ if (typeof s.workingDirectory === 'string' && s.workingDirectory) {
63
+ this.log(`Directory: ${s.workingDirectory}`);
64
+ }
60
65
  this.log(`Archived: ${s.archivedAt ? 'yes' : 'no'}`);
61
66
  this.log(`Parent: ${s.parentSpaceId ?? '(root)'}`);
62
67
  this.log(`Depth: ${s.depth ?? 0}`);
@@ -30,6 +30,7 @@ export default class SpacesUpdate extends BaseCommand {
30
30
  'auto-start-on-backlog-exit': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
31
31
  'triage-external-intake': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
32
32
  'default-task-template': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
33
+ 'working-directory': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
33
34
  };
34
35
  run(): Promise<void>;
35
36
  }
@@ -135,6 +135,12 @@ class SpacesUpdate extends base_command_1.BaseCommand {
135
135
  description: 'Default task template for new tasks in this space — a template id, reference, or ' +
136
136
  'exact title (the server resolves it to the template). Pass "none" to clear.',
137
137
  }),
138
+ 'working-directory': core_1.Flags.string({
139
+ description: "Space-level working directory for this space's local Harness runs — an absolute " +
140
+ "path on the executing daemon's filesystem. The middle layer of task → space → " +
141
+ 'agent: a per-task override wins, the agent default applies when neither is set. ' +
142
+ 'Pass "none" to clear.',
143
+ }),
138
144
  };
139
145
  async run() {
140
146
  this.requireAuth();
@@ -159,11 +165,21 @@ class SpacesUpdate extends base_command_1.BaseCommand {
159
165
  autoStartOnBacklogExit: flags['auto-start-on-backlog-exit'],
160
166
  triageExternalIntake: flags['triage-external-intake'],
161
167
  defaultTaskTemplateId: clearable(flags['default-task-template']),
168
+ // Per-space working-directory default. 'none'/'clear'/'null' ⇒ null
169
+ // ($unset → resolution falls through to the agent default).
170
+ workingDirectory: (0, tasks_1.workingDirectoryFromFlag)(flags['working-directory']),
162
171
  estimateScale: flags['estimate-scale'],
163
172
  estimateScaleMigration: flags['estimate-migration'],
164
173
  defaultEstimate: flags['default-estimate'],
165
174
  useDefaultEstimateForUnestimated: flags['use-default-estimate'],
166
175
  });
176
+ // Post-merge, so --working-directory and --from-json are checked
177
+ // identically. A relative path stores fine and then silently falls back on
178
+ // the daemon at run time — refuse it here, where the mistake can be named.
179
+ const workingDirectoryError = (0, tasks_1.workingDirectoryValidationError)(payload.workingDirectory);
180
+ if (workingDirectoryError) {
181
+ this.error(workingDirectoryError, { exit: 2 });
182
+ }
167
183
  // Worktree-isolation default lives on the space's deliveryPolicy subdoc,
168
184
  // which the API replaces wholesale on update. Merge into the policy the user
169
185
  // supplied via --from-json, else the space's current policy, so setting just
@@ -65,6 +65,7 @@ export default class TasksCreate extends BaseCommand {
65
65
  estimate: import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
66
66
  tag: import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
67
67
  'label-id': import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
68
+ 'working-directory': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
68
69
  worktree: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
69
70
  'lifecycle-definition': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
70
71
  'lifecycle-version': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
@@ -226,6 +226,11 @@ class TasksCreate extends base_command_1.BaseCommand {
226
226
  'An unknown name is refused, never created. Manage labels with `skrr labels`.',
227
227
  multiple: true,
228
228
  }),
229
+ 'working-directory': core_1.Flags.string({
230
+ description: "Working directory for this task's local Harness runs — an absolute path on " +
231
+ "the daemon's filesystem. Overrides the agent's default; omit to inherit the " +
232
+ 'agent default, then the daemon session default.',
233
+ }),
229
234
  worktree: core_1.Flags.boolean({
230
235
  allowNo: true,
231
236
  description: "Isolate this task's autonomous runs in a one-shot git worktree (--worktree), " +
@@ -402,6 +402,51 @@ class TasksShow extends base_command_1.BaseCommand {
402
402
  this.log(`Result: ${priorLine}`);
403
403
  }
404
404
  this.log(`Executor: ${taskExecutorLabel(t, this.config.bin)}`);
405
+ // Effective run directory, resolved the same way the server resolves it —
406
+ // most specific scope wins: task → space → agent. The daemon layers below
407
+ // (session.defaultCwd → --cwd) are the daemon's own and cannot be read
408
+ // here, so when no owned layer wins the row is omitted rather than
409
+ // labelled "inherited". Two extra reads, paid only when the row can say
410
+ // something (a detail view).
411
+ if (typeof t.workingDirectory === 'string' && t.workingDirectory) {
412
+ this.log(`Directory: ${t.workingDirectory} (task)`);
413
+ }
414
+ else {
415
+ let spaceDir = null;
416
+ let agentDir = null;
417
+ if (typeof t.spaceId === 'string' && t.spaceId) {
418
+ try {
419
+ const space = (await data_provider_1.dataService.getSpace(t.spaceId));
420
+ if (typeof space?.workingDirectory === 'string' && space.workingDirectory) {
421
+ spaceDir = space.workingDirectory;
422
+ }
423
+ }
424
+ catch {
425
+ // Unreadable space → no claim, not a wrong one.
426
+ }
427
+ }
428
+ if (!spaceDir) {
429
+ const agentRef = (typeof t.driAgentId === 'string' && t.driAgentId) ||
430
+ (Array.isArray(t.assigneeAgentIds) ? t.assigneeAgentIds[0] : null);
431
+ if (agentRef) {
432
+ try {
433
+ const agentRow = (await data_provider_1.dataService.getAgentById({
434
+ agent_id: agentRef,
435
+ }));
436
+ if (typeof agentRow?.workingDirectory === 'string' && agentRow.workingDirectory) {
437
+ agentDir = agentRow.workingDirectory;
438
+ }
439
+ }
440
+ catch {
441
+ // Same rule: unreadable → silence.
442
+ }
443
+ }
444
+ }
445
+ const inheritedDir = spaceDir ?? agentDir;
446
+ if (inheritedDir) {
447
+ this.log(`Directory: ${inheritedDir} (${spaceDir ? 'space' : 'agent'} default)`);
448
+ }
449
+ }
405
450
  this.log(`Priority: ${t.priority ?? '-'}`);
406
451
  this.log(`Parent: ${t.parentId ?? '-'}`);
407
452
  this.log(`Space: ${t.spaceId ?? '-'}`);
@@ -58,6 +58,7 @@ export default class TasksUpdate extends BaseCommand {
58
58
  tag: import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
59
59
  'label-id': import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
60
60
  'repair-release-pins': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
61
+ 'working-directory': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
61
62
  worktree: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
62
63
  };
63
64
  run(): Promise<void>;
@@ -168,6 +168,11 @@ class TasksUpdate extends base_command_1.BaseCommand {
168
168
  'rejected with AGENT_RELEASE_PIN_STATE_INVALID: pins that govern no current ' +
169
169
  'assignee are dropped and broken ones are re-resolved (recorded in the activity log).',
170
170
  }),
171
+ 'working-directory': core_1.Flags.string({
172
+ description: "Working directory for this task's local Harness runs — an absolute path on " +
173
+ 'the daemon\'s filesystem. Overrides the agent\'s default. Pass "none" to ' +
174
+ 'clear back to inheriting the agent default.',
175
+ }),
171
176
  worktree: core_1.Flags.boolean({
172
177
  allowNo: true,
173
178
  description: "Isolate this task's autonomous runs in a git worktree (--worktree) or force " +
@@ -348,6 +353,10 @@ class TasksUpdate extends base_command_1.BaseCommand {
348
353
  // Worktree isolation override. --worktree ⇒ 'ephemeral', --no-worktree ⇒
349
354
  // 'shared', omitted ⇒ unchanged. (Clear-to-inherit via --from-json.)
350
355
  executionIsolation: (0, tasks_2.executionIsolationFromWorktreeFlag)(flags.worktree),
356
+ // Per-task working-directory override. --working-directory <abs path> ⇒
357
+ // set; "none"/"clear"/"null" ⇒ null (clears → inherit the agent
358
+ // default); omitted ⇒ unchanged.
359
+ workingDirectory: (0, tasks_2.workingDirectoryFromFlag)(flags['working-directory']),
351
360
  // A mutation option, not a field — `undefined` unless explicitly asked
352
361
  // for, so it never counts toward the "nothing to update" check below.
353
362
  repairReleasePins: flags['repair-release-pins'] ? true : undefined,
@@ -361,6 +370,18 @@ class TasksUpdate extends base_command_1.BaseCommand {
361
370
  retryable: false,
362
371
  });
363
372
  }
373
+ // Post-merge, so flag and --from-json inputs are checked identically. A
374
+ // relative path stores fine and then silently falls back at run time —
375
+ // refuse it here, where the mistake can still be named.
376
+ const workingDirectoryError = (0, tasks_2.workingDirectoryValidationError)(payload.workingDirectory);
377
+ if (workingDirectoryError) {
378
+ this.failWithCliError({
379
+ message: workingDirectoryError,
380
+ code: 'INVALID_TASK_WORKING_DIRECTORY',
381
+ exit: 2,
382
+ retryable: false,
383
+ });
384
+ }
364
385
  if (payloadKeys.length === 0) {
365
386
  this.failWithCliError({
366
387
  message: 'Nothing to update. Pass at least one flag (e.g. --title) or --from-json.',
@@ -20,6 +20,7 @@ export default class TasksUpsert extends BaseCommand {
20
20
  'assignee-user': import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
21
21
  'execution-mode': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
22
22
  'due-date': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
23
+ 'working-directory': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
23
24
  tag: import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
24
25
  };
25
26
  run(): Promise<void>;
@@ -57,6 +57,10 @@ class TasksUpsert extends base_command_1.BaseCommand {
57
57
  options: ['agent', 'human'],
58
58
  }),
59
59
  'due-date': core_1.Flags.string({ description: 'Due date (ISO string)' }),
60
+ 'working-directory': core_1.Flags.string({
61
+ description: "Working directory for this task's local Harness runs — an absolute path on " +
62
+ 'the daemon\'s filesystem. Pass "none" to clear an existing override.',
63
+ }),
60
64
  tag: core_1.Flags.string({ description: 'Replace/create tag (repeatable)', multiple: true }),
61
65
  };
62
66
  async run() {
@@ -75,6 +79,18 @@ class TasksUpsert extends base_command_1.BaseCommand {
75
79
  });
76
80
  }
77
81
  const payload = (0, tasks_1.buildTaskCreatePayload)(body, flags);
82
+ // Checked here, not inside validateTaskCreatePayload's create-leg call —
83
+ // an upsert that resolves to UPDATE never reaches that validator, and a
84
+ // relative path would silently no-op at run time on the daemon.
85
+ const workingDirectoryError = (0, tasks_1.workingDirectoryValidationError)(payload.workingDirectory);
86
+ if (workingDirectoryError) {
87
+ this.failWithCliError({
88
+ message: workingDirectoryError,
89
+ code: 'INVALID_TASK_WORKING_DIRECTORY',
90
+ exit: 2,
91
+ retryable: false,
92
+ });
93
+ }
78
94
  let scope;
79
95
  try {
80
96
  scope = await (0, tasks_1.resolveTaskCreateScope)(payload);
@@ -94,6 +94,20 @@ export type AgenticStreamOptions = {
94
94
  /** How long to keep trying to attach before reporting a detach. Default 30s. */
95
95
  attachTimeoutMs?: number;
96
96
  };
97
+ /**
98
+ * What a permission request would DO, in one line a person can judge.
99
+ *
100
+ * The prompt used to be `Allow tool <name>?` and nothing else, so a shell
101
+ * approval read "Allow tool unknown? [y/N]" and a person approved a command
102
+ * they never saw (OSK-12119). The command, or else the path, or else the
103
+ * arguments, is the part that decides the answer.
104
+ *
105
+ * The text is chosen by the agent and printed to a terminal, so control
106
+ * characters are escaped: an embedded carriage return or escape sequence could
107
+ * otherwise redraw the line and show the person a different command than the
108
+ * one they are approving.
109
+ */
110
+ export declare function describePermissionTarget(toolInput: unknown): string | null;
97
111
  /**
98
112
  * Small Node-side adapter for the existing Agentic Socket.IO contract.
99
113
  * It deliberately normalizes only lifecycle/text/tool events; the server's
@@ -159,6 +173,25 @@ export type ProjectedToolEvent = {
159
173
  * of the run. The pairing is unambiguous within a turn.
160
174
  */
161
175
  export declare function projectDaemonToolEvents(envelopes: unknown[], namesByCall: Map<string, string>): ProjectedToolEvent[];
176
+ /**
177
+ * WHO or WHAT decided a permission request, as a reader needs it (OSK-12113).
178
+ *
179
+ * `by` names the surface or fallback that resolved the ask and is a closed wire
180
+ * enum; `basis` beside it says what decided. The distinction is the finding: an
181
+ * edit acceptEdits applied on its own was recorded `by: "cli"`, which read as
182
+ * the CLI user having approved something nobody was asked about.
183
+ */
184
+ export declare function describePermissionDecider(ev: {
185
+ by?: unknown;
186
+ basis?: unknown;
187
+ mode?: unknown;
188
+ surface?: unknown;
189
+ }): string;
190
+ /**
191
+ * Daemon `permission-resolved` envelopes as `permission.resolved` events, named
192
+ * by the tool their `permission-ask` announced and carrying who decided.
193
+ */
194
+ export declare function projectPermissionResolutions(envelopes: unknown[], toolsByRequest: Map<string, string>): AgenticStreamEvent[];
162
195
  /**
163
196
  * The part of a cumulative snapshot that has not been shown yet.
164
197
  *
@@ -262,8 +295,32 @@ export type SeqGap = {
262
295
  from: number;
263
296
  to: number;
264
297
  };
298
+ /**
299
+ * `service` envelopes that carry a `notice` — words for the person, not
300
+ * diagnostics — projected as `session.notice` events. Today the only notice is
301
+ * `harness-session-reset`: the local harness could not resume its previous
302
+ * session and began a new one, so its local working context is gone.
303
+ */
304
+ export declare function projectSessionNotices(envelopes: unknown[]): AgenticStreamEvent[];
265
305
  export declare function detectSeqGap(highestApplied: number, incoming: number): SeqGap | null;
266
- export declare function seqGapNotice(gap: SeqGap): string;
306
+ /**
307
+ * The notice a reader sees when chunks went missing.
308
+ *
309
+ * It names `run.result` only when this client will actually fill `run.result`
310
+ * from the persisted message (`recordBackfill`). The sentence used to promise
311
+ * it unconditionally, while `run.result.text` was folded from the envelopes
312
+ * that DID arrive — so the one run told to trust `run.result` was the run whose
313
+ * `run.result.text` was empty, with the answer only in the record (OSK-12132).
314
+ */
315
+ export declare function seqGapNotice(gap: SeqGap, options?: {
316
+ recordBackfill?: boolean;
317
+ }): string;
318
+ /**
319
+ * The permission mode a `run.started` reports, and where it came from
320
+ * (OSK-12089). `null` when the event carries no mode report at all — a cloud
321
+ * turn from an older server, say — so nothing is claimed.
322
+ */
323
+ export declare function describeRunPermission(event: AgenticStreamEvent): string | null;
267
324
  export declare class AgenticStreamClient {
268
325
  private readonly options;
269
326
  private socket;
@@ -291,6 +348,8 @@ export declare class AgenticStreamClient {
291
348
  private readonly seenLifecycle;
292
349
  /** `call` → tool name, so a daemon `tool-call-end` can be named (OSK-7730). */
293
350
  private readonly toolNamesByCall;
351
+ /** `permission-ask` requestId → tool name, to name its resolution (OSK-12113). */
352
+ private readonly toolNamesByPermission;
294
353
  /** Text already written to the terminal, for suffix-only rendering. */
295
354
  private renderedText;
296
355
  private idleTimer;
@@ -372,6 +431,14 @@ export declare class AgenticStreamClient {
372
431
  private emitToolEvent;
373
432
  private emit;
374
433
  private finish;
434
+ /**
435
+ * The answer as the durable record holds it, for a run whose stream had a
436
+ * hole. Bounded: the record is written before the terminal event is sent, so
437
+ * it is normally there at once, and a record that stays silent must not hold
438
+ * the run open — the reader then gets what the stream delivered.
439
+ */
440
+ private readRecordedText;
441
+ private settle;
375
442
  /**
376
443
  * OSK-2944 — a transient connect failure must not end an ACCEPTED run.
377
444
  *