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
@@ -0,0 +1,146 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const ACTIVE_STATUSES = new Set(['observed', 'open', 'attempted']);
7
+ const KEYWORD_STOP_WORDS = new Set([
8
+ 'and', 'are', 'for', 'from', 'into', 'not', 'of', 'on', 'or', 'the', 'this', 'to', 'with', 'without',
9
+ ]);
10
+
11
+ function readJson(filePath) {
12
+ try {
13
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
14
+ return parsed && typeof parsed === 'object' ? parsed : {};
15
+ } catch {
16
+ return {};
17
+ }
18
+ }
19
+
20
+ function readLessonText(filePath) {
21
+ let content = '';
22
+ try {
23
+ content = fs.readFileSync(filePath, 'utf8');
24
+ } catch {
25
+ return new Map();
26
+ }
27
+
28
+ const bySlug = new Map();
29
+ for (const line of content.split(/\r?\n/)) {
30
+ const match = line.match(/^\s*-\s*\*\*\[([^\]]+)\]\s*([a-z0-9][a-z0-9-]*)\*\*\s*[—:-]+\s*(?:pass|fail)?\s*[—:-]*\s*(.*)$/i);
31
+ if (!match || bySlug.has(match[2])) continue;
32
+ const text = match[3].replace(/^\[resolved\]\s*/i, '').trim();
33
+ if (text) bySlug.set(match[2], text);
34
+ }
35
+ return bySlug;
36
+ }
37
+
38
+ function isPromptRule(mechanism) {
39
+ if (mechanism && typeof mechanism === 'object') {
40
+ const kind = String(mechanism.type || mechanism.kind || '').toLowerCase();
41
+ return kind === 'prompt-rule' || kind === 'prompt_rule' || mechanism.prompt_rule === true;
42
+ }
43
+ return /^\s*prompt[-_\s]?rule\b/i.test(String(mechanism || ''));
44
+ }
45
+
46
+ function lessonIsEligible(lesson) {
47
+ const status = String(lesson && lesson.status || '').toLowerCase();
48
+ if (ACTIVE_STATUSES.has(status)) return true;
49
+ return status === 'resolved' && isPromptRule(lesson.mechanism);
50
+ }
51
+
52
+ function normalizeFile(file) {
53
+ return String(file || '')
54
+ .trim()
55
+ .replace(/\\/g, '/')
56
+ .replace(/^\.\//, '')
57
+ .replace(/\/{2,}/g, '/')
58
+ .replace(/\/$/, '');
59
+ }
60
+
61
+ function pathSpecificity(file) {
62
+ const normalized = normalizeFile(file);
63
+ const segments = normalized.split('/').filter(Boolean).length;
64
+ return (segments * 100) + normalized.length;
65
+ }
66
+
67
+ function overlapStrength(appliesTo, file) {
68
+ const lessonPath = normalizeFile(appliesTo).toLowerCase();
69
+ const taskPath = normalizeFile(file).toLowerCase();
70
+ if (!lessonPath || !taskPath) return 0;
71
+ if (lessonPath === taskPath) return 3;
72
+ if (taskPath.endsWith(`/${lessonPath}`) || lessonPath.endsWith(`/${taskPath}`)) return 3;
73
+ if (taskPath.startsWith(`${lessonPath}/`) || lessonPath.startsWith(`${taskPath}/`)) return 2;
74
+ return 0;
75
+ }
76
+
77
+ function slugKeywords(slug) {
78
+ return [...new Set(String(slug || '').toLowerCase().split(/[^a-z0-9]+/)
79
+ .filter((word) => word.length >= 3 && !KEYWORD_STOP_WORDS.has(word)))];
80
+ }
81
+
82
+ function keywordAppears(text, keyword) {
83
+ const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
84
+ return new RegExp(`(^|[^a-z0-9])${escaped}([^a-z0-9]|$)`, 'i').test(text);
85
+ }
86
+
87
+ function bestMatch({ slug, lesson, briefText, files }) {
88
+ const brief = String(briefText || '');
89
+ const briefLower = brief.toLowerCase().replace(/\\/g, '/');
90
+ const appliesTo = Array.isArray(lesson.applies_to) ? lesson.applies_to : [];
91
+ let best = null;
92
+
93
+ for (const lessonFile of appliesTo) {
94
+ const normalizedLessonFile = normalizeFile(lessonFile);
95
+ if (!normalizedLessonFile) continue;
96
+ for (const taskFile of files) {
97
+ const strength = overlapStrength(normalizedLessonFile, taskFile);
98
+ if (!strength) continue;
99
+ const score = 300000 + (strength * 10000) + pathSpecificity(normalizedLessonFile) - appliesTo.length;
100
+ if (!best || score > best.score) {
101
+ best = { score, why: `file overlaps ${normalizedLessonFile}` };
102
+ }
103
+ }
104
+ if (briefLower.includes(normalizedLessonFile.toLowerCase())) {
105
+ const score = 200000 + pathSpecificity(normalizedLessonFile) - appliesTo.length;
106
+ if (!best || score > best.score) {
107
+ best = { score, why: `brief mentions ${normalizedLessonFile}` };
108
+ }
109
+ }
110
+ }
111
+
112
+ const matchedKeywords = slugKeywords(slug).filter((keyword) => keywordAppears(brief, keyword));
113
+ if (matchedKeywords.length) {
114
+ const keywordLength = matchedKeywords.reduce((total, keyword) => total + keyword.length, 0);
115
+ const score = 100000 + (matchedKeywords.length * 1000) + keywordLength;
116
+ if (!best || score > best.score) {
117
+ best = { score, why: `brief mentions ${matchedKeywords.join(', ')}` };
118
+ }
119
+ }
120
+
121
+ return best;
122
+ }
123
+
124
+ function matchLessons({ briefText = '', files = [], root = process.cwd() } = {}) {
125
+ const metadata = readJson(path.join(root, 'atris', 'lessons.json'));
126
+ const lessonText = readLessonText(path.join(root, 'atris', 'lessons.md'));
127
+ const normalizedFiles = (Array.isArray(files) ? files : [files]).map(normalizeFile).filter(Boolean);
128
+ const matches = [];
129
+
130
+ for (const [slug, lesson] of Object.entries(metadata)) {
131
+ if (slug === '_schema' || !lesson || typeof lesson !== 'object') continue;
132
+ if (!lessonIsEligible(lesson)) continue;
133
+ const text = lessonText.get(slug);
134
+ if (!text) continue;
135
+ const matched = bestMatch({ slug, lesson, briefText, files: normalizedFiles });
136
+ if (!matched) continue;
137
+ matches.push({ slug, text, why_matched: matched.why, score: matched.score });
138
+ }
139
+
140
+ return matches
141
+ .sort((left, right) => right.score - left.score || left.slug.localeCompare(right.slug))
142
+ .slice(0, 5)
143
+ .map(({ slug, text, why_matched }) => ({ slug, text, why_matched }));
144
+ }
145
+
146
+ module.exports = { matchLessons };
@@ -20,8 +20,6 @@ const HEADLESS_CAPABLE_RUNNERS = new Set([
20
20
  'codex',
21
21
  'cursor',
22
22
  'devin',
23
- 'hermes',
24
- 'hermes-agent',
25
23
  ]);
26
24
  const FINDING_PRIORITY = {
27
25
  repeated_auth_failure: 500,
@@ -311,7 +309,7 @@ function missionLockPidAlive(pid) {
311
309
  }
312
310
  }
313
311
 
314
- function staleMissionLockFinding(root, nowIso, pidAlive = missionLockPidAlive) {
312
+ function missionLockRecords(root, pidAlive = missionLockPidAlive) {
315
313
  const stateDir = path.join(root, '.atris', 'state');
316
314
  let names = [];
317
315
  try {
@@ -319,18 +317,21 @@ function staleMissionLockFinding(root, nowIso, pidAlive = missionLockPidAlive) {
319
317
  } catch {
320
318
  return [];
321
319
  }
322
- const stale = [];
323
- for (const name of names) {
320
+ return names.map((name) => {
324
321
  const file = path.join(stateDir, name);
325
322
  let lock = null;
326
323
  try { lock = JSON.parse(fs.readFileSync(file, 'utf8')); } catch {}
327
324
  const pid = Number(lock && lock.pid);
328
325
  if (!Number.isInteger(pid) || pid <= 0) {
329
- stale.push({ lock: name, reason: 'unparseable', pid: null });
330
- } else if (!pidAlive(pid)) {
331
- stale.push({ lock: name, reason: 'dead-pid', pid });
326
+ return { lock: name, reason: 'unparseable', pid: null, stale: true };
332
327
  }
333
- }
328
+ const alive = pidAlive(pid);
329
+ return { lock: name, reason: alive ? 'live-pid' : 'dead-pid', pid, stale: !alive };
330
+ });
331
+ }
332
+
333
+ function staleMissionLockFinding(root, nowIso, pidAlive = missionLockPidAlive) {
334
+ const stale = missionLockRecords(root, pidAlive).filter((lock) => lock.stale);
334
335
  if (!stale.length) return [];
335
336
  return [{
336
337
  kind: 'stale_mission_lock',
@@ -400,17 +401,8 @@ function scanLoopReceipts({ root = process.cwd(), now = new Date(), freshnessMs
400
401
 
401
402
  module.exports = {
402
403
  scanLoopReceipts,
403
- repeatedFailureFindings,
404
- authFailureFindings,
405
- rewardFlatlineFinding,
406
- zeroDueCapableFinding,
407
- staleMissionLockFinding,
408
- isDueCapableMission,
404
+ missionLockRecords,
409
405
  latestMissionRecords,
410
- orderFindings,
411
406
  nonPositiveRewardPressure,
412
- applyRewardPressure,
413
407
  FINDING_PRIORITY,
414
- DEFAULT_RECEIPT_LIMIT,
415
- DEFAULT_FINDING_FRESHNESS_MS,
416
408
  };
package/lib/manifest.js CHANGED
@@ -251,9 +251,7 @@ module.exports = {
251
251
  buildManifest,
252
252
  computeLocalHashes,
253
253
  threeWayCompare,
254
- getManifestPath,
255
254
  SKIP_DIRS,
256
255
  isIgnoredSyncPath,
257
256
  filterSyncFiles,
258
- isSyncTextBuffer,
259
257
  };
@@ -227,6 +227,5 @@ function runAliveTick(name, opts = {}) {
227
227
 
228
228
  module.exports = {
229
229
  runAliveTick,
230
- resolveMemberOperateScript,
231
230
  resolveAtrisBin,
232
231
  };
@@ -129,14 +129,9 @@ function memberArgFromArgv(argv = process.argv) {
129
129
  }
130
130
 
131
131
  module.exports = {
132
- SWITCHES_REL,
133
- switchesPath,
134
- loadMemberSwitches,
135
- saveMemberSwitches,
136
132
  isLocalMember,
137
133
  getMemberSwitch,
138
134
  isMemberAwake,
139
- isMemberLoopAwake,
140
135
  setMemberAwake,
141
136
  setMemberLoopAwake,
142
137
  readLoopFlag,
@@ -498,7 +498,6 @@ function writeMissionArtifact(mission, options = {}) {
498
498
  }
499
499
 
500
500
  module.exports = {
501
- buildMissionArtifactBlocks,
502
501
  missionArtifactPaths,
503
502
  writeMissionArtifact,
504
503
  };
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ function normalizeHumanAsk(ask) {
4
+ if (typeof ask === 'string') {
5
+ return { text: ask, answered_at: null };
6
+ }
7
+ if (!ask || typeof ask !== 'object' || Array.isArray(ask)) {
8
+ return { text: String(ask || ''), answered_at: null };
9
+ }
10
+ return {
11
+ ...ask,
12
+ text: String(ask.text || ''),
13
+ answered_at: ask.answered_at || null,
14
+ };
15
+ }
16
+
17
+ function normalizeHumanAsks(asks) {
18
+ return Array.isArray(asks) ? asks.map(normalizeHumanAsk) : [];
19
+ }
20
+
21
+ function openHumanAsks(asks) {
22
+ return normalizeHumanAsks(asks).filter((ask) => ask.text.trim() && !ask.answered_at);
23
+ }
24
+
25
+ module.exports = {
26
+ normalizeHumanAsks,
27
+ openHumanAsks,
28
+ };
@@ -0,0 +1,296 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const { spawnSync } = require('child_process');
7
+ const { DENIED_TAGS } = require('./fleet');
8
+
9
+ const PATH_SURFACES = [
10
+ ['middleware/proxy', /(?:^|[/_.-])(?:middleware|proxy)(?:[/_.-]|$)/i],
11
+ ['csp', /(?:^|[/_.-])csp(?:[/_.-]|$)/i],
12
+ ['auth', /(?:^|[/_.-])auth(?:entication|orization)?(?:[/_.-]|$)/i],
13
+ ['session', /(?:^|[/_.-])sessions?(?:[/_.-]|$)/i],
14
+ ['cookie', /(?:^|[/_.-])cookies?(?:[/_.-]|$)/i],
15
+ ['cors', /(?:^|[/_.-])cors(?:[/_.-]|$)/i],
16
+ ['sandbox', /(?:^|[/_.-])sandbox(?:[/_.-]|$)/i],
17
+ ['permission', /(?:^|[/_.-])permissions?(?:[/_.-]|$)/i],
18
+ ];
19
+
20
+ const CONTENT_SURFACES = [
21
+ ['csp', /\bContent-Security-Policy\b/i],
22
+ ['csp nonce', /\bnonce\b/i],
23
+ ['iframe sandbox', /\bsandbox\s*=/i],
24
+ ['cookie', /\bSet-Cookie\b/i],
25
+ ['cors', /\bAccess-Control-Allow-[A-Za-z-]+\b/i],
26
+ ['public dynamic rendering', /\bforce-dynamic\b/i],
27
+ ['auth header', /\b(?:Authorization\s*[:=].*\bBearer\b|headers?\b[^\n]*\bauthorization\b|\bauthorization\b[^\n]*headers?\b)/i],
28
+ ];
29
+
30
+ function normalizedTags(tags) {
31
+ const list = Array.isArray(tags) ? tags : [tags];
32
+ return list
33
+ .flatMap((tag) => String(tag || '').split(','))
34
+ .map((tag) => tag.trim().toLowerCase().replace(/_/g, '-'))
35
+ .filter(Boolean);
36
+ }
37
+
38
+ function changedPathsFromDiff(diff) {
39
+ const paths = [];
40
+ for (const line of String(diff || '').split(/\r?\n/)) {
41
+ const header = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
42
+ if (header) {
43
+ paths.push(header[1], header[2]);
44
+ continue;
45
+ }
46
+ const file = line.match(/^(?:---|\+\+\+) (?:[ab]\/)?(.+)$/);
47
+ if (file && file[1] !== '/dev/null') paths.push(file[1]);
48
+ }
49
+ return Array.from(new Set(paths));
50
+ }
51
+
52
+ // Prose is not a protected surface. Docs, judge cards, wikis, journals and run
53
+ // receipts routinely *describe* CSP or force-dynamic without changing any
54
+ // behaviour — replaying this guard over 60 real commits, 5 of 6 hits were
55
+ // markdown that merely mentioned the words. A guard that pauses on every judge
56
+ // card gets switched off, which is worse than no guard. Content signals
57
+ // therefore apply to code only; PATH signals still apply to every file.
58
+ const CODE_FILE = /\.(?:[cm]?[jt]sx?|py|rb|go|rs|java|kt|swift|php|sh|bash|zsh|sql|ya?ml|toml|env)$/i;
59
+
60
+ function isCodeFile(file) {
61
+ const f = String(file || '').trim();
62
+ if (!f || f === '/dev/null') return false;
63
+ if (/\.(?:md|mdx|markdown|txt|rst|adoc)$/i.test(f)) return false;
64
+ return CODE_FILE.test(f);
65
+ }
66
+
67
+ // Walk the diff file-by-file so a changed line is attributed to the file it
68
+ // came from, then keep only lines from code files.
69
+ function changedCodeLinesFromDiff(diff) {
70
+ const lines = [];
71
+ let inCode = false;
72
+ for (const line of String(diff || '').split(/\r?\n/)) {
73
+ const header = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
74
+ if (header) {
75
+ inCode = isCodeFile(header[1]) || isCodeFile(header[2]);
76
+ continue;
77
+ }
78
+ if (/^(?:---|\+\+\+) /.test(line)) continue;
79
+ if (!inCode) continue;
80
+ if (/^[+-]/.test(line)) lines.push(line.slice(1));
81
+ }
82
+ return lines;
83
+ }
84
+
85
+ function matchProtectedMissionDiff(diff, { tags = [] } = {}) {
86
+ const matches = [];
87
+ const add = (surface, signal, detail) => {
88
+ if (matches.some((match) => match.surface === surface && match.signal === signal && match.detail === detail)) return;
89
+ matches.push({ surface, signal, detail });
90
+ };
91
+
92
+ for (const tag of normalizedTags(tags)) {
93
+ if (DENIED_TAGS.includes(tag)) add(tag, 'tag', tag);
94
+ }
95
+
96
+ for (const file of changedPathsFromDiff(diff)) {
97
+ for (const [surface, pattern] of PATH_SURFACES) {
98
+ if (pattern.test(file)) add(surface, 'path', file);
99
+ }
100
+ }
101
+
102
+ for (const line of changedCodeLinesFromDiff(diff)) {
103
+ for (const [surface, pattern] of CONTENT_SURFACES) {
104
+ if (pattern.test(line)) add(surface, 'content', 'changed line');
105
+ }
106
+ }
107
+
108
+ return {
109
+ protected: matches.length > 0,
110
+ surfaces: Array.from(new Set(matches.map((match) => match.surface))),
111
+ matches,
112
+ };
113
+ }
114
+
115
+ function runGitDiff(git, args, root) {
116
+ const result = git(args, root);
117
+ if (!result || result.error || result.status !== 0) {
118
+ const detail = result?.error?.message
119
+ || String(result?.stderr || result?.stdout || `git ${args.join(' ')} failed`).trim();
120
+ return { ok: false, detail: detail || 'git diff could not be read' };
121
+ }
122
+ return { ok: true, diff: String(result.stdout || '') };
123
+ }
124
+
125
+ function defaultGit(gitBin = 'git') {
126
+ return (args, root) => spawnSync(gitBin, args, {
127
+ cwd: root,
128
+ encoding: 'utf8',
129
+ timeout: 10000,
130
+ maxBuffer: 64 * 1024 * 1024,
131
+ });
132
+ }
133
+
134
+ function readMissionDiff({
135
+ root = process.cwd(),
136
+ baseRef = '',
137
+ git = defaultGit(),
138
+ includeUnstaged = true,
139
+ } = {}) {
140
+ const parts = [];
141
+ const staged = runGitDiff(git, ['diff', '--cached', '--no-ext-diff', '--unified=0', '--no-color', '--'], root);
142
+ if (!staged.ok) return staged;
143
+ parts.push(staged.diff);
144
+
145
+ if (includeUnstaged) {
146
+ const unstaged = runGitDiff(git, ['diff', '--no-ext-diff', '--unified=0', '--no-color', '--'], root);
147
+ if (!unstaged.ok) return unstaged;
148
+ parts.push(unstaged.diff);
149
+ }
150
+
151
+ if (baseRef) {
152
+ const committed = runGitDiff(git, ['diff', '--no-ext-diff', '--unified=0', '--no-color', baseRef, 'HEAD', '--'], root);
153
+ if (!committed.ok) return committed;
154
+ parts.push(committed.diff);
155
+ }
156
+
157
+ return { ok: true, diff: parts.filter(Boolean).join('\n') };
158
+ }
159
+
160
+ function protectedPause(reason, detail = {}) {
161
+ return {
162
+ ok: false,
163
+ allowed: false,
164
+ status: 'paused-for-review',
165
+ reason,
166
+ ...detail,
167
+ };
168
+ }
169
+
170
+ function unreadableMissionGuard(error) {
171
+ return protectedPause('mission diff could not be read; repair the tooling failure and rerun', {
172
+ status: 'paused-for-retry',
173
+ unreadable: true,
174
+ detail: error?.message || String(error || 'unknown diff read failure'),
175
+ surfaces: ['unreadable diff'],
176
+ matches: [],
177
+ });
178
+ }
179
+
180
+ function inspectMissionProtectedDiff({
181
+ root = process.cwd(),
182
+ baseRef = '',
183
+ tags = [],
184
+ readDiff = readMissionDiff,
185
+ git,
186
+ includeUnstaged = true,
187
+ } = {}) {
188
+ let read;
189
+ try {
190
+ read = readDiff({ root, baseRef, tags, git, includeUnstaged });
191
+ } catch (error) {
192
+ return unreadableMissionGuard(error);
193
+ }
194
+ if (!read || read.ok !== true) {
195
+ return unreadableMissionGuard(read?.detail);
196
+ }
197
+
198
+ const matched = matchProtectedMissionDiff(read.diff, { tags });
199
+ if (matched.protected) {
200
+ return protectedPause(`protected mission surface requires human review: ${matched.surfaces.join(', ')}`, {
201
+ unreadable: false,
202
+ surfaces: matched.surfaces,
203
+ matches: matched.matches,
204
+ });
205
+ }
206
+ return {
207
+ ok: true,
208
+ allowed: true,
209
+ status: 'clear',
210
+ reason: 'mission diff contains no protected surface',
211
+ unreadable: false,
212
+ surfaces: [],
213
+ matches: [],
214
+ };
215
+ }
216
+
217
+ function guardMissionLanding(options = {}) {
218
+ const inspection = inspectMissionProtectedDiff(options);
219
+ if (!inspection.allowed) return inspection;
220
+ const land = typeof options.land === 'function' ? options.land : () => ({ ok: true, landed: true });
221
+ return { ...land(), protected_lane_guard: inspection };
222
+ }
223
+
224
+ function gitSubcommand(args) {
225
+ const consumesValue = new Set(['-C', '-c', '--git-dir', '--work-tree', '--namespace', '--config-env']);
226
+ for (let index = 0; index < args.length; index += 1) {
227
+ const arg = String(args[index] || '');
228
+ if (consumesValue.has(arg)) {
229
+ index += 1;
230
+ continue;
231
+ }
232
+ if (arg.startsWith('-')) continue;
233
+ return arg;
234
+ }
235
+ return '';
236
+ }
237
+
238
+ function prepareMissionGitGuard({
239
+ root = process.cwd(),
240
+ tags = [],
241
+ pathValue = process.env.PATH || '',
242
+ } = {}) {
243
+ const resolved = spawnSync('sh', ['-c', 'command -v git'], {
244
+ cwd: root,
245
+ encoding: 'utf8',
246
+ timeout: 5000,
247
+ });
248
+ const gitBin = String(resolved.stdout || '').trim();
249
+ if (resolved.status !== 0 || !gitBin) {
250
+ throw new Error('mission git guard could not resolve git');
251
+ }
252
+
253
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'atris-mission-git-'));
254
+ const wrapper = path.join(dir, 'git');
255
+ const modulePath = __filename;
256
+ const config = { root: path.resolve(root), tags: normalizedTags(tags), gitBin };
257
+ const source = [
258
+ '#!/usr/bin/env node',
259
+ "'use strict';",
260
+ `const { spawnSync } = require('child_process');`,
261
+ `const guard = require(${JSON.stringify(modulePath)});`,
262
+ `const config = ${JSON.stringify(config)};`,
263
+ gitSubcommand.toString(),
264
+ defaultGit.toString(),
265
+ `const args = process.argv.slice(2);`,
266
+ `if (gitSubcommand(args) === 'commit') {`,
267
+ ` const result = guard.inspectMissionProtectedDiff({ root: config.root, tags: config.tags, includeUnstaged: false, git: defaultGit(config.gitBin) });`,
268
+ ` if (!result.allowed) {`,
269
+ ` process.stderr.write(result.reason + '\\n');`,
270
+ ` process.exit(78);`,
271
+ ` }`,
272
+ `}`,
273
+ `const child = spawnSync(config.gitBin, args, { cwd: process.cwd(), stdio: 'inherit' });`,
274
+ `if (child.error) { process.stderr.write(child.error.message + '\\n'); process.exit(1); }`,
275
+ `process.exit(child.status == null ? 1 : child.status);`,
276
+ '',
277
+ ].join('\n');
278
+ fs.writeFileSync(wrapper, source, { encoding: 'utf8', mode: 0o755 });
279
+
280
+ return {
281
+ env: { ...process.env, PATH: `${dir}${path.delimiter}${pathValue}` },
282
+ pathPrefix: dir,
283
+ cleanup() {
284
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch {}
285
+ },
286
+ };
287
+ }
288
+
289
+ module.exports = {
290
+ defaultGit,
291
+ matchProtectedMissionDiff,
292
+ unreadableMissionGuard,
293
+ inspectMissionProtectedDiff,
294
+ guardMissionLanding,
295
+ prepareMissionGitGuard,
296
+ };
@@ -842,6 +842,4 @@ module.exports = {
842
842
  buildMissionRoom,
843
843
  writeMissionRoomReceipt,
844
844
  missionRoomLines,
845
- writeThinkingMemory,
846
- collectMissionRoomContext,
847
845
  };
@@ -322,6 +322,5 @@ function readAtrisGoalState(cwd = process.cwd()) {
322
322
  module.exports = {
323
323
  missionRunIntentFromMessage,
324
324
  runRuntimeMissionLoop,
325
- formatRuntimeMissionLoopReceipt,
326
325
  readAtrisGoalState,
327
326
  };
package/lib/next-moves.js CHANGED
@@ -844,37 +844,19 @@ function nextMoves(root = process.cwd(), limit = 3) {
844
844
  module.exports = {
845
845
  moveId,
846
846
  norm,
847
- titleKey,
848
847
  isGenericInboxPlaceholder,
849
- WEIGHT,
850
848
  SELF_IMPROVEMENT_SEED_TITLE,
851
- cleanSuggestedTargetTitle,
852
- suggestedTargetFromReportText,
853
849
  latestSuggestedTarget,
854
- parseInboxTitles,
855
- cleanInboxMoveTitle,
856
850
  readRoadmapOpenItems,
857
- readWaitingWishCards,
858
- readActionableMissionCards,
859
- readActiveMissions,
860
- readUnreviewedShippedWishCards,
861
- readDreamCards,
862
851
  markDreamCardConsumed,
863
- dreamCardId,
864
852
  readEndgameMove,
865
- readActiveTasks,
866
- isInternalNextMoveTask,
867
853
  readHandledTaskTitles,
868
854
  latestInboxItems,
869
855
  todayInboxItems,
870
- gatherCandidates,
871
856
  pickNextMoves,
872
- readNextParked,
873
857
  parkNextCard,
874
858
  nextCards,
875
- addMissionOnlyFallback,
876
859
  nextMoves,
877
- readDecisions,
878
860
  recordDecision,
879
861
  seedInboxFromMove,
880
862
  pickRoadmapSeed,