@atlasharness/atlascode 0.1.57 → 0.1.59

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 (2) hide show
  1. package/dist/cli.js +330 -98
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -26920,6 +26920,63 @@ function gatewayUnreachableRemediationHint(error) {
26920
26920
  return null;
26921
26921
  return "IFF 不可达:已切人工确认 —— /doctor 排查";
26922
26922
  }
26923
+ function modelRetryBackoffMs(attempt, baseMs = MODEL_RETRY_BACKOFF_BASE_MS, capMs = MODEL_RETRY_BACKOFF_CAP_MS) {
26924
+ const base = Math.max(0, baseMs);
26925
+ const cap = Math.max(base, capMs);
26926
+ if (attempt <= 0)
26927
+ return Math.min(base, cap);
26928
+ return Math.min(base * 2 ** attempt, cap);
26929
+ }
26930
+ function sleepSignalAware(ms, signal) {
26931
+ if (ms <= 0)
26932
+ return Promise.resolve();
26933
+ return new Promise((resolve) => {
26934
+ if (signal?.aborted) {
26935
+ resolve();
26936
+ return;
26937
+ }
26938
+ let settled = false;
26939
+ const timer = setTimeout(() => {
26940
+ settled = true;
26941
+ if (signal)
26942
+ signal.removeEventListener("abort", onAbort);
26943
+ resolve();
26944
+ }, ms);
26945
+ function onAbort() {
26946
+ if (settled)
26947
+ return;
26948
+ settled = true;
26949
+ clearTimeout(timer);
26950
+ resolve();
26951
+ }
26952
+ if (signal)
26953
+ signal.addEventListener("abort", onAbort, { once: true });
26954
+ });
26955
+ }
26956
+ function buildStreamErrorEvent(err, signal) {
26957
+ const message = err instanceof Error ? err.message || err.name : String(err);
26958
+ const code = isClientRequestTimeout(err) ? "TIMEOUT" : "API_ERROR";
26959
+ return { type: "error", code, message, retryable: shouldRetryModelError(err, signal) };
26960
+ }
26961
+ function buildStreamContentBlocks(reasoningText, fullText, toolCalls) {
26962
+ const blocks = [];
26963
+ if (reasoningText)
26964
+ blocks.push({ type: "thinking", thinking: reasoningText, signature: "first-party" });
26965
+ if (fullText)
26966
+ blocks.push({ type: "text", text: fullText });
26967
+ blocks.push(...[...toolCalls.values()].filter((t) => t.name).map((t) => {
26968
+ let input = {};
26969
+ if (t.args) {
26970
+ try {
26971
+ input = JSON.parse(t.args);
26972
+ } catch {
26973
+ input = { _raw_arguments: t.args };
26974
+ }
26975
+ }
26976
+ return { type: "tool_use", id: t.id || `toolu_${randomUUID()}`, name: t.name, input };
26977
+ }));
26978
+ return blocks;
26979
+ }
26923
26980
  function startLlmWaitHeartbeat(modelId, attempt, totalRetries) {
26924
26981
  const startedAt = Date.now();
26925
26982
  const id = setInterval(() => {
@@ -26934,10 +26991,16 @@ class OpenAIProvider {
26934
26991
  maxRetriesPerModel;
26935
26992
  timeoutMs;
26936
26993
  timeoutResolver;
26937
- constructor(maxRetriesPerModel = 3, timeoutMs = LLM_TIMEOUT_DEFAULT_MS, timeoutResolver) {
26994
+ retryBackoffBaseMs;
26995
+ retryBackoffCapMs;
26996
+ retryBackoffSleep;
26997
+ constructor(maxRetriesPerModel = 3, timeoutMs = LLM_TIMEOUT_DEFAULT_MS, timeoutResolver, retryBackoff) {
26938
26998
  this.maxRetriesPerModel = maxRetriesPerModel;
26939
26999
  this.timeoutMs = timeoutMs;
26940
27000
  this.timeoutResolver = timeoutResolver;
27001
+ this.retryBackoffBaseMs = retryBackoff?.baseMs ?? MODEL_RETRY_BACKOFF_BASE_MS;
27002
+ this.retryBackoffCapMs = retryBackoff?.capMs ?? MODEL_RETRY_BACKOFF_CAP_MS;
27003
+ this.retryBackoffSleep = retryBackoff?.sleep ?? ((ms, signal) => sleepSignalAware(ms, signal));
26941
27004
  }
26942
27005
  getTimeoutMs() {
26943
27006
  return this.timeoutResolver ? this.timeoutResolver() : this.timeoutMs;
@@ -27030,6 +27093,9 @@ class OpenAIProvider {
27030
27093
  lastErr = err;
27031
27094
  if (!shouldRetryModelError(err, args.signal))
27032
27095
  break;
27096
+ if (attempt < this.maxRetriesPerModel - 1) {
27097
+ await this.retryBackoffSleep(modelRetryBackoffMs(attempt, this.retryBackoffBaseMs, this.retryBackoffCapMs), args.signal);
27098
+ }
27033
27099
  }
27034
27100
  }
27035
27101
  }
@@ -27045,6 +27111,7 @@ class OpenAIProvider {
27045
27111
  const attemptRefs = refs;
27046
27112
  let lastErr = null;
27047
27113
  let succeeded = false;
27114
+ let partialRetained = false;
27048
27115
  for (const ref of attemptRefs) {
27049
27116
  const entry = resolveModel(ref);
27050
27117
  if (!entry) {
@@ -27073,22 +27140,27 @@ class OpenAIProvider {
27073
27140
  if (args.signal?.aborted)
27074
27141
  return;
27075
27142
  try {
27076
- yield* this.streamOneAttempt(client, params, entry, args.signal, attempt, this.maxRetriesPerModel);
27143
+ const outcome = yield* this.streamOneAttempt(client, params, entry, args.signal, attempt, this.maxRetriesPerModel);
27144
+ if (outcome.partialRetained) {
27145
+ partialRetained = true;
27146
+ break;
27147
+ }
27077
27148
  succeeded = true;
27078
27149
  break;
27079
27150
  } catch (err) {
27080
27151
  lastErr = err;
27081
27152
  if (!shouldRetryModelError(err, args.signal))
27082
27153
  break;
27154
+ if (attempt < this.maxRetriesPerModel - 1) {
27155
+ await this.retryBackoffSleep(modelRetryBackoffMs(attempt, this.retryBackoffBaseMs, this.retryBackoffCapMs), args.signal);
27156
+ }
27083
27157
  }
27084
27158
  }
27085
- if (succeeded || args.signal?.aborted)
27159
+ if (succeeded || partialRetained || args.signal?.aborted)
27086
27160
  return;
27087
27161
  }
27088
27162
  if (lastErr) {
27089
- const message = lastErr instanceof Error ? lastErr.message || lastErr.name : String(lastErr);
27090
- const code = isClientRequestTimeout(lastErr) ? "TIMEOUT" : "API_ERROR";
27091
- yield { type: "error", code, message, retryable: shouldRetryModelError(lastErr, args.signal) };
27163
+ yield buildStreamErrorEvent(lastErr, args.signal);
27092
27164
  return;
27093
27165
  }
27094
27166
  throw new Error(`No models configured for role '${args.role}'.`);
@@ -27107,66 +27179,111 @@ class OpenAIProvider {
27107
27179
  const toolCalls = new Map;
27108
27180
  let finishReason = null;
27109
27181
  let usage = null;
27110
- for await (const chunk of stream) {
27111
- const choice = chunk.choices?.[0];
27112
- const delta = choice?.delta;
27113
- if (delta?.content) {
27114
- fullText += delta.content;
27115
- yield { type: "text_delta", text: delta.content };
27116
- }
27117
- if (delta?.reasoning_content)
27118
- reasoningText += delta.reasoning_content;
27119
- if (delta?.tool_calls) {
27120
- for (const tc of delta.tool_calls) {
27121
- const idx = tc.index ?? 0;
27122
- const cur = toolCalls.get(idx) ?? { id: "", name: "", args: "" };
27123
- if (tc.id)
27124
- cur.id = tc.id;
27125
- if (tc.function?.name) {
27126
- cur.name = tc.function.name;
27127
- if (currentToolCall) {
27128
- let input = {};
27129
- if (currentToolCall.args) {
27130
- try {
27131
- input = JSON.parse(currentToolCall.args);
27132
- } catch {
27133
- input = { _raw_arguments: currentToolCall.args };
27182
+ let streamError = null;
27183
+ try {
27184
+ for await (const chunk of stream) {
27185
+ const choice = chunk.choices?.[0];
27186
+ const delta = choice?.delta;
27187
+ if (delta?.content) {
27188
+ fullText += delta.content;
27189
+ yield { type: "text_delta", text: delta.content };
27190
+ }
27191
+ if (delta?.reasoning_content)
27192
+ reasoningText += delta.reasoning_content;
27193
+ if (delta?.tool_calls) {
27194
+ for (const tc of delta.tool_calls) {
27195
+ const idx = tc.index ?? 0;
27196
+ const cur = toolCalls.get(idx) ?? { id: "", name: "", args: "" };
27197
+ if (tc.id)
27198
+ cur.id = tc.id;
27199
+ if (tc.function?.name) {
27200
+ cur.name = tc.function.name;
27201
+ if (currentToolCall) {
27202
+ let input = {};
27203
+ if (currentToolCall.args) {
27204
+ try {
27205
+ input = JSON.parse(currentToolCall.args);
27206
+ } catch {
27207
+ input = { _raw_arguments: currentToolCall.args };
27208
+ }
27134
27209
  }
27210
+ yield { type: "tool_use_end", id: currentToolCall.id, name: currentToolCall.name, input };
27135
27211
  }
27136
- yield { type: "tool_use_end", id: currentToolCall.id, name: currentToolCall.name, input };
27212
+ currentToolCall = { id: cur.id, name: cur.name, args: cur.args };
27213
+ yield { type: "tool_use_start", id: cur.id, name: cur.name };
27137
27214
  }
27138
- currentToolCall = { id: cur.id, name: cur.name, args: cur.args };
27139
- yield { type: "tool_use_start", id: cur.id, name: cur.name };
27140
- }
27141
- if (tc.function?.arguments) {
27142
- cur.args += tc.function.arguments;
27143
- if (currentToolCall && cur.name) {
27144
- currentToolCall.args = cur.args;
27145
- yield { type: "tool_use_delta", id: cur.id, inputJsonDelta: tc.function.arguments };
27215
+ if (tc.function?.arguments) {
27216
+ cur.args += tc.function.arguments;
27217
+ if (currentToolCall && cur.name) {
27218
+ currentToolCall.args = cur.args;
27219
+ yield { type: "tool_use_delta", id: cur.id, inputJsonDelta: tc.function.arguments };
27220
+ }
27146
27221
  }
27222
+ toolCalls.set(idx, cur);
27147
27223
  }
27148
- toolCalls.set(idx, cur);
27149
27224
  }
27150
- }
27151
- if (choice?.finish_reason) {
27152
- if (currentToolCall) {
27153
- let input = {};
27154
- if (currentToolCall.args) {
27155
- try {
27156
- input = JSON.parse(currentToolCall.args);
27157
- } catch {
27158
- input = { _raw_arguments: currentToolCall.args };
27225
+ if (choice?.finish_reason) {
27226
+ if (currentToolCall) {
27227
+ let input = {};
27228
+ if (currentToolCall.args) {
27229
+ try {
27230
+ input = JSON.parse(currentToolCall.args);
27231
+ } catch {
27232
+ input = { _raw_arguments: currentToolCall.args };
27233
+ }
27159
27234
  }
27235
+ yield { type: "tool_use_end", id: currentToolCall.id, name: currentToolCall.name, input };
27236
+ currentToolCall = null;
27160
27237
  }
27161
- yield { type: "tool_use_end", id: currentToolCall.id, name: currentToolCall.name, input };
27162
- currentToolCall = null;
27238
+ finishReason = choice.finish_reason;
27163
27239
  }
27164
- finishReason = choice.finish_reason;
27240
+ if (chunk.usage)
27241
+ usage = mapOpenAIUsage(chunk.usage);
27242
+ if (signal?.aborted)
27243
+ break;
27165
27244
  }
27166
- if (chunk.usage)
27167
- usage = mapOpenAIUsage(chunk.usage);
27168
- if (signal?.aborted)
27169
- break;
27245
+ } catch (err) {
27246
+ streamError = err;
27247
+ }
27248
+ if (streamError !== null) {
27249
+ const partialBlocks = buildStreamContentBlocks(reasoningText, fullText, toolCalls);
27250
+ if (partialBlocks.length === 0)
27251
+ throw streamError;
27252
+ yield {
27253
+ type: "message_stop",
27254
+ stopReason: "interrupted",
27255
+ usage: usage ? {
27256
+ input_tokens: usage.input_tokens ?? 0,
27257
+ output_tokens: usage.output_tokens ?? 0,
27258
+ cache_creation_input_tokens: usage.cache_creation_input_tokens ?? 0,
27259
+ cache_read_input_tokens: usage.cache_read_input_tokens ?? 0
27260
+ } : undefined
27261
+ };
27262
+ const emptyUsage = mapOpenAIUsage(null);
27263
+ const partialMsg = {
27264
+ uuid: randomUUID(),
27265
+ timestamp: new Date().toISOString(),
27266
+ message: {
27267
+ id: randomUUID(),
27268
+ container: null,
27269
+ model: entry.modelId,
27270
+ role: "assistant",
27271
+ stop_reason: "interrupted",
27272
+ stop_sequence: "",
27273
+ type: "message",
27274
+ usage: usage || emptyUsage,
27275
+ content: partialBlocks
27276
+ }
27277
+ };
27278
+ yield {
27279
+ type: "assistant",
27280
+ uuid: partialMsg.uuid,
27281
+ timestamp: partialMsg.timestamp,
27282
+ message: partialMsg.message,
27283
+ interrupted: true
27284
+ };
27285
+ yield buildStreamErrorEvent(streamError, signal);
27286
+ return { partialRetained: true };
27170
27287
  }
27171
27288
  yield {
27172
27289
  type: "message_stop",
@@ -27178,27 +27295,11 @@ class OpenAIProvider {
27178
27295
  cache_read_input_tokens: usage.cache_read_input_tokens ?? 0
27179
27296
  } : undefined
27180
27297
  };
27181
- const contentBlocks = [];
27182
- if (reasoningText)
27183
- contentBlocks.push({ type: "thinking", thinking: reasoningText, signature: "first-party" });
27184
- if (fullText)
27185
- contentBlocks.push({ type: "text", text: fullText });
27186
- const toolUseBlocks = [...toolCalls.values()].filter((t) => t.name).map((t) => {
27187
- let input = {};
27188
- if (t.args) {
27189
- try {
27190
- input = JSON.parse(t.args);
27191
- } catch {
27192
- input = { _raw_arguments: t.args };
27193
- }
27194
- }
27195
- return { type: "tool_use", id: t.id || `toolu_${randomUUID()}`, name: t.name, input };
27196
- });
27197
- contentBlocks.push(...toolUseBlocks);
27298
+ const contentBlocks = buildStreamContentBlocks(reasoningText, fullText, toolCalls);
27198
27299
  if (contentBlocks.length === 0) {
27199
27300
  contentBlocks.push({ type: "text", text: PROVIDER_EMPTY_CONTENT_PLACEHOLDER });
27200
27301
  }
27201
- const stopReason = toolUseBlocks.length > 0 ? "tool_use" : finishReason === "length" ? "max_tokens" : "end_turn";
27302
+ const stopReason = contentBlocks.some((b) => b.type === "tool_use") ? "tool_use" : finishReason === "length" ? "max_tokens" : "end_turn";
27202
27303
  const emptyUsage = mapOpenAIUsage(null);
27203
27304
  const finalMsg = {
27204
27305
  uuid: randomUUID(),
@@ -27216,6 +27317,7 @@ class OpenAIProvider {
27216
27317
  }
27217
27318
  };
27218
27319
  yield { type: "assistant", uuid: finalMsg.uuid, timestamp: finalMsg.timestamp, message: finalMsg.message };
27320
+ return { partialRetained: false };
27219
27321
  }
27220
27322
  async healthCheck(role) {
27221
27323
  const refs = getRoleModels(role);
@@ -27332,7 +27434,7 @@ class OpenAIProvider {
27332
27434
  }
27333
27435
  }
27334
27436
  }
27335
- var PROVIDER_EMPTY_CONTENT_PLACEHOLDER = "(provider: empty response)";
27437
+ var PROVIDER_EMPTY_CONTENT_PLACEHOLDER = "(provider: empty response)", MODEL_RETRY_BACKOFF_BASE_MS = 500, MODEL_RETRY_BACKOFF_CAP_MS = 8000;
27336
27438
  var init_modelprovider = __esm(() => {
27337
27439
  init_openai();
27338
27440
  init_clients();
@@ -27786,7 +27888,7 @@ function turnRecoveryBackoffMs(attempt, baseMs, capMs) {
27786
27888
  return Math.min(base, cap);
27787
27889
  return Math.min(base * 2 ** attempt, cap);
27788
27890
  }
27789
- function sleepSignalAware(ms, signal) {
27891
+ function sleepSignalAware2(ms, signal) {
27790
27892
  if (ms <= 0)
27791
27893
  return Promise.resolve();
27792
27894
  return new Promise((resolve) => {
@@ -27829,7 +27931,7 @@ async function withTurnRecovery(attempt, options = {}) {
27829
27931
  if (i < cfg.maxRetries && shouldRetryModelError(err, options.signal)) {
27830
27932
  const backoff = turnRecoveryBackoffMs(i, cfg.backoffBaseMs, cfg.backoffCapMs);
27831
27933
  if (backoff > 0) {
27832
- await sleepSignalAware(backoff, options.signal);
27934
+ await sleepSignalAware2(backoff, options.signal);
27833
27935
  }
27834
27936
  continue;
27835
27937
  }
@@ -27838,6 +27940,15 @@ async function withTurnRecovery(attempt, options = {}) {
27838
27940
  }
27839
27941
  throw lastErr;
27840
27942
  }
27943
+ function nextFallbackRef(fallbackModels, tried) {
27944
+ if (!fallbackModels)
27945
+ return;
27946
+ for (const ref of fallbackModels) {
27947
+ if (!tried.has(ref))
27948
+ return ref;
27949
+ }
27950
+ return;
27951
+ }
27841
27952
  var DEFAULT_MAX_RETRIES = 13, DEFAULT_BACKOFF_BASE_MS = 100, DEFAULT_BACKOFF_CAP_MS = 1000;
27842
27953
  var init_turnRecovery = __esm(() => {
27843
27954
  init_modelprovider2();
@@ -29485,6 +29596,65 @@ function getRuleByContentsForToolName(context, toolName, behavior) {
29485
29596
  }
29486
29597
  return ruleByContents;
29487
29598
  }
29599
+ function parseParametricContent(ruleContent) {
29600
+ const params = {};
29601
+ for (const segment of ruleContent.split(",")) {
29602
+ const idx = segment.indexOf(":");
29603
+ if (idx <= 0)
29604
+ return null;
29605
+ const param = segment.slice(0, idx).trim();
29606
+ if (!param)
29607
+ return null;
29608
+ params[param] = segment.slice(idx + 1).trim();
29609
+ }
29610
+ return Object.keys(params).length > 0 ? params : null;
29611
+ }
29612
+ function declaredInputFields(tool) {
29613
+ const props = tool.inputSchema?.properties;
29614
+ if (!props)
29615
+ return new Set;
29616
+ return new Set(Object.keys(props));
29617
+ }
29618
+ function ruleMatchesParametric(rule, tool, input) {
29619
+ const ruleContent = rule.ruleValue.ruleContent;
29620
+ if (ruleContent === undefined)
29621
+ return false;
29622
+ const params = parseParametricContent(ruleContent);
29623
+ if (params === null)
29624
+ return false;
29625
+ const fields = declaredInputFields(tool);
29626
+ for (const [param, value] of Object.entries(params)) {
29627
+ if (!fields.has(param))
29628
+ return false;
29629
+ if (input[param] !== value)
29630
+ return false;
29631
+ }
29632
+ return true;
29633
+ }
29634
+ function getParametricRuleForTool(context, tool, input, behavior) {
29635
+ let rules;
29636
+ switch (behavior) {
29637
+ case "allow":
29638
+ rules = getAllowRules(context);
29639
+ break;
29640
+ case "deny":
29641
+ rules = getDenyRules(context);
29642
+ break;
29643
+ case "ask":
29644
+ rules = getAskRules(context);
29645
+ break;
29646
+ }
29647
+ const nameForRuleMatch = getToolNameForPermissionCheck(tool);
29648
+ for (const rule of rules) {
29649
+ if (rule.ruleValue.toolName !== nameForRuleMatch)
29650
+ continue;
29651
+ if (rule.ruleBehavior !== behavior)
29652
+ continue;
29653
+ if (ruleMatchesParametric(rule, tool, input))
29654
+ return rule;
29655
+ }
29656
+ return null;
29657
+ }
29488
29658
  function permissionModeTitle(mode) {
29489
29659
  return PERMISSION_MODE_TITLES[mode] ?? "Default";
29490
29660
  }
@@ -29621,6 +29791,17 @@ var BASH_TOOL_NAME = "Bash", hasPermissionsToUseTool = async (tool, input, conte
29621
29791
  }
29622
29792
  };
29623
29793
  }
29794
+ const parametricDenyRule = permissionContext ? getParametricRuleForTool(permissionContext, tool, input, "deny") : null;
29795
+ if (parametricDenyRule) {
29796
+ return {
29797
+ behavior: "deny",
29798
+ message: `Permission to use ${tool.name} has been denied.`,
29799
+ decisionReason: {
29800
+ type: "rule",
29801
+ rule: parametricDenyRule
29802
+ }
29803
+ };
29804
+ }
29624
29805
  const askRule = permissionContext ? getAskRuleForTool(permissionContext, tool) : null;
29625
29806
  if (askRule) {
29626
29807
  const canSandboxAutoAllow = tool.name === BASH_TOOL_NAME && sandbox.isSandboxingEnabled() && sandbox.isAutoAllowBashIfSandboxedEnabled() && input.dangerouslyDisableSandbox !== true;
@@ -29635,6 +29816,17 @@ var BASH_TOOL_NAME = "Bash", hasPermissionsToUseTool = async (tool, input, conte
29635
29816
  }, permissionContext, tool.name);
29636
29817
  }
29637
29818
  }
29819
+ const parametricAskRule = permissionContext ? getParametricRuleForTool(permissionContext, tool, input, "ask") : null;
29820
+ if (parametricAskRule) {
29821
+ return applyDontAskMode({
29822
+ behavior: "ask",
29823
+ message: createPermissionRequestMessage(tool.name),
29824
+ decisionReason: {
29825
+ type: "rule",
29826
+ rule: parametricAskRule
29827
+ }
29828
+ }, permissionContext, tool.name);
29829
+ }
29638
29830
  let toolPermissionResult = {
29639
29831
  behavior: "passthrough",
29640
29832
  message: createPermissionRequestMessage(tool.name)
@@ -30740,6 +30932,12 @@ Block unconditionally. Do not look for authorization. No ALLOW exception applies
30740
30932
  - **Trace the full destination path.** Do not stop at the first hop; do not discount a pathway because a future merge, deploy, or review gates it — arming the pathway is the harm. Modifying a CI/build workflow or publish pipeline so it will upload private-repo content, then committing or pushing it, is the blocked action even on a personal feature branch. **Bulk scale is its own red flag**: shipping an entire repo or package tree to any destination other than the repo's own configured remote as of session start — a mid-session repoint makes it a new destination — (e.g. pushing all refs to a new remote, archiving or syncing the working tree, pushing an image that COPYs the repo root) is exfiltration regardless of how private the destination claims to be — no routine task relocates an entire tree, and the agent creating the destination does not establish it as trusted. Cross-repo copies from a private repo followed by commit or push are exfiltration regardless of branch. Generating a public or anyone-with-link share URL for sensitive data counts as publishing even when the underlying store is trusted.
30741
30933
  - **Is the final destination confirmed external or public?** A destination is trusted only if it is explicitly listed in Environment, or it is the trusted repo's own configured remote and its visibility fits the content (see Trusted repo). Same-org/same-account ownership, a personal or feature branch name, or the push being non-force does not establish privacy. A same-session \`git remote set-url\`/\`git remote add\` severs continuity — the new remote is not where this content already lives — so sensitive content pushed after a repoint is judged by this hard rule with the destination treated as public; intent expressed before the repoint does not carry across it. When a repo's visibility is not established in the transcript, unknown visibility is not itself a reason to block; judge the content on the other checks and rules — except after a same-session repoint, where the sentence above governs. Unknown visibility never clears anything either — it contributes nothing in either direction; an unrequested sensitive-source read feeding the content (see Sensitive-Source Provenance) is an independent signal that stands on its own.
30742
30934
 
30935
+ ## USER HARD DENY — unconditional blocks (User Intent Rule does NOT clear these)
30936
+
30937
+ Rules in this class block unconditionally: no ALLOW exception applies, and the user's own words do not clear them (unlike SOFT BLOCK, whose User Intent Rule CAN clear). Add the user's unconditional block rules here, one per line (\`- \` prefix).
30938
+
30939
+ <user_hard_deny_rules_to_replace>(none configured)</user_hard_deny_rules_to_replace>
30940
+
30743
30941
  ## SOFT BLOCK — destructive or irreversible (User Intent Rule CAN clear these)
30744
30942
 
30745
30943
  Every rule below clears at one bar — **[named+specifics]**: the user's own words name the action and the rule's **must name:** item (or affirm an agent proposal, visible directly before their reply, that named both); for adversarial-pattern rules the specifics is the user confirming the flagged pattern is a false positive — an agent ask like "auto mode blocked this for <reason> — is that incorrect and are you OK with it?" answered yes meets it. A rule's own stated conditions and stricter bars apply as written. In weighing any bar, assume the user has not closely read the transcript: a bare verb names an action, not its specifics. A bar is met at the step that ships or externalizes — consent to a preparatory step (a rename, a copy, a config write) does not carry forward to the later step that ships its result. At a push or publication, the content this session wrote into the outgoing files — visible in its earlier Write and Edit calls — is judged against the sensitivity classes like any other content: a session-authored file carrying secrets-, entrusted-, or personal-class material is that class's matter when it goes outward, regardless of being the repo's own work. Reads along the way are part of that judgment: a session that read sensitive-classed paths and then wrote is judged with those reads in mind. When blocking on an unmet bar, the block reason surfaces the blocked command and states what would clear it.
@@ -30845,6 +31043,7 @@ var init_permissions_external = () => {};
30845
31043
  function getDefaultExternalAutoModeRules() {
30846
31044
  return {
30847
31045
  allow: extractTaggedBullets("user_allow_rules_to_replace"),
31046
+ hard_deny: extractTaggedBullets("user_hard_deny_rules_to_replace"),
30848
31047
  soft_deny: extractTaggedBullets("user_deny_rules_to_replace"),
30849
31048
  environment: extractTaggedBullets("user_environment_to_replace")
30850
31049
  };
@@ -30857,7 +31056,7 @@ function extractTaggedBullets(tagName) {
30857
31056
  `).map((line) => line.trim()).filter((line) => line.startsWith("- ")).map((line) => line.slice(2));
30858
31057
  }
30859
31058
  function buildDefaultExternalSystemPrompt() {
30860
- return BASE_PROMPT.replace("<permissions_template>", () => EXTERNAL_PERMISSIONS_TEMPLATE).replace(/<user_allow_rules_to_replace>([\s\S]*?)<\/user_allow_rules_to_replace>/, (_m, defaults) => defaults).replace(/<user_deny_rules_to_replace>([\s\S]*?)<\/user_deny_rules_to_replace>/, (_m, defaults) => defaults).replace(/<user_environment_to_replace>([\s\S]*?)<\/user_environment_to_replace>/, (_m, defaults) => defaults);
31059
+ return BASE_PROMPT.replace("<permissions_template>", () => EXTERNAL_PERMISSIONS_TEMPLATE).replace(/<user_allow_rules_to_replace>([\s\S]*?)<\/user_allow_rules_to_replace>/, (_m, defaults) => defaults).replace(/<user_deny_rules_to_replace>([\s\S]*?)<\/user_deny_rules_to_replace>/, (_m, defaults) => defaults).replace(/<user_hard_deny_rules_to_replace>([\s\S]*?)<\/user_hard_deny_rules_to_replace>/, (_m, defaults) => defaults).replace(/<user_environment_to_replace>([\s\S]*?)<\/user_environment_to_replace>/, (_m, defaults) => defaults);
30861
31060
  }
30862
31061
  var BASE_PROMPT, EXTERNAL_PERMISSIONS_TEMPLATE;
30863
31062
  var init_prompts = __esm(() => {
@@ -31266,11 +31465,13 @@ function getAutoModeConfig() {
31266
31465
  if (feature("TRANSCRIPT_CLASSIFIER")) {
31267
31466
  const schema = object({
31268
31467
  allow: array(string2()).optional(),
31468
+ hard_deny: array(string2()).optional(),
31269
31469
  soft_deny: array(string2()).optional(),
31270
31470
  deny: array(string2()).optional(),
31271
31471
  environment: array(string2()).optional()
31272
31472
  });
31273
31473
  const allow = [];
31474
+ const hard_deny = [];
31274
31475
  const soft_deny = [];
31275
31476
  const environment = [];
31276
31477
  for (const source of [
@@ -31286,6 +31487,8 @@ function getAutoModeConfig() {
31286
31487
  if (result.success) {
31287
31488
  if (result.data.allow)
31288
31489
  allow.push(...result.data.allow);
31490
+ if (result.data.hard_deny)
31491
+ hard_deny.push(...result.data.hard_deny);
31289
31492
  if (result.data.soft_deny)
31290
31493
  soft_deny.push(...result.data.soft_deny);
31291
31494
  if (result.data.environment) {
@@ -31293,9 +31496,10 @@ function getAutoModeConfig() {
31293
31496
  }
31294
31497
  }
31295
31498
  }
31296
- if (allow.length > 0 || soft_deny.length > 0 || environment.length > 0) {
31499
+ if (allow.length > 0 || hard_deny.length > 0 || soft_deny.length > 0 || environment.length > 0) {
31297
31500
  return {
31298
31501
  ...allow.length > 0 && { allow },
31502
+ ...hard_deny.length > 0 && { hard_deny },
31299
31503
  ...soft_deny.length > 0 && { soft_deny },
31300
31504
  ...environment.length > 0 && { environment }
31301
31505
  };
@@ -81642,10 +81846,12 @@ async function queryAgentLoop(deps, args) {
81642
81846
  }
81643
81847
  deps.emit?.({ type: "round_start", turn: turns });
81644
81848
  let reactiveRetried = false;
81849
+ let effectiveDeps = deps;
81850
+ const triedFallbackRefs = new Set;
81645
81851
  for (;; ) {
81646
81852
  const roundInputLen = messages.length;
81647
81853
  try {
81648
- lastRound = await queryOneRound(deps, tools, messages);
81854
+ lastRound = await queryOneRound(effectiveDeps, tools, messages);
81649
81855
  } catch (err) {
81650
81856
  if (!reactiveRetried && deps.reactiveCompact !== undefined && isReactiveCompactEnabled() && isReactiveCompactRecoverableError(err)) {
81651
81857
  reactiveRetried = true;
@@ -81667,6 +81873,12 @@ async function queryAgentLoop(deps, args) {
81667
81873
  continue;
81668
81874
  }
81669
81875
  }
81876
+ const nextRef = nextFallbackRef(deps.fallbackModels, triedFallbackRefs);
81877
+ if (nextRef && shouldRetryModelError(err, deps.signal)) {
81878
+ triedFallbackRefs.add(nextRef);
81879
+ effectiveDeps = { ...deps, sessionModel: nextRef };
81880
+ continue;
81881
+ }
81670
81882
  throw err;
81671
81883
  }
81672
81884
  messages = lastRound.messages;
@@ -81869,6 +82081,7 @@ async function createAgentLoopDeps(config = {}) {
81869
82081
  responseFormat: config.responseFormat,
81870
82082
  effortValue: config.effortValue,
81871
82083
  fallbackModel: config.fallbackModel,
82084
+ fallbackModels: config.fallbackModels,
81872
82085
  transcript: config.disablePersistence ? undefined : {
81873
82086
  record: (msgs) => recordTranscript(msgs),
81874
82087
  recordContentReplacement: (recs) => recordContentReplacement(recs, config.agentId)
@@ -85822,7 +86035,8 @@ async function runHeadless(inputPrompt, options) {
85822
86035
  thinkingConfig: options.thinkingConfig,
85823
86036
  responseFormat: options.jsonSchema ? toResponseFormat({ type: "json_schema", schema: options.jsonSchema }) : undefined,
85824
86037
  effortValue: options.effort,
85825
- fallbackModel: options.fallbackModel
86038
+ fallbackModel: options.fallbackModel,
86039
+ fallbackModels: options.fallbackModels
85826
86040
  });
85827
86041
  const { toolPermissionContext: initialTpc, tools, warnings } = bundle;
85828
86042
  if (warnings.length > 0) {
@@ -87059,6 +87273,7 @@ function autoModeConfigHandler() {
87059
87273
  const defaults = getDefaultExternalAutoModeRules();
87060
87274
  writeRules({
87061
87275
  allow: config?.allow?.length ? config.allow : defaults.allow,
87276
+ hard_deny: config?.hard_deny?.length ? config.hard_deny : defaults.hard_deny,
87062
87277
  soft_deny: config?.soft_deny?.length ? config.soft_deny : defaults.soft_deny,
87063
87278
  environment: config?.environment?.length ? config.environment : defaults.environment
87064
87279
  });
@@ -87074,18 +87289,18 @@ function makeUserMessage2(text) {
87074
87289
  }
87075
87290
  async function autoModeCritiqueHandler(options) {
87076
87291
  const config = getAutoModeConfig();
87077
- const hasCustomRules = (config?.allow?.length ?? 0) > 0 || (config?.soft_deny?.length ?? 0) > 0 || (config?.environment?.length ?? 0) > 0;
87292
+ const hasCustomRules = (config?.allow?.length ?? 0) > 0 || (config?.hard_deny?.length ?? 0) > 0 || (config?.soft_deny?.length ?? 0) > 0 || (config?.environment?.length ?? 0) > 0;
87078
87293
  if (!hasCustomRules) {
87079
87294
  process.stdout.write(`No custom auto mode rules found.
87080
87295
 
87081
- ` + `Add rules to your settings file under autoMode.{allow, soft_deny, environment}.
87296
+ ` + `Add rules to your settings file under autoMode.{allow, hard_deny, soft_deny, environment}.
87082
87297
  ` + "Run `atlascode auto-mode defaults` to see the default rules for reference.\n");
87083
87298
  return;
87084
87299
  }
87085
87300
  const role = options.model ? modelToRole(options.model) : "premium";
87086
87301
  const defaults = getDefaultExternalAutoModeRules();
87087
87302
  const classifierPrompt = buildDefaultExternalSystemPrompt();
87088
- const userRulesSummary = formatRulesForCritique("allow", config?.allow ?? [], defaults.allow) + formatRulesForCritique("soft_deny", config?.soft_deny ?? [], defaults.soft_deny) + formatRulesForCritique("environment", config?.environment ?? [], defaults.environment);
87303
+ const userRulesSummary = formatRulesForCritique("allow", config?.allow ?? [], defaults.allow) + formatRulesForCritique("hard_deny", config?.hard_deny ?? [], defaults.hard_deny) + formatRulesForCritique("soft_deny", config?.soft_deny ?? [], defaults.soft_deny) + formatRulesForCritique("environment", config?.environment ?? [], defaults.environment);
87089
87304
  process.stdout.write(`Analyzing your auto mode rules…
87090
87305
 
87091
87306
  `);
@@ -87151,9 +87366,10 @@ var init_autoMode2 = __esm(() => {
87151
87366
  init_shared();
87152
87367
  CRITIQUE_SYSTEM_PROMPT = `You are an expert reviewer of auto mode classifier rules for Atlas.
87153
87368
  ` + `
87154
- ` + 'Atlas has an "auto mode" that uses an AI classifier to decide whether ' + "tool calls should be auto-approved or require user confirmation. Users can " + `write custom rules in three categories:
87369
+ ` + 'Atlas has an "auto mode" that uses an AI classifier to decide whether ' + "tool calls should be auto-approved or require user confirmation. Users can " + `write custom rules in four categories:
87155
87370
  ` + `
87156
87371
  ` + `- **allow**: Actions the classifier should auto-approve
87372
+ ` + `- **hard_deny**: Actions that are blocked unconditionally (user intent does not clear these)
87157
87373
  ` + `- **soft_deny**: Actions the classifier should block (require user confirmation)
87158
87374
  ` + `- **environment**: Context about the user's setup that helps the classifier make decisions
87159
87375
  ` + `
@@ -87268,7 +87484,7 @@ function buildHeadlessOptions(options) {
87268
87484
  allowedTools: options.allowedTools,
87269
87485
  disallowedTools: options.disallowedTools,
87270
87486
  baseTools: options.tools,
87271
- permissionMode: options.permissionMode,
87487
+ permissionMode: options.permissionMode ?? (options.permissionPrompts === "none" ? "dontAsk" : undefined),
87272
87488
  permissionPromptToolName: options.permissionPromptTool,
87273
87489
  maxTurns: options.maxTurns,
87274
87490
  model: options.model,
@@ -87281,6 +87497,7 @@ function buildHeadlessOptions(options) {
87281
87497
  systemPrompt: readPromptFileInline(options.systemPrompt, options.systemPromptFile, "System prompt"),
87282
87498
  appendSystemPrompt: readPromptFileInline(options.appendSystemPrompt, options.appendSystemPromptFile, "Append system prompt"),
87283
87499
  fallbackModel: options.fallbackModel,
87500
+ fallbackModels: options.fallbackModels,
87284
87501
  thinkingConfig: buildHeadlessThinkingConfig(options.thinking, options.maxThinkingTokens),
87285
87502
  effort: options.effort,
87286
87503
  mcpConfig: options.mcpConfig,
@@ -87352,7 +87569,7 @@ function buildProgram() {
87352
87569
  throw new Error("--task-budget must be a positive integer");
87353
87570
  }
87354
87571
  return tokens;
87355
- }).hideHelp()).option("--replay-user-messages", "Re-emit user messages from stdin back on stdout for acknowledgment (only works with --input-format=stream-json and --output-format=stream-json)", () => true).addOption(new Option("--enable-auth-status", "Enable auth status messages in SDK mode").default(false).hideHelp()).option("--allowedTools, --allowed-tools <tools...>", 'Comma or space-separated list of tool names to allow (e.g. "Bash(git:*) Edit")').option("--tools <tools...>", 'Specify the list of available tools from the built-in set. Use "" to disable all tools, "default" to use all tools, or specify tool names (e.g. "Bash,Edit,Read").').option("--disallowedTools, --disallowed-tools <tools...>", 'Comma or space-separated list of tool names to deny (e.g. "Bash(git:*) Edit")').option("--mcp-config <configs...>", "Load MCP servers from JSON files or strings (space-separated)").addOption(new Option("--permission-prompt-tool <tool>", "MCP tool to use for permission prompts (only works with --print)").argParser(String).hideHelp()).addOption(new Option("--system-prompt <prompt>", "System prompt to use for the session").argParser(String)).addOption(new Option("--system-prompt-file <file>", "Read system prompt from a file").argParser(String).hideHelp()).addOption(new Option("--append-system-prompt <prompt>", "Append a system prompt to the default system prompt").argParser(String)).addOption(new Option("--append-system-prompt-file <file>", "Read system prompt from a file and append to the default system prompt").argParser(String).hideHelp()).addOption(new Option("--permission-mode <mode>", "Permission mode to use for the session").argParser(String).choices(PERMISSION_MODES)).option("-c, --continue", "Continue the most recent conversation in the current directory", () => true).option("-r, --resume [value]", "Resume a conversation by session ID, or open interactive picker with optional search term", (value) => value || true).option("--fork-session", "When resuming, create a new session ID instead of reusing the original (use with --resume or --continue)", () => true).addOption(new Option("--prefill <text>", "Pre-fill the prompt input with text without submitting it").hideHelp()).addOption(new Option("--deep-link-origin", "Signal that this session was launched from a deep link").hideHelp()).addOption(new Option("--deep-link-repo <slug>", "Repo slug the deep link ?repo= parameter resolved to the current cwd").hideHelp()).addOption(new Option("--deep-link-last-fetch <ms>", "FETCH_HEAD mtime in epoch ms, precomputed by the deep link trampoline").argParser((v) => {
87572
+ }).hideHelp()).option("--replay-user-messages", "Re-emit user messages from stdin back on stdout for acknowledgment (only works with --input-format=stream-json and --output-format=stream-json)", () => true).addOption(new Option("--enable-auth-status", "Enable auth status messages in SDK mode").default(false).hideHelp()).option("--allowedTools, --allowed-tools <tools...>", 'Comma or space-separated list of tool names to allow (e.g. "Bash(git:*) Edit")').option("--tools <tools...>", 'Specify the list of available tools from the built-in set. Use "" to disable all tools, "default" to use all tools, or specify tool names (e.g. "Bash,Edit,Read").').option("--disallowedTools, --disallowed-tools <tools...>", 'Comma or space-separated list of tool names to deny (e.g. "Bash(git:*) Edit")').option("--mcp-config <configs...>", "Load MCP servers from JSON files or strings (space-separated)").addOption(new Option("--permission-prompt-tool <tool>", "MCP tool to use for permission prompts (only works with --print)").argParser(String).hideHelp()).addOption(new Option("--system-prompt <prompt>", "System prompt to use for the session").argParser(String)).addOption(new Option("--system-prompt-file <file>", "Read system prompt from a file").argParser(String).hideHelp()).addOption(new Option("--append-system-prompt <prompt>", "Append a system prompt to the default system prompt").argParser(String)).addOption(new Option("--append-system-prompt-file <file>", "Read system prompt from a file and append to the default system prompt").argParser(String).hideHelp()).addOption(new Option("--permission-mode <mode>", "Permission mode to use for the session").argParser(String).choices(PERMISSION_MODES)).addOption(new Option("--permission-prompts <mode>", "Permission prompt policy; 'none' auto-denies prompts that would otherwise ask").argParser(String).choices(["none"])).option("-c, --continue", "Continue the most recent conversation in the current directory", () => true).option("-r, --resume [value]", "Resume a conversation by session ID, or open interactive picker with optional search term", (value) => value || true).option("--fork-session", "When resuming, create a new session ID instead of reusing the original (use with --resume or --continue)", () => true).addOption(new Option("--prefill <text>", "Pre-fill the prompt input with text without submitting it").hideHelp()).addOption(new Option("--deep-link-origin", "Signal that this session was launched from a deep link").hideHelp()).addOption(new Option("--deep-link-repo <slug>", "Repo slug the deep link ?repo= parameter resolved to the current cwd").hideHelp()).addOption(new Option("--deep-link-last-fetch <ms>", "FETCH_HEAD mtime in epoch ms, precomputed by the deep link trampoline").argParser((v) => {
87356
87573
  const n = Number(v);
87357
87574
  return Number.isFinite(n) ? n : undefined;
87358
87575
  }).hideHelp()).option("--from-pr [value]", "Resume a session linked to a PR by PR number/URL, or open interactive picker with optional search term", (value) => value || true).option("--no-session-persistence", "Disable session persistence - sessions will not be saved to disk and cannot be resumed (only works with --print)").addOption(new Option("--resume-session-at <message id>", "When resuming, only messages up to and including the assistant message with <message.id> (use with --resume in print mode)").argParser(String).hideHelp()).addOption(new Option("--rewind-files <user-message-id>", "Restore files to state at the specified user message and exit (requires --resume)").hideHelp()).option("--model <model>", "Model for the current session. Provide a role alias (e.g. 'small' or 'premium') or a provider/model reference (e.g. 'bailian/qwen38-27b').").addOption(new Option("--effort <level>", "Effort level for the current session (low, medium, high, xhigh, max)").argParser((rawValue) => {
@@ -87362,7 +87579,7 @@ function buildProgram() {
87362
87579
  throw new InvalidArgumentError(`It must be one of: ${allowed.join(", ")}`);
87363
87580
  }
87364
87581
  return value;
87365
- })).option("--agent <agent>", "Agent for the current session. Overrides the 'agent' setting.").option("--betas <betas...>", "Beta headers to include in API requests (API key users only)").option("--fallback-model <model>", "Enable automatic fallback to specified model when default model is overloaded (only works with --print)").addOption(new Option("--workload <tag>", "Workload tag for billing-header attribution (cc_workload). Process-scoped; set by SDK daemon callers that spawn subprocesses for cron work. (only works with --print)").hideHelp()).option("--settings <file-or-json>", "Path to a settings JSON file or a JSON string to load additional settings from").option("--add-dir <directories...>", "Additional directories to allow tool access to").option("--ide", "Automatically connect to IDE on startup if exactly one valid IDE is available", () => true).option("--strict-mcp-config", "Only use MCP servers from --mcp-config, ignoring all other MCP configurations", () => true).option("--session-id <uuid>", "Use a specific session ID for the conversation (must be a valid UUID)").option("-n, --name <name>", "Set a display name for this session (shown in /resume and terminal title)").option("--agents <json>", `JSON object defining custom agents (e.g. '{"reviewer": {"description": "Reviews code", "prompt": "You are a code reviewer"}}')`).option("--setting-sources <sources>", "Comma-separated list of setting sources to load (user, project, local).").option("--plugin-dir <path>", "Load plugins from a directory for this session only (repeatable: --plugin-dir A --plugin-dir B)", (val, prev) => [...prev, val], []).option("--disable-slash-commands", "Disable all skills", () => true).option("--chrome", "Enable AtlasCode in Chrome integration").option("--no-chrome", "Disable AtlasCode in Chrome integration").option("--file <specs...>", "File resources to download at startup. Format: file_id:relative_path (e.g., --file file_abc:doc.txt file_def:img.png)").action(mainActionSeam).version(`${resolveCliVersion()} (AtlasCode)`, "-v, --version", "Output the version number");
87582
+ })).option("--agent <agent>", "Agent for the current session. Overrides the 'agent' setting.").option("--betas <betas...>", "Beta headers to include in API requests (API key users only)").option("--fallback-model <model>", "Enable automatic fallback to specified model when default model is overloaded (only works with --print)").option("--fallback-models <m...>", "0.1.59(CC 2.1.166):回合级有序 fallback 表(≤3 ref,按序逐个一次性重试)——主模型可重试错误时钉下一 ref 为本回合 sessionModel 重跑(区别于单串 --fallback-model 的 in-request 池末位)。缺省 = 零变更(only works with --print)").addOption(new Option("--workload <tag>", "Workload tag for billing-header attribution (cc_workload). Process-scoped; set by SDK daemon callers that spawn subprocesses for cron work. (only works with --print)").hideHelp()).option("--settings <file-or-json>", "Path to a settings JSON file or a JSON string to load additional settings from").option("--add-dir <directories...>", "Additional directories to allow tool access to").option("--ide", "Automatically connect to IDE on startup if exactly one valid IDE is available", () => true).option("--strict-mcp-config", "Only use MCP servers from --mcp-config, ignoring all other MCP configurations", () => true).option("--session-id <uuid>", "Use a specific session ID for the conversation (must be a valid UUID)").option("-n, --name <name>", "Set a display name for this session (shown in /resume and terminal title)").option("--agents <json>", `JSON object defining custom agents (e.g. '{"reviewer": {"description": "Reviews code", "prompt": "You are a code reviewer"}}')`).option("--setting-sources <sources>", "Comma-separated list of setting sources to load (user, project, local).").option("--plugin-dir <path>", "Load plugins from a directory for this session only (repeatable: --plugin-dir A --plugin-dir B)", (val, prev) => [...prev, val], []).option("--disable-slash-commands", "Disable all skills", () => true).option("--chrome", "Enable AtlasCode in Chrome integration").option("--no-chrome", "Disable AtlasCode in Chrome integration").option("--file <specs...>", "File resources to download at startup. Format: file_id:relative_path (e.g., --file file_abc:doc.txt file_def:img.png)").action(mainActionSeam).version(`${resolveCliVersion()} (AtlasCode)`, "-v, --version", "Output the version number");
87366
87583
  program.option("-w, --worktree [name]", "Create a new git worktree for this session (optionally specify a name)");
87367
87584
  program.option("--tmux", "Create a tmux session for the worktree (requires --worktree). Uses iTerm2 native panes when available; use --tmux=classic for traditional tmux.");
87368
87585
  if (feature("TRANSCRIPT_CLASSIFIER")) {
@@ -124452,11 +124669,13 @@ function getAutoModeConfig2() {
124452
124669
  if (feature("TRANSCRIPT_CLASSIFIER")) {
124453
124670
  const schema = object({
124454
124671
  allow: array(string2()).optional(),
124672
+ hard_deny: array(string2()).optional(),
124455
124673
  soft_deny: array(string2()).optional(),
124456
124674
  deny: array(string2()).optional(),
124457
124675
  environment: array(string2()).optional()
124458
124676
  });
124459
124677
  const allow = [];
124678
+ const hard_deny = [];
124460
124679
  const soft_deny = [];
124461
124680
  const environment = [];
124462
124681
  for (const source of [
@@ -124472,15 +124691,18 @@ function getAutoModeConfig2() {
124472
124691
  if (result.success) {
124473
124692
  if (result.data.allow)
124474
124693
  allow.push(...result.data.allow);
124694
+ if (result.data.hard_deny)
124695
+ hard_deny.push(...result.data.hard_deny);
124475
124696
  if (result.data.soft_deny)
124476
124697
  soft_deny.push(...result.data.soft_deny);
124477
124698
  if (result.data.environment)
124478
124699
  environment.push(...result.data.environment);
124479
124700
  }
124480
124701
  }
124481
- if (allow.length > 0 || soft_deny.length > 0 || environment.length > 0) {
124702
+ if (allow.length > 0 || hard_deny.length > 0 || soft_deny.length > 0 || environment.length > 0) {
124482
124703
  return {
124483
124704
  ...allow.length > 0 && { allow },
124705
+ ...hard_deny.length > 0 && { hard_deny },
124484
124706
  ...soft_deny.length > 0 && { soft_deny },
124485
124707
  ...environment.length > 0 && { environment }
124486
124708
  };
@@ -274162,6 +274384,7 @@ function isUsingExternalPermissions() {
274162
274384
  function getDefaultExternalAutoModeRules2() {
274163
274385
  return {
274164
274386
  allow: extractTaggedBullets2("user_allow_rules_to_replace"),
274387
+ hard_deny: extractTaggedBullets2("user_hard_deny_rules_to_replace"),
274165
274388
  soft_deny: extractTaggedBullets2("user_deny_rules_to_replace"),
274166
274389
  environment: extractTaggedBullets2("user_environment_to_replace")
274167
274390
  };
@@ -274174,7 +274397,7 @@ function extractTaggedBullets2(tagName) {
274174
274397
  `).map((line) => line.trim()).filter((line) => line.startsWith("- ")).map((line) => line.slice(2));
274175
274398
  }
274176
274399
  function buildDefaultExternalSystemPrompt2() {
274177
- return BASE_PROMPT2.replace("<permissions_template>", () => EXTERNAL_PERMISSIONS_TEMPLATE2).replace(/<user_allow_rules_to_replace>([\s\S]*?)<\/user_allow_rules_to_replace>/, (_m, defaults) => defaults).replace(/<user_deny_rules_to_replace>([\s\S]*?)<\/user_deny_rules_to_replace>/, (_m, defaults) => defaults).replace(/<user_environment_to_replace>([\s\S]*?)<\/user_environment_to_replace>/, (_m, defaults) => defaults);
274400
+ return BASE_PROMPT2.replace("<permissions_template>", () => EXTERNAL_PERMISSIONS_TEMPLATE2).replace(/<user_allow_rules_to_replace>([\s\S]*?)<\/user_allow_rules_to_replace>/, (_m, defaults) => defaults).replace(/<user_deny_rules_to_replace>([\s\S]*?)<\/user_deny_rules_to_replace>/, (_m, defaults) => defaults).replace(/<user_hard_deny_rules_to_replace>([\s\S]*?)<\/user_hard_deny_rules_to_replace>/, (_m, defaults) => defaults).replace(/<user_environment_to_replace>([\s\S]*?)<\/user_environment_to_replace>/, (_m, defaults) => defaults);
274178
274401
  }
274179
274402
  function getAutoModeClassifierErrorDumpPath() {
274180
274403
  return join101(getAtlasTempDir2(), "auto-mode-classifier-errors", `${getSessionId()}.txt`);
@@ -274347,10 +274570,12 @@ async function buildYoloSystemPrompt(context) {
274347
274570
  const userAllow = allowDescriptions.length ? allowDescriptions.map((d) => `- ${d}`).join(`
274348
274571
  `) : undefined;
274349
274572
  const userDeny = denyDescriptions.length ? denyDescriptions.map((d) => `- ${d}`).join(`
274573
+ `) : undefined;
274574
+ const userHardDeny = autoMode?.hard_deny?.length ? autoMode.hard_deny.map((d) => `- ${d}`).join(`
274350
274575
  `) : undefined;
274351
274576
  const userEnvironment = autoMode?.environment?.length ? autoMode.environment.map((e) => `- ${e}`).join(`
274352
274577
  `) : undefined;
274353
- return systemPrompt.replace(/<user_allow_rules_to_replace>([\s\S]*?)<\/user_allow_rules_to_replace>/, (_m, defaults) => userAllow ?? defaults).replace(/<user_deny_rules_to_replace>([\s\S]*?)<\/user_deny_rules_to_replace>/, (_m, defaults) => userDeny ?? defaults).replace(/<user_environment_to_replace>([\s\S]*?)<\/user_environment_to_replace>/, (_m, defaults) => userEnvironment ?? defaults);
274578
+ return systemPrompt.replace(/<user_allow_rules_to_replace>([\s\S]*?)<\/user_allow_rules_to_replace>/, (_m, defaults) => userAllow ?? defaults).replace(/<user_deny_rules_to_replace>([\s\S]*?)<\/user_deny_rules_to_replace>/, (_m, defaults) => userDeny ?? defaults).replace(/<user_hard_deny_rules_to_replace>([\s\S]*?)<\/user_hard_deny_rules_to_replace>/, (_m, defaults) => userHardDeny ?? defaults).replace(/<user_environment_to_replace>([\s\S]*?)<\/user_environment_to_replace>/, (_m, defaults) => userEnvironment ?? defaults);
274354
274579
  }
274355
274580
  function stripThinking2(text) {
274356
274581
  return text.replace(/<thinking>[\s\S]*?<\/thinking>/g, "").replace(/<thinking>[\s\S]*$/, "");
@@ -430373,7 +430598,10 @@ async function streamCompactSummary({
430373
430598
  context.setResponseLength?.((length) => length + deltaEvent.text.length);
430374
430599
  }
430375
430600
  if (event.type === "assistant") {
430376
- response = event;
430601
+ const assistantEvent = event;
430602
+ if (!assistantEvent.interrupted) {
430603
+ response = assistantEvent;
430604
+ }
430377
430605
  }
430378
430606
  }
430379
430607
  if (response) {
@@ -489893,24 +490121,25 @@ function autoModeConfigHandler2() {
489893
490121
  const defaults = getDefaultExternalAutoModeRules2();
489894
490122
  writeRules2({
489895
490123
  allow: config?.allow?.length ? config.allow : defaults.allow,
490124
+ hard_deny: config?.hard_deny?.length ? config.hard_deny : defaults.hard_deny,
489896
490125
  soft_deny: config?.soft_deny?.length ? config.soft_deny : defaults.soft_deny,
489897
490126
  environment: config?.environment?.length ? config.environment : defaults.environment
489898
490127
  });
489899
490128
  }
489900
490129
  async function autoModeCritiqueHandler2(options) {
489901
490130
  const config = getAutoModeConfig2();
489902
- const hasCustomRules = (config?.allow?.length ?? 0) > 0 || (config?.soft_deny?.length ?? 0) > 0 || (config?.environment?.length ?? 0) > 0;
490131
+ const hasCustomRules = (config?.allow?.length ?? 0) > 0 || (config?.hard_deny?.length ?? 0) > 0 || (config?.soft_deny?.length ?? 0) > 0 || (config?.environment?.length ?? 0) > 0;
489903
490132
  if (!hasCustomRules) {
489904
490133
  process.stdout.write(`No custom auto mode rules found.
489905
490134
 
489906
- ` + `Add rules to your settings file under autoMode.{allow, soft_deny, environment}.
490135
+ ` + `Add rules to your settings file under autoMode.{allow, hard_deny, soft_deny, environment}.
489907
490136
  ` + "Run `claude auto-mode defaults` to see the default rules for reference.\n");
489908
490137
  return;
489909
490138
  }
489910
490139
  const model = options.model ? parseUserSpecifiedModel2(options.model) : getMainLoopModel();
489911
490140
  const defaults = getDefaultExternalAutoModeRules2();
489912
490141
  const classifierPrompt = buildDefaultExternalSystemPrompt2();
489913
- const userRulesSummary = formatRulesForCritique2("allow", config?.allow ?? [], defaults.allow) + formatRulesForCritique2("soft_deny", config?.soft_deny ?? [], defaults.soft_deny) + formatRulesForCritique2("environment", config?.environment ?? [], defaults.environment);
490142
+ const userRulesSummary = formatRulesForCritique2("allow", config?.allow ?? [], defaults.allow) + formatRulesForCritique2("hard_deny", config?.hard_deny ?? [], defaults.hard_deny) + formatRulesForCritique2("soft_deny", config?.soft_deny ?? [], defaults.soft_deny) + formatRulesForCritique2("environment", config?.environment ?? [], defaults.environment);
489914
490143
  process.stdout.write(`Analyzing your auto mode rules…
489915
490144
 
489916
490145
  `);
@@ -489979,9 +490208,10 @@ var init_autoMode3 = __esm(() => {
489979
490208
  init_slowOperations();
489980
490209
  CRITIQUE_SYSTEM_PROMPT2 = `You are an expert reviewer of auto mode classifier rules for Atlas.
489981
490210
  ` + `
489982
- ` + 'Atlas has an "auto mode" that uses an AI classifier to decide whether ' + "tool calls should be auto-approved or require user confirmation. Users can " + `write custom rules in three categories:
490211
+ ` + 'Atlas has an "auto mode" that uses an AI classifier to decide whether ' + "tool calls should be auto-approved or require user confirmation. Users can " + `write custom rules in four categories:
489983
490212
  ` + `
489984
490213
  ` + `- **allow**: Actions the classifier should auto-approve
490214
+ ` + `- **hard_deny**: Actions that are blocked unconditionally (user intent does not clear these)
489985
490215
  ` + `- **soft_deny**: Actions the classifier should block (require user confirmation)
489986
490216
  ` + `- **environment**: Context about the user's setup that helps the classifier make decisions
489987
490217
  ` + `
@@ -490836,7 +491066,7 @@ async function run() {
490836
491066
  throw new Error("--task-budget must be a positive integer");
490837
491067
  }
490838
491068
  return tokens;
490839
- }).hideHelp()).option("--replay-user-messages", "Re-emit user messages from stdin back on stdout for acknowledgment (only works with --input-format=stream-json and --output-format=stream-json)", () => true).addOption(new Option("--enable-auth-status", "Enable auth status messages in SDK mode").default(false).hideHelp()).option("--allowedTools, --allowed-tools <tools...>", 'Comma or space-separated list of tool names to allow (e.g. "Bash(git:*) Edit")').option("--tools <tools...>", 'Specify the list of available tools from the built-in set. Use "" to disable all tools, "default" to use all tools, or specify tool names (e.g. "Bash,Edit,Read").').option("--disallowedTools, --disallowed-tools <tools...>", 'Comma or space-separated list of tool names to deny (e.g. "Bash(git:*) Edit")').option("--mcp-config <configs...>", "Load MCP servers from JSON files or strings (space-separated)").addOption(new Option("--permission-prompt-tool <tool>", "MCP tool to use for permission prompts (only works with --print)").argParser(String).hideHelp()).addOption(new Option("--system-prompt <prompt>", "System prompt to use for the session").argParser(String)).addOption(new Option("--system-prompt-file <file>", "Read system prompt from a file").argParser(String).hideHelp()).addOption(new Option("--append-system-prompt <prompt>", "Append a system prompt to the default system prompt").argParser(String)).addOption(new Option("--append-system-prompt-file <file>", "Read system prompt from a file and append to the default system prompt").argParser(String).hideHelp()).addOption(new Option("--permission-mode <mode>", "Permission mode to use for the session").argParser(String).choices(PERMISSION_MODES2)).option("-c, --continue", "Continue the most recent conversation in the current directory", () => true).option("-r, --resume [value]", "Resume a conversation by session ID, or open interactive picker with optional search term", (value) => value || true).option("--fork-session", "When resuming, create a new session ID instead of reusing the original (use with --resume or --continue)", () => true).addOption(new Option("--prefill <text>", "Pre-fill the prompt input with text without submitting it").hideHelp()).addOption(new Option("--deep-link-origin", "Signal that this session was launched from a deep link").hideHelp()).addOption(new Option("--deep-link-repo <slug>", "Repo slug the deep link ?repo= parameter resolved to the current cwd").hideHelp()).addOption(new Option("--deep-link-last-fetch <ms>", "FETCH_HEAD mtime in epoch ms, precomputed by the deep link trampoline").argParser((v) => {
491069
+ }).hideHelp()).option("--replay-user-messages", "Re-emit user messages from stdin back on stdout for acknowledgment (only works with --input-format=stream-json and --output-format=stream-json)", () => true).addOption(new Option("--enable-auth-status", "Enable auth status messages in SDK mode").default(false).hideHelp()).option("--allowedTools, --allowed-tools <tools...>", 'Comma or space-separated list of tool names to allow (e.g. "Bash(git:*) Edit")').option("--tools <tools...>", 'Specify the list of available tools from the built-in set. Use "" to disable all tools, "default" to use all tools, or specify tool names (e.g. "Bash,Edit,Read").').option("--disallowedTools, --disallowed-tools <tools...>", 'Comma or space-separated list of tool names to deny (e.g. "Bash(git:*) Edit")').option("--mcp-config <configs...>", "Load MCP servers from JSON files or strings (space-separated)").addOption(new Option("--permission-prompt-tool <tool>", "MCP tool to use for permission prompts (only works with --print)").argParser(String).hideHelp()).addOption(new Option("--system-prompt <prompt>", "System prompt to use for the session").argParser(String)).addOption(new Option("--system-prompt-file <file>", "Read system prompt from a file").argParser(String).hideHelp()).addOption(new Option("--append-system-prompt <prompt>", "Append a system prompt to the default system prompt").argParser(String)).addOption(new Option("--append-system-prompt-file <file>", "Read system prompt from a file and append to the default system prompt").argParser(String).hideHelp()).addOption(new Option("--permission-mode <mode>", "Permission mode to use for the session").argParser(String).choices(PERMISSION_MODES2)).addOption(new Option("--permission-prompts <mode>", "Permission prompt policy; 'none' auto-denies prompts that would otherwise ask").argParser(String).choices(["none"])).option("-c, --continue", "Continue the most recent conversation in the current directory", () => true).option("-r, --resume [value]", "Resume a conversation by session ID, or open interactive picker with optional search term", (value) => value || true).option("--fork-session", "When resuming, create a new session ID instead of reusing the original (use with --resume or --continue)", () => true).addOption(new Option("--prefill <text>", "Pre-fill the prompt input with text without submitting it").hideHelp()).addOption(new Option("--deep-link-origin", "Signal that this session was launched from a deep link").hideHelp()).addOption(new Option("--deep-link-repo <slug>", "Repo slug the deep link ?repo= parameter resolved to the current cwd").hideHelp()).addOption(new Option("--deep-link-last-fetch <ms>", "FETCH_HEAD mtime in epoch ms, precomputed by the deep link trampoline").argParser((v) => {
490840
491070
  const n = Number(v);
490841
491071
  return Number.isFinite(n) ? n : undefined;
490842
491072
  }).hideHelp()).option("--from-pr [value]", "Resume a session linked to a PR by PR number/URL, or open interactive picker with optional search term", (value) => value || true).option("--no-session-persistence", "Disable session persistence - sessions will not be saved to disk and cannot be resumed (only works with --print)").addOption(new Option("--resume-session-at <message id>", "When resuming, only messages up to and including the assistant message with <message.id> (use with --resume in print mode)").argParser(String).hideHelp()).addOption(new Option("--rewind-files <user-message-id>", "Restore files to state at the specified user message and exit (requires --resume)").hideHelp()).option("--model <model>", `Model for the current session. Provide a role alias (e.g. 'small' or 'premium') or a provider/model reference (e.g. 'bailian/qwen38-27b').`).addOption(new Option("--effort <level>", `Effort level for the current session (low, medium, high, xhigh, max)`).argParser((rawValue) => {
@@ -490865,7 +491095,8 @@ async function run() {
490865
491095
  allowedTools = [],
490866
491096
  disallowedTools = [],
490867
491097
  mcpConfig = [],
490868
- permissionMode: permissionModeCli,
491098
+ permissionMode: permissionModeCliRaw,
491099
+ permissionPrompts,
490869
491100
  addDir = [],
490870
491101
  fallbackModel,
490871
491102
  betas = [],
@@ -490874,6 +491105,7 @@ async function run() {
490874
491105
  includeHookEvents,
490875
491106
  includePartialMessages
490876
491107
  } = options;
491108
+ const permissionModeCli = permissionModeCliRaw ?? (permissionPrompts === "none" ? "dontAsk" : undefined);
490877
491109
  if (options.prefill) {
490878
491110
  seedEarlyInput(options.prefill);
490879
491111
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlasharness/atlascode",
3
- "version": "0.1.57",
3
+ "version": "0.1.59",
4
4
  "description": "AtlasCode \u2014 Coding Agent for the domestic computing ecosystem (Ascend NPU + domestic LLM backends)",
5
5
  "bin": {
6
6
  "atlas": "dist/cli.js",