atris 3.38.0 → 3.40.0

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 (77) hide show
  1. package/AGENTS.md +25 -6
  2. package/atris/PERSONA.md +8 -4
  3. package/atris.md +7 -0
  4. package/ax +2 -1
  5. package/bin/atris.js +30 -5
  6. package/commands/agent-spawn.js +13 -11
  7. package/commands/autoland.js +28 -77
  8. package/commands/bench.js +10 -12
  9. package/commands/business.js +345 -0
  10. package/commands/chat-scan.js +5 -7
  11. package/commands/codex-goal.js +8 -10
  12. package/commands/console.js +19 -3
  13. package/commands/decide.js +166 -0
  14. package/commands/deck.js +1 -4
  15. package/commands/drill.js +14 -24
  16. package/commands/engine.js +196 -8
  17. package/commands/gm.js +8 -6
  18. package/commands/harvest.js +1 -4
  19. package/commands/init.js +23 -3
  20. package/commands/land.js +8 -14
  21. package/commands/launchpad.js +1 -14
  22. package/commands/lifecycle.js +5 -5
  23. package/commands/log.js +55 -5
  24. package/commands/member.js +558 -574
  25. package/commands/mission.js +456 -237
  26. package/commands/pack.js +2746 -164
  27. package/commands/play.js +6 -4
  28. package/commands/probe.js +2 -2
  29. package/commands/pulse.js +15 -16
  30. package/commands/release.js +10 -9
  31. package/commands/router.js +5 -4
  32. package/commands/site-deploy.js +870 -0
  33. package/commands/site.js +11 -2
  34. package/commands/slop.js +14 -2
  35. package/commands/stream.js +4 -18
  36. package/commands/task.js +880 -558
  37. package/commands/taste.js +101 -0
  38. package/commands/team.js +83 -3
  39. package/commands/vercel.js +4 -2
  40. package/commands/voice.js +195 -0
  41. package/commands/watch.js +1 -22
  42. package/commands/wiki.js +1 -4
  43. package/commands/workflow.js +2 -2
  44. package/commands/worktree.js +1 -14
  45. package/commands/xp.js +27 -24
  46. package/lib/accept-verify-gate.js +5 -1
  47. package/lib/arg-parser.js +41 -0
  48. package/lib/auto-accept-certified.js +116 -1
  49. package/lib/autoland.js +66 -0
  50. package/lib/bench/runner.js +19 -1
  51. package/lib/context-gatherer.js +7 -1
  52. package/lib/engine-registry.js +141 -20
  53. package/lib/falsifier-probe.js +84 -0
  54. package/lib/fleet.js +65 -15
  55. package/lib/git-spawn.js +15 -0
  56. package/lib/json-file.js +37 -0
  57. package/lib/known-commands.js +2 -2
  58. package/lib/lesson-preflight.js +146 -0
  59. package/lib/loop-doctor.js +0 -2
  60. package/lib/mission-human-asks.js +28 -0
  61. package/lib/mission-protected-lane.js +4 -1
  62. package/lib/official-cli-integration.js +47 -2
  63. package/lib/orb-context.js +8 -1
  64. package/lib/pack-capabilities.js +685 -0
  65. package/lib/router-brain.js +51 -1
  66. package/lib/runner-command.js +0 -6
  67. package/lib/self-drive.js +44 -13
  68. package/lib/task-db.js +137 -3
  69. package/lib/task-decision.js +50 -0
  70. package/lib/taste-lessons.js +153 -0
  71. package/lib/tool-result-encode.js +17 -1
  72. package/lib/voice-gate.js +66 -0
  73. package/lib/wish-audit.js +1 -1
  74. package/lib/wish-delegate.js +1 -1
  75. package/lib/zip.js +95 -7
  76. package/package.json +2 -1
  77. package/templates/business-starter/persona.md +9 -0
package/lib/voice-gate.js CHANGED
@@ -215,10 +215,76 @@ function isRetiredFillerReason(text) {
215
215
  return RETIRED_FILLER_REASON.test(String(text || '').replace(/\s+/g, ' ').trim());
216
216
  }
217
217
 
218
+ // One table turns every landing/refusal reason code into a plain sentence a
219
+ // fried reader can act on: what happened, and what unblocks it. JSON output
220
+ // keeps the raw codes; only human text goes through here. Fragments carry no
221
+ // trailing period because callers stitch them into their own sentences.
222
+ const LANDING_REASON_SENTENCES = {
223
+ accept_all_but_protected: 'the accept-everything policy covers it, so it lands unless a protected lane applies',
224
+ already_auto_accepted: 'it already landed on its own earlier, nothing left to do',
225
+ autoland_policy_missing_owner: 'the self-landing policy has no owner recorded, turn it on again to set one',
226
+ autoland_policy_off: 'self-landing is off, so everything waits for you',
227
+ certified_independent_review: 'an independent reviewer certified it, so it can land',
228
+ certified_strict_verify: 'it is certified and its recorded check passed, so it can land',
229
+ dead_exports: 'the change leaves behind code nothing calls, delete what the hygiene check names and re-certify',
230
+ declared_protected_lane: 'it touches a protected lane, so it waits for your decision',
231
+ forced_completion_needs_human: 'it was pushed to done without proof, so only a human can accept it',
232
+ insufficient_review_passes: 'it has not been reviewed enough times yet, one more pass unblocks it',
233
+ judge_equals_worker: 'built and judged by the same actor, hand the review to someone else',
234
+ mission_xp_requires_end_to_end_receipt: 'the proof does not show the whole flow working start to finish, attach that receipt or move it back to do',
235
+ needs_independent_reviewer: 'built and judged by the same actor, a review from someone else unblocks it',
236
+ needs_second_actor_review: 'the same actor built and reviewed it, someone else has to look before it lands',
237
+ needs_second_reviewer_or_third_pass: 'it needs one more independent check first',
238
+ no_verify_command: 'no check command was given, add one so the work can prove itself',
239
+ not_agent_certified: 'no reviewer has certified it yet, a second review unblocks it',
240
+ not_in_review: 'it is not up for review yet, finish the work and move it to review first',
241
+ probation_needs_review: 'the builder is still earning trust, so a human look is required before it lands',
242
+ proof_not_executed: 'the proof was written but never actually run, execute the check and record what it showed',
243
+ proof_required: 'no proof was given, say what was run and what it showed',
244
+ proof_unmerged_or_draft_pr_boundary: 'its proof points at an unmerged draft, merge it or prove the work another way',
245
+ receipt_verifier_failed: 'a saved receipt failed its re-check, fix what it names and re-certify',
246
+ strict_verify_missing: 'no recorded check command to re-run, add one and re-certify',
247
+ untagged_protected_lane_text: 'the description reads like protected-lane work without the tag, tag it or decide it yourself',
248
+ verification_pending: 'the recorded check has not been re-run yet, the next hourly pass runs it',
249
+ verifier_is_builder: 'the re-check actor built this row, another actor must re-check',
250
+ verify_command_not_allowed: 'its recorded check is not on the list this machine may run alone, run it yourself to unblock',
251
+ verify_failed: 'its check command failed on re-run, fix the failure and re-certify',
252
+ verify_passed: 'its check command passed on re-run',
253
+ verify_unrunnable: 'its check command could not even start, fix the command and re-certify',
254
+ verify_workdir_missing: 'the folder its check was recorded in is gone, re-run the check from a real folder',
255
+ verify_worktree_missing: 'the folder its check needs is gone, rebuild it or run the check by hand',
256
+ weak_proof: 'the proof is too thin to trust, cite a command, receipt, or approval',
257
+ };
258
+
259
+ const DENIED_TAG_SENTENCES = {
260
+ billing: 'it touches money, so it is your decision',
261
+ deploy: 'it is a deploy, so it is your decision',
262
+ security: 'it touches security, so it is your decision',
263
+ customer: 'it is customer-facing, so it is your decision',
264
+ external: 'it is outward-facing, so it is your decision',
265
+ feedback: 'it answers customer feedback, so it is your decision',
266
+ };
267
+
268
+ function plainLandingReason(reason) {
269
+ const code = String(reason || '').trim();
270
+ if (!code) return 'no reason was recorded, re-run the review to get one';
271
+ if (LANDING_REASON_SENTENCES[code]) return LANDING_REASON_SENTENCES[code];
272
+ const denied = code.match(/^denied_tag_(.+)$/);
273
+ if (denied) {
274
+ return DENIED_TAG_SENTENCES[denied[1]]
275
+ || `it is tagged ${denied[1].replace(/_/g, ' ')}, a protected lane, so it is your decision`;
276
+ }
277
+ const approval = code.match(/^approval_(.+)$/);
278
+ if (approval) return `a human already marked it ${approval[1].replace(/_/g, ' ')}, so it will not land on its own`;
279
+ // Unknown codes still never leak snake_case to a human.
280
+ return code.replace(/_/g, ' ');
281
+ }
282
+
218
283
  module.exports = {
219
284
  gateForHuman,
220
285
  isRetiredFillerReason,
221
286
  landingWhyClause,
222
287
  numberWord,
288
+ plainLandingReason,
223
289
  scanText,
224
290
  };
package/lib/wish-audit.js CHANGED
@@ -167,7 +167,7 @@ const BUDGET_LABELS = {
167
167
  const WISH_SWEEP_LIMIT = 3;
168
168
  const WISH_MISSION_RUNNER = 'claude';
169
169
  const TEST_VERIFY_COMMAND = 'node --test';
170
- const JOURNAL_RESULT_TEXT = 'a written result will be waiting in your journal';
170
+ const JOURNAL_RESULT_TEXT = 'the wish is recorded and the result comes back for review';
171
171
  const WAITING_INPUT_STATUSES = new Set(['needs_input', 'waiting_input']);
172
172
  const PATH_SLOT_QUESTION = 'Where should I find the file or folder you named?';
173
173
  const AUDIENCE_WORDS = /\b(?:for|user|users|customer|customers|operator|operators|agent|agents|developer|developers|admin|admins|member|members|team|teams|visitor|visitors|client|clients|student|students|founder|founders|newcomer|newcomers)\b/i;
@@ -726,7 +726,7 @@ function reviewWish(positionals, root = process.cwd(), options = {}) {
726
726
  review_score: score.value === null ? reviewWordScore(reviewText) : score.value,
727
727
  reviewed_by: actorName(),
728
728
  });
729
- console.log(`Review saved for ${quoteText(target.wish.text || target.wish.id)}. It will shape the next run.`);
729
+ console.log(`Review saved for ${quoteText(target.wish.text || target.wish.id)}. It shapes future runs once distilled into lessons.`);
730
730
  return 0;
731
731
  }
732
732
 
package/lib/zip.js CHANGED
@@ -11,6 +11,13 @@ const UTF8_FLAG = 0x0800;
11
11
  const DEFLATE = 8;
12
12
  const STORE = 0;
13
13
 
14
+ const ZIP_LIMITS = Object.freeze({
15
+ maxZipBytes: 5 * 1024 * 1024,
16
+ maxUnpackedBytes: 20 * 1024 * 1024,
17
+ maxEntryBytes: 20 * 1024 * 1024,
18
+ maxEntries: 500,
19
+ });
20
+
14
21
  let crcTable = null;
15
22
 
16
23
  function getCrcTable() {
@@ -155,24 +162,56 @@ function assertReadable(buffer, offset, length, label) {
155
162
  }
156
163
  }
157
164
 
158
- function readZipBuffer(input) {
165
+ function readLimits(options = {}) {
166
+ return {
167
+ maxZipBytes: options.maxZipBytes ?? ZIP_LIMITS.maxZipBytes,
168
+ maxUnpackedBytes: options.maxUnpackedBytes ?? ZIP_LIMITS.maxUnpackedBytes,
169
+ maxEntryBytes: options.maxEntryBytes ?? ZIP_LIMITS.maxEntryBytes,
170
+ maxEntries: options.maxEntries ?? ZIP_LIMITS.maxEntries,
171
+ };
172
+ }
173
+
174
+ function assertWithinLimit(value, limit, message) {
175
+ if (!Number.isFinite(limit) || limit < 0) {
176
+ throw new Error('invalid zip read limit');
177
+ }
178
+ if (value > limit) throw new Error(message);
179
+ }
180
+
181
+ function readZipBuffer(input, options = {}) {
159
182
  const buffer = Buffer.isBuffer(input) ? input : Buffer.from(input);
183
+ const limits = readLimits(options);
184
+ assertWithinLimit(
185
+ buffer.length,
186
+ limits.maxZipBytes,
187
+ `refusing zip: archive size ${buffer.length} bytes exceeds ${limits.maxZipBytes} byte limit`,
188
+ );
160
189
  const endOffset = findEndOfCentralDirectory(buffer);
161
190
  const entryCount = buffer.readUInt16LE(endOffset + 10);
162
191
  const centralSize = buffer.readUInt32LE(endOffset + 12);
163
192
  const centralOffset = buffer.readUInt32LE(endOffset + 16);
193
+ assertWithinLimit(
194
+ entryCount,
195
+ limits.maxEntries,
196
+ `refusing zip: ${entryCount} entries exceeds ${limits.maxEntries} entry limit`,
197
+ );
164
198
  assertReadable(buffer, centralOffset, centralSize, 'central directory');
165
199
 
166
- const entries = [];
200
+ const metadata = [];
201
+ const centralEnd = centralOffset + centralSize;
202
+ let declaredUnpackedBytes = 0;
167
203
  let cursor = centralOffset;
168
204
  for (let i = 0; i < entryCount; i += 1) {
169
- assertReadable(buffer, cursor, 46, 'central directory entry');
205
+ if (cursor + 46 > centralEnd) {
206
+ throw new Error('invalid zip: truncated central directory entry');
207
+ }
170
208
  if (buffer.readUInt32LE(cursor) !== CENTRAL_DIRECTORY_HEADER) {
171
209
  throw new Error('invalid zip: malformed central directory');
172
210
  }
173
211
 
174
212
  const flags = buffer.readUInt16LE(cursor + 8);
175
213
  const method = buffer.readUInt16LE(cursor + 10);
214
+ const expectedCrc = buffer.readUInt32LE(cursor + 16);
176
215
  const compressedSize = buffer.readUInt32LE(cursor + 20);
177
216
  const uncompressedSize = buffer.readUInt32LE(cursor + 24);
178
217
  const nameLength = buffer.readUInt16LE(cursor + 28);
@@ -180,7 +219,9 @@ function readZipBuffer(input) {
180
219
  const commentLength = buffer.readUInt16LE(cursor + 32);
181
220
  const localOffset = buffer.readUInt32LE(cursor + 42);
182
221
  const nameStart = cursor + 46;
183
- assertReadable(buffer, nameStart, nameLength + extraLength + commentLength, 'central directory name');
222
+ if (nameStart + nameLength + extraLength + commentLength > centralEnd) {
223
+ throw new Error('invalid zip: truncated central directory name');
224
+ }
184
225
  const name = buffer.slice(nameStart, nameStart + nameLength).toString('utf8');
185
226
  cursor = nameStart + nameLength + extraLength + commentLength;
186
227
 
@@ -189,6 +230,39 @@ function readZipBuffer(input) {
189
230
  throw new Error(`invalid zip: unsupported compression method ${method} for ${name}`);
190
231
  }
191
232
 
233
+ assertWithinLimit(
234
+ uncompressedSize,
235
+ limits.maxEntryBytes,
236
+ `refusing zip: entry ${name} declares ${uncompressedSize} unpacked bytes, exceeding ${limits.maxEntryBytes} byte entry limit`,
237
+ );
238
+ declaredUnpackedBytes += uncompressedSize;
239
+ assertWithinLimit(
240
+ declaredUnpackedBytes,
241
+ limits.maxUnpackedBytes,
242
+ `refusing zip: declared unpacked size ${declaredUnpackedBytes} bytes exceeds ${limits.maxUnpackedBytes} byte limit`,
243
+ );
244
+
245
+ metadata.push({
246
+ name,
247
+ flags,
248
+ method,
249
+ expectedCrc,
250
+ compressedSize,
251
+ uncompressedSize,
252
+ localOffset,
253
+ });
254
+ }
255
+
256
+ const entries = [];
257
+ for (const entry of metadata) {
258
+ const {
259
+ name,
260
+ method,
261
+ expectedCrc,
262
+ compressedSize,
263
+ uncompressedSize,
264
+ localOffset,
265
+ } = entry;
192
266
  assertReadable(buffer, localOffset, 30, 'local file header');
193
267
  if (buffer.readUInt32LE(localOffset) !== LOCAL_FILE_HEADER) {
194
268
  throw new Error(`invalid zip: missing local header for ${name}`);
@@ -198,18 +272,31 @@ function readZipBuffer(input) {
198
272
  const dataStart = localOffset + 30 + localNameLength + localExtraLength;
199
273
  assertReadable(buffer, dataStart, compressedSize, `file data for ${name}`);
200
274
  const compressed = buffer.slice(dataStart, dataStart + compressedSize);
201
- const data = method === STORE ? Buffer.from(compressed) : zlib.inflateRawSync(compressed);
275
+ let data;
276
+ try {
277
+ data = method === STORE
278
+ ? Buffer.from(compressed)
279
+ : zlib.inflateRawSync(compressed, { maxOutputLength: Math.max(1, uncompressedSize) });
280
+ } catch (error) {
281
+ if (error && error.code === 'ERR_BUFFER_TOO_LARGE') {
282
+ throw new Error(`invalid zip: inflated data exceeds declared size for ${name}`);
283
+ }
284
+ throw new Error(`invalid zip: could not inflate ${name}: ${error.message}`);
285
+ }
202
286
  if (data.length !== uncompressedSize) {
203
287
  throw new Error(`invalid zip: size mismatch for ${name}`);
204
288
  }
289
+ if (crc32(data) !== expectedCrc) {
290
+ throw new Error(`invalid zip: checksum mismatch for ${name}`);
291
+ }
205
292
  entries.push({ name, data });
206
293
  }
207
294
 
208
295
  return entries;
209
296
  }
210
297
 
211
- function readZipFile(filePath) {
212
- return readZipBuffer(fs.readFileSync(filePath));
298
+ function readZipFile(filePath, options = {}) {
299
+ return readZipBuffer(fs.readFileSync(filePath), options);
213
300
  }
214
301
 
215
302
  module.exports = {
@@ -217,4 +304,5 @@ module.exports = {
217
304
  writeZipFile,
218
305
  readZipBuffer,
219
306
  readZipFile,
307
+ ZIP_LIMITS,
220
308
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "3.38.0",
3
+ "version": "3.40.0",
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": {
@@ -57,6 +57,7 @@
57
57
  "test": "node --test",
58
58
  "test:fast": "xargs node --test < test/fast-tests.txt",
59
59
  "lint:decks": "node scripts/lint-all-decks.js",
60
+ "audit:markdown-whitespace": "node scripts/audit-markdown-whitespace.js",
60
61
  "publish:release": "node scripts/publish-atris-release.js",
61
62
  "prepare": "cp scripts/pre-commit .git/hooks/pre-commit 2>/dev/null && cp scripts/commit-msg .git/hooks/commit-msg 2>/dev/null && cp scripts/prepare-commit-msg .git/hooks/prepare-commit-msg 2>/dev/null && chmod +x .git/hooks/pre-commit .git/hooks/commit-msg .git/hooks/prepare-commit-msg || true"
62
63
  },
@@ -18,6 +18,15 @@ Keep this file aligned with `atris/persona.md`.
18
18
  - Concrete examples over abstractions
19
19
  - Cite the source when answering domain questions
20
20
 
21
+ ## The Way Of Talking (every message a human reads)
22
+
23
+ - Plain words. Say what happened and what it means for the reader — cause and effect, not machinery.
24
+ - No task codes, commit hashes, or internal system names in the message body. If the reader needs a code to act, put ONE copyable command at the end.
25
+ - No insider terms without defining them in the same breath.
26
+ - One idea per line. Outcome first. Detail lives in files, not the message.
27
+ - Lists: spell the numbers as words (One. Two. Three.), each item its own paragraph with a blank line between. Never markdown numbered lists — they render cramped.
28
+ - The test: a sharp, tired non-engineer follows it on first read. If not, rewrite.
29
+
21
30
  ## Anti-patterns
22
31
 
23
32
  - No em dashes in outbound copy