atris 3.37.1 → 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 (164) hide show
  1. package/AGENTS.md +25 -6
  2. package/README.md +2 -0
  3. package/atris/PERSONA.md +11 -11
  4. package/atris.md +17 -2
  5. package/ax +160 -34
  6. package/bin/atris.js +78 -6
  7. package/commands/activate.js +16 -3
  8. package/commands/agent-spawn.js +13 -18
  9. package/commands/autoland.js +76 -47
  10. package/commands/autopilot-front.js +0 -4
  11. package/commands/autopilot.js +0 -12
  12. package/commands/bench.js +10 -12
  13. package/commands/brain.js +0 -7
  14. package/commands/brief.js +0 -2
  15. package/commands/business-sync.js +0 -1
  16. package/commands/business.js +452 -12
  17. package/commands/chat-scan.js +5 -7
  18. package/commands/clean.js +31 -2
  19. package/commands/close.js +46 -11
  20. package/commands/cloud.js +0 -1
  21. package/commands/codex-goal.js +8 -12
  22. package/commands/computer.js +6 -5
  23. package/commands/console.js +64 -11
  24. package/commands/decide.js +166 -0
  25. package/commands/deck.js +30 -12
  26. package/commands/dream.js +0 -7
  27. package/commands/drill.js +14 -25
  28. package/commands/engine.js +206 -26
  29. package/commands/experiments.js +0 -4
  30. package/commands/fleet-report.js +12 -4
  31. package/commands/game.js +261 -0
  32. package/commands/gm.js +17 -7
  33. package/commands/goal.js +0 -6
  34. package/commands/harvest.js +1 -5
  35. package/commands/improve.js +29 -8
  36. package/commands/init.js +41 -37
  37. package/commands/integrations.js +0 -7
  38. package/commands/land.js +77 -20
  39. package/commands/launchpad.js +1 -16
  40. package/commands/lesson.js +17 -7
  41. package/commands/lifecycle.js +5 -5
  42. package/commands/log.js +55 -5
  43. package/commands/loop-front.js +0 -2
  44. package/commands/loops.js +6 -5
  45. package/commands/meet.js +0 -2
  46. package/commands/member.js +622 -573
  47. package/commands/mission.js +790 -314
  48. package/commands/next.js +0 -1
  49. package/commands/now.js +0 -12
  50. package/commands/one-lap.js +8 -8
  51. package/commands/orb.js +0 -4
  52. package/commands/pack-craft.js +0 -1
  53. package/commands/pack.js +3599 -424
  54. package/commands/play.js +6 -6
  55. package/commands/probe.js +12 -6
  56. package/commands/pulse.js +88 -64
  57. package/commands/push.js +0 -4
  58. package/commands/radar.js +14 -11
  59. package/commands/recover.js +340 -0
  60. package/commands/release.js +10 -9
  61. package/commands/report.js +0 -1
  62. package/commands/router.js +320 -0
  63. package/commands/run-front.js +0 -2
  64. package/commands/search.js +0 -3
  65. package/commands/site-deploy.js +870 -0
  66. package/commands/site.js +11 -2
  67. package/commands/skill.js +7 -2
  68. package/commands/slop.js +237 -7
  69. package/commands/status.js +6 -0
  70. package/commands/stream.js +4 -20
  71. package/commands/study.js +0 -3
  72. package/commands/sync-checkout.js +51 -0
  73. package/commands/sync.js +11 -40
  74. package/commands/task.js +999 -595
  75. package/commands/taste.js +101 -0
  76. package/commands/teach.js +86 -0
  77. package/commands/team.js +83 -3
  78. package/commands/unknowns.js +0 -10
  79. package/commands/update.js +0 -1
  80. package/commands/vercel.js +4 -2
  81. package/commands/visualize.js +0 -5
  82. package/commands/voice.js +195 -0
  83. package/commands/watch.js +1 -28
  84. package/commands/wiki.js +42 -15
  85. package/commands/wish.js +0 -1
  86. package/commands/workflow.js +2 -2
  87. package/commands/worktree.js +88 -28
  88. package/commands/xp.js +28 -30
  89. package/commands/youtube.js +0 -2
  90. package/lib/accept-verify-gate.js +126 -0
  91. package/lib/activity-stream.js +0 -2
  92. package/lib/arg-parser.js +41 -0
  93. package/lib/auto-accept-certified.js +261 -3
  94. package/lib/autoland.js +66 -3
  95. package/lib/ax-auto-lane.js +68 -7
  96. package/lib/bench/context.js +0 -2
  97. package/lib/bench/report.js +0 -1
  98. package/lib/bench/runner.js +18 -11
  99. package/lib/brief-ledger.js +0 -6
  100. package/lib/business-simulate.js +382 -0
  101. package/lib/chat-log-scan.js +0 -5
  102. package/lib/checkout-sync.js +52 -0
  103. package/lib/claude-boot-block.js +48 -0
  104. package/lib/cloud-mission.js +0 -6
  105. package/lib/codex-flight.js +0 -1
  106. package/lib/conductor-artifacts.js +25 -0
  107. package/lib/context-gatherer.js +7 -2
  108. package/lib/deck-compose.js +0 -1
  109. package/lib/deck-history.js +0 -1
  110. package/lib/deck-layout.js +0 -5
  111. package/lib/deck-review.js +0 -7
  112. package/lib/deck-schema.js +0 -1
  113. package/lib/default-verifier.js +0 -2
  114. package/lib/engine-registry.js +157 -28
  115. package/lib/experiments/daily.js +0 -6
  116. package/lib/falsifier-probe.js +84 -0
  117. package/lib/fleet.js +275 -42
  118. package/lib/functional-owner.js +0 -3
  119. package/lib/git-spawn.js +15 -0
  120. package/lib/inspect-fields.js +0 -1
  121. package/lib/json-file.js +37 -0
  122. package/lib/known-commands.js +2 -2
  123. package/lib/learnings.js +0 -2
  124. package/lib/lesson-preflight.js +146 -0
  125. package/lib/loop-doctor.js +11 -19
  126. package/lib/manifest.js +0 -2
  127. package/lib/member-alive.js +0 -1
  128. package/lib/member-switches.js +0 -5
  129. package/lib/mission-artifact.js +0 -1
  130. package/lib/mission-human-asks.js +28 -0
  131. package/lib/mission-protected-lane.js +296 -0
  132. package/lib/mission-room.js +0 -2
  133. package/lib/mission-runtime-loop.js +0 -1
  134. package/lib/next-moves.js +0 -18
  135. package/lib/official-cli-integration.js +47 -6
  136. package/lib/orb-context.js +14 -4
  137. package/lib/orb-scorecard.js +0 -1
  138. package/lib/outbound-send-gate.js +0 -6
  139. package/lib/pack-capabilities.js +685 -0
  140. package/lib/permission-grants.js +0 -6
  141. package/lib/policy-lessons.js +0 -2
  142. package/lib/pulse.js +68 -15
  143. package/lib/review-integrity.js +0 -2
  144. package/lib/router-brain.js +51 -3
  145. package/lib/runner-command.js +0 -10
  146. package/lib/runtime-bootstrap.js +0 -1
  147. package/lib/scorecard.js +0 -1
  148. package/lib/self-drive.js +45 -14
  149. package/lib/state-detection.js +0 -6
  150. package/lib/task-db.js +137 -7
  151. package/lib/task-decision.js +50 -0
  152. package/lib/taste-lessons.js +153 -0
  153. package/lib/team-presence.js +0 -1
  154. package/lib/tool-result-encode.js +17 -1
  155. package/lib/voice-gate.js +129 -2
  156. package/lib/wiki.js +152 -9
  157. package/lib/wish-audit.js +21 -7
  158. package/lib/wish-delegate.js +27 -21
  159. package/lib/wish-stats.js +0 -5
  160. package/lib/wish-store.js +0 -1
  161. package/lib/workspace-safety.js +0 -2
  162. package/lib/zip.js +95 -8
  163. package/package.json +2 -1
  164. package/templates/business-starter/persona.md +9 -0
@@ -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
 
@@ -919,9 +919,13 @@ function startWishDelegation(wish, audit, root, options = {}) {
919
919
  const reused = missionPayload && missionPayload.action === 'mission_reused';
920
920
  // A delegated wish must have a live worker, not just a filed mission. Start
921
921
  // the detached driver for any runner that can work headlessly; runners that
922
- // need a live session keep an honest not-started marker instead.
922
+ // need a live session keep an honest not-started marker instead. A one-lap
923
+ // intake is the exception: the calling lap dispatches in-process through the
924
+ // sealed review sandbox, so a detached driver would race it with a second,
925
+ // ungated dispatch of the same ask.
923
926
  let driver = null;
924
- if (mission && !missionRunnerNeedsLiveSession(mission.runner) && process.env.ATRIS_WISH_NO_DRIVER !== '1') {
927
+ const lapOwnsDispatch = options.oneLap === true;
928
+ if (mission && !lapOwnsDispatch && !missionRunnerNeedsLiveSession(mission.runner) && process.env.ATRIS_WISH_NO_DRIVER !== '1') {
925
929
  driver = spawnMissionDriver(mission, root);
926
930
  }
927
931
  const workerStarted = Boolean(driver && driver.ok && (driver.pid || driver.already_running));
@@ -940,9 +944,11 @@ function startWishDelegation(wish, audit, root, options = {}) {
940
944
  worker_started: workerStarted,
941
945
  ...(driver && driver.pid ? { driver_pid: driver.pid } : {}),
942
946
  ...(mission && !workerStarted ? {
943
- worker_note: missionRunnerNeedsLiveSession(mission.runner)
944
- ? `waiting for a live ${mission.runner || 'session'} to pick this up`
945
- : `worker did not start; run: atris mission run ${mission.id}`,
947
+ worker_note: lapOwnsDispatch
948
+ ? 'the one-lap dispatches this in-process'
949
+ : missionRunnerNeedsLiveSession(mission.runner)
950
+ ? `waiting for a live ${mission.runner || 'session'} to pick this up`
951
+ : `worker did not start; run: atris mission run ${mission.id}`,
946
952
  } : {}),
947
953
  engine: reused ? (mission?.runner || engine) : engine,
948
954
  validator: audit.validator.id,
@@ -1422,7 +1428,12 @@ function dispatchWishHeadlessly(wish, root, reason) {
1422
1428
  questions: audit.questions,
1423
1429
  };
1424
1430
  }
1425
- const { record } = appendDelegatedWishRecord(wish, audit, root);
1431
+ // Preserve one-lap ownership through sweep/headless grant: without this,
1432
+ // a later atris wish sweep would spawn the detached mission driver and race
1433
+ // the lap's sealed in-process dispatch (CLI-1170).
1434
+ const { record } = appendDelegatedWishRecord(wish, audit, root, {
1435
+ oneLap: wish.one_lap === true,
1436
+ });
1426
1437
  return {
1427
1438
  id: wish.id,
1428
1439
  status: 'delegated',
@@ -1531,6 +1542,9 @@ function runCapturedWish(text, root = process.cwd(), options = {}) {
1531
1542
  status: 'captured',
1532
1543
  };
1533
1544
  if (options.parentId) wish.parent_id = String(options.parentId);
1545
+ // One-lap provenance lives on the record so a later `wish answer` also
1546
+ // skips the detached driver: the lap owns dispatch in-process (80de4ae1).
1547
+ if (options.singleTask === true) wish.one_lap = true;
1534
1548
  if (engineOverride) wish.requested_engine = engineOverride;
1535
1549
  if (options.metric && options.metric.ok) wish.metric = options.metric.expression;
1536
1550
  captureWishToJournal(text, root);
@@ -1539,7 +1553,10 @@ function runCapturedWish(text, root = process.cwd(), options = {}) {
1539
1553
  const taskText = wishBodyText(text);
1540
1554
  const verifyPlan = options.verifyPlan || deriveVerifyPlan(text);
1541
1555
  const decompositionVerifyPlan = options.verifyPlan || (verifyPlan.status === 'explicit' ? verifyPlan : null);
1542
- const audit = auditWish(taskText, root, { engineOverride });
1556
+ // A one-lap intake owns dispatch and validation policy in-process, so its
1557
+ // route must stay deterministic across machines: never interview about the
1558
+ // reviewer just because no validator binary is installed here.
1559
+ const audit = auditWish(taskText, root, { engineOverride, assumeValidator: options.singleTask === true });
1543
1560
  if (builderMode) return builderWish(wish, audit, root, asJson);
1544
1561
  const parts = options.singleTask ? null : analyzeWishParts(text, root);
1545
1562
  if (parts && noMission) return captureOnlyWish(wish, null, root, asJson);
@@ -1552,6 +1569,7 @@ function runCapturedWish(text, root = process.cwd(), options = {}) {
1552
1569
  return delegateWish(wish, audit, root, asJson, {
1553
1570
  taskText,
1554
1571
  recordText: wish.text,
1572
+ oneLap: options.singleTask === true,
1555
1573
  ...(options.metric ? { metric: options.metric } : {}),
1556
1574
  verifyPlan,
1557
1575
  userVerifyProvided: options.userVerifyProvided === true || verifyPlan.status === 'explicit',
@@ -1622,7 +1640,7 @@ function applyAnswer(wish, answer, root, options = {}) {
1622
1640
  const audit = bestGuessAfterRepeatedQuestion(answeredWish, audited, root);
1623
1641
  if (!audit.ok) return askForInput(answeredWish, audit, root, asJson);
1624
1642
  if (noMission) return captureOnlyWish(answeredWish, audit, root, asJson);
1625
- return delegateWish(answeredWish, audit, root, asJson, {});
1643
+ return delegateWish(answeredWish, audit, root, asJson, { oneLap: wish.one_lap === true });
1626
1644
  }
1627
1645
 
1628
1646
  function sameWaitingPart(left, right) {
@@ -1811,25 +1829,13 @@ function grantWish(positionals, root = process.cwd(), options = {}) {
1811
1829
  module.exports = {
1812
1830
  REVIEW_WORD_SCORES,
1813
1831
  answerWish,
1814
- appendDelegatedWishRecord,
1815
- appendNeedsInputRecord,
1816
- askForInput,
1817
- captureOnlyWish,
1818
1832
  captureWishToJournal,
1819
- delegateWish,
1820
- dispatchWishHeadlessly,
1821
- fulfillLandedWishes,
1822
1833
  grantWish,
1823
- metricVerifierCommand,
1824
- listedWishes,
1825
- openWishes,
1826
1834
  parseMetricExpression,
1827
1835
  printBoard,
1828
1836
  printList,
1829
1837
  printReviewNudges,
1830
1838
  printRewards,
1831
- reviewNudgeWishes,
1832
- reviewWordScore,
1833
1839
  reviewWish,
1834
1840
  runAgainWish,
1835
1841
  runCapturedWish,
package/lib/wish-stats.js CHANGED
@@ -173,11 +173,6 @@ function printWishStats(root = process.cwd(), options = {}) {
173
173
  }
174
174
 
175
175
  module.exports = {
176
- buildWishStats,
177
- haikuRates,
178
- missionReceiptFiles,
179
176
  printWishStats,
180
177
  verifierPassed,
181
- wishStatsLogPath,
182
- writeWishStats,
183
178
  };
package/lib/wish-store.js CHANGED
@@ -338,7 +338,6 @@ module.exports = {
338
338
  improveWishes,
339
339
  lessonsFile,
340
340
  pruneWishLessons,
341
- readWishLessonLines,
342
341
  wishLessonsBrief,
343
342
  wishLessonsSummary,
344
343
  latestWishEventMap,
@@ -80,8 +80,6 @@ function assertSafeWorkspaceRoot(dir, { slug, op } = {}) {
80
80
  }
81
81
 
82
82
  module.exports = {
83
- dangerousWorkspaceReason,
84
- safeFallbackDir,
85
83
  resolveSafeOutputDir,
86
84
  assertSafeWorkspaceRoot,
87
85
  };
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,5 +304,5 @@ module.exports = {
217
304
  writeZipFile,
218
305
  readZipBuffer,
219
306
  readZipFile,
220
- crc32,
307
+ ZIP_LIMITS,
221
308
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "3.37.1",
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