atris 3.36.0 → 3.37.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/atris.js CHANGED
@@ -14,6 +14,15 @@ const os = require('os');
14
14
  const crypto = require('crypto');
15
15
  const PACKAGE_JSON_PATH = path.join(__dirname, '..', 'package.json');
16
16
 
17
+ // Exit without truncating piped stdout: process.exit() drops whatever is still
18
+ // queued in the pipe buffer (large outputs died at 512-byte boundaries). Let
19
+ // the event loop drain naturally; an unref'd timer forces exit if a stray
20
+ // handle keeps the process alive.
21
+ function exitWhenFlushed(code) {
22
+ process.exitCode = code;
23
+ setTimeout(() => process.exit(code), 3000).unref();
24
+ }
25
+
17
26
  let CLI_VERSION = 'unknown';
18
27
  try {
19
28
  const pkgRaw = fs.readFileSync(PACKAGE_JSON_PATH, 'utf8');
@@ -2100,8 +2109,8 @@ if (command === 'init') {
2100
2109
  .catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
2101
2110
  } else if (command === 'youtube') {
2102
2111
  require('../commands/youtube').youtubeCommand(process.argv.slice(3))
2103
- .then(() => process.exit(0))
2104
- .catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
2112
+ .then(() => exitWhenFlushed(0))
2113
+ .catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); exitWhenFlushed(1); });
2105
2114
  } else if (command === 'run') {
2106
2115
  const args = process.argv.slice(3);
2107
2116
  if (args[0] === 'logs') {
@@ -1487,7 +1487,7 @@ function missionMetricLine(mission, indent = ' ') {
1487
1487
  return [`${indent}metric: ${metric}${last === null ? '' : ` (last: ${last})`}`];
1488
1488
  }
1489
1489
 
1490
- function renderMemberNowMarkdown(owner, missions) {
1490
+ function renderMemberNowMarkdown(owner, missions, root = process.cwd()) {
1491
1491
  const lines = [
1492
1492
  '# Now',
1493
1493
  '',
@@ -1505,6 +1505,8 @@ function renderMemberNowMarkdown(owner, missions) {
1505
1505
  lines.push('');
1506
1506
  lines.push(`- id: ${mission.id}`);
1507
1507
  lines.push(`- status: ${missionHumanStatusText(mission)}`);
1508
+ const debtLine = missionVerificationDebtLine(missionVerificationDebt(mission, root));
1509
+ if (debtLine) lines.push(`- ${debtLine}`);
1508
1510
  lines.push(`- cadence: ${mission.cadence}`);
1509
1511
  lines.push(`- runner: ${mission.runner}${mission.model ? ` (${mission.model})` : ''}`);
1510
1512
  lines.push(`- lane: ${mission.lane}`);
@@ -1533,7 +1535,7 @@ function renderMemberMissionState(owner, root = process.cwd()) {
1533
1535
  const missions = listMissions(root).filter((mission) => mission.owner === owner);
1534
1536
  const nowPath = path.join(dir, 'now.md');
1535
1537
  removeLegacyGeneratedMissionViews(dir);
1536
- fs.writeFileSync(nowPath, renderMemberNowMarkdown(owner, missions), 'utf8');
1538
+ fs.writeFileSync(nowPath, renderMemberNowMarkdown(owner, missions, root), 'utf8');
1537
1539
  return { missionPath, nowPath };
1538
1540
  }
1539
1541
 
@@ -2006,6 +2008,39 @@ function missionRunTrustedObjective(rawObjective, room, target) {
2006
2008
  return missionRunPreflightObjective(rawObjective, room, room?.owner || 'mission-lead');
2007
2009
  }
2008
2010
 
2011
+ const MISSION_RUN_SHAPING_STOPWORDS = new Set([
2012
+ 'should', 'would', 'could', 'with', 'that', 'this', 'from', 'into',
2013
+ 'atris', 'mission', 'room', 'just', 'work', 'works',
2014
+ ]);
2015
+
2016
+ function missionRunSignificantTokens(value) {
2017
+ return String(value || '')
2018
+ .toLowerCase()
2019
+ .replace(/[^a-z]+/g, ' ')
2020
+ .split(/\s+/)
2021
+ .filter((token) => token.length >= 4 && !MISSION_RUN_SHAPING_STOPWORDS.has(token));
2022
+ }
2023
+
2024
+ function guardMissionRunShaping(rawObjective, shapedObjective, selectedTarget) {
2025
+ const rawTokens = missionRunSignificantTokens(rawObjective);
2026
+ const shapedTokens = new Set(missionRunSignificantTokens(shapedObjective));
2027
+ const sharesSignificantToken = [...rawTokens].some((token) => shapedTokens.has(token));
2028
+ if (rawTokens.length < 4 || sharesSignificantToken) {
2029
+ return { shapedObjective, selectedTarget, shapingRejected: false, shapingRejectedReason: '' };
2030
+ }
2031
+
2032
+ const rejectedTitle = missionRunConcreteTitle(selectedTarget?.title)
2033
+ || missionRunConcreteTitle(shapedObjective)
2034
+ || 'untitled shaping';
2035
+ const rejectedRef = selectedTarget?.ref || selectedTarget?.task_id || 'no ref';
2036
+ return {
2037
+ shapedObjective: rawObjective,
2038
+ selectedTarget: null,
2039
+ shapingRejected: true,
2040
+ shapingRejectedReason: `shaping rejected: "${rejectedTitle}" (${rejectedRef}) shares no significant tokens with the raw objective`,
2041
+ };
2042
+ }
2043
+
2009
2044
  function buildMissionRunRoomPreflight(rawObjective, args = [], options = {}) {
2010
2045
  if (!shouldMissionRunRoomPreflight(rawObjective, args)) return null;
2011
2046
  const root = options.root || process.cwd();
@@ -2034,27 +2069,33 @@ function buildMissionRunRoomPreflight(rawObjective, args = [], options = {}) {
2034
2069
  const shapedObjective = trustedRun
2035
2070
  ? missionRunTrustedObjective(rawObjective, written.room, selectedTarget)
2036
2071
  : missionRunPreflightObjective(rawObjective, written.room, ownerResolution.owner);
2037
- const taskSpineRequired = !selectedTarget && (explicitPreflight || signalPreflight);
2072
+ const shaping = guardMissionRunShaping(rawObjective, shapedObjective, selectedTarget);
2073
+ const acceptedTarget = shaping.selectedTarget;
2074
+ const taskSpineRequired = !acceptedTarget && (explicitPreflight || signalPreflight);
2038
2075
  return {
2039
2076
  schema: 'atris.mission_run_preflight.v1',
2040
2077
  source: 'mission_room',
2041
2078
  raw_objective: rawObjective,
2042
- shaped_objective: shapedObjective,
2043
- visible_goal_objective: shapedObjective,
2079
+ shaped_objective: shaping.shapedObjective,
2080
+ visible_goal_objective: shaping.shapedObjective,
2044
2081
  room_name: written.room.name,
2045
2082
  room_receipt_path: written.relativePath,
2046
2083
  owner: ownerResolution.owner,
2047
2084
  owner_resolution: ownerResolution.reason,
2048
2085
  trusted_run: trustedRun,
2049
- selected_target: selectedTarget ? {
2050
- title: selectedTarget.title,
2051
- source: selectedTarget.source,
2052
- task_id: selectedTarget.task_id || null,
2053
- ref: selectedTarget.ref || null,
2054
- why: selectedTarget.why || '',
2086
+ selected_target: acceptedTarget ? {
2087
+ title: acceptedTarget.title,
2088
+ source: acceptedTarget.source,
2089
+ task_id: acceptedTarget.task_id || null,
2090
+ ref: acceptedTarget.ref || null,
2091
+ why: acceptedTarget.why || '',
2055
2092
  } : null,
2093
+ ...(shaping.shapingRejected ? {
2094
+ shaping_rejected: true,
2095
+ shaping_rejected_reason: shaping.shapingRejectedReason,
2096
+ } : {}),
2056
2097
  task_spine_required: taskSpineRequired,
2057
- next_action: selectedTarget
2098
+ next_action: acceptedTarget
2058
2099
  ? 'run one proof tick for the selected existing task'
2059
2100
  : (taskSpineRequired ? 'attach task spine, then run one proof tick' : 'run one proof tick'),
2060
2101
  };
@@ -4749,6 +4790,68 @@ function missionTimelineTickIndex(tick) {
4749
4790
  return Number.isInteger(tickIndex) && tickIndex > 0 ? tickIndex : null;
4750
4791
  }
4751
4792
 
4793
+ // A tick's verification state, made first-class so an unchecked increment reads
4794
+ // as a red in `atris mission report` and now.md instead of hiding inside a
4795
+ // receipt. A ran tick with no verifier result is the debt this surfaces: the
4796
+ // tick was recorded but nothing was checked, so the increment is unproven.
4797
+ function missionTickVerification(tick) {
4798
+ const ran = !!tick && typeof tick === 'object' && tick.status === 'ran';
4799
+ if (tick && tick.verifier_passed === true) {
4800
+ return { verified: true, state: 'verified', unchecked: false };
4801
+ }
4802
+ if (tick && tick.verifier_passed === false) {
4803
+ return { verified: false, state: 'failed', unchecked: false };
4804
+ }
4805
+ return { verified: false, state: ran ? 'unchecked' : 'skipped', unchecked: ran };
4806
+ }
4807
+
4808
+ // Roll every ran tick for a mission into a verification-debt tally. `unchecked`
4809
+ // counts ran ticks that recorded no verifier result at all — the "tick recorded
4810
+ // but nothing was checked" red the report and now.md rollup surface and count
4811
+ // against the mission.
4812
+ function missionVerificationDebt(mission, root = process.cwd()) {
4813
+ const paths = statePaths(root);
4814
+ let files = [];
4815
+ try {
4816
+ files = fs.readdirSync(paths.runsDir)
4817
+ .filter((file) => file.startsWith('mission-') && file.endsWith('.json'))
4818
+ .map((file) => path.join(paths.runsDir, file));
4819
+ } catch {
4820
+ files = [];
4821
+ }
4822
+ let ran = 0;
4823
+ let verified = 0;
4824
+ let unchecked = 0;
4825
+ const seen = new Set();
4826
+ for (const file of files) {
4827
+ let receipt = null;
4828
+ try {
4829
+ receipt = JSON.parse(fs.readFileSync(file, 'utf8'));
4830
+ } catch {
4831
+ continue;
4832
+ }
4833
+ if (!receipt || receipt.mission_id !== mission.id) continue;
4834
+ for (const tick of missionReceiptTicks(receipt)) {
4835
+ if (!tick || tick.status !== 'ran') continue;
4836
+ const key = `${tick.tick_index || ''}:${tick.finished_at || tick.started_at || ''}`;
4837
+ if (seen.has(key)) continue;
4838
+ seen.add(key);
4839
+ ran += 1;
4840
+ const verification = missionTickVerification(tick);
4841
+ if (verification.verified) verified += 1;
4842
+ if (verification.unchecked) unchecked += 1;
4843
+ }
4844
+ }
4845
+ return { ran, verified, unchecked };
4846
+ }
4847
+
4848
+ // One glanceable red line for a mission carrying unverified-tick debt. Empty
4849
+ // when every ran tick was checked, so clean missions stay quiet.
4850
+ function missionVerificationDebtLine(debt) {
4851
+ if (!debt || !debt.unchecked) return '';
4852
+ return `unverified ticks: ${debt.unchecked} of ${debt.ran} ran no check ⚠ — treat those increments as unproven`;
4853
+ }
4854
+
4752
4855
  function missionReportTimeline(mission, root = process.cwd(), limit = 6) {
4753
4856
  const paths = statePaths(root);
4754
4857
  let files = [];
@@ -4778,11 +4881,14 @@ function missionReportTimeline(mission, root = process.cwd(), limit = 6) {
4778
4881
  const key = `${tick.tick_index || ''}:${at}:${summary}`;
4779
4882
  if (seen.has(key)) continue;
4780
4883
  seen.add(key);
4884
+ const verification = missionTickVerification(tick);
4781
4885
  items.push({
4782
4886
  at,
4783
4887
  tick_index: missionTimelineTickIndex(tick),
4784
4888
  title: missionTimelineTitle(tick, summary),
4785
4889
  summary,
4890
+ verified: verification.verified,
4891
+ verify_state: verification.state,
4786
4892
  receipt_path: receiptPath,
4787
4893
  });
4788
4894
  }
@@ -5040,6 +5146,7 @@ function missionReportFor(mission, root = process.cwd()) {
5040
5146
  const receipt = readMissionReceipt(verifierReceiptPath, root);
5041
5147
  const explicitWorkerReceipt = readMissionReceipt(mission.worker_receipt_path, root);
5042
5148
  const timeline = missionReportTimeline(mission, root);
5149
+ const verificationDebt = missionVerificationDebt(mission, root);
5043
5150
  const workerCheckedIn = Boolean(explicitWorkerReceipt) || timeline.length > 0;
5044
5151
  const workerReceiptPath = mission.worker_receipt_path || (receipt && verifierReceiptPath) || null;
5045
5152
  const verifierPassed = mission.verifier_result && mission.verifier_result.passed === true;
@@ -5060,6 +5167,7 @@ function missionReportFor(mission, root = process.cwd()) {
5060
5167
  : missionHumanStatusText(mission),
5061
5168
  budget_continuation: budgetContinuation,
5062
5169
  operator_outcome: operatorOutcome,
5170
+ verification: verificationDebt,
5063
5171
  worker: mission.worker || missionWorkerLabel(mission),
5064
5172
  worker_summary: missionWorkerSummary(mission, receipt),
5065
5173
  timeline,
@@ -5114,6 +5222,9 @@ function reportMission(args) {
5114
5222
  ` What happened: ${report.operator_outcome}`,
5115
5223
  ` Worker: ${report.worker}`,
5116
5224
  ` Worker summary: ${report.worker_summary}`,
5225
+ ...(report.verification && report.verification.unchecked ? [
5226
+ ` ⚠ Unverified: ${report.verification.unchecked} of ${report.verification.ran} tick(s) recorded but ran no check; treat those increments as unproven.`,
5227
+ ] : []),
5117
5228
  ...(report.timeline && report.timeline.length ? [
5118
5229
  ' Timeline:',
5119
5230
  ...report.timeline.map((item) => ` - ${report.budget_continuation ? item.summary : item.title}`),
@@ -10587,6 +10698,11 @@ module.exports = {
10587
10698
  missionLandingLines,
10588
10699
  missionVerifierCheckedText,
10589
10700
  missionVerifierHighLevelTestText,
10701
+ missionTickVerification,
10702
+ missionVerificationDebt,
10703
+ missionVerificationDebtLine,
10704
+ missionReportFor,
10705
+ renderMemberNowMarkdown,
10590
10706
  buildEngineVerifyPrompt,
10591
10707
  engineVerifierResultFromRun,
10592
10708
  missionFullBudgetRemainingSeconds,
package/commands/task.js CHANGED
@@ -5533,7 +5533,11 @@ function cmdReviews(args) {
5533
5533
  }
5534
5534
  approvalItems.forEach((item, index) => {
5535
5535
  if (index > 0) console.log('');
5536
- console.log(`${index + 1}. ${gateForHuman(item.title, { title: item.title }).text}`);
5536
+ const ref = item.display_id || taskRef(item.id);
5537
+ const badge = item.evidence?.any_forced
5538
+ ? ' [evidence:forced]'
5539
+ : item.evidence?.all_passing ? ' [evidence:passing]' : '';
5540
+ console.log(`${index + 1}. ${gateForHuman(item.title, { title: item.title }).text} (${ref})${badge}`);
5537
5541
  if (item.landing) {
5538
5542
  taskReviewLandingLines(item).forEach(line => console.log(line));
5539
5543
  if (verbose && item.result?.saved) console.log(` saved: ${item.result.saved}`);
@@ -7,17 +7,35 @@ const GREETING_MAX_CHARS = 40;
7
7
  const DECISION_FACTOR_MIN_COMMAS = 3;
8
8
  const TRADEOFF_DEPTH_MIN_CHARS = 100;
9
9
 
10
- const LOOKUP_WORD_RE = /\b(?:what|who|when|where|list|show|define|convert)\b/i;
10
+ // Anchored to the start: a lookup word mid-sentence is usually a subordinate
11
+ // clause (my fan spins when i run tests), not a lookup.
12
+ const LOOKUP_START_RE = /^(?:(?:please|pls|hey|hi|yo)[,: ]+)?(?:whats|whos|wheres|what|who|when|where|list|show|define)\b/i;
11
13
  // Words that turn a lookup-shaped message into a judgment call: never fast.
12
- const JUDGMENT_WORD_RE = /\b(?:should|tradeoffs?|better|best|recommend|worth)\b/i;
13
- const HEAVY_REASONING_RE = /\b(?:design(?:s|ed|ing)?|architect(?:s|ed|ing)?|prov(?:e|es|ed|ing)|analy(?:ze|zes|zed|zing|se|ses|sed|sing)|compar(?:e|es|ed|ing)|plan(?:s|ned|ning)?|sketch(?:es|ed|ing)?)\b/i;
14
+ const JUDGMENT_WORD_RE = /\b(?:should|tradeoffs?|better|best|recommend|worth|good way)\b/i;
15
+ const HEAVY_REASONING_RE = /\b(?:design(?:s|ed|ing)?|architect(?:s|ed|ing)?|prov(?:e|es|ed|ing)|analy(?:ze|zes|zed|zing|se|ses|sed|sing)|compar(?:e|es|ed|ing)|plan(?:s|ned|ning)?|sketch(?:es|ed|ing)?)\b|\bwalk(?:s|ed|ing)? (?:me |us )?through\b/i;
14
16
  const DECISION_PHRASE_RE = /\b(?:should|whether|decide|deciding)\b/i;
15
17
  const ARGUE_BOTH_RE = /\bargue\b|\bboth sides\b|\bsteelman\b/i;
16
18
  const CODE_EDIT_RE = /\b(?:fix(?:es|ed|ing)?|refactor(?:s|ed|ing)?|renam(?:e|es|ed|ing)|debug(?:s|ged|ging)?)\b/i;
17
19
  const CODE_DIAGNOSIS_RE = /\bwhy\b|\bwrong\b|\bfail(?:s|ed|ing)?\b|\bresolv(?:e|es|ed|ing)?\b|\bbroken\b|\bnot work/i;
18
- const CODE_CONTEXT_RE = /```|(?:^|\n)\s*(?:traceback\b|(?:[a-z]+)?(?:error|exception):|at\s+\S+\s+\()/im;
20
+ const CODE_CONTEXT_RE = /```|\bregexp?\b|(?:^|\n)\s*(?:traceback\b|(?:[a-z]+)?(?:error|exception):|at\s+\S+\s+\()/im;
19
21
  const ERROR_EXPLAIN_RE = /\bexplain (?:this|the) error\b|\bwhat does (?:this|the) error mean\b/i;
22
+ // Yes/no capability checks are lookups even without a lookup keyword.
23
+ const YESNO_LOOKUP_RE = /^(?:does|is|are|can|do)\b/i;
24
+ // Tiny how-do-i asks are muscle-memory lookups; longer ones are advice.
25
+ const HOWTO_LOOKUP_RE = /^how (?:do|does|did) (?:i|we|you)\b/i;
26
+ const HOWTO_LOOKUP_MAX_CHARS = 40;
27
+ const YESNO_LOOKUP_MAX_CHARS = 80;
28
+ // Troubleshooting language means advice, not recall: keep it out of fast.
29
+ const TROUBLE_RE = /\bfail(?:s|ed|ing)?\b|\bcrash(?:es|ed|ing)?\b|\bbroken\b|\bnot working\b/i;
30
+ const WEIGHING_WORD_RE = /\b(?:consider|weigh|rank|estimate|think (?:about|through))\b/i;
31
+ const WEIGHING_FACTOR_MIN_COMMAS = 2;
20
32
  const TRANSFORM_START_RE = /^(?:convert|summarize|translate)\b/i;
33
+ // Reshaping pasted content is editing work, not analysis; question marks
34
+ // inside the paste do not make it a multi-question ask.
35
+ const TRANSFORM_INTENT_RE = /^(?:turn|clean up|rewrite|reword|compress|tighten|polish)\b/i;
36
+ // Possessives point at workspace context the model must gather first.
37
+ const OWN_CONTEXT_RE = /\b(?:our|my)\b/i;
38
+ const SPANISH_LOOKUP_RE = /^(?:que|qu\u00e9|cual|cu\u00e1l|quien|qui\u00e9n|cuando|cu\u00e1ndo|donde|d\u00f3nde|como|c\u00f3mo)\b/i;
21
39
  const GREETING_RE = /^(?:hey|hi|hello|yo|thanks|thank you|ok|okay|cool|nice|got it|great|perfect|yep|no worries)\b/i;
22
40
 
23
41
  function threshold(value, fallback) {
@@ -35,11 +53,15 @@ function pickLane(message, opts = {}) {
35
53
  if (text.length > longInputMinChars) {
36
54
  return { lane: 'max', reason: 'max fits this long input.' };
37
55
  }
56
+ if (TRANSFORM_INTENT_RE.test(text) && !HEAVY_REASONING_RE.test(text)) {
57
+ return { lane: 'pro', reason: 'pro fits reshaping the content you pasted.' };
58
+ }
38
59
  if (questionCount > 1) {
39
60
  return { lane: 'max', reason: 'max fits a request with multiple questions.' };
40
61
  }
41
62
  if (ARGUE_BOTH_RE.test(text)
42
- || (DECISION_PHRASE_RE.test(text) && commaCount >= DECISION_FACTOR_MIN_COMMAS)) {
63
+ || (DECISION_PHRASE_RE.test(text) && commaCount >= DECISION_FACTOR_MIN_COMMAS)
64
+ || (WEIGHING_WORD_RE.test(text) && commaCount >= WEIGHING_FACTOR_MIN_COMMAS)) {
43
65
  return { lane: 'max', reason: 'max fits a decision weighing several factors.' };
44
66
  }
45
67
  if (HEAVY_REASONING_RE.test(text)) {
@@ -54,7 +76,12 @@ function pickLane(message, opts = {}) {
54
76
  if (ERROR_EXPLAIN_RE.test(text) && !hasCodeContext) {
55
77
  return { lane: 'fast', reason: 'fast fits a plain error explanation.' };
56
78
  }
57
- if (TRANSFORM_START_RE.test(text) && text.length <= shortLookupMaxChars) {
79
+ if (SPANISH_LOOKUP_RE.test(text) && text.length <= shortLookupMaxChars
80
+ && !JUDGMENT_WORD_RE.test(text) && !TROUBLE_RE.test(text)) {
81
+ return { lane: 'fast', reason: 'fast fits this short lookup.' };
82
+ }
83
+ if (TRANSFORM_START_RE.test(text) && text.length <= shortLookupMaxChars
84
+ && !OWN_CONTEXT_RE.test(text)) {
58
85
  return { lane: 'fast', reason: 'fast fits this small transform.' };
59
86
  }
60
87
  if (GREETING_RE.test(text) && text.length <= GREETING_MAX_CHARS) {
@@ -63,9 +90,22 @@ function pickLane(message, opts = {}) {
63
90
  if (text.length > 0 && text.length <= TINY_MESSAGE_MAX_CHARS && !CODE_EDIT_RE.test(text)) {
64
91
  return { lane: 'fast', reason: 'fast fits this tiny message.' };
65
92
  }
93
+ if (text.length <= HOWTO_LOOKUP_MAX_CHARS
94
+ && HOWTO_LOOKUP_RE.test(text)
95
+ && !JUDGMENT_WORD_RE.test(text)
96
+ && !TROUBLE_RE.test(text)) {
97
+ return { lane: 'fast', reason: 'fast fits this quick how-to.' };
98
+ }
99
+ if (text.length <= YESNO_LOOKUP_MAX_CHARS
100
+ && YESNO_LOOKUP_RE.test(text)
101
+ && !JUDGMENT_WORD_RE.test(text)
102
+ && !TROUBLE_RE.test(text)) {
103
+ return { lane: 'fast', reason: 'fast fits this yes or no lookup.' };
104
+ }
66
105
  if (text.length <= shortLookupMaxChars
67
- && LOOKUP_WORD_RE.test(text)
68
- && !JUDGMENT_WORD_RE.test(text)) {
106
+ && LOOKUP_START_RE.test(text)
107
+ && !JUDGMENT_WORD_RE.test(text)
108
+ && !TROUBLE_RE.test(text)) {
69
109
  return { lane: 'fast', reason: 'fast fits this short factual lookup.' };
70
110
  }
71
111
  return { lane: 'pro', reason: 'pro fits this general request.' };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "3.36.0",
3
+ "version": "3.37.1",
4
4
  "description": "you say what you want in plain words. atris builds it, checks it, and shows you proof.",
5
5
  "main": "bin/atris.js",
6
6
  "bin": {