atris 3.0.1 → 3.2.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 +31 -0
- package/atris/skills/endgame/SKILL.md +19 -1
- package/atris/skills/improve/SKILL.md +65 -62
- package/atris/skills/launch/SKILL.md +62 -0
- package/atris/skills/tidy/SKILL.md +84 -0
- package/bin/atris.js +9 -2
- package/commands/autopilot.js +847 -43
- package/commands/business.js +157 -35
- package/commands/experiments.js +1 -1
- package/commands/release.js +183 -0
- package/commands/research.js +52 -0
- package/commands/sync.js +108 -15
- package/commands/verify.js +3 -3
- package/commands/wiki.js +45 -25
- package/lib/reward-config.js +24 -0
- package/lib/scorecard.js +301 -0
- package/lib/todo.js +12 -2
- package/lib/wiki.js +87 -56
- package/package.json +3 -2
package/lib/scorecard.js
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { parseTodo } = require('./todo');
|
|
4
|
+
|
|
5
|
+
const PRIVATE_MEMORY_ROOT = '.atris/presidio';
|
|
6
|
+
|
|
7
|
+
function ensurePrivateMemoryDir(atrisDir) {
|
|
8
|
+
const privateDir = path.join(path.dirname(atrisDir), PRIVATE_MEMORY_ROOT);
|
|
9
|
+
fs.mkdirSync(privateDir, { recursive: true });
|
|
10
|
+
return privateDir;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getScorecardsPath(atrisDir) {
|
|
14
|
+
return path.join(ensurePrivateMemoryDir(atrisDir), 'scorecards.md');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parsePickedAt(value) {
|
|
18
|
+
if (!value) return null;
|
|
19
|
+
const match = String(value).trim().match(/^(\d{4}-\d{2}-\d{2})(?:\s+(\d{2}:\d{2}))?/);
|
|
20
|
+
if (!match) return null;
|
|
21
|
+
|
|
22
|
+
const [, datePart, timePart = '00:00'] = match;
|
|
23
|
+
const parsed = new Date(`${datePart}T${timePart}:00`);
|
|
24
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseTickDate(dateStr, timeLabel) {
|
|
28
|
+
const match = String(timeLabel || '').trim().toLowerCase().match(/^(\d{1,2}):(\d{2})(?:\s*(am|pm))?$/);
|
|
29
|
+
if (!match) return null;
|
|
30
|
+
|
|
31
|
+
let hours = parseInt(match[1], 10);
|
|
32
|
+
const minutes = parseInt(match[2], 10);
|
|
33
|
+
const meridiem = match[3] || null;
|
|
34
|
+
|
|
35
|
+
if (meridiem === 'pm' && hours !== 12) hours += 12;
|
|
36
|
+
if (meridiem === 'am' && hours === 12) hours = 0;
|
|
37
|
+
|
|
38
|
+
const parsed = new Date(`${dateStr}T00:00:00`);
|
|
39
|
+
parsed.setHours(hours, minutes, 0, 0);
|
|
40
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function listLogFiles(atrisDir, startDate, endDate = new Date()) {
|
|
44
|
+
const logsDir = path.join(atrisDir, 'logs');
|
|
45
|
+
if (!fs.existsSync(logsDir)) return [];
|
|
46
|
+
|
|
47
|
+
const startKey = startDate.toISOString().slice(0, 10);
|
|
48
|
+
const endKey = endDate.toISOString().slice(0, 10);
|
|
49
|
+
const files = [];
|
|
50
|
+
|
|
51
|
+
for (const year of fs.readdirSync(logsDir)) {
|
|
52
|
+
const yearDir = path.join(logsDir, year);
|
|
53
|
+
let stat;
|
|
54
|
+
try {
|
|
55
|
+
stat = fs.statSync(yearDir);
|
|
56
|
+
} catch {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (!stat.isDirectory()) continue;
|
|
60
|
+
|
|
61
|
+
for (const entry of fs.readdirSync(yearDir)) {
|
|
62
|
+
if (!entry.endsWith('.md')) continue;
|
|
63
|
+
const dateKey = entry.replace(/\.md$/, '');
|
|
64
|
+
if (dateKey < startKey || dateKey > endKey) continue;
|
|
65
|
+
files.push({ dateKey, file: path.join(yearDir, entry) });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
files.sort((a, b) => a.dateKey.localeCompare(b.dateKey));
|
|
70
|
+
return files;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function readNotesSection(content) {
|
|
74
|
+
const match = String(content || '').match(/## Notes\n([\s\S]*?)(?=\n##\s|$)/);
|
|
75
|
+
return match ? match[1] : '';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function collectRewardStats(atrisDir, pickedAt) {
|
|
79
|
+
const startAt = parsePickedAt(pickedAt);
|
|
80
|
+
if (!startAt) {
|
|
81
|
+
return { totalReward: 0, totalTicks: 0, haltedTicks: 0 };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let totalReward = 0;
|
|
85
|
+
let totalTicks = 0;
|
|
86
|
+
let haltedTicks = 0;
|
|
87
|
+
|
|
88
|
+
for (const { dateKey, file } of listLogFiles(atrisDir, startAt)) {
|
|
89
|
+
const notes = readNotesSection(fs.readFileSync(file, 'utf8'));
|
|
90
|
+
if (!notes) continue;
|
|
91
|
+
|
|
92
|
+
const lines = notes.split('\n');
|
|
93
|
+
for (let i = 0; i < lines.length; i++) {
|
|
94
|
+
const headerMatch = lines[i].match(/^- (\d{1,2}:\d{2}(?:\s*[ap]m)?)$/i);
|
|
95
|
+
if (!headerMatch) continue;
|
|
96
|
+
|
|
97
|
+
const tickAt = parseTickDate(dateKey, headerMatch[1]);
|
|
98
|
+
if (!tickAt || tickAt < startAt) continue;
|
|
99
|
+
|
|
100
|
+
let reward = null;
|
|
101
|
+
let halted = false;
|
|
102
|
+
let j = i + 1;
|
|
103
|
+
for (; j < lines.length; j++) {
|
|
104
|
+
const current = lines[j];
|
|
105
|
+
if (j > i + 1 && /^- (\d{1,2}:\d{2}(?:\s*[ap]m)?)$/i.test(current)) break;
|
|
106
|
+
if (current && !current.startsWith(' ')) break;
|
|
107
|
+
|
|
108
|
+
const trimmed = current.trim();
|
|
109
|
+
if (!trimmed) continue;
|
|
110
|
+
if (/^Reward:\s*-?\d+$/i.test(trimmed)) {
|
|
111
|
+
reward = parseInt(trimmed.replace(/^Reward:\s*/i, ''), 10);
|
|
112
|
+
}
|
|
113
|
+
if (/review flagged issues|verify failed|hit an error|stopped for a manual check/i.test(trimmed)) {
|
|
114
|
+
halted = true;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (reward !== null) {
|
|
119
|
+
totalReward += reward;
|
|
120
|
+
totalTicks += 1;
|
|
121
|
+
if (halted) haltedTicks += 1;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
i = j - 1;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { totalReward, totalTicks, haltedTicks };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function countLessonsGenerated(atrisDir, pickedAt) {
|
|
132
|
+
const startAt = parsePickedAt(pickedAt);
|
|
133
|
+
if (!startAt) return 0;
|
|
134
|
+
|
|
135
|
+
const lessonsPath = path.join(atrisDir, 'lessons.md');
|
|
136
|
+
if (!fs.existsSync(lessonsPath)) return 0;
|
|
137
|
+
|
|
138
|
+
const startDateKey = startAt.toISOString().slice(0, 10);
|
|
139
|
+
return fs.readFileSync(lessonsPath, 'utf8')
|
|
140
|
+
.split('\n')
|
|
141
|
+
.reduce((count, line) => {
|
|
142
|
+
const match = line.match(/^- \*\*\[(\d{4}-\d{2}-\d{2})\]/);
|
|
143
|
+
return match && match[1] >= startDateKey ? count + 1 : count;
|
|
144
|
+
}, 0);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function buildScorecardData(atrisDir, { slug, pickedAt } = {}) {
|
|
148
|
+
if (!slug) {
|
|
149
|
+
throw new Error('Scorecard: slug is required');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const todoPath = path.join(atrisDir, 'TODO.md');
|
|
153
|
+
const todo = parseTodo(todoPath);
|
|
154
|
+
const startAt = parsePickedAt(pickedAt) || new Date();
|
|
155
|
+
const rewardStats = collectRewardStats(atrisDir, pickedAt);
|
|
156
|
+
const completedEndgame = todo.completed.filter(t => t.tag === 'endgame').length;
|
|
157
|
+
const activeEndgame = todo.backlog.filter(t => t.tag === 'endgame').length
|
|
158
|
+
+ todo.inProgress.filter(t => t.tag === 'endgame').length;
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
slug,
|
|
162
|
+
startDate: startAt.toISOString().slice(0, 10),
|
|
163
|
+
endDate: new Date().toISOString().slice(0, 10),
|
|
164
|
+
tasksShipped: completedEndgame,
|
|
165
|
+
tasksAttempted: completedEndgame + activeEndgame,
|
|
166
|
+
wallClockHours: Math.max(0, (Date.now() - startAt.getTime()) / (1000 * 60 * 60)),
|
|
167
|
+
haltRatio: rewardStats.totalTicks > 0 ? rewardStats.haltedTicks / rewardStats.totalTicks : 0,
|
|
168
|
+
totalReward: rewardStats.totalReward,
|
|
169
|
+
lessonsGenerated: countLessonsGenerated(atrisDir, pickedAt),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Write a scorecard when an endgame closes.
|
|
175
|
+
*
|
|
176
|
+
* @param {string} atrisDir - Path to atris/ directory
|
|
177
|
+
* @param {object} data - Scorecard data
|
|
178
|
+
* - slug: endgame slug (e.g., "loop-self-seeds-horizons")
|
|
179
|
+
* - startDate: ISO date when endgame started
|
|
180
|
+
* - endDate: ISO date when endgame ended (default: today)
|
|
181
|
+
* - tasksShipped: number of tasks completed
|
|
182
|
+
* - tasksAttempted: number of tasks started
|
|
183
|
+
* - wallClockHours: total hours (float)
|
|
184
|
+
* - haltRatio: fraction of ticks that halted (e.g., 0.1)
|
|
185
|
+
* - totalReward: sum of per-tick reward scores
|
|
186
|
+
* - lessonsGenerated: number of lessons appended to lessons.md
|
|
187
|
+
*/
|
|
188
|
+
function writeScorecard(atrisDir, data) {
|
|
189
|
+
const {
|
|
190
|
+
slug,
|
|
191
|
+
startDate,
|
|
192
|
+
endDate = new Date().toISOString().split('T')[0],
|
|
193
|
+
tasksShipped = 0,
|
|
194
|
+
tasksAttempted = 0,
|
|
195
|
+
wallClockHours = 0,
|
|
196
|
+
haltRatio = 0,
|
|
197
|
+
totalReward = 0,
|
|
198
|
+
lessonsGenerated = 0,
|
|
199
|
+
} = data;
|
|
200
|
+
|
|
201
|
+
// Validate required fields
|
|
202
|
+
if (!slug) {
|
|
203
|
+
throw new Error('Scorecard: slug is required');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const scorecardsPath = getScorecardsPath(atrisDir);
|
|
207
|
+
|
|
208
|
+
// Ensure scorecards.md exists
|
|
209
|
+
if (!fs.existsSync(scorecardsPath)) {
|
|
210
|
+
const template = `# scorecards.md — Endgame Results\n\n> Append-only. One line per closed endgame. Records outcome metrics from the horizon.\n\n---\n\n`;
|
|
211
|
+
fs.writeFileSync(scorecardsPath, template, 'utf8');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Format: - **[date] slug** — shipped: X/Y — wall-clock: Nh — halt: Z% — reward: total — lessons: N
|
|
215
|
+
const haltPercent = Math.round(haltRatio * 100);
|
|
216
|
+
const wallClockStr = wallClockHours < 1 ? `${Math.round(wallClockHours * 60)}m` : `${wallClockHours.toFixed(1)}h`;
|
|
217
|
+
const line = `- **[${endDate}] ${slug}** — shipped: ${tasksShipped}/${tasksAttempted} — wall-clock: ${wallClockStr} — halt: ${haltPercent}% — reward: ${totalReward} — lessons: ${lessonsGenerated}\n`;
|
|
218
|
+
|
|
219
|
+
// Append to file
|
|
220
|
+
fs.appendFileSync(scorecardsPath, line, 'utf8');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Detect if the current endgame in TODO.md is complete (all endgame tasks in Completed).
|
|
225
|
+
* Returns { complete: boolean, endgameSlug: string | null }
|
|
226
|
+
*/
|
|
227
|
+
function detectEndgameCompletion(atrisDir) {
|
|
228
|
+
const todoPath = path.join(atrisDir, 'TODO.md');
|
|
229
|
+
if (!fs.existsSync(todoPath)) {
|
|
230
|
+
return { complete: false, endgameSlug: null };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const todo = parseTodo(todoPath);
|
|
234
|
+
|
|
235
|
+
// Find the current endgame section
|
|
236
|
+
const endgameSectionMatch = fs.readFileSync(todoPath, 'utf8')
|
|
237
|
+
.match(/## Endgame\n\n\*\*Slug:\*\*\s*(\S+)/);
|
|
238
|
+
|
|
239
|
+
if (!endgameSectionMatch) {
|
|
240
|
+
return { complete: false, endgameSlug: null };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const slug = endgameSectionMatch[1];
|
|
244
|
+
|
|
245
|
+
// Check if there are any endgame-tagged tasks in backlog or in-progress
|
|
246
|
+
const hasActiveEndgame = todo.backlog.some(t => t.tag === 'endgame')
|
|
247
|
+
|| todo.inProgress.some(t => t.tag === 'endgame');
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
complete: !hasActiveEndgame,
|
|
251
|
+
endgameSlug: slug,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Parse scorecards.md and return array of scorecard objects.
|
|
257
|
+
*/
|
|
258
|
+
function readScorecards(atrisDir) {
|
|
259
|
+
const scorecardsPath = getScorecardsPath(atrisDir);
|
|
260
|
+
if (!fs.existsSync(scorecardsPath)) return [];
|
|
261
|
+
|
|
262
|
+
const content = fs.readFileSync(scorecardsPath, 'utf8');
|
|
263
|
+
const scorecards = [];
|
|
264
|
+
|
|
265
|
+
for (const line of content.split('\n')) {
|
|
266
|
+
const match = line.match(/^- \*\*\[(.+?)\]\s+(.+?)\*\*\s*—\s*shipped:\s*(\d+)\/(\d+)\s*—\s*wall-clock:\s*(.+?)\s*—\s*halt:\s*(\d+)%\s*—\s*reward:\s*(\d+)\s*—\s*lessons:\s*(\d+)$/);
|
|
267
|
+
if (!match) continue;
|
|
268
|
+
|
|
269
|
+
const [, endDate, slug, shipped, attempted, wallClockStr, haltPercent, reward, lessons] = match;
|
|
270
|
+
|
|
271
|
+
// Parse wall-clock back to hours
|
|
272
|
+
let wallClockHours = 0;
|
|
273
|
+
if (wallClockStr.endsWith('h')) {
|
|
274
|
+
wallClockHours = parseFloat(wallClockStr);
|
|
275
|
+
} else if (wallClockStr.endsWith('m')) {
|
|
276
|
+
wallClockHours = parseInt(wallClockStr) / 60;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
scorecards.push({
|
|
280
|
+
endDate,
|
|
281
|
+
slug,
|
|
282
|
+
tasksShipped: parseInt(shipped),
|
|
283
|
+
tasksAttempted: parseInt(attempted),
|
|
284
|
+
wallClockHours,
|
|
285
|
+
haltRatio: parseInt(haltPercent) / 100,
|
|
286
|
+
totalReward: parseInt(reward),
|
|
287
|
+
lessonsGenerated: parseInt(lessons),
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return scorecards;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
module.exports = {
|
|
295
|
+
PRIVATE_MEMORY_ROOT,
|
|
296
|
+
getScorecardsPath,
|
|
297
|
+
buildScorecardData,
|
|
298
|
+
writeScorecard,
|
|
299
|
+
readScorecards,
|
|
300
|
+
detectEndgameCompletion,
|
|
301
|
+
};
|
package/lib/todo.js
CHANGED
|
@@ -33,8 +33,8 @@ function parseSection(content, sectionName) {
|
|
|
33
33
|
const line = rawLine.trimEnd();
|
|
34
34
|
|
|
35
35
|
// New task line: - **T1:** Description or - **T1a:** Description [tag] [tag]
|
|
36
|
-
// Accepts task IDs like T1, W3b, M12c
|
|
37
|
-
const taskMatch = line.match(/^- \*\*([A-
|
|
36
|
+
// Accepts task IDs like T1, W3b, M12c, R1, T#1 — letter(s), optional symbols, digits, optional trailing letter.
|
|
37
|
+
const taskMatch = line.match(/^- \*\*([A-Za-z][A-Za-z0-9#]*\d[a-z]?):\*\*\s*(.+)$/);
|
|
38
38
|
if (taskMatch) {
|
|
39
39
|
if (current) tasks.push(current);
|
|
40
40
|
// Capture ALL bracketed tags in the line, not just the last one. Endgame is priority.
|
|
@@ -47,6 +47,7 @@ function parseSection(content, sectionName) {
|
|
|
47
47
|
tags: allTags,
|
|
48
48
|
claimed: null,
|
|
49
49
|
stage: null,
|
|
50
|
+
verify: null,
|
|
50
51
|
};
|
|
51
52
|
continue;
|
|
52
53
|
}
|
|
@@ -60,6 +61,7 @@ function parseSection(content, sectionName) {
|
|
|
60
61
|
tag: null,
|
|
61
62
|
claimed: null,
|
|
62
63
|
stage: null,
|
|
64
|
+
verify: null,
|
|
63
65
|
});
|
|
64
66
|
continue;
|
|
65
67
|
}
|
|
@@ -73,6 +75,7 @@ function parseSection(content, sectionName) {
|
|
|
73
75
|
tag: null,
|
|
74
76
|
claimed: null,
|
|
75
77
|
stage: null,
|
|
78
|
+
verify: null,
|
|
76
79
|
});
|
|
77
80
|
continue;
|
|
78
81
|
}
|
|
@@ -92,6 +95,13 @@ function parseSection(content, sectionName) {
|
|
|
92
95
|
current.stage = stageMatch[1].trim();
|
|
93
96
|
continue;
|
|
94
97
|
}
|
|
98
|
+
|
|
99
|
+
// Verify line
|
|
100
|
+
const verifyMatch = line.match(/\*\*Verify:\*\*\s*(.+)$/) || line.match(/Verify:\s*(.+)$/);
|
|
101
|
+
if (verifyMatch) {
|
|
102
|
+
current.verify = verifyMatch[1].trim();
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
95
105
|
}
|
|
96
106
|
|
|
97
107
|
if (current) tasks.push(current);
|
package/lib/wiki.js
CHANGED
|
@@ -2,6 +2,7 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
|
|
4
4
|
const WIKI_ROOT = 'atris/wiki';
|
|
5
|
+
const PRIVATE_WIKI_ROOT = '.atris/presidio';
|
|
5
6
|
const LEGACY_WIKI_ROOT = 'wiki';
|
|
6
7
|
const WIKI_BRIEFS_SUBDIR = 'briefs';
|
|
7
8
|
const LEGACY_WIKI_BRIEFS_SUBDIR = 'syntheses';
|
|
@@ -9,6 +10,14 @@ const WIKI_SUBDIRS = ['people', 'systems', 'concepts', WIKI_BRIEFS_SUBDIR];
|
|
|
9
10
|
const WIKI_STATUS_FILE = 'STATUS.md';
|
|
10
11
|
const WIKI_CONTENT_SUBDIRS = WIKI_SUBDIRS.map((subdir) => path.join(WIKI_ROOT, subdir));
|
|
11
12
|
|
|
13
|
+
function getWikiRoot(mode = 'public') {
|
|
14
|
+
return mode === 'private' ? PRIVATE_WIKI_ROOT : WIKI_ROOT;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getWikiLinkRoot(mode = 'public') {
|
|
18
|
+
return mode === 'private' ? PRIVATE_WIKI_ROOT : 'atris/wiki';
|
|
19
|
+
}
|
|
20
|
+
|
|
12
21
|
function today() {
|
|
13
22
|
return new Date().toISOString().slice(0, 10);
|
|
14
23
|
}
|
|
@@ -17,10 +26,12 @@ function nowTime() {
|
|
|
17
26
|
return new Date().toTimeString().slice(0, 5);
|
|
18
27
|
}
|
|
19
28
|
|
|
20
|
-
function protocolMarkdown() {
|
|
29
|
+
function protocolMarkdown(mode = 'public') {
|
|
30
|
+
const wikiRoot = getWikiRoot(mode);
|
|
31
|
+
const wikiLinkRoot = getWikiLinkRoot(mode);
|
|
21
32
|
return `# Atris Wiki Protocol
|
|
22
33
|
|
|
23
|
-
This wiki lives in \`${
|
|
34
|
+
This wiki lives in \`${wikiRoot}/\`.
|
|
24
35
|
|
|
25
36
|
## Purpose
|
|
26
37
|
|
|
@@ -28,20 +39,20 @@ Turn raw project context into a living memory the next agent can pick up cold.
|
|
|
28
39
|
|
|
29
40
|
## Shape
|
|
30
41
|
|
|
31
|
-
- \`${
|
|
32
|
-
- \`${
|
|
33
|
-
- \`${
|
|
34
|
-
- \`${
|
|
35
|
-
- \`${
|
|
36
|
-
- \`${
|
|
37
|
-
- \`${
|
|
38
|
-
- \`${
|
|
42
|
+
- \`${wikiRoot}/wiki.md\` - this protocol
|
|
43
|
+
- \`${wikiRoot}/index.md\` - catalog grouped by page type
|
|
44
|
+
- \`${wikiRoot}/log.md\` - append-only ingest and lint history
|
|
45
|
+
- \`${wikiRoot}/STATUS.md\` - plain-English health summary
|
|
46
|
+
- \`${wikiRoot}/people/\` - humans (employees, contacts, stakeholders)
|
|
47
|
+
- \`${wikiRoot}/systems/\` - tools, tables, dashboards, services, products
|
|
48
|
+
- \`${wikiRoot}/concepts/\` - patterns, frameworks, recurring ideas
|
|
49
|
+
- \`${wikiRoot}/${WIKI_BRIEFS_SUBDIR}/\` - multi-page briefs and cross-cutting analysis
|
|
39
50
|
|
|
40
51
|
## Rules
|
|
41
52
|
|
|
42
53
|
- Read the full source before writing.
|
|
43
54
|
- Merge new facts into existing pages. Do not overwrite history blindly.
|
|
44
|
-
- Add cross-references with \`[[
|
|
55
|
+
- Add cross-references with \`[[${wikiLinkRoot}/...]]\` links.
|
|
45
56
|
- Keep \`index.md\`, \`log.md\`, and \`STATUS.md\` in sync with page changes.
|
|
46
57
|
- If something is unclear or contradictory, say so directly.
|
|
47
58
|
`;
|
|
@@ -155,15 +166,18 @@ function migrateLegacyBriefsDir(wikiDir) {
|
|
|
155
166
|
rewriteLegacyWikiReferences(wikiDir);
|
|
156
167
|
}
|
|
157
168
|
|
|
158
|
-
function ensureWikiScaffold(projectRoot = process.cwd()) {
|
|
159
|
-
const
|
|
169
|
+
function ensureWikiScaffold(projectRoot = process.cwd(), mode = 'public') {
|
|
170
|
+
const wikiRoot = getWikiRoot(mode);
|
|
171
|
+
const wikiDir = path.join(projectRoot, wikiRoot);
|
|
160
172
|
fs.mkdirSync(wikiDir, { recursive: true });
|
|
161
|
-
|
|
173
|
+
if (mode === 'public') {
|
|
174
|
+
migrateLegacyBriefsDir(wikiDir);
|
|
175
|
+
}
|
|
162
176
|
for (const subdir of WIKI_SUBDIRS) {
|
|
163
177
|
fs.mkdirSync(path.join(wikiDir, subdir), { recursive: true });
|
|
164
178
|
}
|
|
165
179
|
|
|
166
|
-
ensureFile(path.join(wikiDir, 'wiki.md'), protocolMarkdown());
|
|
180
|
+
ensureFile(path.join(wikiDir, 'wiki.md'), protocolMarkdown(mode));
|
|
167
181
|
ensureFile(path.join(wikiDir, 'index.md'), indexMarkdown());
|
|
168
182
|
ensureFile(path.join(wikiDir, 'log.md'), logMarkdown());
|
|
169
183
|
ensureFile(path.join(wikiDir, WIKI_STATUS_FILE), statusMarkdown());
|
|
@@ -171,7 +185,12 @@ function ensureWikiScaffold(projectRoot = process.cwd()) {
|
|
|
171
185
|
return wikiDir;
|
|
172
186
|
}
|
|
173
187
|
|
|
174
|
-
function findLocalWikiDir(projectRoot = process.cwd(), slug = null) {
|
|
188
|
+
function findLocalWikiDir(projectRoot = process.cwd(), slug = null, mode = 'public') {
|
|
189
|
+
if (mode === 'private') {
|
|
190
|
+
const privateDir = path.join(projectRoot, PRIVATE_WIKI_ROOT);
|
|
191
|
+
return fs.existsSync(privateDir) ? privateDir : null;
|
|
192
|
+
}
|
|
193
|
+
|
|
175
194
|
const tries = [
|
|
176
195
|
path.join(projectRoot, WIKI_ROOT),
|
|
177
196
|
path.join(projectRoot, LEGACY_WIKI_ROOT),
|
|
@@ -190,8 +209,8 @@ function normalizeWikiOnlyPrefix(prefix) {
|
|
|
190
209
|
return null;
|
|
191
210
|
}
|
|
192
211
|
|
|
193
|
-
function readWikiStatus(projectRoot = process.cwd(), slug = null) {
|
|
194
|
-
const wikiDir = findLocalWikiDir(projectRoot, slug);
|
|
212
|
+
function readWikiStatus(projectRoot = process.cwd(), slug = null, mode = 'public') {
|
|
213
|
+
const wikiDir = findLocalWikiDir(projectRoot, slug, mode);
|
|
195
214
|
if (!wikiDir) return null;
|
|
196
215
|
|
|
197
216
|
const statusPath = path.join(wikiDir, WIKI_STATUS_FILE);
|
|
@@ -275,8 +294,9 @@ function parseFrontmatter(content) {
|
|
|
275
294
|
return frontmatter;
|
|
276
295
|
}
|
|
277
296
|
|
|
278
|
-
function readWikiPages(projectRoot = process.cwd()) {
|
|
279
|
-
const
|
|
297
|
+
function readWikiPages(projectRoot = process.cwd(), mode = 'public') {
|
|
298
|
+
const wikiRoot = getWikiRoot(mode);
|
|
299
|
+
const wikiDir = path.join(projectRoot, wikiRoot);
|
|
280
300
|
const pages = [];
|
|
281
301
|
|
|
282
302
|
for (const subdir of WIKI_SUBDIRS) {
|
|
@@ -302,8 +322,8 @@ function normalizeSourcePath(projectRoot, source) {
|
|
|
302
322
|
return path.normalize(path.join(projectRoot, source));
|
|
303
323
|
}
|
|
304
324
|
|
|
305
|
-
function findStaleWikiPages(projectRoot = process.cwd()) {
|
|
306
|
-
return readWikiPages(projectRoot)
|
|
325
|
+
function findStaleWikiPages(projectRoot = process.cwd(), mode = 'public') {
|
|
326
|
+
return readWikiPages(projectRoot, mode)
|
|
307
327
|
.map((page) => {
|
|
308
328
|
const sources = Array.isArray(page.frontmatter.sources) ? page.frontmatter.sources : [];
|
|
309
329
|
if (sources.length === 0) return null;
|
|
@@ -345,13 +365,14 @@ function findStaleWikiPages(projectRoot = process.cwd()) {
|
|
|
345
365
|
}
|
|
346
366
|
|
|
347
367
|
function extractWikiLinks(content) {
|
|
348
|
-
const matches = content.match(/\[\[(atris\/wiki\/[^\]]+?)\]\]/g) || [];
|
|
368
|
+
const matches = content.match(/\[\[((?:atris\/wiki|\.atris\/presidio)\/[^\]]+?)\]\]/g) || [];
|
|
349
369
|
return matches.map((match) => match.slice(2, -2));
|
|
350
370
|
}
|
|
351
371
|
|
|
352
|
-
function findWikiOrphans(projectRoot = process.cwd()) {
|
|
353
|
-
const
|
|
354
|
-
const
|
|
372
|
+
function findWikiOrphans(projectRoot = process.cwd(), mode = 'public') {
|
|
373
|
+
const wikiRoot = getWikiRoot(mode);
|
|
374
|
+
const pages = readWikiPages(projectRoot, mode);
|
|
375
|
+
const indexPath = path.join(projectRoot, wikiRoot, 'index.md');
|
|
355
376
|
const indexContent = fs.existsSync(indexPath) ? fs.readFileSync(indexPath, 'utf8') : '';
|
|
356
377
|
|
|
357
378
|
const inboundLinks = new Map();
|
|
@@ -423,8 +444,8 @@ function parseStatusBullets(content) {
|
|
|
423
444
|
return bullets;
|
|
424
445
|
}
|
|
425
446
|
|
|
426
|
-
function writeWikiStatus(projectRoot = process.cwd(), report) {
|
|
427
|
-
const wikiDir = ensureWikiScaffold(projectRoot);
|
|
447
|
+
function writeWikiStatus(projectRoot = process.cwd(), report, mode = 'public') {
|
|
448
|
+
const wikiDir = ensureWikiScaffold(projectRoot, mode);
|
|
428
449
|
const statusPath = path.join(wikiDir, WIKI_STATUS_FILE);
|
|
429
450
|
const existing = fs.existsSync(statusPath) ? fs.readFileSync(statusPath, 'utf8') : '';
|
|
430
451
|
const bullets = parseStatusBullets(existing);
|
|
@@ -444,8 +465,8 @@ function writeWikiStatus(projectRoot = process.cwd(), report) {
|
|
|
444
465
|
return statusPath;
|
|
445
466
|
}
|
|
446
467
|
|
|
447
|
-
function appendWikiLog(projectRoot = process.cwd(), summary, details = []) {
|
|
448
|
-
const wikiDir = ensureWikiScaffold(projectRoot);
|
|
468
|
+
function appendWikiLog(projectRoot = process.cwd(), summary, details = [], mode = 'public') {
|
|
469
|
+
const wikiDir = ensureWikiScaffold(projectRoot, mode);
|
|
449
470
|
const logPath = path.join(wikiDir, 'log.md');
|
|
450
471
|
let content = fs.existsSync(logPath) ? fs.readFileSync(logPath, 'utf8') : '# Atris Wiki Log\n';
|
|
451
472
|
const dateHeader = `## ${today()}`;
|
|
@@ -471,17 +492,20 @@ function formatSourceList(sourceValue) {
|
|
|
471
492
|
.join(', ');
|
|
472
493
|
}
|
|
473
494
|
|
|
474
|
-
|
|
495
|
+
function buildWikiSchema(mode = 'public') {
|
|
496
|
+
const wikiRoot = getWikiRoot(mode);
|
|
497
|
+
const wikiLinkRoot = getWikiLinkRoot(mode);
|
|
498
|
+
return `The wiki lives in ${wikiRoot}/.
|
|
475
499
|
|
|
476
500
|
Structure:
|
|
477
|
-
- ${
|
|
478
|
-
- ${
|
|
479
|
-
- ${
|
|
480
|
-
- ${
|
|
481
|
-
- ${
|
|
482
|
-
- ${
|
|
483
|
-
- ${
|
|
484
|
-
- ${
|
|
501
|
+
- ${wikiRoot}/wiki.md - protocol for future agents
|
|
502
|
+
- ${wikiRoot}/index.md - catalog grouped by type
|
|
503
|
+
- ${wikiRoot}/log.md - append-only activity log
|
|
504
|
+
- ${wikiRoot}/STATUS.md - plain-English health summary
|
|
505
|
+
- ${wikiRoot}/people/ - one page per human
|
|
506
|
+
- ${wikiRoot}/systems/ - one page per tool, table, dashboard, service, or product
|
|
507
|
+
- ${wikiRoot}/concepts/ - pattern and framework pages
|
|
508
|
+
- ${wikiRoot}/${WIKI_BRIEFS_SUBDIR}/ - cross-cutting briefs referencing 3+ pages
|
|
485
509
|
|
|
486
510
|
Page format:
|
|
487
511
|
---
|
|
@@ -497,7 +521,7 @@ tags: [tag1, tag2]
|
|
|
497
521
|
# Title
|
|
498
522
|
Body in markdown.
|
|
499
523
|
## Cross-References
|
|
500
|
-
- [[
|
|
524
|
+
- [[${wikiLinkRoot}/people/related.md]] - why related
|
|
501
525
|
|
|
502
526
|
Rules:
|
|
503
527
|
- Read every listed source fully before writing
|
|
@@ -505,20 +529,23 @@ Rules:
|
|
|
505
529
|
- Keep index.md, log.md, and STATUS.md current
|
|
506
530
|
- Flag contradictions directly instead of smoothing them over
|
|
507
531
|
- Never modify the raw source documents you ingested`;
|
|
532
|
+
}
|
|
508
533
|
|
|
509
|
-
function buildIngestPrompt(sourceValue) {
|
|
534
|
+
function buildIngestPrompt(sourceValue, mode = 'public') {
|
|
535
|
+
const wikiRoot = getWikiRoot(mode);
|
|
536
|
+
const wikiLinkRoot = getWikiLinkRoot(mode);
|
|
510
537
|
return `Atris wiki ingest: ${formatSourceList(sourceValue)}
|
|
511
|
-
${
|
|
538
|
+
${buildWikiSchema(mode)}
|
|
512
539
|
|
|
513
540
|
Workflow:
|
|
514
541
|
1. Read every source in: ${sourceValue}
|
|
515
|
-
2. Ensure ${
|
|
542
|
+
2. Ensure ${wikiRoot}/ exists with wiki.md, index.md, log.md, STATUS.md, and the 3 page subfolders
|
|
516
543
|
3. Extract people, systems, and concepts worth preserving
|
|
517
|
-
4. Create or update pages under ${
|
|
518
|
-
5. Add cross-references using [[
|
|
519
|
-
6. Update ${
|
|
520
|
-
7. Append an INGEST entry to ${
|
|
521
|
-
8. Refresh ${
|
|
544
|
+
4. Create or update pages under ${wikiRoot}/, merging with existing facts instead of replacing them
|
|
545
|
+
5. Add cross-references using [[${wikiLinkRoot}/...]] links
|
|
546
|
+
6. Update ${wikiRoot}/index.md with one-line descriptions of touched pages
|
|
547
|
+
7. Append an INGEST entry to ${wikiRoot}/log.md under today's date
|
|
548
|
+
8. Refresh ${wikiRoot}/STATUS.md in plain English for a non-technical reader
|
|
522
549
|
|
|
523
550
|
Quality bar:
|
|
524
551
|
- Ask clarifying questions if the source is ambiguous
|
|
@@ -527,18 +554,20 @@ Quality bar:
|
|
|
527
554
|
- Leave the wiki sharper than you found it`;
|
|
528
555
|
}
|
|
529
556
|
|
|
530
|
-
function buildQueryPrompt(question) {
|
|
557
|
+
function buildQueryPrompt(question, mode = 'public') {
|
|
558
|
+
const wikiRoot = getWikiRoot(mode);
|
|
531
559
|
return `Atris wiki query: ${question}
|
|
532
560
|
|
|
533
|
-
Read ${
|
|
534
|
-
Answer from the wiki with direct references to page paths under ${
|
|
561
|
+
Read ${wikiRoot}/index.md first, then the most relevant pages.
|
|
562
|
+
Answer from the wiki with direct references to page paths under ${wikiRoot}/.
|
|
535
563
|
If the answer reveals a reusable insight, offer to save it as a brief page.`;
|
|
536
564
|
}
|
|
537
565
|
|
|
538
|
-
function buildLintPrompt() {
|
|
566
|
+
function buildLintPrompt(mode = 'public') {
|
|
567
|
+
const wikiRoot = getWikiRoot(mode);
|
|
539
568
|
return `Atris wiki lint pass
|
|
540
569
|
|
|
541
|
-
Read ${
|
|
570
|
+
Read ${wikiRoot}/index.md, crawl the referenced pages, and inspect the local wiki.
|
|
542
571
|
|
|
543
572
|
Checks:
|
|
544
573
|
1. Every page referenced by index.md exists
|
|
@@ -546,8 +575,8 @@ Checks:
|
|
|
546
575
|
3. Orphan pages are listed
|
|
547
576
|
4. Contradictions are called out plainly
|
|
548
577
|
5. Gaps worth ingesting next are listed concretely
|
|
549
|
-
6. ${
|
|
550
|
-
7. ${
|
|
578
|
+
6. ${wikiRoot}/STATUS.md is rewritten in plain English
|
|
579
|
+
7. ${wikiRoot}/log.md gets a LINT entry under today's date
|
|
551
580
|
|
|
552
581
|
Output:
|
|
553
582
|
- Clear summary for a non-technical reader
|
|
@@ -557,11 +586,13 @@ Output:
|
|
|
557
586
|
|
|
558
587
|
module.exports = {
|
|
559
588
|
WIKI_ROOT,
|
|
589
|
+
PRIVATE_WIKI_ROOT,
|
|
560
590
|
LEGACY_WIKI_ROOT,
|
|
561
591
|
WIKI_SUBDIRS,
|
|
562
592
|
WIKI_CONTENT_SUBDIRS,
|
|
563
|
-
WIKI_SCHEMA,
|
|
593
|
+
WIKI_SCHEMA: buildWikiSchema(),
|
|
564
594
|
WIKI_STATUS_FILE,
|
|
595
|
+
getWikiRoot,
|
|
565
596
|
ensureWikiScaffold,
|
|
566
597
|
findLocalWikiDir,
|
|
567
598
|
normalizeWikiOnlyPrefix,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "atris",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Atris — an operating system for intelligence. Integrates with any agent.",
|
|
5
5
|
"main": "bin/atris.js",
|
|
6
6
|
"bin": {
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
"atris/skills/"
|
|
33
33
|
],
|
|
34
34
|
"scripts": {
|
|
35
|
-
"test": "node --test"
|
|
35
|
+
"test": "node --test",
|
|
36
|
+
"prepare": "cp scripts/pre-commit .git/hooks/pre-commit 2>/dev/null && chmod +x .git/hooks/pre-commit || true"
|
|
36
37
|
},
|
|
37
38
|
"keywords": [
|
|
38
39
|
"atrisdev",
|