atris 2.6.2 → 3.0.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 (54) hide show
  1. package/README.md +124 -34
  2. package/atris/CLAUDE.md +5 -1
  3. package/atris/atris.md +4 -0
  4. package/atris/features/README.md +24 -0
  5. package/atris/skills/autopilot/SKILL.md +74 -75
  6. package/atris/skills/endgame/SKILL.md +179 -0
  7. package/atris/skills/flow/SKILL.md +121 -0
  8. package/atris/skills/improve/SKILL.md +84 -0
  9. package/atris/skills/loop/SKILL.md +72 -0
  10. package/atris/skills/wiki/SKILL.md +61 -0
  11. package/atris/team/executor/MEMBER.md +10 -4
  12. package/atris/team/navigator/MEMBER.md +2 -0
  13. package/atris/team/validator/MEMBER.md +8 -5
  14. package/atris.md +33 -0
  15. package/bin/atris.js +210 -41
  16. package/commands/activate.js +28 -2
  17. package/commands/align.js +720 -0
  18. package/commands/auth.js +75 -2
  19. package/commands/autopilot.js +1213 -270
  20. package/commands/browse.js +100 -0
  21. package/commands/business.js +785 -12
  22. package/commands/clean.js +107 -2
  23. package/commands/computer.js +429 -0
  24. package/commands/context-sync.js +78 -8
  25. package/commands/experiments.js +351 -0
  26. package/commands/feedback.js +150 -0
  27. package/commands/fleet.js +395 -0
  28. package/commands/fork.js +127 -0
  29. package/commands/init.js +50 -1
  30. package/commands/learn.js +407 -0
  31. package/commands/lifecycle.js +94 -0
  32. package/commands/loop.js +114 -0
  33. package/commands/publish.js +129 -0
  34. package/commands/pull.js +434 -48
  35. package/commands/push.js +312 -164
  36. package/commands/review.js +149 -0
  37. package/commands/run.js +76 -43
  38. package/commands/serve.js +360 -0
  39. package/commands/setup.js +1 -1
  40. package/commands/soul.js +381 -0
  41. package/commands/status.js +119 -1
  42. package/commands/sync.js +147 -1
  43. package/commands/terminal.js +201 -0
  44. package/commands/wiki.js +376 -0
  45. package/commands/workflow.js +191 -74
  46. package/commands/workspace-clean.js +3 -3
  47. package/lib/endstate.js +259 -0
  48. package/lib/learnings.js +235 -0
  49. package/lib/manifest.js +1 -0
  50. package/lib/todo.js +9 -5
  51. package/lib/wiki.js +578 -0
  52. package/package.json +2 -2
  53. package/utils/api.js +48 -36
  54. package/utils/auth.js +1 -0
@@ -2,6 +2,45 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { getLogPath } = require('../lib/journal');
4
4
 
5
+ function wrapWorkflowText(text, width = 76) {
6
+ const normalized = String(text || '').replace(/\s+/g, ' ').trim();
7
+ if (!normalized) return [''];
8
+
9
+ const words = normalized.split(' ');
10
+ const lines = [];
11
+ let current = '';
12
+
13
+ for (const word of words) {
14
+ if (!current) {
15
+ current = word;
16
+ continue;
17
+ }
18
+ if ((current + ' ' + word).length <= width) {
19
+ current += ' ' + word;
20
+ } else {
21
+ lines.push(current);
22
+ current = word;
23
+ }
24
+ }
25
+
26
+ if (current) lines.push(current);
27
+ return lines;
28
+ }
29
+
30
+ function printWorkflowBrief(lines) {
31
+ console.log('');
32
+ for (const line of lines) {
33
+ if (!line) {
34
+ console.log('');
35
+ continue;
36
+ }
37
+ for (const wrapped of wrapWorkflowText(line)) {
38
+ console.log(wrapped);
39
+ }
40
+ }
41
+ console.log('');
42
+ }
43
+
5
44
  async function planAtris(userInput = null) {
6
45
  const { loadConfig } = require('../utils/config');
7
46
  const { loadCredentials } = require('../utils/auth');
@@ -78,7 +117,10 @@ async function planAtris(userInput = null) {
78
117
  const inboxCount = inboxContext
79
118
  ? inboxContext
80
119
  .split('\n')
81
- .filter((line) => line.trim().startsWith('-'))
120
+ .filter((line) => {
121
+ const t = line.trim();
122
+ return t.startsWith('- ') && t.length > 2;
123
+ })
82
124
  .length
83
125
  : 0;
84
126
 
@@ -122,6 +164,20 @@ async function planAtris(userInput = null) {
122
164
  const lessonsRef = fs.existsSync(lessonsPath) ? path.relative(process.cwd(), lessonsPath) : null;
123
165
  console.log(`- Lessons: ${lessonsRef || 'atris/lessons.md (none yet)'}`);
124
166
  console.log(`- Journal (today): ${journalPath}`);
167
+
168
+ // Show top learnings if available
169
+ try {
170
+ const { loadLearnings } = require('../lib/learnings');
171
+ const learnings = loadLearnings().filter(e => e._effectiveConfidence >= 7 && e.insight !== '[REMOVED]').slice(0, 3);
172
+ if (learnings.length > 0) {
173
+ console.log('');
174
+ console.log('🧠 Prior learnings (high confidence):');
175
+ for (const l of learnings) {
176
+ console.log(` [${l._effectiveConfidence}/10] ${l.type}/${l.key}: ${l.insight}`);
177
+ }
178
+ }
179
+ } catch {}
180
+
125
181
  console.log('');
126
182
  console.log(`📥 Inbox items: ${inboxCount}`);
127
183
  console.log('');
@@ -442,6 +498,20 @@ async function doAtris() {
442
498
  console.log(`- MAP: ${mapDisplay}`);
443
499
  console.log(`- TODO: ${taskSourcePath || 'atris/TODO.md (missing)'}`);
444
500
  console.log(`- Features index: ${featuresReadmeRef || 'atris/features/README.md (missing)'}`);
501
+
502
+ // Show top learnings during execution
503
+ try {
504
+ const { loadLearnings } = require('../lib/learnings');
505
+ const learnings = loadLearnings().filter(e => e._effectiveConfidence >= 7 && e.insight !== '[REMOVED]').slice(0, 3);
506
+ if (learnings.length > 0) {
507
+ console.log('');
508
+ console.log('🧠 Prior learnings (apply during build):');
509
+ for (const l of learnings) {
510
+ console.log(` [${l._effectiveConfidence}/10] ${l.type}/${l.key}: ${l.insight}`);
511
+ }
512
+ }
513
+ } catch {}
514
+
445
515
  console.log('');
446
516
 
447
517
  const backlogCount = workspaceSummary && Array.isArray(workspaceSummary.backlogTasks)
@@ -766,64 +836,89 @@ async function reviewAtris() {
766
836
  }
767
837
  }
768
838
 
769
- console.log('');
770
- console.log('┌─────────────────────────────────────────────────────────────┐');
771
- console.log('│ Atris Review — Validator Agent Activated │');
772
- console.log('└─────────────────────────────────────────────────────────────┘');
773
- console.log('');
774
-
775
- console.log('📁 CONTEXT FILES (agent should read):');
776
- console.log(`- Validator spec: ${validatorPath}`);
777
- console.log(`- Testing guide: ${testingGuideRef || '(none found)'}`);
778
- console.log(`- Persona: ${personaRef || 'atris/PERSONA.md (missing)'}`);
779
839
  const mapDisplay = mapPath
780
840
  ? `${mapPath}${mapIsPlaceholder ? ' (placeholder — generate first)' : ''}`
781
841
  : 'atris/MAP.md (missing)';
782
- console.log(`- MAP: ${mapDisplay}`);
783
- console.log(`- TODO: ${todoPathRef || 'atris/TODO.md (missing)'}`);
784
- console.log(`- Journal (today): ${journalPathRef}`);
785
- console.log(`- Features index: ${featuresReadmeRef || 'atris/features/README.md (missing)'}`);
786
- console.log('');
787
842
 
788
- console.log(`🧪 Feature validate scripts found: ${featureValidateRefs.length}`);
789
- if (featureValidateRefs.length > 0) {
790
- featureValidateRefs.slice(0, 3).forEach((ref) => console.log(`- ${ref}`));
791
- if (featureValidateRefs.length > 3) {
792
- console.log(`- ... (+${featureValidateRefs.length - 3} more)`);
843
+ if (showFull) {
844
+ console.log('');
845
+ console.log('┌─────────────────────────────────────────────────────────────┐');
846
+ console.log('│ Atris Review — Validator Agent Activated │');
847
+ console.log('└─────────────────────────────────────────────────────────────┘');
848
+ console.log('');
849
+
850
+ console.log('📁 CONTEXT FILES (agent should read):');
851
+ console.log(`- Validator spec: ${validatorPath}`);
852
+ console.log(`- Testing guide: ${testingGuideRef || '(none found)'}`);
853
+ console.log(`- Persona: ${personaRef || 'atris/PERSONA.md (missing)'}`);
854
+ console.log(`- MAP: ${mapDisplay}`);
855
+ console.log(`- TODO: ${todoPathRef || 'atris/TODO.md (missing)'}`);
856
+ console.log(`- Journal (today): ${journalPathRef}`);
857
+ console.log(`- Features index: ${featuresReadmeRef || 'atris/features/README.md (missing)'}`);
858
+ console.log('');
859
+
860
+ console.log(`🧪 Feature validate scripts found: ${featureValidateRefs.length}`);
861
+ if (featureValidateRefs.length > 0) {
862
+ featureValidateRefs.slice(0, 3).forEach((ref) => console.log(`- ${ref}`));
863
+ if (featureValidateRefs.length > 3) {
864
+ console.log(`- ... (+${featureValidateRefs.length - 3} more)`);
865
+ }
793
866
  }
794
- }
795
- console.log('');
867
+ console.log('');
796
868
 
797
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
798
- console.log('📋 COPY/PASTE PROMPT FOR YOUR CODING AGENT:');
799
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
800
- console.log('');
801
- console.log('You are the Validator.');
802
- console.log('');
803
- console.log('Read these files:');
804
- console.log(`- ${validatorPath}`);
805
- if (testingGuideRef) console.log(`- ${testingGuideRef}`);
806
- if (personaRef) console.log(`- ${personaRef}`);
807
- if (mapPath) console.log(`- ${mapPath}`);
808
- if (todoPathRef) console.log(`- ${todoPathRef}`);
809
- console.log(`- ${journalPathRef}`);
810
- if (featuresReadmeRef) console.log(`- ${featuresReadmeRef}`);
811
- console.log('');
812
- if (!mapPath || mapIsPlaceholder) {
813
- console.log('Note: If `atris/MAP.md` is missing or placeholder, generate it from `atris/atris.md` before validating file:line references.');
869
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
870
+ console.log('📋 COPY/PASTE PROMPT FOR YOUR CODING AGENT:');
871
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
872
+ console.log('');
873
+ } else {
874
+ const readinessBits = [
875
+ `MAP is ${mapPath ? 'present' : 'missing'}`,
876
+ `TODO is ${todoPathRef ? 'present' : 'missing'}`,
877
+ `${featureValidateRefs.length} feature validate script${featureValidateRefs.length === 1 ? '' : 's'} ${featureValidateRefs.length === 1 ? 'is' : 'are'} queued`
878
+ ];
879
+ const decision = (mapPath && todoPathRef)
880
+ ? 'Decision: hold final approval until the validator run finishes.'
881
+ : 'Decision: hold. Review setup is incomplete and needs fixing first.';
882
+
883
+ printWorkflowBrief([
884
+ 'I checked the review setup.',
885
+ readinessBits.join(', ') + '.',
886
+ '',
887
+ 'This step prepares the validator. It does not mean the change has passed review yet.',
888
+ 'Next I will run tests, walk each validate.md, and clear completed tasks out of TODO.',
889
+ '',
890
+ decision,
891
+ 'Run `atris review --verbose` for the full prompt and appendix.'
892
+ ]);
893
+ }
894
+ if (showFull) {
895
+ console.log('You are the Validator.');
896
+ console.log('');
897
+ console.log('Read these files:');
898
+ console.log(`- ${validatorPath}`);
899
+ if (testingGuideRef) console.log(`- ${testingGuideRef}`);
900
+ if (personaRef) console.log(`- ${personaRef}`);
901
+ if (mapPath) console.log(`- ${mapPath}`);
902
+ if (todoPathRef) console.log(`- ${todoPathRef}`);
903
+ console.log(`- ${journalPathRef}`);
904
+ if (featuresReadmeRef) console.log(`- ${featuresReadmeRef}`);
905
+ console.log('');
906
+ if (!mapPath || mapIsPlaceholder) {
907
+ console.log('Note: If `atris/MAP.md` is missing or placeholder, generate it from `atris/atris.md` before validating file:line references.');
908
+ console.log('');
909
+ }
910
+ console.log('Workflow:');
911
+ console.log('1) Run the project test suite (follow TESTING_GUIDE if present).');
912
+ console.log('2) Execute any `atris/features/*/validate.md` scripts; if a step fails, fix + rerun.');
913
+ console.log('3) Clean TODO.md: delete completed tasks. Target state = 0.');
914
+ console.log(' If a task fails validation, move back to ## Backlog with note.');
915
+ console.log('4) Log to atris/team/validator/journal/YYYY-MM-DD.md');
916
+ console.log(' (Task, Result, Issues found, Learned)');
917
+ console.log('5) If anything surprised you, append to atris/lessons.md.');
918
+ console.log('');
919
+ console.log('Done when: ✅ All good. TODO.md clean. Ready for human testing.');
814
920
  console.log('');
815
921
  }
816
- console.log('Workflow:');
817
- console.log('1) Run the project test suite (follow TESTING_GUIDE if present).');
818
- console.log('2) Execute any `atris/features/*/validate.md` scripts; if a step fails, fix + rerun.');
819
- console.log('3) Clean TODO.md: delete completed tasks. Target state = 0.');
820
- console.log(' If a task fails validation, move back to ## Backlog with note.');
821
- console.log('4) Log to atris/team/validator/journal/YYYY-MM-DD.md');
822
- console.log(' (Task, Result, Issues found, Learned)');
823
- console.log('5) If anything surprised you, append to atris/lessons.md.');
824
- console.log('');
825
- console.log('Done when: ✅ All good. TODO.md clean. Ready for human testing.');
826
- console.log('');
827
922
 
828
923
  if (showFull) {
829
924
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
@@ -851,13 +946,12 @@ async function reviewAtris() {
851
946
  }
852
947
  }
853
948
 
854
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
855
- console.log('💡 Next: Run "atris do" to fix any issues, then "atris review" again');
856
- if (!showFull) {
857
- console.log(' Tip: `atris review --full` prints full spec/context for copy/paste.');
949
+ if (showFull) {
950
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
951
+ console.log('💡 Next: Run "atris do" to fix any issues, then "atris review" again');
952
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
953
+ console.log('');
858
954
  }
859
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
860
- console.log('');
861
955
 
862
956
  // Check execution mode
863
957
  if (executionMode === 'agent') {
@@ -981,19 +1075,25 @@ async function reviewAtris() {
981
1075
  const hasHandoff = /## Handoff[\s\S]*?\*\*Context:\*\*/.test(journalContent);
982
1076
 
983
1077
  if (hasCompletions && !hasHandoff) {
984
- console.log('');
985
- console.log('┌─────────────────────────────────────────────────────────────┐');
986
- console.log('│ 📝 SESSION HANDOFF │');
987
- console.log('├─────────────────────────────────────────────────────────────┤');
988
- console.log('│ You have completions today. Write a handoff for next session│');
989
- console.log('│ │');
990
- console.log('│ Add to ## Handoff section in today\'s journal: │');
991
- console.log('│ **Context:** [2 lines - what was accomplished] │');
992
- console.log('│ **Blockers:** [any issues hit, or "none"] │');
993
- console.log('│ **Next:** [1 clear action for next session] │');
994
- console.log('│ **Learned:** [key insight or pattern discovered] │');
995
- console.log('└─────────────────────────────────────────────────────────────┘');
996
- console.log('');
1078
+ if (showFull) {
1079
+ console.log('');
1080
+ console.log('┌─────────────────────────────────────────────────────────────┐');
1081
+ console.log('│ 📝 SESSION HANDOFF │');
1082
+ console.log('├─────────────────────────────────────────────────────────────┤');
1083
+ console.log('│ You have completions today. Write a handoff for next session│');
1084
+ console.log('│ │');
1085
+ console.log('│ Add to ## Handoff section in today\'s journal: │');
1086
+ console.log('│ **Context:** [2 lines - what was accomplished] │');
1087
+ console.log('│ **Blockers:** [any issues hit, or "none"] │');
1088
+ console.log('│ **Next:** [1 clear action for next session] │');
1089
+ console.log('│ **Learned:** [key insight or pattern discovered] │');
1090
+ console.log('└─────────────────────────────────────────────────────────────┘');
1091
+ console.log('');
1092
+ } else {
1093
+ console.log('');
1094
+ console.log('you have completions today. add a ## Handoff block to the journal (context / blockers / next / learned).');
1095
+ console.log('');
1096
+ }
997
1097
  }
998
1098
  }
999
1099
 
@@ -1001,10 +1101,14 @@ async function reviewAtris() {
1001
1101
  if (!process.stdin.isTTY) return;
1002
1102
 
1003
1103
  console.log('');
1004
- console.log('┌─────────────────────────────────────────────────────────────┐');
1005
- console.log('│ 💡 Any learnings? │');
1006
- console.log('│ (Enter insight, or press Enter to skip) │');
1007
- console.log('└─────────────────────────────────────────────────────────────┘');
1104
+ if (showFull) {
1105
+ console.log('┌─────────────────────────────────────────────────────────────┐');
1106
+ console.log('│ 💡 Any learnings? │');
1107
+ console.log('│ (Enter insight, or press Enter to skip) │');
1108
+ console.log('└─────────────────────────────────────────────────────────────┘');
1109
+ } else {
1110
+ console.log('any learnings? (enter to skip)');
1111
+ }
1008
1112
 
1009
1113
  const readline = require('readline');
1010
1114
  const rl = readline.createInterface({
@@ -1035,6 +1139,19 @@ async function reviewAtris() {
1035
1139
  console.log('');
1036
1140
  console.log(`✓ Logged to journal: ${learning}`);
1037
1141
  }
1142
+
1143
+ // Also log to structured learnings (if learnings module exists)
1144
+ try {
1145
+ const { addLearning } = require('../lib/learnings');
1146
+ const insight = answer.trim();
1147
+ // Auto-classify: starts with "don't" or "never" or "avoid" → pitfall, else pattern
1148
+ const type = /^(don't|never|avoid|watch out|careful)/i.test(insight) ? 'pitfall' : 'pattern';
1149
+ const key = insight.toLowerCase().replace(/[^a-z0-9\s]/g, '').split(/\s+/).slice(0, 4).join('-');
1150
+ addLearning({ type, key, insight, confidence: 7, source: 'review', files: [] });
1151
+ console.log(`✓ Saved to learnings: [7/10] ${type}/${key}`);
1152
+ } catch {
1153
+ // learnings module not available — skip silently
1154
+ }
1038
1155
  }
1039
1156
 
1040
1157
  console.log('');
@@ -63,7 +63,7 @@ async function cleanWorkspace() {
63
63
  workspaceId = businesses[slug].workspace_id;
64
64
  businessName = businesses[slug].name || slug;
65
65
  } else {
66
- const listResult = await apiRequestJson('/businesses/', { method: 'GET', token: creds.token });
66
+ const listResult = await apiRequestJson('/business/', { method: 'GET', token: creds.token });
67
67
  if (!listResult.ok) {
68
68
  console.error(`Failed to fetch businesses: ${listResult.error || listResult.status}`);
69
69
  process.exit(1);
@@ -100,7 +100,7 @@ async function cleanWorkspace() {
100
100
  console.log(`Scanning ${businessName}...`);
101
101
 
102
102
  const result = await apiRequestJson(
103
- `/businesses/${businessId}/workspaces/${workspaceId}/snapshot?include_content=false`,
103
+ `/business/${businessId}/workspaces/${workspaceId}/snapshot?include_content=false`,
104
104
  { method: 'GET', token: creds.token, timeoutMs: 60000 }
105
105
  );
106
106
 
@@ -220,7 +220,7 @@ async function cleanWorkspace() {
220
220
  const batch = filesToDelete.slice(i, i + BATCH_SIZE);
221
221
 
222
222
  const syncResult = await apiRequestJson(
223
- `/businesses/${businessId}/workspaces/${workspaceId}/sync`,
223
+ `/business/${businessId}/workspaces/${workspaceId}/sync`,
224
224
  {
225
225
  method: 'POST',
226
226
  token: creds.token,
@@ -0,0 +1,259 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { spawnSync } = require('child_process');
4
+
5
+ const RESULTS_HEADER = 'timestamp\ttrack\trepo\ttask\tstatus\tscore\treviewed\ttests\tartifacts\tinterventions\tnotes\n';
6
+
7
+ function readTextIfExists(filePath) {
8
+ if (!filePath || !fs.existsSync(filePath)) return null;
9
+ return fs.readFileSync(filePath, 'utf8');
10
+ }
11
+
12
+ function runGit(repoDir, args) {
13
+ if (!repoDir || !fs.existsSync(repoDir)) return null;
14
+ const result = spawnSync('git', ['-C', repoDir, ...args], { encoding: 'utf8' });
15
+ if (result.error || result.status !== 0) return null;
16
+ return (result.stdout || '').trim();
17
+ }
18
+
19
+ function getGitHead(repoDir) {
20
+ return runGit(repoDir, ['rev-parse', 'HEAD']);
21
+ }
22
+
23
+ function collectChangedFiles(repoDir, beforeSha, afterSha, prefix = '') {
24
+ const changed = new Set();
25
+
26
+ const addLines = (output) => {
27
+ if (!output) return;
28
+ output.split('\n')
29
+ .map((line) => line.trim())
30
+ .filter(Boolean)
31
+ .forEach((line) => changed.add(prefix + line));
32
+ };
33
+
34
+ if (beforeSha && afterSha && beforeSha !== afterSha) {
35
+ addLines(runGit(repoDir, ['diff', '--name-only', `${beforeSha}..${afterSha}`]));
36
+ }
37
+
38
+ addLines(runGit(repoDir, ['diff', '--name-only']));
39
+ addLines(runGit(repoDir, ['diff', '--cached', '--name-only']));
40
+ addLines(runGit(repoDir, ['ls-files', '--others', '--exclude-standard']));
41
+
42
+ return [...changed].sort();
43
+ }
44
+
45
+ function buildRunId(track) {
46
+ return `${new Date().toISOString().replace(/[:.]/g, '-')}-${track}`;
47
+ }
48
+
49
+ function buildWikiArtifact(beforeText, afterText) {
50
+ if (!beforeText && !afterText) {
51
+ return { status: 'missing' };
52
+ }
53
+
54
+ if (beforeText === afterText) {
55
+ return { status: 'not_applicable', before: beforeText || '', after: afterText || '' };
56
+ }
57
+
58
+ return { status: 'updated', before: beforeText || '', after: afterText || '' };
59
+ }
60
+
61
+ function summarizeReview(text) {
62
+ const clean = (text || '').trim().replace(/\s+/g, ' ');
63
+ if (!clean) return 'no review output captured';
64
+ return clean.length > 400 ? `${clean.slice(0, 397)}...` : clean;
65
+ }
66
+
67
+ function inferTestResults(text) {
68
+ const lines = (text || '').split('\n');
69
+ const matches = [];
70
+
71
+ for (const line of lines) {
72
+ if (!/(npm|pnpm|yarn|node\s+--test|pytest|python\s+-m|cargo test|go test)/i.test(line)) {
73
+ continue;
74
+ }
75
+
76
+ let status = 'not_run';
77
+ if (/\b(pass|passed|green|clean)\b/i.test(line)) status = 'pass';
78
+ if (/\b(fail|failed|error)\b/i.test(line)) status = 'fail';
79
+
80
+ matches.push({
81
+ command: line.trim(),
82
+ status,
83
+ });
84
+ }
85
+
86
+ if (matches.length > 0) return matches;
87
+
88
+ return [{
89
+ command: '(no explicit test command captured)',
90
+ status: 'not_run',
91
+ }];
92
+ }
93
+
94
+ function scoreEndstateArtifact(artifact) {
95
+ const breakdown = {
96
+ reviewed_completion: artifact.review?.status === 'pass' ? 40 : 0,
97
+ test_outcome: 0,
98
+ artifact_completeness: 0,
99
+ wiki_memory: artifact.wiki?.status === 'missing' ? 0 : 10,
100
+ operator_load: Math.max(0, 10 - ((artifact.interventions?.count || 0) * 2)),
101
+ };
102
+
103
+ const tests = Array.isArray(artifact.tests) ? artifact.tests : [];
104
+ const executed = tests.filter((test) => test.status !== 'not_run');
105
+ if (executed.length > 0) {
106
+ const passed = executed.filter((test) => test.status === 'pass').length;
107
+ breakdown.test_outcome = Math.round((passed / executed.length) * 25);
108
+ }
109
+
110
+ const required = [
111
+ 'run_id',
112
+ 'track',
113
+ 'repo_commits',
114
+ 'task_brief',
115
+ 'prompt_context',
116
+ 'changed_files',
117
+ 'tests',
118
+ 'review',
119
+ 'wiki',
120
+ 'elapsed_seconds',
121
+ 'interventions',
122
+ ];
123
+ const present = required.filter((key) => Object.prototype.hasOwnProperty.call(artifact, key)).length;
124
+ breakdown.artifact_completeness = Math.round((present / required.length) * 15);
125
+
126
+ const total = Object.values(breakdown).reduce((sum, value) => sum + value, 0);
127
+ return { total, breakdown };
128
+ }
129
+
130
+ function getArtifactScore(artifact) {
131
+ if (typeof artifact?.score === 'number') return artifact.score;
132
+ return scoreEndstateArtifact(artifact).total;
133
+ }
134
+
135
+ function getReviewRank(artifact) {
136
+ const status = artifact?.review?.status;
137
+ if (status === 'pass') return 2;
138
+ if (status === 'draft') return 1;
139
+ return 0;
140
+ }
141
+
142
+ function readLatestArtifact(packDir) {
143
+ const artifactsDir = path.join(packDir, 'artifacts');
144
+ if (!fs.existsSync(artifactsDir)) {
145
+ throw new Error(`No artifacts found at ${artifactsDir}`);
146
+ }
147
+
148
+ const files = fs.readdirSync(artifactsDir)
149
+ .filter((file) => file.endsWith('.json'))
150
+ .sort();
151
+
152
+ if (files.length === 0) {
153
+ throw new Error(`No artifact JSON files found at ${artifactsDir}`);
154
+ }
155
+
156
+ const filePath = path.join(artifactsDir, files[files.length - 1]);
157
+ return {
158
+ filePath,
159
+ artifact: JSON.parse(fs.readFileSync(filePath, 'utf8')),
160
+ };
161
+ }
162
+
163
+ function compareEndstateArtifacts(baselineEntry, stackEntry) {
164
+ const baselineScore = getArtifactScore(baselineEntry.artifact);
165
+ const stackScore = getArtifactScore(stackEntry.artifact);
166
+ const baselineReview = getReviewRank(baselineEntry.artifact);
167
+ const stackReview = getReviewRank(stackEntry.artifact);
168
+
169
+ const stackWins = stackScore > baselineScore && stackReview >= baselineReview;
170
+ const leader = stackScore === baselineScore
171
+ ? 'tie'
172
+ : (stackScore > baselineScore ? 'stack' : 'baseline');
173
+
174
+ let reason = '';
175
+ if (stackWins) {
176
+ reason = `Stack leads ${stackScore} to ${baselineScore} and does not lose reviewed completion.`;
177
+ } else if (stackScore === baselineScore) {
178
+ reason = `Scores are tied at ${stackScore}/100. The stack must beat the baseline on total score.`;
179
+ } else if (stackScore < baselineScore) {
180
+ reason = `Baseline leads ${baselineScore} to ${stackScore}. The stack must beat the baseline on total score.`;
181
+ } else {
182
+ reason = 'The stack improved total score but lost reviewed completion, so it does not clear the Level 1 rule.';
183
+ }
184
+
185
+ return {
186
+ winner: stackWins ? 'stack' : 'none',
187
+ leader,
188
+ reason,
189
+ baselineScore,
190
+ stackScore,
191
+ };
192
+ }
193
+
194
+ function ensureResultsFile(resultsPath) {
195
+ if (!fs.existsSync(resultsPath)) {
196
+ fs.writeFileSync(resultsPath, RESULTS_HEADER, 'utf8');
197
+ return;
198
+ }
199
+
200
+ const content = fs.readFileSync(resultsPath, 'utf8');
201
+ if (!content.trim()) {
202
+ fs.writeFileSync(resultsPath, RESULTS_HEADER, 'utf8');
203
+ }
204
+ }
205
+
206
+ function toTsvField(value) {
207
+ return String(value ?? '')
208
+ .replace(/\t/g, ' ')
209
+ .replace(/\r?\n/g, ' ')
210
+ .trim();
211
+ }
212
+
213
+ function appendResultsRow(resultsPath, artifactPath, artifact, score) {
214
+ ensureResultsFile(resultsPath);
215
+
216
+ const tests = Array.isArray(artifact.tests) ? artifact.tests : [];
217
+ const passed = tests.filter((test) => test.status === 'pass').length;
218
+ const ran = tests.filter((test) => test.status !== 'not_run').length;
219
+
220
+ const row = [
221
+ new Date().toISOString(),
222
+ artifact.track,
223
+ 'atris-cli+atrisos-backend',
224
+ artifact.task_brief,
225
+ artifact.review?.status || 'draft',
226
+ score.total,
227
+ artifact.review?.status === 'pass' ? 'yes' : 'no',
228
+ ran > 0 ? `${passed}/${ran} pass` : 'not-run',
229
+ path.relative(path.dirname(resultsPath), artifactPath),
230
+ artifact.interventions?.count || 0,
231
+ artifact.notes || '',
232
+ ].map(toTsvField).join('\t');
233
+
234
+ fs.appendFileSync(resultsPath, `${row}\n`, 'utf8');
235
+ }
236
+
237
+ function writeArtifact(packDir, artifact) {
238
+ const artifactsDir = path.join(packDir, 'artifacts');
239
+ fs.mkdirSync(artifactsDir, { recursive: true });
240
+ const filePath = path.join(artifactsDir, `${artifact.run_id}.json`);
241
+ fs.writeFileSync(filePath, JSON.stringify(artifact, null, 2), 'utf8');
242
+ return filePath;
243
+ }
244
+
245
+ module.exports = {
246
+ appendResultsRow,
247
+ buildRunId,
248
+ buildWikiArtifact,
249
+ compareEndstateArtifacts,
250
+ collectChangedFiles,
251
+ getArtifactScore,
252
+ getGitHead,
253
+ inferTestResults,
254
+ readLatestArtifact,
255
+ readTextIfExists,
256
+ scoreEndstateArtifact,
257
+ summarizeReview,
258
+ writeArtifact,
259
+ };