atris 3.35.0 → 3.37.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/AGENTS.md +37 -0
- package/README.md +5 -3
- package/atris/GETTING_STARTED.md +1 -1
- package/atris/atris.md +3 -0
- package/atris/policies/day-loop-voice.md +102 -0
- package/atris/policies/outbound-artifact-gate.md +2 -0
- package/atris/skills/design/SKILL.md +56 -32
- package/atris/skills/endgame/SKILL.md +12 -6
- package/atris/skills/engines/SKILL.md +22 -4
- package/atris/skills/fable-method/SKILL.md +66 -0
- package/atris/skills/improve/SKILL.md +65 -45
- package/atris/skills/youtube/SKILL.md +10 -1
- package/atris.md +2 -0
- package/ax +147 -19
- package/bin/atris.js +576 -267
- package/commands/activate.js +194 -88
- package/commands/agents.js +166 -0
- package/commands/autoland.js +459 -107
- package/commands/autopilot-front.js +20 -2
- package/commands/autopilot.js +118 -2
- package/commands/avail.js +407 -0
- package/commands/bench.js +188 -0
- package/commands/brain.js +3 -0
- package/commands/brief.js +651 -0
- package/commands/business-sync.js +192 -6
- package/commands/clean.js +50 -24
- package/commands/close.js +1083 -0
- package/commands/cloud.js +245 -0
- package/commands/compile.js +292 -1
- package/commands/computer.js +150 -3
- package/commands/dream.js +365 -0
- package/commands/drill.js +371 -0
- package/commands/engine.js +993 -32
- package/commands/experiments.js +28 -0
- package/commands/feedback.js +34 -12
- package/commands/fleet-report.js +206 -0
- package/commands/gm.js +23 -0
- package/commands/goal.js +247 -0
- package/commands/improve.js +642 -26
- package/commands/init.js +72 -44
- package/commands/interview.js +67 -1
- package/commands/land.js +152 -52
- package/commands/lifecycle.js +39 -3
- package/commands/log.js +84 -1
- package/commands/loops.js +220 -16
- package/commands/meet.js +220 -0
- package/commands/member.js +511 -34
- package/commands/mission.js +3144 -338
- package/commands/next.js +137 -0
- package/commands/now.js +220 -25
- package/commands/one-lap.js +776 -0
- package/commands/orb.js +314 -0
- package/commands/pack-craft.js +179 -0
- package/commands/pack.js +823 -0
- package/commands/play.js +3 -2
- package/commands/probe.js +30 -3
- package/commands/pulse.js +241 -46
- package/commands/push.js +260 -82
- package/commands/rainmaker.js +49 -0
- package/commands/report.js +415 -0
- package/commands/scout.js +147 -0
- package/commands/search.js +363 -0
- package/commands/skill.js +47 -3
- package/commands/slop.js +50 -2
- package/commands/soul.js +1 -1
- package/commands/stream.js +861 -0
- package/commands/study.js +693 -0
- package/commands/sync.js +67 -54
- package/commands/task.js +1346 -117
- package/commands/team.js +73 -0
- package/commands/verify.js +96 -0
- package/commands/watch.js +303 -0
- package/commands/wish.js +500 -0
- package/commands/workflow.js +11 -5
- package/commands/worktree.js +234 -13
- package/commands/xp.js +29 -11
- package/lib/auto-accept-certified.js +331 -34
- package/lib/autoland.js +319 -54
- package/lib/ax-auto-lane.js +96 -0
- package/lib/bench/context.js +147 -0
- package/lib/bench/engines.js +141 -0
- package/lib/bench/report.js +140 -0
- package/lib/bench/runner.js +512 -0
- package/lib/brief-ledger.js +350 -0
- package/lib/cloud-mission.js +259 -0
- package/lib/codex-flight.js +154 -0
- package/lib/default-runner.js +45 -0
- package/lib/default-verifier.js +70 -0
- package/lib/engine-registry.js +232 -0
- package/lib/experiments/daily.js +640 -0
- package/lib/fleet.js +2219 -67
- package/lib/improve-vitals-html.js +171 -0
- package/lib/known-commands.js +58 -0
- package/lib/loop-doctor.js +416 -0
- package/lib/member-switches.js +144 -0
- package/lib/mission-room.js +1 -0
- package/lib/mission-root.js +52 -0
- package/lib/next-moves.js +327 -10
- package/lib/one-lap-validator.js +60 -0
- package/lib/orb-context.js +477 -0
- package/lib/orb-scorecard.js +224 -0
- package/lib/policy-lessons.js +52 -1
- package/lib/pulse.js +277 -3
- package/lib/receipt-block.js +168 -0
- package/lib/receipt-evidence.js +65 -4
- package/lib/router-brain.js +352 -0
- package/lib/runner-command.js +10 -0
- package/lib/self-drive.js +258 -0
- package/lib/short-name.js +103 -0
- package/lib/spawn-env.js +18 -0
- package/lib/state-detection.js +56 -1
- package/lib/sync-status.js +59 -0
- package/lib/task-db.js +108 -29
- package/lib/task-proof.js +23 -1
- package/lib/team-presence.js +260 -0
- package/lib/tool-result-encode.js +7 -0
- package/lib/trust-tiers.js +90 -0
- package/lib/usage.js +107 -0
- package/lib/voice-gate.js +163 -0
- package/lib/wish-audit.js +1368 -0
- package/lib/wish-delegate.js +1840 -0
- package/lib/wish-design.js +110 -0
- package/lib/wish-stats.js +183 -0
- package/lib/wish-store.js +354 -0
- package/lib/zip.js +221 -0
- package/package.json +3 -1
- package/templates/loops/atris/loops/LOOPS.md +55 -0
- package/templates/loops/atris/loops/TICK.md +24 -0
- package/templates/loops/atris/loops/feedback.md +22 -0
- package/templates/loops/atris/loops/quality.md +22 -0
- package/templates/loops/atris/wiki/systems/loops.md +41 -0
- package/utils/api.js +5 -1
- package/utils/auth.js +57 -21
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const DESIGN_BRIEF_PATHS = [
|
|
7
|
+
path.join('atris', 'skills', 'design', 'SKILL.md'),
|
|
8
|
+
path.join('atris', 'policies', 'atris-design.md'),
|
|
9
|
+
path.join('.atris', 'theme.json'),
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const FRONTEND_WISH_RE = /\b(ui|page|design|frontend|component|style|theme|landing|dashboard|layout|css|button|font|color|hero|nav|cards?)\b/i;
|
|
13
|
+
const NEGATIVE_COLLOCATION_RE = /\b(?:code\s+style|page\s+size|button\s+up)\b/i;
|
|
14
|
+
const BACKEND_CONTEXT_RE = /\b(?:api|json|endpoint|terminal|log|logs|deploy|release|script|build|pipeline|step|compress|minify|server|cache|config|webhook|payloads?|secrets?)\b/i;
|
|
15
|
+
const PATH_CONTEXT_RE = /(?:^|\s)(?:lib|bin|scripts)\/|\b[A-Za-z0-9_.-]+\/[A-Za-z0-9_.\/-]+\b/;
|
|
16
|
+
const WEAK_FRONTEND_RE = /\b(?:page|design|style|theme|screen|button|font|color|landing)\b/i;
|
|
17
|
+
const WEAK_ASSET_SURFACE_RE = /\b(?:cards?|images?)\b/i;
|
|
18
|
+
const VISUAL_PERCEPTION_RE = /\b(?:cramped|spacing|padding|whitespace|dense|crowded|looks?\s+like|looks|boring|bland|ugly|prettier|pretty|beautiful|gorgeous|plain|breathing\s+room|washes?\s+out)\b/i;
|
|
19
|
+
const UI_SURFACE_RE = /\b(?:bar|headers?|footers?|toolbar|sidebar|modal|dropdown|tooltip|screen|website|site|mobile|viewport|cards?|charts?)\b/i;
|
|
20
|
+
const SOLID_UI_SURFACE_RE = /\b(?:bar|headers?|footers?|toolbar|sidebar|modal|dropdown|tooltip|website|site|mobile|viewport|cards?|charts?)\b/i;
|
|
21
|
+
const STATE_COLLOCATION_RE = /\b(?:empty|loading|error)\s+state\b/i;
|
|
22
|
+
const STRONG_FRONTEND_RE = /\b(?:ui|frontend|component|dashboard|layout|css|hero|nav)\b/i;
|
|
23
|
+
const FOLD_COLLOCATION_RE = /\b(?:above\s+the\s+fold|below\s+the\s+fold|the\s+fold|fold)\b/i;
|
|
24
|
+
const MODE_COLLOCATION_RE = /\bdark\s+mode\b/i;
|
|
25
|
+
|
|
26
|
+
function toPosixPath(value) {
|
|
27
|
+
return String(value || '').split(path.sep).join('/');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readPackageJson(filePath) {
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function detectDesignContext(workspaceRoot = process.cwd()) {
|
|
39
|
+
const root = path.resolve(workspaceRoot || process.cwd());
|
|
40
|
+
const briefPaths = [];
|
|
41
|
+
|
|
42
|
+
for (const relativePath of DESIGN_BRIEF_PATHS) {
|
|
43
|
+
if (fs.existsSync(path.join(root, relativePath))) briefPaths.push(toPosixPath(relativePath));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const packagePath = path.join(root, 'package.json');
|
|
47
|
+
let auditCommand = null;
|
|
48
|
+
if (fs.existsSync(packagePath)) {
|
|
49
|
+
const manifest = readPackageJson(packagePath);
|
|
50
|
+
const scripts = manifest && typeof manifest === 'object' ? manifest.scripts : null;
|
|
51
|
+
if (scripts && Object.prototype.hasOwnProperty.call(scripts, 'audit:design')) {
|
|
52
|
+
briefPaths.push('package.json');
|
|
53
|
+
auditCommand = 'npm run audit:design';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
hasDesign: briefPaths.length > 0,
|
|
59
|
+
briefPaths,
|
|
60
|
+
auditCommand,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isFrontendWish(text) {
|
|
65
|
+
const raw = String(text || '');
|
|
66
|
+
if (!raw.trim()) return false;
|
|
67
|
+
if (NEGATIVE_COLLOCATION_RE.test(raw)) return false;
|
|
68
|
+
if (/\blanding\b(?!\s+page\b)/i.test(raw)) return false;
|
|
69
|
+
|
|
70
|
+
const weakMatches = raw.match(new RegExp(WEAK_FRONTEND_RE.source, 'gi')) || [];
|
|
71
|
+
const visualPositive = VISUAL_PERCEPTION_RE.test(raw) || FOLD_COLLOCATION_RE.test(raw) || MODE_COLLOCATION_RE.test(raw);
|
|
72
|
+
const backendContext = BACKEND_CONTEXT_RE.test(raw) || PATH_CONTEXT_RE.test(raw);
|
|
73
|
+
if (backendContext
|
|
74
|
+
&& WEAK_ASSET_SURFACE_RE.test(raw)
|
|
75
|
+
&& !STRONG_FRONTEND_RE.test(raw)
|
|
76
|
+
&& !visualPositive
|
|
77
|
+
&& !STATE_COLLOCATION_RE.test(raw)
|
|
78
|
+
&& !/\blanding\s+page\b/i.test(raw)) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
const weakOnly = weakMatches.length > 0
|
|
82
|
+
&& !STRONG_FRONTEND_RE.test(raw)
|
|
83
|
+
&& !visualPositive
|
|
84
|
+
&& !UI_SURFACE_RE.test(raw)
|
|
85
|
+
&& !STATE_COLLOCATION_RE.test(raw)
|
|
86
|
+
&& !/\blanding\s+page\b/i.test(raw);
|
|
87
|
+
if (weakOnly && backendContext) return false;
|
|
88
|
+
const backendWeakOnly = weakMatches.length > 0
|
|
89
|
+
&& backendContext
|
|
90
|
+
&& !STRONG_FRONTEND_RE.test(raw)
|
|
91
|
+
&& !visualPositive
|
|
92
|
+
&& !SOLID_UI_SURFACE_RE.test(raw)
|
|
93
|
+
&& !STATE_COLLOCATION_RE.test(raw)
|
|
94
|
+
&& !/\blanding\s+page\b/i.test(raw);
|
|
95
|
+
if (backendWeakOnly) return false;
|
|
96
|
+
|
|
97
|
+
return STATE_COLLOCATION_RE.test(raw)
|
|
98
|
+
|| visualPositive
|
|
99
|
+
|| UI_SURFACE_RE.test(raw)
|
|
100
|
+
|| FOLD_COLLOCATION_RE.test(raw)
|
|
101
|
+
|| MODE_COLLOCATION_RE.test(raw)
|
|
102
|
+
|| /\blanding\s+page\b/i.test(raw)
|
|
103
|
+
|| STRONG_FRONTEND_RE.test(raw)
|
|
104
|
+
|| FRONTEND_WISH_RE.test(raw);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = {
|
|
108
|
+
detectDesignContext,
|
|
109
|
+
isFrontendWish,
|
|
110
|
+
};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { readWishEvents, readWishes } = require('./wish-store');
|
|
6
|
+
|
|
7
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
8
|
+
const WEEK_MS = 7 * DAY_MS;
|
|
9
|
+
|
|
10
|
+
function parseTime(value) {
|
|
11
|
+
const parsed = Date.parse(value || '');
|
|
12
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function countInWindow(items, now, startOffset, endOffset, timeFn) {
|
|
16
|
+
const start = now - endOffset;
|
|
17
|
+
const end = now - startOffset;
|
|
18
|
+
return items.filter((item) => {
|
|
19
|
+
const time = timeFn(item);
|
|
20
|
+
return time !== null && time >= start && time < end;
|
|
21
|
+
}).length;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function wishCreatedTime(wish) {
|
|
25
|
+
return parseTime(wish && (wish.first_ts || wish.ts));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function reviewTime(review) {
|
|
29
|
+
return parseTime(review && review.ts);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isShippedWish(wish) {
|
|
33
|
+
const status = String(wish && wish.status || '').trim();
|
|
34
|
+
const verifyStatus = String(wish && wish.verify_status || '').trim();
|
|
35
|
+
return status === 'complete'
|
|
36
|
+
|| status === 'completed'
|
|
37
|
+
|| ['verified', 'passed', 'success'].includes(verifyStatus);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function formatRatio(top, bottom) {
|
|
41
|
+
if (!bottom) return '0.00';
|
|
42
|
+
return (top / bottom).toFixed(2);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function formatAverage(scores) {
|
|
46
|
+
if (!scores.length) return 'n/a';
|
|
47
|
+
const value = scores.reduce((sum, score) => sum + score, 0) / scores.length;
|
|
48
|
+
return Number.isInteger(value) ? String(value) : value.toFixed(2);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function scoresInWindow(reviews, now, startOffset, endOffset) {
|
|
52
|
+
const start = now - endOffset;
|
|
53
|
+
const end = now - startOffset;
|
|
54
|
+
return reviews
|
|
55
|
+
.filter((review) => {
|
|
56
|
+
const time = reviewTime(review);
|
|
57
|
+
return time !== null && time >= start && time < end;
|
|
58
|
+
})
|
|
59
|
+
.map((review) => review.review_score)
|
|
60
|
+
.filter((score) => score !== undefined && score !== null && String(score).trim() !== '')
|
|
61
|
+
.map((score) => Number(score))
|
|
62
|
+
.filter((score) => Number.isFinite(score));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readJsonFile(file) {
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function missionReceiptFiles(root = process.cwd()) {
|
|
74
|
+
const runsDir = path.join(root, 'atris', 'runs');
|
|
75
|
+
try {
|
|
76
|
+
return fs.readdirSync(runsDir, { withFileTypes: true })
|
|
77
|
+
.filter((entry) => entry.isFile() && /^mission-.*\.json$/.test(entry.name))
|
|
78
|
+
.map((entry) => path.join(runsDir, entry.name));
|
|
79
|
+
} catch {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function verifierPassed(receipt) {
|
|
85
|
+
const bools = [
|
|
86
|
+
receipt && receipt.verifier_result && receipt.verifier_result.passed,
|
|
87
|
+
receipt && receipt.result && receipt.result.verifier_result && receipt.result.verifier_result.passed,
|
|
88
|
+
receipt && receipt.result && receipt.result.tick && receipt.result.tick.verifier_passed,
|
|
89
|
+
receipt && receipt.result && receipt.result.passed,
|
|
90
|
+
];
|
|
91
|
+
for (const value of bools) {
|
|
92
|
+
if (typeof value === 'boolean') return value;
|
|
93
|
+
}
|
|
94
|
+
const statuses = [
|
|
95
|
+
receipt && receipt.verifier_result && receipt.verifier_result.status,
|
|
96
|
+
receipt && receipt.result && receipt.result.verifier_result && receipt.result.verifier_result.status,
|
|
97
|
+
];
|
|
98
|
+
for (const value of statuses) {
|
|
99
|
+
const status = Number(value);
|
|
100
|
+
if (Number.isInteger(status)) return status === 0;
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function haikuRates(root = process.cwd()) {
|
|
106
|
+
const rates = {
|
|
107
|
+
haiku: { passed: 0, total: 0 },
|
|
108
|
+
other: { passed: 0, total: 0 },
|
|
109
|
+
};
|
|
110
|
+
for (const file of missionReceiptFiles(root)) {
|
|
111
|
+
const receipt = readJsonFile(file);
|
|
112
|
+
if (!receipt || (receipt.schema && receipt.schema !== 'atris.mission_receipt.v1')) continue;
|
|
113
|
+
const runner = String(receipt.result && receipt.result.frozen && receipt.result.frozen.runner || '').trim().toLowerCase();
|
|
114
|
+
if (!runner) continue;
|
|
115
|
+
const bucket = runner === 'haiku' ? rates.haiku : rates.other;
|
|
116
|
+
bucket.total += 1;
|
|
117
|
+
if (verifierPassed(receipt)) bucket.passed += 1;
|
|
118
|
+
}
|
|
119
|
+
return rates;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function buildWishStats(root = process.cwd(), options = {}) {
|
|
123
|
+
const now = Number.isFinite(options.now) ? options.now : Date.now();
|
|
124
|
+
const wishes = readWishes(root);
|
|
125
|
+
const reviews = readWishEvents(root).filter((event) => event && event.kind === 'review');
|
|
126
|
+
const shipped = wishes.filter(isShippedWish);
|
|
127
|
+
const thisWeekWishes = countInWindow(wishes, now, 0, WEEK_MS, wishCreatedTime);
|
|
128
|
+
const priorWeekWishes = countInWindow(wishes, now, WEEK_MS, WEEK_MS * 2, wishCreatedTime);
|
|
129
|
+
const thisWeekScores = scoresInWindow(reviews, now, 0, WEEK_MS);
|
|
130
|
+
const priorWeekScores = scoresInWindow(reviews, now, WEEK_MS, WEEK_MS * 2);
|
|
131
|
+
const rates = haikuRates(root);
|
|
132
|
+
const lines = [
|
|
133
|
+
`wishes this week: ${thisWeekWishes}, prior week ${priorWeekWishes}`,
|
|
134
|
+
`reviews per shipped wish: ${formatRatio(reviews.length, shipped.length)}`,
|
|
135
|
+
`average score trend: ${formatAverage(thisWeekScores)} this week, ${formatAverage(priorWeekScores)} prior week`,
|
|
136
|
+
`haiku pass rate: ${rates.haiku.passed}/${rates.haiku.total}, all other runners ${rates.other.passed}/${rates.other.total}`,
|
|
137
|
+
];
|
|
138
|
+
return {
|
|
139
|
+
lines,
|
|
140
|
+
wishes_this_week: thisWeekWishes,
|
|
141
|
+
wishes_prior_week: priorWeekWishes,
|
|
142
|
+
reviews_per_shipped_wish: formatRatio(reviews.length, shipped.length),
|
|
143
|
+
average_score_this_week: formatAverage(thisWeekScores),
|
|
144
|
+
average_score_prior_week: formatAverage(priorWeekScores),
|
|
145
|
+
haiku: rates.haiku,
|
|
146
|
+
other: rates.other,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function localDateName(date = new Date()) {
|
|
151
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function wishStatsLogPath(root = process.cwd(), date = new Date()) {
|
|
155
|
+
return path.join(root, 'atris', 'logs', String(date.getFullYear()), `${localDateName(date)}.md`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function writeWishStats(root = process.cwd(), lines = [], options = {}) {
|
|
159
|
+
const date = options.date instanceof Date ? options.date : new Date();
|
|
160
|
+
const file = wishStatsLogPath(root, date);
|
|
161
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
162
|
+
const existing = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
|
|
163
|
+
const prefix = existing.trim() ? '\n\n' : '';
|
|
164
|
+
fs.appendFileSync(file, `${prefix}## Wish stats\n${lines.join('\n')}\n`, 'utf8');
|
|
165
|
+
return file;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function printWishStats(root = process.cwd(), options = {}) {
|
|
169
|
+
const stats = buildWishStats(root, options);
|
|
170
|
+
if (options.write) writeWishStats(root, stats.lines, options);
|
|
171
|
+
console.log(stats.lines.join('\n'));
|
|
172
|
+
return 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = {
|
|
176
|
+
buildWishStats,
|
|
177
|
+
haikuRates,
|
|
178
|
+
missionReceiptFiles,
|
|
179
|
+
printWishStats,
|
|
180
|
+
verifierPassed,
|
|
181
|
+
wishStatsLogPath,
|
|
182
|
+
writeWishStats,
|
|
183
|
+
};
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { nextRecordNumber } = require('./short-name');
|
|
7
|
+
|
|
8
|
+
function stateFile(root = process.cwd()) {
|
|
9
|
+
return path.join(root, '.atris', 'state', 'wishes.jsonl');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function stampIso() {
|
|
13
|
+
return new Date().toISOString();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function todayName(date = new Date()) {
|
|
17
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function slugify(value) {
|
|
21
|
+
return String(value || 'wish')
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
24
|
+
.replace(/^-+|-+$/g, '')
|
|
25
|
+
.slice(0, 36) || 'wish';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function wishId(text, ts = stampIso()) {
|
|
29
|
+
const hash = crypto.createHash('sha1').update(`${text}:${ts}`).digest('hex').slice(0, 8);
|
|
30
|
+
return `wish-${todayName(new Date(ts))}-${slugify(text)}-${hash}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function appendJsonLine(file, record) {
|
|
34
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
35
|
+
fs.appendFileSync(file, `${JSON.stringify(record)}\n`, 'utf8');
|
|
36
|
+
return file;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function appendWishRecord(root, record) {
|
|
40
|
+
return appendJsonLine(stateFile(root), record);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function nextWishNumber(root = process.cwd()) {
|
|
44
|
+
return nextRecordNumber(readJsonLines(stateFile(root)));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readJsonLines(file) {
|
|
48
|
+
if (!fs.existsSync(file)) return [];
|
|
49
|
+
return fs.readFileSync(file, 'utf8')
|
|
50
|
+
.split(/\r?\n/)
|
|
51
|
+
.map((line) => line.trim())
|
|
52
|
+
.filter(Boolean)
|
|
53
|
+
.map((line) => {
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(line);
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
.filter(Boolean);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function eventAnswers(event) {
|
|
64
|
+
const answers = [];
|
|
65
|
+
for (const key of ['answer', 'answer_text', 'grant_answer']) {
|
|
66
|
+
const value = event && event[key];
|
|
67
|
+
if (String(value || '').trim()) answers.push(String(value));
|
|
68
|
+
}
|
|
69
|
+
if (Array.isArray(event && event.answers)) {
|
|
70
|
+
answers.push(...event.answers.map(String).filter((value) => value.trim()));
|
|
71
|
+
}
|
|
72
|
+
return answers;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function readWishEvents(root = process.cwd()) {
|
|
76
|
+
return readJsonLines(stateFile(root));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function readWishes(root = process.cwd()) {
|
|
80
|
+
const byId = new Map();
|
|
81
|
+
for (const event of readWishEvents(root)) {
|
|
82
|
+
if (event && event.kind === 'steer') {
|
|
83
|
+
const wishId = String(event.wish_id || '').trim();
|
|
84
|
+
if (!wishId) continue;
|
|
85
|
+
const current = byId.get(wishId) || {
|
|
86
|
+
id: wishId,
|
|
87
|
+
ts: event.ts,
|
|
88
|
+
text: '',
|
|
89
|
+
answers: [],
|
|
90
|
+
reviews: [],
|
|
91
|
+
steers: [],
|
|
92
|
+
first_ts: event.ts,
|
|
93
|
+
};
|
|
94
|
+
const steer = {
|
|
95
|
+
ts: event.ts,
|
|
96
|
+
note: event.note || '',
|
|
97
|
+
steered_by: event.steered_by || null,
|
|
98
|
+
mission_id: event.mission_id || null,
|
|
99
|
+
};
|
|
100
|
+
const steers = [...(current.steers || []), steer];
|
|
101
|
+
byId.set(wishId, {
|
|
102
|
+
...current,
|
|
103
|
+
steers,
|
|
104
|
+
steer_count: steers.length,
|
|
105
|
+
latest_steer: steer,
|
|
106
|
+
});
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (event && event.kind === 'review') {
|
|
110
|
+
const wishId = String(event.wish_id || '').trim();
|
|
111
|
+
if (!wishId) continue;
|
|
112
|
+
const current = byId.get(wishId) || {
|
|
113
|
+
id: wishId,
|
|
114
|
+
ts: event.ts,
|
|
115
|
+
text: '',
|
|
116
|
+
answers: [],
|
|
117
|
+
reviews: [],
|
|
118
|
+
steers: [],
|
|
119
|
+
first_ts: event.ts,
|
|
120
|
+
};
|
|
121
|
+
const review = {
|
|
122
|
+
ts: event.ts,
|
|
123
|
+
review_text: event.review_text || '',
|
|
124
|
+
review_score: event.review_score === undefined ? null : event.review_score,
|
|
125
|
+
reviewed_by: event.reviewed_by || null,
|
|
126
|
+
};
|
|
127
|
+
const reviews = [...(current.reviews || []), review];
|
|
128
|
+
byId.set(wishId, {
|
|
129
|
+
...current,
|
|
130
|
+
reviews,
|
|
131
|
+
reviewed: true,
|
|
132
|
+
review_count: reviews.length,
|
|
133
|
+
latest_review: review,
|
|
134
|
+
});
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (!event || !event.id) continue;
|
|
138
|
+
const current = byId.get(event.id) || {
|
|
139
|
+
id: event.id,
|
|
140
|
+
ts: event.ts,
|
|
141
|
+
text: event.text || '',
|
|
142
|
+
answers: [],
|
|
143
|
+
reviews: [],
|
|
144
|
+
steers: [],
|
|
145
|
+
first_ts: event.ts,
|
|
146
|
+
};
|
|
147
|
+
const next = {
|
|
148
|
+
...current,
|
|
149
|
+
...event,
|
|
150
|
+
first_ts: current.first_ts || event.ts,
|
|
151
|
+
answers: current.answers || [],
|
|
152
|
+
reviews: current.reviews || [],
|
|
153
|
+
steers: current.steers || [],
|
|
154
|
+
};
|
|
155
|
+
const answers = eventAnswers(event);
|
|
156
|
+
if (answers.length) next.answers = [...next.answers, ...answers];
|
|
157
|
+
byId.set(event.id, next);
|
|
158
|
+
}
|
|
159
|
+
return Array.from(byId.values())
|
|
160
|
+
.sort((a, b) => String(a.first_ts || a.ts || '').localeCompare(String(b.first_ts || b.ts || '')));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function improveCursorFile(root = process.cwd()) {
|
|
164
|
+
return path.join(root, '.atris', 'state', 'wish-improve.cursor.json');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function lessonsFile(root = process.cwd()) {
|
|
168
|
+
return path.join(root, '.claude', 'skills', 'wish', 'LESSONS.md');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const LESSONS_HEADER = [
|
|
172
|
+
'# Wish lessons',
|
|
173
|
+
'',
|
|
174
|
+
'Reviews from `atris wish review` land here via `atris wish improve`.',
|
|
175
|
+
'The model running the wish skill distills raw inbox entries into one-line lessons, then deletes the raw entries.',
|
|
176
|
+
'',
|
|
177
|
+
'## Lessons',
|
|
178
|
+
'',
|
|
179
|
+
'## Review inbox (raw, distill me)',
|
|
180
|
+
'',
|
|
181
|
+
].join('\n');
|
|
182
|
+
|
|
183
|
+
function readImproveCursor(root = process.cwd()) {
|
|
184
|
+
try {
|
|
185
|
+
const parsed = JSON.parse(fs.readFileSync(improveCursorFile(root), 'utf8'));
|
|
186
|
+
return String((parsed && parsed.last_ts) || '');
|
|
187
|
+
} catch {
|
|
188
|
+
return '';
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function writeImproveCursor(root, lastTs) {
|
|
193
|
+
const file = improveCursorFile(root);
|
|
194
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
195
|
+
fs.writeFileSync(file, `${JSON.stringify({ last_ts: lastTs, updated: stampIso() }, null, 2)}\n`, 'utf8');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function readLessonsSection(root, heading, cap = Infinity) {
|
|
199
|
+
try {
|
|
200
|
+
const content = fs.readFileSync(lessonsFile(root), 'utf8');
|
|
201
|
+
const match = content.match(new RegExp(`## ${heading}\\n([\\s\\S]*?)(?=\\n## |$)`));
|
|
202
|
+
if (!match) return [];
|
|
203
|
+
return match[1]
|
|
204
|
+
.split('\n')
|
|
205
|
+
.map((line) => line.trim())
|
|
206
|
+
.filter((line) => line.startsWith('- '))
|
|
207
|
+
.slice(0, cap);
|
|
208
|
+
} catch {
|
|
209
|
+
return [];
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function readWishLessonLines(root = process.cwd(), cap = 10) {
|
|
214
|
+
return readLessonsSection(root, 'Lessons', cap);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function wishLessonsSummary(root = process.cwd()) {
|
|
218
|
+
const scores = readWishEvents(root)
|
|
219
|
+
.filter((event) => event && event.kind === 'review' && typeof event.review_score === 'number')
|
|
220
|
+
.sort((a, b) => String(a.ts || '').localeCompare(String(b.ts || '')))
|
|
221
|
+
.map((event) => event.review_score);
|
|
222
|
+
const avg = (list) => (list.length ? Number((list.reduce((s, n) => s + n, 0) / list.length).toFixed(2)) : null);
|
|
223
|
+
return {
|
|
224
|
+
lessons: readLessonsSection(root, 'Lessons'),
|
|
225
|
+
pruned: readLessonsSection(root, 'Pruned \\(did not move the score\\)'),
|
|
226
|
+
inbox: readLessonsSection(root, 'Review inbox \\(raw, distill me\\)').length,
|
|
227
|
+
reviews: scores.length,
|
|
228
|
+
avg_all: avg(scores),
|
|
229
|
+
avg_last5: avg(scores.slice(-5)),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function wishLessonsBrief(root = process.cwd(), cap = 10) {
|
|
234
|
+
const lines = readWishLessonLines(root, cap);
|
|
235
|
+
if (!lines.length) return '';
|
|
236
|
+
return ['Lessons from past wishes (apply these):', ...lines].join('\n');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const PRUNE_MIN_REVIEWS_AFTER = 3;
|
|
240
|
+
|
|
241
|
+
function pruneWishLessons(root = process.cwd(), options = {}) {
|
|
242
|
+
const quiet = options.quiet === true;
|
|
243
|
+
const say = quiet ? () => {} : console.log.bind(console);
|
|
244
|
+
const file = lessonsFile(root);
|
|
245
|
+
if (!fs.existsSync(file)) return [];
|
|
246
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
247
|
+
const match = content.match(/## Lessons\n([\s\S]*?)(?=\n## |$)/);
|
|
248
|
+
if (!match) return [];
|
|
249
|
+
const scored = readWishEvents(root)
|
|
250
|
+
.filter((event) => event && event.kind === 'review' && typeof event.review_score === 'number')
|
|
251
|
+
.map((event) => ({ ts: String(event.ts || ''), score: event.review_score }))
|
|
252
|
+
.filter((event) => event.ts)
|
|
253
|
+
.sort((a, b) => a.ts.localeCompare(b.ts));
|
|
254
|
+
const avg = (list) => list.reduce((sum, item) => sum + item.score, 0) / list.length;
|
|
255
|
+
const pruned = [];
|
|
256
|
+
const kept = match[1].split('\n').filter((line) => {
|
|
257
|
+
const trimmed = line.trim();
|
|
258
|
+
const dated = trimmed.match(/^- (\d{4}-\d{2}-\d{2}):/);
|
|
259
|
+
if (!dated) return true;
|
|
260
|
+
const since = dated[1];
|
|
261
|
+
const before = scored.filter((event) => event.ts.slice(0, 10) < since);
|
|
262
|
+
const after = scored.filter((event) => event.ts.slice(0, 10) >= since);
|
|
263
|
+
if (!before.length || after.length < PRUNE_MIN_REVIEWS_AFTER) return true;
|
|
264
|
+
if (avg(after) >= avg(before)) return true;
|
|
265
|
+
pruned.push({
|
|
266
|
+
lesson: trimmed,
|
|
267
|
+
avg_before: Number(avg(before).toFixed(2)),
|
|
268
|
+
avg_after: Number(avg(after).toFixed(2)),
|
|
269
|
+
reviews_after: after.length,
|
|
270
|
+
});
|
|
271
|
+
return false;
|
|
272
|
+
});
|
|
273
|
+
if (!pruned.length) return [];
|
|
274
|
+
let next = content.replace(match[0], `## Lessons\n${kept.join('\n')}`);
|
|
275
|
+
const graveyard = pruned.map((item) =>
|
|
276
|
+
`${item.lesson} [pruned ${todayName()}: avg ${item.avg_before} -> ${item.avg_after} over ${item.reviews_after} reviews]`);
|
|
277
|
+
if (next.includes('## Pruned (did not move the score)')) {
|
|
278
|
+
next = next.replace('## Pruned (did not move the score)', `## Pruned (did not move the score)\n${graveyard.join('\n')}`);
|
|
279
|
+
} else {
|
|
280
|
+
next = `${next.replace(/\s*$/, '')}\n\n## Pruned (did not move the score)\n${graveyard.join('\n')}\n`;
|
|
281
|
+
}
|
|
282
|
+
fs.writeFileSync(file, next, 'utf8');
|
|
283
|
+
for (const item of pruned) {
|
|
284
|
+
say(`wish improve: pruned lesson (avg ${item.avg_before} -> ${item.avg_after} over ${item.reviews_after} reviews): ${item.lesson}`);
|
|
285
|
+
}
|
|
286
|
+
return pruned;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function improveWishes(root = process.cwd(), options = {}) {
|
|
290
|
+
const quiet = options.quiet === true;
|
|
291
|
+
const say = quiet ? () => {} : console.log.bind(console);
|
|
292
|
+
const cursor = readImproveCursor(root);
|
|
293
|
+
pruneWishLessons(root, options);
|
|
294
|
+
const reviews = readWishEvents(root)
|
|
295
|
+
.filter((event) => event && event.kind === 'review' && String(event.ts || ''))
|
|
296
|
+
.filter((event) => String(event.ts) > cursor)
|
|
297
|
+
.sort((a, b) => String(a.ts).localeCompare(String(b.ts)));
|
|
298
|
+
if (!reviews.length) {
|
|
299
|
+
say('wish improve: nothing new.');
|
|
300
|
+
return 0;
|
|
301
|
+
}
|
|
302
|
+
const scores = reviews
|
|
303
|
+
.map((event) => event.review_score)
|
|
304
|
+
.filter((score) => typeof score === 'number');
|
|
305
|
+
const avg = scores.length
|
|
306
|
+
? (scores.reduce((sum, score) => sum + score, 0) / scores.length).toFixed(2)
|
|
307
|
+
: 'n/a';
|
|
308
|
+
const negatives = scores.filter((score) => score < 0).length;
|
|
309
|
+
const file = lessonsFile(root);
|
|
310
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
311
|
+
let content = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : LESSONS_HEADER;
|
|
312
|
+
if (!content.includes('## Review inbox (raw, distill me)')) {
|
|
313
|
+
content = `${content.replace(/\s*$/, '')}\n\n## Review inbox (raw, distill me)\n`;
|
|
314
|
+
}
|
|
315
|
+
const lines = reviews.map((event) => {
|
|
316
|
+
const score = event.review_score === null || event.review_score === undefined ? 'none' : event.review_score;
|
|
317
|
+
return `- ${event.ts} | ${event.wish_id || 'unknown-wish'} | score ${score} | ${String(event.review_text || '').trim()} (by ${event.reviewed_by || 'unknown'})`;
|
|
318
|
+
});
|
|
319
|
+
content = `${content.replace(/\s*$/, '')}\n${lines.join('\n')}\n`;
|
|
320
|
+
fs.writeFileSync(file, content, 'utf8');
|
|
321
|
+
writeImproveCursor(root, String(reviews[reviews.length - 1].ts));
|
|
322
|
+
say(`wish improve: ingested ${reviews.length} review${reviews.length === 1 ? '' : 's'} (avg score ${avg}, ${negatives} negative) into ${path.relative(root, file)}.`);
|
|
323
|
+
return 0;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function latestWishEventMap(root = process.cwd()) {
|
|
327
|
+
const byId = new Map();
|
|
328
|
+
for (const event of readWishEvents(root)) {
|
|
329
|
+
if (event && event.id) byId.set(event.id, event);
|
|
330
|
+
}
|
|
331
|
+
return byId;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
module.exports = {
|
|
335
|
+
appendJsonLine,
|
|
336
|
+
appendWishRecord,
|
|
337
|
+
eventAnswers,
|
|
338
|
+
improveWishes,
|
|
339
|
+
lessonsFile,
|
|
340
|
+
pruneWishLessons,
|
|
341
|
+
readWishLessonLines,
|
|
342
|
+
wishLessonsBrief,
|
|
343
|
+
wishLessonsSummary,
|
|
344
|
+
latestWishEventMap,
|
|
345
|
+
nextWishNumber,
|
|
346
|
+
readJsonLines,
|
|
347
|
+
readWishEvents,
|
|
348
|
+
readWishes,
|
|
349
|
+
slugify,
|
|
350
|
+
stampIso,
|
|
351
|
+
stateFile,
|
|
352
|
+
todayName,
|
|
353
|
+
wishId,
|
|
354
|
+
};
|