@atlasharness/atlascode 0.1.58 → 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 +147 -20
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -27940,6 +27940,15 @@ async function withTurnRecovery(attempt, options = {}) {
27940
27940
  }
27941
27941
  throw lastErr;
27942
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
+ }
27943
27952
  var DEFAULT_MAX_RETRIES = 13, DEFAULT_BACKOFF_BASE_MS = 100, DEFAULT_BACKOFF_CAP_MS = 1000;
27944
27953
  var init_turnRecovery = __esm(() => {
27945
27954
  init_modelprovider2();
@@ -29587,6 +29596,65 @@ function getRuleByContentsForToolName(context, toolName, behavior) {
29587
29596
  }
29588
29597
  return ruleByContents;
29589
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
+ }
29590
29658
  function permissionModeTitle(mode) {
29591
29659
  return PERMISSION_MODE_TITLES[mode] ?? "Default";
29592
29660
  }
@@ -29723,6 +29791,17 @@ var BASH_TOOL_NAME = "Bash", hasPermissionsToUseTool = async (tool, input, conte
29723
29791
  }
29724
29792
  };
29725
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
+ }
29726
29805
  const askRule = permissionContext ? getAskRuleForTool(permissionContext, tool) : null;
29727
29806
  if (askRule) {
29728
29807
  const canSandboxAutoAllow = tool.name === BASH_TOOL_NAME && sandbox.isSandboxingEnabled() && sandbox.isAutoAllowBashIfSandboxedEnabled() && input.dangerouslyDisableSandbox !== true;
@@ -29737,6 +29816,17 @@ var BASH_TOOL_NAME = "Bash", hasPermissionsToUseTool = async (tool, input, conte
29737
29816
  }, permissionContext, tool.name);
29738
29817
  }
29739
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
+ }
29740
29830
  let toolPermissionResult = {
29741
29831
  behavior: "passthrough",
29742
29832
  message: createPermissionRequestMessage(tool.name)
@@ -30842,6 +30932,12 @@ Block unconditionally. Do not look for authorization. No ALLOW exception applies
30842
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.
30843
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.
30844
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
+
30845
30941
  ## SOFT BLOCK — destructive or irreversible (User Intent Rule CAN clear these)
30846
30942
 
30847
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.
@@ -30947,6 +31043,7 @@ var init_permissions_external = () => {};
30947
31043
  function getDefaultExternalAutoModeRules() {
30948
31044
  return {
30949
31045
  allow: extractTaggedBullets("user_allow_rules_to_replace"),
31046
+ hard_deny: extractTaggedBullets("user_hard_deny_rules_to_replace"),
30950
31047
  soft_deny: extractTaggedBullets("user_deny_rules_to_replace"),
30951
31048
  environment: extractTaggedBullets("user_environment_to_replace")
30952
31049
  };
@@ -30959,7 +31056,7 @@ function extractTaggedBullets(tagName) {
30959
31056
  `).map((line) => line.trim()).filter((line) => line.startsWith("- ")).map((line) => line.slice(2));
30960
31057
  }
30961
31058
  function buildDefaultExternalSystemPrompt() {
30962
- 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);
30963
31060
  }
30964
31061
  var BASE_PROMPT, EXTERNAL_PERMISSIONS_TEMPLATE;
30965
31062
  var init_prompts = __esm(() => {
@@ -31368,11 +31465,13 @@ function getAutoModeConfig() {
31368
31465
  if (feature("TRANSCRIPT_CLASSIFIER")) {
31369
31466
  const schema = object({
31370
31467
  allow: array(string2()).optional(),
31468
+ hard_deny: array(string2()).optional(),
31371
31469
  soft_deny: array(string2()).optional(),
31372
31470
  deny: array(string2()).optional(),
31373
31471
  environment: array(string2()).optional()
31374
31472
  });
31375
31473
  const allow = [];
31474
+ const hard_deny = [];
31376
31475
  const soft_deny = [];
31377
31476
  const environment = [];
31378
31477
  for (const source of [
@@ -31388,6 +31487,8 @@ function getAutoModeConfig() {
31388
31487
  if (result.success) {
31389
31488
  if (result.data.allow)
31390
31489
  allow.push(...result.data.allow);
31490
+ if (result.data.hard_deny)
31491
+ hard_deny.push(...result.data.hard_deny);
31391
31492
  if (result.data.soft_deny)
31392
31493
  soft_deny.push(...result.data.soft_deny);
31393
31494
  if (result.data.environment) {
@@ -31395,9 +31496,10 @@ function getAutoModeConfig() {
31395
31496
  }
31396
31497
  }
31397
31498
  }
31398
- 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) {
31399
31500
  return {
31400
31501
  ...allow.length > 0 && { allow },
31502
+ ...hard_deny.length > 0 && { hard_deny },
31401
31503
  ...soft_deny.length > 0 && { soft_deny },
31402
31504
  ...environment.length > 0 && { environment }
31403
31505
  };
@@ -81744,10 +81846,12 @@ async function queryAgentLoop(deps, args) {
81744
81846
  }
81745
81847
  deps.emit?.({ type: "round_start", turn: turns });
81746
81848
  let reactiveRetried = false;
81849
+ let effectiveDeps = deps;
81850
+ const triedFallbackRefs = new Set;
81747
81851
  for (;; ) {
81748
81852
  const roundInputLen = messages.length;
81749
81853
  try {
81750
- lastRound = await queryOneRound(deps, tools, messages);
81854
+ lastRound = await queryOneRound(effectiveDeps, tools, messages);
81751
81855
  } catch (err) {
81752
81856
  if (!reactiveRetried && deps.reactiveCompact !== undefined && isReactiveCompactEnabled() && isReactiveCompactRecoverableError(err)) {
81753
81857
  reactiveRetried = true;
@@ -81769,6 +81873,12 @@ async function queryAgentLoop(deps, args) {
81769
81873
  continue;
81770
81874
  }
81771
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
+ }
81772
81882
  throw err;
81773
81883
  }
81774
81884
  messages = lastRound.messages;
@@ -81971,6 +82081,7 @@ async function createAgentLoopDeps(config = {}) {
81971
82081
  responseFormat: config.responseFormat,
81972
82082
  effortValue: config.effortValue,
81973
82083
  fallbackModel: config.fallbackModel,
82084
+ fallbackModels: config.fallbackModels,
81974
82085
  transcript: config.disablePersistence ? undefined : {
81975
82086
  record: (msgs) => recordTranscript(msgs),
81976
82087
  recordContentReplacement: (recs) => recordContentReplacement(recs, config.agentId)
@@ -85924,7 +86035,8 @@ async function runHeadless(inputPrompt, options) {
85924
86035
  thinkingConfig: options.thinkingConfig,
85925
86036
  responseFormat: options.jsonSchema ? toResponseFormat({ type: "json_schema", schema: options.jsonSchema }) : undefined,
85926
86037
  effortValue: options.effort,
85927
- fallbackModel: options.fallbackModel
86038
+ fallbackModel: options.fallbackModel,
86039
+ fallbackModels: options.fallbackModels
85928
86040
  });
85929
86041
  const { toolPermissionContext: initialTpc, tools, warnings } = bundle;
85930
86042
  if (warnings.length > 0) {
@@ -87161,6 +87273,7 @@ function autoModeConfigHandler() {
87161
87273
  const defaults = getDefaultExternalAutoModeRules();
87162
87274
  writeRules({
87163
87275
  allow: config?.allow?.length ? config.allow : defaults.allow,
87276
+ hard_deny: config?.hard_deny?.length ? config.hard_deny : defaults.hard_deny,
87164
87277
  soft_deny: config?.soft_deny?.length ? config.soft_deny : defaults.soft_deny,
87165
87278
  environment: config?.environment?.length ? config.environment : defaults.environment
87166
87279
  });
@@ -87176,18 +87289,18 @@ function makeUserMessage2(text) {
87176
87289
  }
87177
87290
  async function autoModeCritiqueHandler(options) {
87178
87291
  const config = getAutoModeConfig();
87179
- 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;
87180
87293
  if (!hasCustomRules) {
87181
87294
  process.stdout.write(`No custom auto mode rules found.
87182
87295
 
87183
- ` + `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}.
87184
87297
  ` + "Run `atlascode auto-mode defaults` to see the default rules for reference.\n");
87185
87298
  return;
87186
87299
  }
87187
87300
  const role = options.model ? modelToRole(options.model) : "premium";
87188
87301
  const defaults = getDefaultExternalAutoModeRules();
87189
87302
  const classifierPrompt = buildDefaultExternalSystemPrompt();
87190
- 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);
87191
87304
  process.stdout.write(`Analyzing your auto mode rules…
87192
87305
 
87193
87306
  `);
@@ -87253,9 +87366,10 @@ var init_autoMode2 = __esm(() => {
87253
87366
  init_shared();
87254
87367
  CRITIQUE_SYSTEM_PROMPT = `You are an expert reviewer of auto mode classifier rules for Atlas.
87255
87368
  ` + `
87256
- ` + '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:
87257
87370
  ` + `
87258
87371
  ` + `- **allow**: Actions the classifier should auto-approve
87372
+ ` + `- **hard_deny**: Actions that are blocked unconditionally (user intent does not clear these)
87259
87373
  ` + `- **soft_deny**: Actions the classifier should block (require user confirmation)
87260
87374
  ` + `- **environment**: Context about the user's setup that helps the classifier make decisions
87261
87375
  ` + `
@@ -87370,7 +87484,7 @@ function buildHeadlessOptions(options) {
87370
87484
  allowedTools: options.allowedTools,
87371
87485
  disallowedTools: options.disallowedTools,
87372
87486
  baseTools: options.tools,
87373
- permissionMode: options.permissionMode,
87487
+ permissionMode: options.permissionMode ?? (options.permissionPrompts === "none" ? "dontAsk" : undefined),
87374
87488
  permissionPromptToolName: options.permissionPromptTool,
87375
87489
  maxTurns: options.maxTurns,
87376
87490
  model: options.model,
@@ -87383,6 +87497,7 @@ function buildHeadlessOptions(options) {
87383
87497
  systemPrompt: readPromptFileInline(options.systemPrompt, options.systemPromptFile, "System prompt"),
87384
87498
  appendSystemPrompt: readPromptFileInline(options.appendSystemPrompt, options.appendSystemPromptFile, "Append system prompt"),
87385
87499
  fallbackModel: options.fallbackModel,
87500
+ fallbackModels: options.fallbackModels,
87386
87501
  thinkingConfig: buildHeadlessThinkingConfig(options.thinking, options.maxThinkingTokens),
87387
87502
  effort: options.effort,
87388
87503
  mcpConfig: options.mcpConfig,
@@ -87454,7 +87569,7 @@ function buildProgram() {
87454
87569
  throw new Error("--task-budget must be a positive integer");
87455
87570
  }
87456
87571
  return tokens;
87457
- }).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) => {
87458
87573
  const n = Number(v);
87459
87574
  return Number.isFinite(n) ? n : undefined;
87460
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) => {
@@ -87464,7 +87579,7 @@ function buildProgram() {
87464
87579
  throw new InvalidArgumentError(`It must be one of: ${allowed.join(", ")}`);
87465
87580
  }
87466
87581
  return value;
87467
- })).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");
87468
87583
  program.option("-w, --worktree [name]", "Create a new git worktree for this session (optionally specify a name)");
87469
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.");
87470
87585
  if (feature("TRANSCRIPT_CLASSIFIER")) {
@@ -124554,11 +124669,13 @@ function getAutoModeConfig2() {
124554
124669
  if (feature("TRANSCRIPT_CLASSIFIER")) {
124555
124670
  const schema = object({
124556
124671
  allow: array(string2()).optional(),
124672
+ hard_deny: array(string2()).optional(),
124557
124673
  soft_deny: array(string2()).optional(),
124558
124674
  deny: array(string2()).optional(),
124559
124675
  environment: array(string2()).optional()
124560
124676
  });
124561
124677
  const allow = [];
124678
+ const hard_deny = [];
124562
124679
  const soft_deny = [];
124563
124680
  const environment = [];
124564
124681
  for (const source of [
@@ -124574,15 +124691,18 @@ function getAutoModeConfig2() {
124574
124691
  if (result.success) {
124575
124692
  if (result.data.allow)
124576
124693
  allow.push(...result.data.allow);
124694
+ if (result.data.hard_deny)
124695
+ hard_deny.push(...result.data.hard_deny);
124577
124696
  if (result.data.soft_deny)
124578
124697
  soft_deny.push(...result.data.soft_deny);
124579
124698
  if (result.data.environment)
124580
124699
  environment.push(...result.data.environment);
124581
124700
  }
124582
124701
  }
124583
- 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) {
124584
124703
  return {
124585
124704
  ...allow.length > 0 && { allow },
124705
+ ...hard_deny.length > 0 && { hard_deny },
124586
124706
  ...soft_deny.length > 0 && { soft_deny },
124587
124707
  ...environment.length > 0 && { environment }
124588
124708
  };
@@ -274264,6 +274384,7 @@ function isUsingExternalPermissions() {
274264
274384
  function getDefaultExternalAutoModeRules2() {
274265
274385
  return {
274266
274386
  allow: extractTaggedBullets2("user_allow_rules_to_replace"),
274387
+ hard_deny: extractTaggedBullets2("user_hard_deny_rules_to_replace"),
274267
274388
  soft_deny: extractTaggedBullets2("user_deny_rules_to_replace"),
274268
274389
  environment: extractTaggedBullets2("user_environment_to_replace")
274269
274390
  };
@@ -274276,7 +274397,7 @@ function extractTaggedBullets2(tagName) {
274276
274397
  `).map((line) => line.trim()).filter((line) => line.startsWith("- ")).map((line) => line.slice(2));
274277
274398
  }
274278
274399
  function buildDefaultExternalSystemPrompt2() {
274279
- 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);
274280
274401
  }
274281
274402
  function getAutoModeClassifierErrorDumpPath() {
274282
274403
  return join101(getAtlasTempDir2(), "auto-mode-classifier-errors", `${getSessionId()}.txt`);
@@ -274449,10 +274570,12 @@ async function buildYoloSystemPrompt(context) {
274449
274570
  const userAllow = allowDescriptions.length ? allowDescriptions.map((d) => `- ${d}`).join(`
274450
274571
  `) : undefined;
274451
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(`
274452
274575
  `) : undefined;
274453
274576
  const userEnvironment = autoMode?.environment?.length ? autoMode.environment.map((e) => `- ${e}`).join(`
274454
274577
  `) : undefined;
274455
- 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);
274456
274579
  }
274457
274580
  function stripThinking2(text) {
274458
274581
  return text.replace(/<thinking>[\s\S]*?<\/thinking>/g, "").replace(/<thinking>[\s\S]*$/, "");
@@ -489998,24 +490121,25 @@ function autoModeConfigHandler2() {
489998
490121
  const defaults = getDefaultExternalAutoModeRules2();
489999
490122
  writeRules2({
490000
490123
  allow: config?.allow?.length ? config.allow : defaults.allow,
490124
+ hard_deny: config?.hard_deny?.length ? config.hard_deny : defaults.hard_deny,
490001
490125
  soft_deny: config?.soft_deny?.length ? config.soft_deny : defaults.soft_deny,
490002
490126
  environment: config?.environment?.length ? config.environment : defaults.environment
490003
490127
  });
490004
490128
  }
490005
490129
  async function autoModeCritiqueHandler2(options) {
490006
490130
  const config = getAutoModeConfig2();
490007
- 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;
490008
490132
  if (!hasCustomRules) {
490009
490133
  process.stdout.write(`No custom auto mode rules found.
490010
490134
 
490011
- ` + `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}.
490012
490136
  ` + "Run `claude auto-mode defaults` to see the default rules for reference.\n");
490013
490137
  return;
490014
490138
  }
490015
490139
  const model = options.model ? parseUserSpecifiedModel2(options.model) : getMainLoopModel();
490016
490140
  const defaults = getDefaultExternalAutoModeRules2();
490017
490141
  const classifierPrompt = buildDefaultExternalSystemPrompt2();
490018
- 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);
490019
490143
  process.stdout.write(`Analyzing your auto mode rules…
490020
490144
 
490021
490145
  `);
@@ -490084,9 +490208,10 @@ var init_autoMode3 = __esm(() => {
490084
490208
  init_slowOperations();
490085
490209
  CRITIQUE_SYSTEM_PROMPT2 = `You are an expert reviewer of auto mode classifier rules for Atlas.
490086
490210
  ` + `
490087
- ` + '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:
490088
490212
  ` + `
490089
490213
  ` + `- **allow**: Actions the classifier should auto-approve
490214
+ ` + `- **hard_deny**: Actions that are blocked unconditionally (user intent does not clear these)
490090
490215
  ` + `- **soft_deny**: Actions the classifier should block (require user confirmation)
490091
490216
  ` + `- **environment**: Context about the user's setup that helps the classifier make decisions
490092
490217
  ` + `
@@ -490941,7 +491066,7 @@ async function run() {
490941
491066
  throw new Error("--task-budget must be a positive integer");
490942
491067
  }
490943
491068
  return tokens;
490944
- }).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) => {
490945
491070
  const n = Number(v);
490946
491071
  return Number.isFinite(n) ? n : undefined;
490947
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) => {
@@ -490970,7 +491095,8 @@ async function run() {
490970
491095
  allowedTools = [],
490971
491096
  disallowedTools = [],
490972
491097
  mcpConfig = [],
490973
- permissionMode: permissionModeCli,
491098
+ permissionMode: permissionModeCliRaw,
491099
+ permissionPrompts,
490974
491100
  addDir = [],
490975
491101
  fallbackModel,
490976
491102
  betas = [],
@@ -490979,6 +491105,7 @@ async function run() {
490979
491105
  includeHookEvents,
490980
491106
  includePartialMessages
490981
491107
  } = options;
491108
+ const permissionModeCli = permissionModeCliRaw ?? (permissionPrompts === "none" ? "dontAsk" : undefined);
490982
491109
  if (options.prefill) {
490983
491110
  seedEarlyInput(options.prefill);
490984
491111
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlasharness/atlascode",
3
- "version": "0.1.58",
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",