@claude-flow/cli 3.71.2 → 3.71.3

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.
@@ -985,12 +985,12 @@ export const agentdbBatch = {
985
985
  // ===== agentdb_context_synthesize — Synthesize context from memories =====
986
986
  export const agentdbContextSynthesize = {
987
987
  name: 'agentdb_context-synthesize',
988
- description: 'Synthesize context from stored memories for a given query Use when generic memory_* tools are wrong because you need AgentDB-specific controllers (HNSW vector search, hierarchical tiers, causal-graph links, pattern store/recall, RaBitQ quantization). For simple key-value persistence, memory_store/memory_retrieve are simpler. For unrelated file work, native Read/Write are fine.',
988
+ description: 'Summarize validated task episodes (task, reward, success, critique) that match a query: observed success rate, reward and critique-derived patterns. Ordinary documents, research notes and graph entities are not episodes, so for them it answers no-memories or no-eligible-episodes; read those with memory_search or memory_retrieve instead. Use when generic memory_* tools are wrong because you need AgentDB-specific controllers (HNSW vector search, hierarchical tiers, causal-graph links, pattern store/recall, RaBitQ quantization). For simple key-value persistence, memory_store/memory_retrieve are simpler. For unrelated file work, native Read/Write are fine.',
989
989
  inputSchema: {
990
990
  type: 'object',
991
991
  properties: {
992
- query: { type: 'string', description: 'Query to synthesize context for' },
993
- maxEntries: { type: 'number', description: 'Maximum entries to include (default: 10)' },
992
+ query: { type: 'string', description: 'Task to find matching validated episodes for' },
993
+ maxEntries: { type: 'number', description: 'Maximum episodes to include (default: 10)' },
994
994
  },
995
995
  required: ['query'],
996
996
  },
@@ -36,6 +36,10 @@ function formatClaimant(claimant) {
36
36
  ? `human:${claimant.userId}:${claimant.name}`
37
37
  : `agent:${claimant.agentId}:${claimant.agentType}`;
38
38
  }
39
+ /** An agent's claim that is still work in hand (not completed). */
40
+ function isOpenAgentClaim(claim) {
41
+ return claim.claimant.type === 'agent' && claim.status !== 'completed';
42
+ }
39
43
  function parseClaimant(str) {
40
44
  const parts = str.split(':');
41
45
  if (parts[0] === 'human' && parts.length >= 3) {
@@ -274,6 +278,10 @@ export const claimsTools = [
274
278
  if (progress !== undefined) {
275
279
  claim.progress = Math.min(100, Math.max(0, progress));
276
280
  }
281
+ // A claim being handed to a named claimant is no longer up for grabs:
282
+ // drop it from the list claims_stealable and claims_steal read, so it
283
+ // cannot be stolen while the handoff is pending or after it is accepted.
284
+ delete store.stealable[issueId];
277
285
  store.claims[issueId] = claim;
278
286
  saveClaims(store);
279
287
  return {
@@ -678,12 +686,12 @@ export const claimsTools = [
678
686
  return { success: false, error: v.error };
679
687
  }
680
688
  const store = loadClaims();
681
- const claims = Object.values(store.claims);
689
+ // A completed claim stays in the store but is no longer work in hand,
690
+ // so it does not count toward an agent's load.
691
+ const openAgentClaims = Object.values(store.claims).filter(isOpenAgentClaim);
682
692
  // Group claims by agent
683
693
  const agentLoads = new Map();
684
- for (const claim of claims) {
685
- if (claim.claimant.type !== 'agent')
686
- continue;
694
+ for (const claim of openAgentClaims) {
687
695
  const key = claim.claimant.agentId;
688
696
  if (!agentLoads.has(key)) {
689
697
  agentLoads.set(key, {
@@ -723,7 +731,7 @@ export const claimsTools = [
723
731
  success: true,
724
732
  loads: result,
725
733
  totalAgents: result.length,
726
- totalClaims: claims.filter(c => c.claimant.type === 'agent').length,
734
+ totalClaims: openAgentClaims.length,
727
735
  avgUtilization: result.length > 0
728
736
  ? result.reduce((sum, l) => sum + l.utilization, 0) / result.length
729
737
  : 0,
@@ -802,12 +810,11 @@ export const claimsTools = [
802
810
  const dryRun = input.dryRun !== false;
803
811
  const targetUtilization = input.targetUtilization || 0.7;
804
812
  const store = loadClaims();
805
- const claims = Object.values(store.claims);
813
+ // Completed claims are not load, as in claims_load.
814
+ const openAgentClaims = Object.values(store.claims).filter(isOpenAgentClaim);
806
815
  // Group by agent
807
816
  const agentLoads = new Map();
808
- for (const claim of claims) {
809
- if (claim.claimant.type !== 'agent')
810
- continue;
817
+ for (const claim of openAgentClaims) {
811
818
  const key = claim.claimant.agentId;
812
819
  if (!agentLoads.has(key)) {
813
820
  agentLoads.set(key, { agentId: key, agentType: claim.claimant.agentType, claims: [] });
@@ -4096,11 +4096,21 @@ export const hooksIntelligenceLearn = {
4096
4096
  inputSchema: {
4097
4097
  type: 'object',
4098
4098
  properties: {
4099
- trajectoryIds: { type: 'array', items: { type: 'string' }, description: 'Specific trajectories to learn from' },
4099
+ trajectoryIds: { type: 'array', items: { type: 'string' }, description: 'Not supported: a learning cycle always covers every recorded trajectory. Supplying this returns an error and runs nothing, rather than learning from more than was asked (#4693).' },
4100
4100
  consolidate: { type: 'boolean', description: 'Run EWC++ consolidation' },
4101
4101
  },
4102
4102
  },
4103
4103
  handler: async (params) => {
4104
+ // #4693: distillLearning() and the background pass have no way to select
4105
+ // trajectories, so a request scoped to some must not trigger learning over all.
4106
+ // Refuse it before anything is imported or mutated; omitting the field is unchanged.
4107
+ if (params.trajectoryIds !== undefined) {
4108
+ return {
4109
+ success: false,
4110
+ learned: false,
4111
+ error: 'hooks_intelligence_learn: trajectoryIds is not supported; a learning cycle always covers every recorded trajectory. Omit trajectoryIds to run it.',
4112
+ };
4113
+ }
4104
4114
  const consolidate = params.consolidate !== false;
4105
4115
  const startTime = Date.now();
4106
4116
  // AUDIT FIX #5: actually TRIGGER a learning/consolidation cycle instead of
@@ -117,6 +117,10 @@ export const taskTools = [
117
117
  const vDesc = validateText(input.description, 'description');
118
118
  if (!vDesc.valid)
119
119
  return { success: false, error: vDesc.error };
120
+ if (input.assignTo !== undefined && (!Array.isArray(input.assignTo)
121
+ || !input.assignTo.every(id => validateIdentifier(id, 'assignTo').valid))) {
122
+ return { success: false, error: 'assignTo must be an array of agent ids' };
123
+ }
120
124
  const store = loadTaskStore();
121
125
  const taskId = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
122
126
  const task = {
@@ -134,6 +138,13 @@ export const taskTools = [
134
138
  };
135
139
  store.tasks[taskId] = task;
136
140
  saveTaskStore(store);
141
+ // Assigning at creation must mark the workers busy, as task_assign and task_update do;
142
+ // otherwise they read idle while holding the task, and task_complete or task_cancel,
143
+ // which only release a worker whose currentTask is this task, never free them either.
144
+ updateAgents(task.assignedTo, (agent) => {
145
+ agent.status = 'busy';
146
+ agent.currentTask = taskId;
147
+ });
137
148
  return {
138
149
  taskId,
139
150
  type: task.type,
@@ -77,15 +77,15 @@ export declare const missionRecordSchema: z.ZodObject<{
77
77
  updatedAt: string;
78
78
  schemaVersion: 1;
79
79
  state: "completed" | "cancelled" | "failed" | "blocked" | "running" | "paused" | "queued" | "draft" | "planned" | "awaitingAuthorization" | "pauseRequested" | "verifying" | "cancelRequested";
80
- acceptance: {
81
- revision: number;
82
- criteriaDigest: string;
83
- };
84
80
  plan: {
85
81
  digest: string;
86
82
  revision: number;
87
83
  taskGraphRef: string;
88
84
  };
85
+ acceptance: {
86
+ revision: number;
87
+ criteriaDigest: string;
88
+ };
89
89
  objective: string;
90
90
  missionId: string;
91
91
  tenantId: string;
@@ -103,15 +103,15 @@ export declare const missionRecordSchema: z.ZodObject<{
103
103
  updatedAt: string;
104
104
  schemaVersion: 1;
105
105
  state: "completed" | "cancelled" | "failed" | "blocked" | "running" | "paused" | "queued" | "draft" | "planned" | "awaitingAuthorization" | "pauseRequested" | "verifying" | "cancelRequested";
106
- acceptance: {
107
- revision: number;
108
- criteriaDigest: string;
109
- };
110
106
  plan: {
111
107
  digest: string;
112
108
  revision: number;
113
109
  taskGraphRef: string;
114
110
  };
111
+ acceptance: {
112
+ revision: number;
113
+ criteriaDigest: string;
114
+ };
115
115
  objective: string;
116
116
  missionId: string;
117
117
  tenantId: string;
@@ -580,7 +580,6 @@ export declare const planInputSchema: z.ZodObject<{
580
580
  } | undefined;
581
581
  }>;
582
582
  }, "strict", z.ZodTypeAny, {
583
- requestId: string;
584
583
  plan: {
585
584
  tasks: {
586
585
  id: string;
@@ -617,10 +616,10 @@ export declare const planInputSchema: z.ZodObject<{
617
616
  baseline?: string | undefined;
618
617
  }[];
619
618
  };
619
+ requestId: string;
620
620
  missionId: string;
621
621
  expectedRevision: number;
622
622
  }, {
623
- requestId: string;
624
623
  plan: {
625
624
  tasks: {
626
625
  id: string;
@@ -657,6 +656,7 @@ export declare const planInputSchema: z.ZodObject<{
657
656
  capabilities?: string[] | undefined;
658
657
  } | undefined;
659
658
  };
659
+ requestId: string;
660
660
  missionId: string;
661
661
  expectedRevision: number;
662
662
  }>;
@@ -14,10 +14,20 @@
14
14
  *
15
15
  * Profile override: `RUFLO_HEADLESS_PLUGINS` is a comma list of plugin names;
16
16
  * `none` (or an empty value) turns plugin loading off.
17
+ *
18
+ * Permission mode: Claude Code 2.1.302 starts a `claude -p` with no
19
+ * permission mode in `manual` mode on third-party providers, with telemetry
20
+ * off, or on a fresh install. A headless worker has nobody to answer a prompt,
21
+ * so a print-mode argv without its own `--permission-mode` or
22
+ * `--dangerously-skip-permissions` gets `--permission-mode auto` — the mode
23
+ * first-party installs already started in. `RUFLO_HEADLESS_PERMISSION_MODE`
24
+ * picks another mode; `none` (or an empty value) passes no flag.
17
25
  */
18
26
  import { type ResolveOptions, type PluginPaths } from './plugin-paths.js';
19
27
  export declare const DEFAULT_WORKER_PLUGINS: readonly string[];
20
28
  export declare const PROFILE_ENV = "RUFLO_HEADLESS_PLUGINS";
29
+ export declare const PERMISSION_MODE_ENV = "RUFLO_HEADLESS_PERMISSION_MODE";
30
+ export declare const DEFAULT_HEADLESS_PERMISSION_MODE = "auto";
21
31
  export interface HeadlessPluginOptions extends ResolveOptions {
22
32
  /** Plugin names to load; defaults to the env profile, else DEFAULT_WORKER_PLUGINS. */
23
33
  profile?: readonly string[];
@@ -40,5 +50,11 @@ export declare function resetHeadlessPluginWarnings(): void;
40
50
  export declare function workerProfile(env?: NodeJS.ProcessEnv, explicit?: readonly string[]): string[];
41
51
  /** Every `--plugin-dir` value already in argv (both `--plugin-dir x` and `--plugin-dir=x`). */
42
52
  export declare function existingPluginDirs(argv: readonly string[]): string[];
53
+ /**
54
+ * The `--permission-mode` args to append to a headless argv: none when the
55
+ * argv is not print mode or already sets a mode, else the env choice
56
+ * (default `auto`). An unknown mode is ignored with the default used instead.
57
+ */
58
+ export declare function headlessPermissionArgs(argv: readonly string[], env?: NodeJS.ProcessEnv): string[];
43
59
  export declare function withHeadlessPlugins<E extends Record<string, string | undefined>>(argv: readonly string[], env: E, opts?: HeadlessPluginOptions): HeadlessPluginResult<E>;
44
60
  //# sourceMappingURL=headless-plugins.d.ts.map
@@ -14,6 +14,14 @@
14
14
  *
15
15
  * Profile override: `RUFLO_HEADLESS_PLUGINS` is a comma list of plugin names;
16
16
  * `none` (or an empty value) turns plugin loading off.
17
+ *
18
+ * Permission mode: Claude Code 2.1.302 starts a `claude -p` with no
19
+ * permission mode in `manual` mode on third-party providers, with telemetry
20
+ * off, or on a fresh install. A headless worker has nobody to answer a prompt,
21
+ * so a print-mode argv without its own `--permission-mode` or
22
+ * `--dangerously-skip-permissions` gets `--permission-mode auto` — the mode
23
+ * first-party installs already started in. `RUFLO_HEADLESS_PERMISSION_MODE`
24
+ * picks another mode; `none` (or an empty value) passes no flag.
17
25
  */
18
26
  import { realpathSync } from 'node:fs';
19
27
  import { basename, resolve } from 'node:path';
@@ -21,6 +29,9 @@ import { ENABLE_ENV } from './install.js';
21
29
  import { resolvePluginPaths } from './plugin-paths.js';
22
30
  export const DEFAULT_WORKER_PLUGINS = ['ruflo-mods'];
23
31
  export const PROFILE_ENV = 'RUFLO_HEADLESS_PLUGINS';
32
+ export const PERMISSION_MODE_ENV = 'RUFLO_HEADLESS_PERMISSION_MODE';
33
+ export const DEFAULT_HEADLESS_PERMISSION_MODE = 'auto';
34
+ const PERMISSION_MODES = new Set(['acceptEdits', 'auto', 'bypassPermissions', 'manual', 'default', 'dontAsk', 'plan']);
24
35
  const warned = new Set();
25
36
  /** For tests: forget which missing plugins were already logged. */
26
37
  export function resetHeadlessPluginWarnings() {
@@ -56,9 +67,27 @@ export function existingPluginDirs(argv) {
56
67
  }
57
68
  return dirs;
58
69
  }
70
+ /**
71
+ * The `--permission-mode` args to append to a headless argv: none when the
72
+ * argv is not print mode or already sets a mode, else the env choice
73
+ * (default `auto`). An unknown mode is ignored with the default used instead.
74
+ */
75
+ export function headlessPermissionArgs(argv, env = process.env) {
76
+ const isPrint = argv.some((a) => a === '-p' || a === '--print');
77
+ if (!isPrint)
78
+ return [];
79
+ const hasMode = argv.some((a) => a === '--permission-mode' || a.startsWith('--permission-mode=') || a === '--dangerously-skip-permissions');
80
+ if (hasMode)
81
+ return [];
82
+ const raw = env[PERMISSION_MODE_ENV];
83
+ if (raw !== undefined && (raw.trim() === '' || raw.trim().toLowerCase() === 'none'))
84
+ return [];
85
+ const mode = raw !== undefined && PERMISSION_MODES.has(raw.trim()) ? raw.trim() : DEFAULT_HEADLESS_PERMISSION_MODE;
86
+ return ['--permission-mode', mode];
87
+ }
59
88
  export function withHeadlessPlugins(argv, env, opts = {}) {
60
89
  const profile = workerProfile(opts.env ?? env, opts.profile);
61
- const outArgv = [...argv];
90
+ const outArgv = [...argv, ...headlessPermissionArgs(argv, opts.env ?? env)];
62
91
  const outEnv = { ...env };
63
92
  if (profile.length === 0)
64
93
  return { argv: outArgv, env: outEnv, added: [], missing: [] };
@@ -1,5 +1,29 @@
1
1
  /** True if a live daemon already holds the project's pidfile. */
2
2
  export declare function isDaemonAlive(projectRoot: string): boolean;
3
+ /**
4
+ * #4654: an explicit `daemon stop` is an opt-out that must survive the next
5
+ * hook. Every ruflo-core plugin hook (PreToolUse/PostToolUse, PreCompact, Stop)
6
+ * runs the CLI, and the CLI auto-starts a daemon whenever none is alive, so a
7
+ * stopped daemon came back within minutes. `daemon stop` now writes this
8
+ * marker, auto-start honours it, and only an explicit `daemon start` clears it.
9
+ */
10
+ export declare const DAEMON_STOP_MARKER = "daemon.stopped";
11
+ /**
12
+ * Set on the daemon that auto-start spawns, so that spawned `daemon start` does
13
+ * not clear a stop marker written after auto-start checked it (a `daemon stop`
14
+ * racing a hook). Only a user-driven start clears the marker.
15
+ */
16
+ export declare const AUTOSTART_ENV = "RUFLO_DAEMON_AUTOSTARTED";
17
+ /** True if the user explicitly stopped the daemon for this project (#4654). */
18
+ export declare function isDaemonStopMarked(projectRoot: string): boolean;
19
+ /**
20
+ * Record an explicit `daemon stop` (#4654). Writes only into a directory that is
21
+ * already a Ruflo project or already has `.claude-flow/`, so `daemon stop` in an
22
+ * unrelated directory does not litter it. Best-effort; never throws.
23
+ */
24
+ export declare function writeDaemonStopMarker(projectRoot: string): boolean;
25
+ /** Clear the stop marker. Called by an explicit `daemon start` (#4654). Never throws. */
26
+ export declare function clearDaemonStopMarker(projectRoot: string): void;
3
27
  /**
4
28
  * Return true only when the directory contains a durable Ruflo project marker.
5
29
  *
@@ -81,6 +81,57 @@ function autostartDisabledByProjectConfig(projectRoot) {
81
81
  }
82
82
  return false;
83
83
  }
84
+ /**
85
+ * #4654: an explicit `daemon stop` is an opt-out that must survive the next
86
+ * hook. Every ruflo-core plugin hook (PreToolUse/PostToolUse, PreCompact, Stop)
87
+ * runs the CLI, and the CLI auto-starts a daemon whenever none is alive, so a
88
+ * stopped daemon came back within minutes. `daemon stop` now writes this
89
+ * marker, auto-start honours it, and only an explicit `daemon start` clears it.
90
+ */
91
+ export const DAEMON_STOP_MARKER = 'daemon.stopped';
92
+ /**
93
+ * Set on the daemon that auto-start spawns, so that spawned `daemon start` does
94
+ * not clear a stop marker written after auto-start checked it (a `daemon stop`
95
+ * racing a hook). Only a user-driven start clears the marker.
96
+ */
97
+ export const AUTOSTART_ENV = 'RUFLO_DAEMON_AUTOSTARTED';
98
+ function stopMarkerPath(projectRoot) {
99
+ return path.join(projectRoot, '.claude-flow', DAEMON_STOP_MARKER);
100
+ }
101
+ /** True if the user explicitly stopped the daemon for this project (#4654). */
102
+ export function isDaemonStopMarked(projectRoot) {
103
+ try {
104
+ return fs.existsSync(stopMarkerPath(projectRoot));
105
+ }
106
+ catch {
107
+ return false;
108
+ }
109
+ }
110
+ /**
111
+ * Record an explicit `daemon stop` (#4654). Writes only into a directory that is
112
+ * already a Ruflo project or already has `.claude-flow/`, so `daemon stop` in an
113
+ * unrelated directory does not litter it. Best-effort; never throws.
114
+ */
115
+ export function writeDaemonStopMarker(projectRoot) {
116
+ try {
117
+ const dir = path.join(projectRoot, '.claude-flow');
118
+ if (!fs.existsSync(dir) && !isRufloProject(projectRoot))
119
+ return false;
120
+ fs.mkdirSync(dir, { recursive: true });
121
+ fs.writeFileSync(stopMarkerPath(projectRoot), JSON.stringify({ stoppedAt: new Date().toISOString(), pid: process.pid }) + '\n');
122
+ return true;
123
+ }
124
+ catch {
125
+ return false;
126
+ }
127
+ }
128
+ /** Clear the stop marker. Called by an explicit `daemon start` (#4654). Never throws. */
129
+ export function clearDaemonStopMarker(projectRoot) {
130
+ try {
131
+ fs.unlinkSync(stopMarkerPath(projectRoot));
132
+ }
133
+ catch { /* absent */ }
134
+ }
84
135
  function autostartDisabled(projectRoot) {
85
136
  if (/^(0|false|no|off)$/i.test(process.env.RUFLO_DAEMON_AUTOSTART ?? ''))
86
137
  return true;
@@ -166,7 +217,7 @@ const defaultSpawn = (projectRoot) => {
166
217
  cwd: projectRoot,
167
218
  detached: true,
168
219
  stdio: 'ignore',
169
- env: { ...process.env },
220
+ env: { ...process.env, [AUTOSTART_ENV]: '1' },
170
221
  });
171
222
  child.unref();
172
223
  };
@@ -186,6 +237,9 @@ export function ensureDaemonRunning(startDir, opts = {}) {
186
237
  if (!isRufloProject(projectRoot)) {
187
238
  return { started: false, reason: 'not a ruflo project' };
188
239
  }
240
+ if (isDaemonStopMarked(projectRoot)) {
241
+ return { started: false, reason: 'stopped by user (daemon stop); run `daemon start` to re-enable' };
242
+ }
189
243
  const alive = (opts.isAlive ?? isDaemonAlive)(projectRoot);
190
244
  if (alive)
191
245
  return { started: false, reason: 'already running' };
@@ -123,6 +123,12 @@ export interface HeadlessExecutionResult {
123
123
  inputTokens?: number;
124
124
  outputTokens?: number;
125
125
  costUsd?: number;
126
+ /**
127
+ * When the stored claude.ai sign-in the worker ran under ends (ISO 8601),
128
+ * from `credential_expires_at` in the `claude -p` JSON result (Claude Code
129
+ * 2.1.318+). Absent for API-key auth and older CLIs.
130
+ */
131
+ credentialExpiresAt?: string;
126
132
  /** Model used for execution */
127
133
  model: string;
128
134
  /** Sandbox mode used */
@@ -202,7 +208,16 @@ export interface ClaudePrintEnvelope {
202
208
  outputTokens?: number;
203
209
  costUsd?: number;
204
210
  durationMs?: number;
211
+ /** ISO 8601 end of the stored claude.ai sign-in (Claude Code 2.1.318+). */
212
+ credentialExpiresAt?: string;
205
213
  }
214
+ /** Days before a sign-in ends at which the executor emits 'credential-expiring' (matches Claude Code's own stderr warning). */
215
+ export declare const CREDENTIAL_EXPIRY_WARN_DAYS = 10;
216
+ /**
217
+ * Normalise `credential_expires_at` to ISO 8601. Accepts an ISO string or an
218
+ * epoch number (seconds or milliseconds); anything else is undefined.
219
+ */
220
+ export declare function normalizeCredentialExpiry(v: unknown): string | undefined;
206
221
  /**
207
222
  * #2661 root-fix — best-effort parse of `claude --print --output-format
208
223
  * json`'s response envelope. Deliberately lenient: probes a couple of
@@ -337,6 +337,23 @@ export function isLocalWorker(type) {
337
337
  export function getModelId(model) {
338
338
  return MODEL_IDS[model];
339
339
  }
340
+ /** Days before a sign-in ends at which the executor emits 'credential-expiring' (matches Claude Code's own stderr warning). */
341
+ export const CREDENTIAL_EXPIRY_WARN_DAYS = 10;
342
+ /**
343
+ * Normalise `credential_expires_at` to ISO 8601. Accepts an ISO string or an
344
+ * epoch number (seconds or milliseconds); anything else is undefined.
345
+ */
346
+ export function normalizeCredentialExpiry(v) {
347
+ let ms;
348
+ if (typeof v === 'number' && Number.isFinite(v) && v > 0)
349
+ ms = v < 1e12 ? v * 1000 : v;
350
+ else if (typeof v === 'string' && v.trim()) {
351
+ const t = Date.parse(v);
352
+ if (Number.isFinite(t))
353
+ ms = t;
354
+ }
355
+ return ms === undefined ? undefined : new Date(ms).toISOString();
356
+ }
340
357
  /**
341
358
  * #2661 root-fix — best-effort parse of `claude --print --output-format
342
359
  * json`'s response envelope. Deliberately lenient: probes a couple of
@@ -371,6 +388,7 @@ export function parseClaudePrintJsonEnvelope(raw) {
371
388
  outputTokens: numOrUndef(usage?.output_tokens ?? usage?.outputTokens),
372
389
  costUsd: numOrUndef(obj.total_cost_usd ?? obj.cost_usd ?? obj.totalCostUsd),
373
390
  durationMs: numOrUndef(obj.duration_ms ?? obj.durationMs),
391
+ credentialExpiresAt: normalizeCredentialExpiry(obj.credential_expires_at ?? obj.credentialExpiresAt),
374
392
  };
375
393
  }
376
394
  /**
@@ -779,7 +797,16 @@ export class HeadlessWorkerExecutor extends EventEmitter {
779
797
  executionId,
780
798
  error: result.error,
781
799
  ...(result.missingPlugins ? { missingPlugins: result.missingPlugins } : {}),
800
+ ...(result.credentialExpiresAt ? { credentialExpiresAt: result.credentialExpiresAt } : {}),
782
801
  };
802
+ // Claude Code 2.1.318: surface a claude.ai sign-in that ends soon, so the
803
+ // daemon can say so before workers start failing on auth.
804
+ if (result.credentialExpiresAt) {
805
+ const daysLeft = (Date.parse(result.credentialExpiresAt) - Date.now()) / 86_400_000;
806
+ if (daysLeft <= CREDENTIAL_EXPIRY_WARN_DAYS) {
807
+ this.emit('credential-expiring', { expiresAt: result.credentialExpiresAt, daysLeft: Math.max(0, Math.floor(daysLeft)), workerType });
808
+ }
809
+ }
783
810
  // Log result
784
811
  this.logExecution(executionId, 'result', JSON.stringify(executionResult, null, 2));
785
812
  // #2661 root-fix — structured per-launch telemetry, receipted
@@ -1182,6 +1209,7 @@ Analyze the above codebase context and provide your response following the forma
1182
1209
  : undefined,
1183
1210
  costUsd: envelope?.costUsd,
1184
1211
  apiDurationMs: envelope?.durationMs,
1212
+ credentialExpiresAt: envelope?.credentialExpiresAt,
1185
1213
  error: code !== 0 ? stderr || `Process exited with code ${code}` : undefined,
1186
1214
  });
1187
1215
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/cli",
3
- "version": "3.71.2",
3
+ "version": "3.71.3",
4
4
  "type": "module",
5
5
  "description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
6
6
  "main": "dist/src/index.js",