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.
- package/README.md +124 -34
- package/atris/CLAUDE.md +5 -1
- package/atris/atris.md +4 -0
- package/atris/features/README.md +24 -0
- package/atris/skills/autopilot/SKILL.md +74 -75
- package/atris/skills/endgame/SKILL.md +179 -0
- package/atris/skills/flow/SKILL.md +121 -0
- package/atris/skills/improve/SKILL.md +84 -0
- package/atris/skills/loop/SKILL.md +72 -0
- package/atris/skills/wiki/SKILL.md +61 -0
- package/atris/team/executor/MEMBER.md +10 -4
- package/atris/team/navigator/MEMBER.md +2 -0
- package/atris/team/validator/MEMBER.md +8 -5
- package/atris.md +33 -0
- package/bin/atris.js +210 -41
- package/commands/activate.js +28 -2
- package/commands/align.js +720 -0
- package/commands/auth.js +75 -2
- package/commands/autopilot.js +1213 -270
- package/commands/browse.js +100 -0
- package/commands/business.js +785 -12
- package/commands/clean.js +107 -2
- package/commands/computer.js +429 -0
- package/commands/context-sync.js +78 -8
- package/commands/experiments.js +351 -0
- package/commands/feedback.js +150 -0
- package/commands/fleet.js +395 -0
- package/commands/fork.js +127 -0
- package/commands/init.js +50 -1
- package/commands/learn.js +407 -0
- package/commands/lifecycle.js +94 -0
- package/commands/loop.js +114 -0
- package/commands/publish.js +129 -0
- package/commands/pull.js +434 -48
- package/commands/push.js +312 -164
- package/commands/review.js +149 -0
- package/commands/run.js +76 -43
- package/commands/serve.js +360 -0
- package/commands/setup.js +1 -1
- package/commands/soul.js +381 -0
- package/commands/status.js +119 -1
- package/commands/sync.js +147 -1
- package/commands/terminal.js +201 -0
- package/commands/wiki.js +376 -0
- package/commands/workflow.js +191 -74
- package/commands/workspace-clean.js +3 -3
- package/lib/endstate.js +259 -0
- package/lib/learnings.js +235 -0
- package/lib/manifest.js +1 -0
- package/lib/todo.js +9 -5
- package/lib/wiki.js +578 -0
- package/package.json +2 -2
- package/utils/api.js +48 -36
- package/utils/auth.js +1 -0
package/commands/workflow.js
CHANGED
|
@@ -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) =>
|
|
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
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
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
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
console.log('
|
|
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
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
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
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
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('/
|
|
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
|
-
`/
|
|
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
|
-
`/
|
|
223
|
+
`/business/${businessId}/workspaces/${workspaceId}/sync`,
|
|
224
224
|
{
|
|
225
225
|
method: 'POST',
|
|
226
226
|
token: creds.token,
|
package/lib/endstate.js
ADDED
|
@@ -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
|
+
};
|