@foxden-app/foxclaw 0.3.17 → 0.3.18

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.
@@ -11,19 +11,20 @@ export interface CodexLocalUsageStats {
11
11
  turns: number;
12
12
  usageEvents: number;
13
13
  totals: CodexLocalUsageTotals;
14
- outputSpeed: CodexLocalOutputSpeedStats;
14
+ responseThroughput: CodexLocalResponseThroughputStats;
15
15
  latestSessionMtimeMs: number | null;
16
16
  }
17
17
  export interface CodexLocalUsageSnapshot {
18
18
  computedAtMs: number;
19
19
  stats: CodexLocalUsageStats;
20
20
  }
21
- export interface CodexLocalOutputSpeedStats {
22
- samples: number;
23
- outputTokens: number;
21
+ export interface CodexLocalResponseThroughputStats {
22
+ completedTurns: number;
23
+ visibleOutputTokens: number;
24
24
  seconds: number;
25
- latestTokensPerSecond: number | null;
26
- latestSampleAtMs: number | null;
25
+ recentCompletedTurns: number;
26
+ recentVisibleOutputTokens: number;
27
+ recentSeconds: number;
27
28
  }
28
29
  export declare function readCodexLocalUsageStats(codexHome?: string): Promise<CodexLocalUsageStats>;
29
30
  export declare function readCodexLocalUsageSnapshot(snapshotPath: string): Promise<CodexLocalUsageSnapshot | null>;
@@ -3,6 +3,7 @@ import fs from 'node:fs/promises';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import readline from 'node:readline';
6
+ const RECENT_COMPLETED_TURNS = 10;
6
7
  export async function readCodexLocalUsageStats(codexHome = resolveCodexHome()) {
7
8
  const sessionFiles = await listJsonlFiles([
8
9
  path.join(codexHome, 'sessions'),
@@ -13,7 +14,7 @@ export async function readCodexLocalUsageStats(codexHome = resolveCodexHome()) {
13
14
  let sessionsWithUsage = 0;
14
15
  let usageEvents = 0;
15
16
  let latestSessionMtimeMs = null;
16
- const outputSpeed = emptyOutputSpeedStats();
17
+ const completedTurnSamples = [];
17
18
  for (const filePath of sessionFiles) {
18
19
  const stat = await fs.stat(filePath).catch(() => null);
19
20
  if (stat) {
@@ -21,7 +22,7 @@ export async function readCodexLocalUsageStats(codexHome = resolveCodexHome()) {
21
22
  }
22
23
  const fileUsage = await readSessionUsage(filePath, turnIds);
23
24
  usageEvents += fileUsage.usageEvents;
24
- addOutputSpeed(outputSpeed, fileUsage.outputSpeed);
25
+ completedTurnSamples.push(...fileUsage.completedTurnSamples);
25
26
  if (fileUsage.totalUsage) {
26
27
  sessionsWithUsage += 1;
27
28
  addUsage(totals, fileUsage.totalUsage);
@@ -33,7 +34,7 @@ export async function readCodexLocalUsageStats(codexHome = resolveCodexHome()) {
33
34
  turns: turnIds.size,
34
35
  usageEvents,
35
36
  totals,
36
- outputSpeed,
37
+ responseThroughput: summarizeResponseThroughput(completedTurnSamples),
37
38
  latestSessionMtimeMs,
38
39
  };
39
40
  }
@@ -87,8 +88,8 @@ async function readSessionUsage(filePath, turnIds) {
87
88
  const reader = readline.createInterface({ input: stream, crlfDelay: Infinity });
88
89
  let totalUsage = null;
89
90
  let usageEvents = 0;
90
- let generationStartMs = null;
91
- const outputSpeed = emptyOutputSpeedStats();
91
+ let activeTurn = null;
92
+ const completedTurnSamples = [];
92
93
  try {
93
94
  for await (const line of reader) {
94
95
  if (!line.trim())
@@ -105,28 +106,49 @@ async function readSessionUsage(filePath, turnIds) {
105
106
  if (turnId) {
106
107
  turnIds.add(turnId);
107
108
  }
108
- if (timestampMs !== null && isGenerationBoundary(event)) {
109
- generationStartMs = timestampMs;
109
+ if (timestampMs !== null
110
+ && turnId
111
+ && event?.type === 'event_msg'
112
+ && event?.payload?.type === 'task_started') {
113
+ activeTurn = { turnId, startedAtMs: timestampMs, visibleOutputTokens: 0 };
110
114
  }
111
115
  const info = event?.payload?.info;
112
116
  const lastTokenUsage = info?.last_token_usage ?? info?.lastTokenUsage;
113
117
  if (lastTokenUsage) {
114
118
  usageEvents += 1;
115
- addOutputSpeedSample(outputSpeed, lastTokenUsage, generationStartMs, timestampMs);
116
- if (timestampMs !== null) {
117
- generationStartMs = timestampMs;
119
+ if (activeTurn) {
120
+ activeTurn.visibleOutputTokens += visibleOutputTokens(lastTokenUsage);
118
121
  }
119
122
  }
120
123
  const totalTokenUsage = info?.total_token_usage ?? info?.totalTokenUsage;
121
124
  if (totalTokenUsage) {
122
125
  totalUsage = totalTokenUsage;
123
126
  }
127
+ const completedTurn = activeTurn;
128
+ if (completedTurn !== null
129
+ && timestampMs !== null
130
+ && turnId
131
+ && completedTurn.turnId === turnId
132
+ && event?.type === 'event_msg'
133
+ && (event?.payload?.type === 'task_complete' || event?.payload?.type === 'turn_aborted')) {
134
+ if (event.payload.type === 'task_complete' && completedTurn.visibleOutputTokens > 0) {
135
+ const seconds = (timestampMs - completedTurn.startedAtMs) / 1000;
136
+ if (Number.isFinite(seconds) && seconds > 0) {
137
+ completedTurnSamples.push({
138
+ completedAtMs: timestampMs,
139
+ visibleOutputTokens: completedTurn.visibleOutputTokens,
140
+ seconds,
141
+ });
142
+ }
143
+ }
144
+ activeTurn = null;
145
+ }
124
146
  }
125
147
  }
126
148
  finally {
127
149
  reader.close();
128
150
  }
129
- return { usageEvents, totalUsage, outputSpeed };
151
+ return { usageEvents, totalUsage, completedTurnSamples };
130
152
  }
131
153
  function emptyTotals() {
132
154
  return {
@@ -137,22 +159,13 @@ function emptyTotals() {
137
159
  totalTokens: 0,
138
160
  };
139
161
  }
140
- function emptyOutputSpeedStats() {
141
- return {
142
- samples: 0,
143
- outputTokens: 0,
144
- seconds: 0,
145
- latestTokensPerSecond: null,
146
- latestSampleAtMs: null,
147
- };
148
- }
149
162
  function isCodexLocalUsageStats(value) {
150
163
  if (!value || typeof value !== 'object') {
151
164
  return false;
152
165
  }
153
166
  const stats = value;
154
167
  const totals = stats.totals;
155
- const speed = stats.outputSpeed;
168
+ const throughput = stats.responseThroughput;
156
169
  return isFiniteNumber(stats.sessionFiles)
157
170
  && isFiniteNumber(stats.sessionsWithUsage)
158
171
  && isFiniteNumber(stats.turns)
@@ -163,12 +176,13 @@ function isCodexLocalUsageStats(value) {
163
176
  && isFiniteNumber(totals?.outputTokens)
164
177
  && isFiniteNumber(totals?.reasoningOutputTokens)
165
178
  && isFiniteNumber(totals?.totalTokens)
166
- && Boolean(speed)
167
- && isFiniteNumber(speed?.samples)
168
- && isFiniteNumber(speed?.outputTokens)
169
- && isFiniteNumber(speed?.seconds)
170
- && isNullableFiniteNumber(speed?.latestTokensPerSecond)
171
- && isNullableFiniteNumber(speed?.latestSampleAtMs)
179
+ && Boolean(throughput)
180
+ && isFiniteNumber(throughput?.completedTurns)
181
+ && isFiniteNumber(throughput?.visibleOutputTokens)
182
+ && isFiniteNumber(throughput?.seconds)
183
+ && isFiniteNumber(throughput?.recentCompletedTurns)
184
+ && isFiniteNumber(throughput?.recentVisibleOutputTokens)
185
+ && isFiniteNumber(throughput?.recentSeconds)
172
186
  && isNullableFiniteNumber(stats.latestSessionMtimeMs);
173
187
  }
174
188
  function isFiniteNumber(value) {
@@ -187,34 +201,21 @@ function addUsage(totals, usage) {
187
201
  totals.reasoningOutputTokens += numberField(usage, 'reasoning_output_tokens', 'reasoningOutputTokens');
188
202
  totals.totalTokens += totalTokens || inputTokens + outputTokens;
189
203
  }
190
- function addOutputSpeed(target, source) {
191
- target.samples += source.samples;
192
- target.outputTokens += source.outputTokens;
193
- target.seconds += source.seconds;
194
- if (source.latestTokensPerSecond !== null
195
- && source.latestSampleAtMs !== null
196
- && (target.latestSampleAtMs === null || source.latestSampleAtMs > target.latestSampleAtMs)) {
197
- target.latestTokensPerSecond = source.latestTokensPerSecond;
198
- target.latestSampleAtMs = source.latestSampleAtMs;
199
- }
204
+ function visibleOutputTokens(usage) {
205
+ return Math.max(0, numberField(usage, 'output_tokens', 'outputTokens')
206
+ - numberField(usage, 'reasoning_output_tokens', 'reasoningOutputTokens'));
200
207
  }
201
- function addOutputSpeedSample(stats, usage, generationStartMs, timestampMs) {
202
- if (generationStartMs === null || timestampMs === null || timestampMs <= generationStartMs) {
203
- return;
204
- }
205
- const outputTokens = numberField(usage, 'output_tokens', 'outputTokens');
206
- if (outputTokens <= 0) {
207
- return;
208
- }
209
- const seconds = (timestampMs - generationStartMs) / 1000;
210
- if (!Number.isFinite(seconds) || seconds <= 0) {
211
- return;
212
- }
213
- stats.samples += 1;
214
- stats.outputTokens += outputTokens;
215
- stats.seconds += seconds;
216
- stats.latestTokensPerSecond = outputTokens / seconds;
217
- stats.latestSampleAtMs = timestampMs;
208
+ function summarizeResponseThroughput(samples) {
209
+ const ordered = samples.slice().sort((left, right) => left.completedAtMs - right.completedAtMs);
210
+ const recent = ordered.slice(-RECENT_COMPLETED_TURNS);
211
+ return {
212
+ completedTurns: ordered.length,
213
+ visibleOutputTokens: ordered.reduce((total, sample) => total + sample.visibleOutputTokens, 0),
214
+ seconds: ordered.reduce((total, sample) => total + sample.seconds, 0),
215
+ recentCompletedTurns: recent.length,
216
+ recentVisibleOutputTokens: recent.reduce((total, sample) => total + sample.visibleOutputTokens, 0),
217
+ recentSeconds: recent.reduce((total, sample) => total + sample.seconds, 0),
218
+ };
218
219
  }
219
220
  function numberField(source, snakeKey, camelKey) {
220
221
  const snakeValue = source[snakeKey];
@@ -229,15 +230,3 @@ function parseTimestampMs(value) {
229
230
  const timestamp = Date.parse(value);
230
231
  return Number.isFinite(timestamp) ? timestamp : null;
231
232
  }
232
- function isGenerationBoundary(event) {
233
- const payload = event?.payload;
234
- if (event?.type === 'response_item' && payload?.type === 'function_call_output') {
235
- return true;
236
- }
237
- if (event?.type !== 'event_msg') {
238
- return false;
239
- }
240
- return payload?.type === 'task_started'
241
- || payload?.type === 'exec_command_end'
242
- || payload?.type === 'user_message';
243
- }
@@ -267,7 +267,7 @@ export declare class BridgeSessionCore {
267
267
  private buildNativeCollaborationMode;
268
268
  private buildCodexUsageStatusLines;
269
269
  private buildCodexLocalUsageStatusLines;
270
- private formatCodexLocalOutputSpeedStatusLines;
270
+ private formatCodexLocalResponseThroughputStatusLines;
271
271
  private resolveFastStatusLabel;
272
272
  private readCachedCodexLocalUsageStats;
273
273
  private refreshCodexLocalUsageIfNeeded;
@@ -4544,11 +4544,12 @@ export class BridgeSessionCore {
4544
4544
  t(locale, 'status_codex_local_tokens', {
4545
4545
  total: formatTokenCount(stats.totals.totalTokens),
4546
4546
  input: formatTokenCount(stats.totals.inputTokens),
4547
+ visible: formatTokenCount(Math.max(0, stats.totals.outputTokens - stats.totals.reasoningOutputTokens)),
4547
4548
  output: formatTokenCount(stats.totals.outputTokens),
4548
4549
  cached: formatTokenCount(stats.totals.cachedInputTokens),
4549
4550
  reasoning: formatTokenCount(stats.totals.reasoningOutputTokens),
4550
4551
  }),
4551
- ...this.formatCodexLocalOutputSpeedStatusLines(locale, stats),
4552
+ ...this.formatCodexLocalResponseThroughputStatusLines(locale, stats),
4552
4553
  t(locale, 'status_codex_local_snapshot_at', {
4553
4554
  value: formatLocalTimestamp(snapshot.computedAtMs / 1000),
4554
4555
  }),
@@ -4559,16 +4560,21 @@ export class BridgeSessionCore {
4559
4560
  return [t(locale, 'status_codex_local_usage_unavailable', { error: formatShortStatusError(error) })];
4560
4561
  }
4561
4562
  }
4562
- formatCodexLocalOutputSpeedStatusLines(locale, stats) {
4563
- const speed = stats.outputSpeed;
4564
- if (speed.samples === 0 || speed.outputTokens <= 0 || speed.seconds <= 0) {
4563
+ formatCodexLocalResponseThroughputStatusLines(locale, stats) {
4564
+ const throughput = stats.responseThroughput;
4565
+ if (throughput.completedTurns === 0
4566
+ || throughput.visibleOutputTokens <= 0
4567
+ || throughput.seconds <= 0
4568
+ || throughput.recentCompletedTurns === 0
4569
+ || throughput.recentVisibleOutputTokens <= 0
4570
+ || throughput.recentSeconds <= 0) {
4565
4571
  return [];
4566
4572
  }
4567
- const avg = speed.outputTokens / speed.seconds;
4568
- return [t(locale, 'status_codex_local_speed', {
4569
- avg: formatCompactNumber(avg),
4570
- latest: speed.latestTokensPerSecond === null ? t(locale, 'unknown') : formatCompactNumber(speed.latestTokensPerSecond),
4571
- samples: formatTokenCount(speed.samples),
4573
+ return [t(locale, 'status_codex_local_throughput', {
4574
+ overall: formatCompactNumber(throughput.visibleOutputTokens / throughput.seconds),
4575
+ recent: formatCompactNumber(throughput.recentVisibleOutputTokens / throughput.recentSeconds),
4576
+ recentTurns: formatTokenCount(throughput.recentCompletedTurns),
4577
+ turns: formatTokenCount(throughput.completedTurns),
4572
4578
  })];
4573
4579
  }
4574
4580
  async resolveFastStatusLabel(locale, settings) {
package/dist/i18n.d.ts CHANGED
@@ -111,8 +111,8 @@ declare const MESSAGES: {
111
111
  readonly status_codex_usage_reset: ", resets {value}";
112
112
  readonly status_codex_usage_unavailable: "Codex usage: unavailable ({error})";
113
113
  readonly status_codex_local_history: "Codex local history: {sessions} sessions, {turns} turns, {events} usage records";
114
- readonly status_codex_local_tokens: "Codex local tokens: total {total}; input {input}, output {output}, cached input {cached}, reasoning output {reasoning}";
115
- readonly status_codex_local_speed: "Codex local output speed: avg {avg} token/s, latest {latest} token/s ({samples} samples)";
114
+ readonly status_codex_local_tokens: "Codex local tokens: total {total}; input {input}, visible output {visible}, reasoning output {reasoning}, total output {output}, cached input {cached}";
115
+ readonly status_codex_local_throughput: "Codex visible reply throughput (end-to-end, excluding reasoning): overall {overall} token/s, last {recentTurns} completed turns {recent} token/s ({turns} completed turns sampled)";
116
116
  readonly status_codex_local_snapshot_at: "Codex local stats snapshot: {value}";
117
117
  readonly status_codex_local_usage_refreshing: "Codex local history: building snapshot in background";
118
118
  readonly status_codex_local_usage_unavailable: "Codex local history: unavailable ({error})";
@@ -675,8 +675,8 @@ declare const MESSAGES: {
675
675
  readonly status_codex_usage_reset: ",重置时间 {value}";
676
676
  readonly status_codex_usage_unavailable: "Codex 用量:无法获取({error})";
677
677
  readonly status_codex_local_history: "Codex 本地历史:{sessions} 个会话,{turns} 轮,{events} 条用量记录";
678
- readonly status_codex_local_tokens: "Codex 本地 Token:总计 {total};输入 {input},输出 {output},缓存输入 {cached},推理输出 {reasoning}";
679
- readonly status_codex_local_speed: "Codex 本地输出速度:平均 {avg} token/s,最近 {latest} token/s({samples} 个样本)";
678
+ readonly status_codex_local_tokens: "Codex 本地 Token:总计 {total};输入 {input},可见输出 {visible},推理输出 {reasoning},总输出 {output},缓存输入 {cached}";
679
+ readonly status_codex_local_throughput: "Codex 可见答复吞吐(端到端,排除推理 token):整体 {overall} token/s,最近 {recentTurns} 个完成轮次 {recent} token/s({turns} 个完成轮次样本)";
680
680
  readonly status_codex_local_snapshot_at: "Codex 本地统计快照:{value}";
681
681
  readonly status_codex_local_usage_refreshing: "Codex 本地历史:正在后台生成统计快照";
682
682
  readonly status_codex_local_usage_unavailable: "Codex 本地历史:无法获取({error})";
package/dist/i18n.js CHANGED
@@ -109,8 +109,8 @@ const MESSAGES = {
109
109
  status_codex_usage_reset: ', resets {value}',
110
110
  status_codex_usage_unavailable: 'Codex usage: unavailable ({error})',
111
111
  status_codex_local_history: 'Codex local history: {sessions} sessions, {turns} turns, {events} usage records',
112
- status_codex_local_tokens: 'Codex local tokens: total {total}; input {input}, output {output}, cached input {cached}, reasoning output {reasoning}',
113
- status_codex_local_speed: 'Codex local output speed: avg {avg} token/s, latest {latest} token/s ({samples} samples)',
112
+ status_codex_local_tokens: 'Codex local tokens: total {total}; input {input}, visible output {visible}, reasoning output {reasoning}, total output {output}, cached input {cached}',
113
+ status_codex_local_throughput: 'Codex visible reply throughput (end-to-end, excluding reasoning): overall {overall} token/s, last {recentTurns} completed turns {recent} token/s ({turns} completed turns sampled)',
114
114
  status_codex_local_snapshot_at: 'Codex local stats snapshot: {value}',
115
115
  status_codex_local_usage_refreshing: 'Codex local history: building snapshot in background',
116
116
  status_codex_local_usage_unavailable: 'Codex local history: unavailable ({error})',
@@ -673,8 +673,8 @@ const MESSAGES = {
673
673
  status_codex_usage_reset: ',重置时间 {value}',
674
674
  status_codex_usage_unavailable: 'Codex 用量:无法获取({error})',
675
675
  status_codex_local_history: 'Codex 本地历史:{sessions} 个会话,{turns} 轮,{events} 条用量记录',
676
- status_codex_local_tokens: 'Codex 本地 Token:总计 {total};输入 {input},输出 {output},缓存输入 {cached},推理输出 {reasoning}',
677
- status_codex_local_speed: 'Codex 本地输出速度:平均 {avg} token/s,最近 {latest} token/s({samples} 个样本)',
676
+ status_codex_local_tokens: 'Codex 本地 Token:总计 {total};输入 {input},可见输出 {visible},推理输出 {reasoning},总输出 {output},缓存输入 {cached}',
677
+ status_codex_local_throughput: 'Codex 可见答复吞吐(端到端,排除推理 token):整体 {overall} token/s,最近 {recentTurns} 个完成轮次 {recent} token/s({turns} 个完成轮次样本)',
678
678
  status_codex_local_snapshot_at: 'Codex 本地统计快照:{value}',
679
679
  status_codex_local_usage_refreshing: 'Codex 本地历史:正在后台生成统计快照',
680
680
  status_codex_local_usage_unavailable: 'Codex 本地历史:无法获取({error})',
@@ -230,7 +230,7 @@ Later commands are sorted by recent usage. Plain text, photos, and files continu
230
230
 
231
231
  ### 3.2 `/status`, `/account`, `/quota`, `/update`
232
232
 
233
- - `/status`: FoxClaw, app-server, current thread binding, model, access, and Codex usage summary. Local session, token, and output-speed metrics use a background-generated historical snapshot instead of scanning large logs during the request.
233
+ - `/status`: FoxClaw, app-server, current thread binding, model, access, and Codex usage summary. Local session, token, and visible-reply-throughput metrics use a background-generated historical snapshot instead of scanning large logs during the request; throughput is computed end-to-end for completed turns, excluding reasoning tokens while including waiting and tool execution time.
234
234
  - `/account`: current Codex account.
235
235
  - `/quota`: Codex usage and quota window.
236
236
  - `/update`: upgrade FoxClaw, run checks, and restart the service; it refuses while a turn, approval, or question is active, then reports the result after restart.
@@ -230,7 +230,7 @@ TG_ALLOWED_TOPIC_ID=42
230
230
 
231
231
  ### 3.2 `/status`、`/account`、`/quota`、`/update`
232
232
 
233
- - `/status`:查看 FoxClaw、app-server、当前绑定线程、模型、权限和 Codex 用量摘要。本地 session/Token/输出速度使用后台生成的历史快照,避免状态查询现场扫描大量日志。
233
+ - `/status`:查看 FoxClaw、app-server、当前绑定线程、模型、权限和 Codex 用量摘要。本地 session/Token/可见答复吞吐使用后台生成的历史快照,避免状态查询现场扫描大量日志;答复吞吐按完成轮次端到端耗时计算,排除推理 token,但包含等待与工具执行时间。
234
234
  - `/account`:查看当前 Codex 登录账号。
235
235
  - `/quota`:查看 Codex 用量和额度窗口。
236
236
  - `/update`:升级 FoxClaw、自检并重启服务;当前有运行中回复、审批或待确认问题时会拒绝执行,重启后会回报结果。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxden-app/foxclaw",
3
- "version": "0.3.17",
3
+ "version": "0.3.18",
4
4
  "description": "Foxden local execution claw for controlling Codex from trusted chat interfaces.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",
@@ -190,10 +190,12 @@ Use this checklist when the user asks for standard closing actions, release wrap
190
190
  - If `doctor` fails only because `DEFAULT_CWD` is missing, report that separately; do not treat it as evidence that the service update failed.
191
191
  7. Publish to npm when requested:
192
192
  - Prefer GitHub Actions trusted publishing via `.github/workflows/publish.yml`: bump and commit the package version, push `main`, then push a matching `v<version>` tag. The tag version must match `package.json`.
193
+ - Treat `workflow_dispatch` only as a retry path from an existing matching release tag; do not manually run publishing from `main`.
193
194
  - Configure npmjs.com trusted publishing for `foxden-app/foxclaw` and workflow `publish.yml`; do not store npm tokens if OIDC trusted publishing is available.
194
195
  - Temporary fallback: store an npm automation/bypass-2FA token as the GitHub Actions secret `NPM_TOKEN`. Never print the token or commit it.
195
196
  - Check `npm whoami` and `npm view @foxden-app/foxclaw version`.
196
197
  - If the target version is already published, bump with `npm version patch --no-git-tag-version` before validation and commit.
198
+ - If a workflow fails before `npm publish`, inspect that failed GitHub Actions step before attributing it to npm trusted publishing.
197
199
  - Manual fallback only: run `BROWSER=true npm publish` in a TTY.
198
200
  - If npm returns `ENEEDAUTH`, report that npm publish is blocked by registry login and leave the package unreported as published.
199
201
  - Never print npm tokens or `.npmrc` auth values.
@@ -73,6 +73,8 @@ Use this path when the repo has `.github/workflows/publish.yml` and npmjs.com ha
73
73
  npm view <package-name> version
74
74
  ```
75
75
 
76
+ If the workflow provides `workflow_dispatch` as a recovery path, dispatch it only from an existing release tag whose version matches `package.json`. Do not dispatch publishing from a branch containing an unpublished version unless the workflow explicitly resolves and validates a release tag.
77
+
76
78
  Do not store or print npm tokens when trusted publishing is available. If trusted publishing is not configured on npmjs.com, the workflow can use a GitHub Actions secret named `NPM_TOKEN` as a temporary fallback. Store only automation/bypass-2FA tokens there, never paste tokens in chat or commit them.
77
79
 
78
80
  ### Manual Publish
@@ -124,4 +126,5 @@ Press ENTER to open in the browser...
124
126
  - If `xdg-open` fails because the environment has no browser, rerun with `BROWSER=true npm publish`.
125
127
  - If the auth link expires or the publish process exits, rerun `BROWSER=true npm publish` to generate a new link.
126
128
  - If the user provides a classic authenticator OTP instead of using the web link, publish can be retried with `npm publish --otp <code>`, but prefer web auth when the user asks for a clickable confirmation link.
129
+ - If a GitHub Actions release fails before the `npm publish` step, such as during checkout or action download authentication, do not diagnose it as a trusted publishing rejection. Inspect the failed step and GitHub Actions status first.
127
130
  - Never print npm tokens or `.npmrc` auth values.