autodev-cli 1.4.85 → 1.4.93

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/out/taskLoop.js CHANGED
@@ -61,7 +61,9 @@ const periodicActions_1 = require("./periodicActions");
61
61
  const discordGateway_1 = require("./discordGateway");
62
62
  const webhookPoller_1 = require("./webhookPoller");
63
63
  const emailPoller_1 = require("./emailPoller");
64
+ const modelInfo_1 = require("./core/modelInfo");
64
65
  const projectMcp_1 = require("./core/projectMcp");
66
+ const projectSkills_1 = require("./core/projectSkills");
65
67
  const configManager_1 = require("./configManager");
66
68
  const agentBackup_1 = require("./agentBackup");
67
69
  const version_1 = require("./version");
@@ -71,6 +73,7 @@ const version_1 = require("./version");
71
73
  const rateLimit_1 = require("./rateLimit");
72
74
  const cliExit_1 = require("./cliExit");
73
75
  const liveNarration_1 = require("./core/liveNarration");
76
+ const redactSecrets_1 = require("./core/redactSecrets");
74
77
  class ContextLengthError extends Error {
75
78
  rawMessage;
76
79
  constructor(rawMessage) {
@@ -202,6 +205,11 @@ class TaskLoopRunner {
202
205
  /** Dispatch attempt counter per task key (id or text). After 3 failed attempts the
203
206
  * loop force-marks the task done so it doesn't block the queue indefinitely. */
204
207
  _taskAttempts = new Map();
208
+ /** Task keys (id or text) flagged as provider hard-failures this session.
209
+ * These must NOT be auto-completed by the give_up / stranded-[~] heuristics —
210
+ * a blocked task stays [~] + reported failed until it is retried. Cleared when
211
+ * a fresh [ ] dispatch of the same task begins (provider presumably recovered). */
212
+ _blockedTasks = new Set();
205
213
  /** Counts completed tasks since the last auto-compact run. */
206
214
  _autoCompactCounter = 0;
207
215
  /** Counts completed tasks since the last session reset. */
@@ -344,6 +352,7 @@ class TaskLoopRunner {
344
352
  this._iterations = 0;
345
353
  this._compactedTaskLines.clear();
346
354
  this._taskAttempts.clear();
355
+ this._blockedTasks.clear();
347
356
  this._autoCompactCounter = 0;
348
357
  this._resetSessionCounter = 0;
349
358
  this._profileSentCounter = 0;
@@ -387,7 +396,13 @@ class TaskLoopRunner {
387
396
  // `cliVersion` rides in the meta merged into every webhook frame — notably
388
397
  // the agent_online (hello) frame — so pixel-office can record which CLI
389
398
  // version each agent runs (surfacing stale, steer-incapable agents).
390
- this._webhook?.setMeta({ provider: settings.provider, cliVersion: version_1.CLI_VERSION, workDir: root, hostname: this._hostname, gitRepo: this._gitRepo, gitBranch: this._gitBranch });
399
+ // `model` the effective friendly model name (Opus 4.8 / kimi-k2.7-code /
400
+ // grok-code). Resolved from the provider's configured override; undefined
401
+ // when the provider runs its account default (office gates the field). Rides
402
+ // in setMeta so it merges into every frame, notably agent_online, where the
403
+ // office records it against the agent for the per-agent model badge.
404
+ const effectiveModel = (0, modelInfo_1.resolveConfiguredModel)(settings);
405
+ this._webhook?.setMeta({ provider: settings.provider, cliVersion: version_1.CLI_VERSION, model: effectiveModel, workDir: root, hostname: this._hostname, gitRepo: this._gitRepo, gitBranch: this._gitBranch });
391
406
  this._discordPoller = (settings.discordToken && settings.discordChannelId && settings.discordOwners)
392
407
  ? new discordPoller_1.DiscordPoller(settings.discordToken, settings.discordChannelId, settings.discordOwners)
393
408
  : null;
@@ -434,6 +449,7 @@ class TaskLoopRunner {
434
449
  // mis-detected as 'claude' by the office's hook-based fallback.
435
450
  provider: this._settings?.provider,
436
451
  cliVersion: version_1.CLI_VERSION,
452
+ model: this._settings ? (0, modelInfo_1.resolveConfiguredModel)(this._settings) : undefined,
437
453
  hostname: this._hostname,
438
454
  workDir: this._workspaceRoot ?? '',
439
455
  gitRepo: this._gitRepo,
@@ -485,6 +501,7 @@ class TaskLoopRunner {
485
501
  this._webhookPoller.setOnSteer((text, onDelivered) => void this._handleSteer(text, onDelivered));
486
502
  this._webhookPoller.setOnCommand((cmd) => this._handleCommand(cmd));
487
503
  this._webhookPoller.setOnMcpUpdate((entries) => this._handleMcpUpdate(entries));
504
+ this._webhookPoller.setOnSkillUpdate((skills) => this._handleSkillUpdate(skills));
488
505
  this._webhookPoller.setOnExportRequest((agentId) => void this._handleExportRequest(agentId));
489
506
  this._webhookPoller.setOnRestoreRequest((agentId, downloadUrl) => void this._handleRestoreRequest(agentId, downloadUrl));
490
507
  this._webhookPoller.setOnExportConfig((exportEnabled, exportDailyBackup, agentId) => this._handleExportConfig(exportEnabled, exportDailyBackup, agentId));
@@ -556,6 +573,7 @@ class TaskLoopRunner {
556
573
  this._notifyWebhook('agent_online', {
557
574
  provider: settings.provider,
558
575
  cliVersion: version_1.CLI_VERSION,
576
+ model: effectiveModel,
559
577
  hostname: this._hostname,
560
578
  workDir: root,
561
579
  gitRepo: this._gitRepo,
@@ -704,6 +722,44 @@ class TaskLoopRunner {
704
722
  }
705
723
  void this.restart();
706
724
  }
725
+ /**
726
+ * Handle skill_update pushed from pixel-office: validate + write the agent's
727
+ * FULL effective skill set to `.claude/skills/<slug>/SKILL.md`. Skills
728
+ * live-reload (Claude re-reads them each run), so this does NOT restart the
729
+ * loop — it just sanitizes, full-replaces on disk, folds a prose block into
730
+ * AGENTS.md for non-Claude providers, and reports the applied names.
731
+ */
732
+ _handleSkillUpdate(skills) {
733
+ const root = this._workspaceRoot;
734
+ if (!root)
735
+ return;
736
+ // Opt-in gate: remote-supplied skills become instruction files a running
737
+ // Claude agent live-reads. Ignore unless explicitly enabled, mirroring
738
+ // mcpUpdateEnabled / enableFileBrowser / gitEnabled.
739
+ if (!this._settings?.skillUpdateEnabled) {
740
+ this._cb?.log('🔒 skill_update ignored — skillUpdateEnabled is off (set it in .autodev/settings.json to allow)');
741
+ return;
742
+ }
743
+ this._cb?.log('🧩 skill_update received — validating and writing .claude/skills…');
744
+ const { safe, rejected } = (0, projectSkills_1.sanitizeRemoteSkills)(root, skills);
745
+ if (rejected.length) {
746
+ this._cb?.log(`⚠️ skill_update dropped ${rejected.length} entr${rejected.length === 1 ? 'y' : 'ies'}: ${rejected.map(r => `${r.name} (${r.reason})`).join(', ')}`);
747
+ }
748
+ try {
749
+ // Full-replace: writes the safe set and prunes previously-synced skills
750
+ // that are gone (even when `safe` is empty — that clears them all).
751
+ const { written, removed } = (0, projectSkills_1.saveProjectSkills)(root, safe);
752
+ // Non-Claude providers can't load SKILL.md — fold a prose block into AGENTS.md.
753
+ if (!(0, projectSkills_1.providerConsumesSkills)(this._settings?.provider)) {
754
+ (0, projectSkills_1.foldSkillsIntoProfile)(root, safe);
755
+ }
756
+ void configManager_1.ConfigManager.reportProjectSkills(root, written, (m) => this._cb?.log(m));
757
+ this._cb?.log(`✅ skills synced — wrote ${written.length}, removed ${removed.length} (.claude/skills). Live-reloads on the next run — no restart.`);
758
+ }
759
+ catch (err) {
760
+ this._cb?.log(`⚠️ skill update failed: ${err instanceof Error ? err.message : String(err)}`);
761
+ }
762
+ }
707
763
  /** Handle export_request from pixel-office: create backup zip and upload. */
708
764
  async _handleExportRequest(agentId) {
709
765
  const root = this._workspaceRoot;
@@ -1162,7 +1218,10 @@ class TaskLoopRunner {
1162
1218
  if (sessionName) {
1163
1219
  ev._session_name = sessionName;
1164
1220
  }
1165
- this._webhookPoller.sendFrame({ type: 'hook_event', data: ev });
1221
+ // Redact secrets from the whole hook object (tool_input/tool_response/
1222
+ // assistant narration/file contents) before it leaves the machine —
1223
+ // this stream is stored + rendered in the office feed/chat.
1224
+ this._webhookPoller.sendFrame({ type: 'hook_event', data: (0, redactSecrets_1.redactDeep)(ev) });
1166
1225
  }
1167
1226
  catch { /* skip malformed lines */ }
1168
1227
  }
@@ -1382,7 +1441,10 @@ class TaskLoopRunner {
1382
1441
  this._cb?.log(`⏳ opencode-cli exited cleanly with [~] task — waiting for external response…`);
1383
1442
  }
1384
1443
  else {
1385
- const stranded = tasks.filter(t => t.status === 'in-progress');
1444
+ // Exclude tasks flagged as provider hard-failures: those were left
1445
+ // [~] ON PURPOSE to signal "blocked / needs attention". Auto-marking
1446
+ // them done here would re-introduce the exact false-green we fixed.
1447
+ const stranded = tasks.filter(t => t.status === 'in-progress' && !this._blockedTasks.has(t.id ?? t.text));
1386
1448
  for (const t of stranded) {
1387
1449
  // Auto-mark as [x] done rather than resetting to [ ].
1388
1450
  // Resetting to [ ] causes an infinite loop: the agent picks it
@@ -1417,6 +1479,10 @@ class TaskLoopRunner {
1417
1479
  // If the agent hasn't marked it done after 3 attempts, force-mark it
1418
1480
  // ourselves and move on so the queue doesn't get stuck.
1419
1481
  const taskKey = task.id ?? task.text;
1482
+ // A fresh [ ] dispatch means this task is being retried — the provider
1483
+ // has presumably recovered, so clear any prior hard-failure flag and let
1484
+ // it complete (or fail) honestly on its own merits this time.
1485
+ this._blockedTasks.delete(taskKey);
1420
1486
  const attempts = (this._taskAttempts.get(taskKey) ?? 0) + 1;
1421
1487
  this._taskAttempts.set(taskKey, attempts);
1422
1488
  const maxAttempts = settings.maxTaskAttempts ?? 3;
@@ -2614,10 +2680,60 @@ class TaskLoopRunner {
2614
2680
  return;
2615
2681
  }
2616
2682
  }
2617
- const decision = exitHandler?.decide() ?? { kind: 'remind' };
2683
+ // opencode's SDK path surfaces a session.error as an `[ERROR] …` line in
2684
+ // its stdout capture yet still writes exit-code 0 — so the process exit
2685
+ // code alone can't see it. Pass that sentinel through so a session.error
2686
+ // ("No model available" on a copilot-backed free plan, etc.) is treated
2687
+ // as the provider hard-failure it is, not a completed task.
2688
+ const stdoutError = ((isOpenCodeCli || isOpencodeSdk) && /\[ERROR\]/.test(exitStdout))
2689
+ ? 'session-error'
2690
+ : null;
2691
+ const decision = exitHandler?.decide({ exitedNonZero, stdoutError })
2692
+ ?? { kind: 'remind' };
2618
2693
  if (decision.kind === 'done') {
2619
2694
  return;
2620
2695
  }
2696
+ if (decision.kind === 'hard_fail') {
2697
+ // The PROVIDER hard-failed (reauth / session error / watchdog-no-output
2698
+ // / crash / model unavailable) and produced no real work. Do NOT mark
2699
+ // the task [x] done — that would report an outage as SUCCESS. Instead
2700
+ // flag it [~] blocked, report an honest failure to the office (task_fail
2701
+ // → TASK_STATE_FAILED, "needs attention") and record a StopFailure hook
2702
+ // so external monitors see it too. The task stays claimable/retryable.
2703
+ const reason = decision.reason;
2704
+ this._failedCount++;
2705
+ const taskKey = task.id ?? task.text;
2706
+ this._blockedTasks.add(taskKey);
2707
+ // Drop the attempt counter so a later retry gets a clean run rather
2708
+ // than being force-marked done on its first re-dispatch.
2709
+ this._taskAttempts.delete(taskKey);
2710
+ this._cb?.log(`⛔ Provider hard-failure (${reason}) — NOT marking done; flagging task as blocked/needs-attention: ${discordLabel(task.text)}`);
2711
+ await todoWriteManager_1.todoWriter.markInProgress(todoPath, task).catch(() => { });
2712
+ const duration = Math.round((Date.now() - taskStartTime) / 1000);
2713
+ const errText = `provider hard-failure: ${reason} (task not completed — needs attention)`;
2714
+ this._notifyWebhook('task_fail', {
2715
+ iteration: this._iterations,
2716
+ task: { text: task.text, id: task.id },
2717
+ duration,
2718
+ error: errText,
2719
+ reason,
2720
+ workDir: this._workspaceRoot,
2721
+ gitRepo: this._gitRepo,
2722
+ gitBranch: this._gitBranch,
2723
+ });
2724
+ this._notifyDiscord(`⛔ **Provider failure — task blocked (not done):**\n${discordLabel(task.text)}\n\`${reason}\``);
2725
+ if (this._workspaceRoot) {
2726
+ (0, liveNarration_1.appendHookEventLine)(this._workspaceRoot, {
2727
+ hook_event_name: 'StopFailure', event_type: 'stop_failure',
2728
+ provider: activeProvider, cwd: this._workspaceRoot, error: reason,
2729
+ title: 'Provider failure', message: errText, tool_name: '',
2730
+ timestamp: new Date().toISOString(),
2731
+ });
2732
+ }
2733
+ cleanup();
2734
+ resolve();
2735
+ return;
2736
+ }
2621
2737
  if (decision.kind === 'deferred') {
2622
2738
  this._cb?.log(`↩️ CLI exited with task [~] deferred — moving to next pending task: ${discordLabel(task.text)}`);
2623
2739
  cleanup();