atris 3.30.1 → 3.30.2
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 +6 -0
- package/atris/AGENTS.md +11 -0
- package/atris/CLAUDE.md +5 -0
- package/atris/atris.md +19 -4
- package/atris/policies/atris-design.md +71 -0
- package/atris/policies/design-seed.md +187 -0
- package/atris/skills/atris/SKILL.md +26 -3
- package/atris/skills/design/SKILL.md +3 -2
- package/atris/skills/loop/SKILL.md +5 -3
- package/atris/team/_template/MEMBER.md +19 -0
- package/atris.md +63 -23
- package/ax +1434 -1698
- package/bin/atris.js +5 -1
- package/commands/agent-spawn.js +2 -2
- package/commands/brain.js +92 -7
- package/commands/brainstorm.js +62 -22
- package/commands/business-sync.js +92 -8
- package/commands/business.js +13 -7
- package/commands/chat-scan.js +102 -0
- package/commands/computer.js +758 -15
- package/commands/deck.js +505 -105
- package/commands/init.js +6 -1
- package/commands/launchpad.js +638 -0
- package/commands/log-sync.js +44 -13
- package/commands/loop-front.js +290 -0
- package/commands/member.js +124 -3
- package/commands/mission.js +653 -30
- package/commands/pull.js +37 -28
- package/commands/pulse.js +11 -8
- package/commands/run.js +79 -39
- package/commands/sync.js +36 -17
- package/commands/task.js +342 -66
- package/commands/xp.js +3 -0
- package/commands/youtube.js +221 -7
- package/decks/README.md +89 -0
- package/decks/archetype-catalog.json +180 -0
- package/decks/atris-antislop-pitch.json +48 -0
- package/decks/atris-archetypes-v2.json +83 -0
- package/decks/atris-one-loop-pitch.json +80 -0
- package/decks/atris-seed-pitch-v3.json +118 -0
- package/decks/atris-seed-pitch-v4-skeleton.json +106 -0
- package/decks/atris-seed-pitch-v5.json +109 -0
- package/decks/atris-seed-pitch-v6.json +137 -0
- package/decks/atris-seed-pitch-v7.json +133 -0
- package/decks/atris-single-shot-proof.json +74 -0
- package/decks/mark-pincus-narrative.json +102 -0
- package/decks/mark-pincus-sourcery.json +94 -0
- package/decks/yash-applied-compute-detailed.json +150 -0
- package/decks/yash-applied-compute-generalist.json +82 -0
- package/decks/yash-applied-compute-narrative.json +54 -0
- package/lib/ax-prefs.js +5 -12
- package/lib/chat-log-scan.js +377 -0
- package/lib/context-gatherer.js +35 -1
- package/lib/deck-compose.js +145 -0
- package/lib/deck-history.js +64 -0
- package/lib/deck-layout.js +169 -0
- package/lib/deck-review.js +431 -0
- package/lib/deck-schema.js +154 -0
- package/lib/file-ops.js +2 -2
- package/lib/functional-owner.js +189 -0
- package/lib/slides-deck.js +512 -58
- package/lib/task-db.js +109 -2
- package/package.json +2 -1
- package/templates/business-starter/team/START_HERE.md +12 -8
- package/utils/auth.js +4 -0
- package/utils/config.js +4 -0
- package/atris/atrisDev.md +0 -717
package/commands/pull.js
CHANGED
|
@@ -100,6 +100,19 @@ function buildPullConflictReviewPacket(outputDir, conflictChanges, remoteContent
|
|
|
100
100
|
});
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
function mergePulledManifestFiles(remoteFiles = {}, onlyPrefixes = null, manifest = null) {
|
|
104
|
+
if (!onlyPrefixes || !manifest || !manifest.files) return remoteFiles;
|
|
105
|
+
const merged = {};
|
|
106
|
+
for (const [p, info] of Object.entries(manifest.files)) {
|
|
107
|
+
const inScope = onlyPrefixes.some((pref) => p.replace(/^\//, '').startsWith(pref));
|
|
108
|
+
if (!inScope) merged[p] = info;
|
|
109
|
+
}
|
|
110
|
+
for (const [p, info] of Object.entries(remoteFiles)) {
|
|
111
|
+
merged[p] = info;
|
|
112
|
+
}
|
|
113
|
+
return merged;
|
|
114
|
+
}
|
|
115
|
+
|
|
103
116
|
async function pullAtris() {
|
|
104
117
|
let arg = process.argv[3];
|
|
105
118
|
|
|
@@ -819,9 +832,32 @@ async function pullBusiness(slug) {
|
|
|
819
832
|
if (unchangedCount > 0) parts.push(`${unchangedCount} unchanged`);
|
|
820
833
|
if (conflictCount > 0) parts.push(`${conflictCount} conflict${conflictCount > 1 ? 's' : ''}`);
|
|
821
834
|
if (parts.length > 0) console.log(` ${parts.join(', ')}.`);
|
|
835
|
+
|
|
836
|
+
let commitHash = null;
|
|
837
|
+
try {
|
|
838
|
+
const headResult = await apiRequestJson(
|
|
839
|
+
`/business/${businessId}/workspaces/${workspaceId}/git/head`,
|
|
840
|
+
{ method: 'GET', token: creds.token }
|
|
841
|
+
);
|
|
842
|
+
if (headResult.ok && headResult.data && headResult.data.commit) {
|
|
843
|
+
commitHash = headResult.data.commit;
|
|
844
|
+
}
|
|
845
|
+
} catch {
|
|
846
|
+
// Git might not be initialized yet — that's fine
|
|
847
|
+
}
|
|
848
|
+
|
|
822
849
|
if (failOnConflict && conflictCount > 0) {
|
|
823
850
|
const timestamp = syncTimestamp();
|
|
824
851
|
const packet = buildPullConflictReviewPacket(outputDir, conflictChanges, conflictRemoteContents, timestamp);
|
|
852
|
+
if (!noManifest) {
|
|
853
|
+
const manifestFiles = mergePulledManifestFiles(remoteFiles, onlyPrefixes, manifest);
|
|
854
|
+
packet.files[`.atris/sync/conflicts/${timestamp}/manifest.json`] = `${JSON.stringify({
|
|
855
|
+
slug: resolvedSlug || slug,
|
|
856
|
+
manifest: buildManifest(manifestFiles, commitHash, { workspaceRoot: outputDir }),
|
|
857
|
+
baseContents: remoteContent,
|
|
858
|
+
deletedRemote: diff.deletedRemote,
|
|
859
|
+
}, null, 2)}\n`;
|
|
860
|
+
}
|
|
825
861
|
writeConflictReviewPacket(outputDir, packet);
|
|
826
862
|
console.log('');
|
|
827
863
|
console.log(` Sync paused: ${conflictCount} conflict${conflictCount === 1 ? '' : 's'} need review before publishing.`);
|
|
@@ -836,20 +872,6 @@ async function pullBusiness(slug) {
|
|
|
836
872
|
}
|
|
837
873
|
}
|
|
838
874
|
|
|
839
|
-
// Get current commit hash from remote (for manifest)
|
|
840
|
-
let commitHash = null;
|
|
841
|
-
try {
|
|
842
|
-
const headResult = await apiRequestJson(
|
|
843
|
-
`/business/${businessId}/workspaces/${workspaceId}/git/head`,
|
|
844
|
-
{ method: 'GET', token: creds.token }
|
|
845
|
-
);
|
|
846
|
-
if (headResult.ok && headResult.data && headResult.data.commit) {
|
|
847
|
-
commitHash = headResult.data.commit;
|
|
848
|
-
}
|
|
849
|
-
} catch {
|
|
850
|
-
// Git might not be initialized yet — that's fine
|
|
851
|
-
}
|
|
852
|
-
|
|
853
875
|
// ANTI-WIPE GUARD: if cloud reported zero in-scope files but local still
|
|
854
876
|
// has in-scope content (i.e. the sweep refused), don't overwrite the
|
|
855
877
|
// manifest with empty data for the scoped subtree. The manifest is the
|
|
@@ -874,20 +896,7 @@ async function pullBusiness(slug) {
|
|
|
874
896
|
// since the last sync get evicted from the manifest. Without this, the
|
|
875
897
|
// push freshness check would forever flag those paths as "deleted on
|
|
876
898
|
// cloud" drift, blocking pushes for no reason.
|
|
877
|
-
|
|
878
|
-
if (onlyPrefixes && manifest && manifest.files) {
|
|
879
|
-
const merged = {};
|
|
880
|
-
// 1. Keep paths from old manifest that are OUTSIDE the scoped prefix.
|
|
881
|
-
for (const [p, info] of Object.entries(manifest.files)) {
|
|
882
|
-
const inScope = onlyPrefixes.some((pref) => p.replace(/^\//, '').startsWith(pref));
|
|
883
|
-
if (!inScope) merged[p] = info;
|
|
884
|
-
}
|
|
885
|
-
// 2. Overwrite the in-scope subtree with what we just pulled (cloud truth).
|
|
886
|
-
for (const [p, info] of Object.entries(remoteFiles)) {
|
|
887
|
-
merged[p] = info;
|
|
888
|
-
}
|
|
889
|
-
manifestFiles = merged;
|
|
890
|
-
}
|
|
899
|
+
const manifestFiles = mergePulledManifestFiles(remoteFiles, onlyPrefixes, manifest);
|
|
891
900
|
if (!noManifest) {
|
|
892
901
|
const newManifest = buildManifest(manifestFiles, commitHash, { workspaceRoot: outputDir });
|
|
893
902
|
saveManifest(resolvedSlug || slug, newManifest);
|
package/commands/pulse.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// `atris pulse`
|
|
3
|
+
// `atris pulse` - the durable overnight self-improvement heartbeat for atris-cli.
|
|
4
4
|
//
|
|
5
5
|
// atris pulse tick run ONE self-improvement tick (what the cron calls)
|
|
6
6
|
// atris pulse status liveness, reward sum, ghost-tick detection
|
|
@@ -57,6 +57,8 @@ Options:
|
|
|
57
57
|
--runner-template <s> Runner command template for installed heartbeat
|
|
58
58
|
--max-ticks <n> Number of foreground ticks for run
|
|
59
59
|
--help, -h Show this help
|
|
60
|
+
|
|
61
|
+
Tip: \`atris loop start --overnight\` is the one front door to this.
|
|
60
62
|
`.trim();
|
|
61
63
|
}
|
|
62
64
|
|
|
@@ -105,7 +107,7 @@ function runMissionEngine(root, { noClaude = false, timeoutMs = 600000 } = {}) {
|
|
|
105
107
|
}
|
|
106
108
|
|
|
107
109
|
// Fallback worker: one headless autopilot tick. This is the path that reaches
|
|
108
|
-
// proposeCandidateHorizons
|
|
110
|
+
// proposeCandidateHorizons - i.e. where the member AUTHORS a new goal when no
|
|
109
111
|
// mission is due, instead of idling.
|
|
110
112
|
function runAutopilotTick(root, { timeoutMs = 600000 } = {}) {
|
|
111
113
|
const cliPath = path.join(__dirname, '..', 'bin', 'atris.js');
|
|
@@ -152,7 +154,7 @@ function gitChangedFiles(root) {
|
|
|
152
154
|
|
|
153
155
|
// A cheap snapshot of the working tree: current HEAD + the set of dirty files.
|
|
154
156
|
// Comparing two snapshots gives a tick's ACTUAL contribution (a new commit, or
|
|
155
|
-
// files it newly dirtied)
|
|
157
|
+
// files it newly dirtied) - never the whole pre-existing dirty tree.
|
|
156
158
|
function gitSnapshot(root) {
|
|
157
159
|
let head = null;
|
|
158
160
|
try {
|
|
@@ -196,7 +198,7 @@ function tickCommand(args, root = process.cwd()) {
|
|
|
196
198
|
|
|
197
199
|
const tickIndex = pulse.nextTickIndex(root);
|
|
198
200
|
|
|
199
|
-
// 'started' receipt
|
|
201
|
+
// 'started' receipt - if the tick dies after this, the orphan surfaces as a ghost.
|
|
200
202
|
pulse.appendPulseReceipt(root, pulse.buildPulseReceipt({
|
|
201
203
|
tickIndex,
|
|
202
204
|
phase: 'started',
|
|
@@ -225,8 +227,8 @@ function tickCommand(args, root = process.cwd()) {
|
|
|
225
227
|
const elapsedMs = Date.now() - startedAt;
|
|
226
228
|
const reward = pulse.scoreTick({ verifyPassed: verify.passed, producedWork });
|
|
227
229
|
const changedTail = committed
|
|
228
|
-
? '
|
|
229
|
-
: (changedFiles.length ? `
|
|
230
|
+
? ' - committed'
|
|
231
|
+
: (changedFiles.length ? ` - ${changedFiles.length} file(s) changed` : '');
|
|
230
232
|
const what = engine.actor === 'autopilot'
|
|
231
233
|
? `autopilot authored/advanced a goal${changedTail}`
|
|
232
234
|
: engine.reason === 'no_due_mission'
|
|
@@ -249,7 +251,7 @@ function tickCommand(args, root = process.cwd()) {
|
|
|
249
251
|
});
|
|
250
252
|
pulse.appendPulseReceipt(root, receipt);
|
|
251
253
|
|
|
252
|
-
// Revive the reward channel
|
|
254
|
+
// Revive the reward channel - but only when there is signal (gate noise).
|
|
253
255
|
let scorecardWritten = false;
|
|
254
256
|
if (pulse.shouldWriteScorecard({ reward })) {
|
|
255
257
|
pulse.appendScorecard(root, pulse.buildPulseScorecardRow({
|
|
@@ -280,7 +282,7 @@ function tickCommand(args, root = process.cwd()) {
|
|
|
280
282
|
if (!asJson) {
|
|
281
283
|
const ghost = priorStale.stale ? ` (recovered ghost tick #${priorStale.tick_index || '?'})` : '';
|
|
282
284
|
const r = reward > 0 ? `+${reward}` : String(reward);
|
|
283
|
-
process.stdout.write(`pulse tick #${tickIndex}: ${what}
|
|
285
|
+
process.stdout.write(`pulse tick #${tickIndex}: ${what} - verify ${verify.passed === null ? 'n/a' : verify.passed ? 'pass' : 'FAIL'} - reward ${r}${ghost}\n`);
|
|
284
286
|
}
|
|
285
287
|
return emit(out, asJson);
|
|
286
288
|
} catch (err) {
|
|
@@ -492,6 +494,7 @@ module.exports = {
|
|
|
492
494
|
pulseCommand,
|
|
493
495
|
tickCommand,
|
|
494
496
|
statusCommand,
|
|
497
|
+
cronInstalled,
|
|
495
498
|
installCommand,
|
|
496
499
|
uninstallCommand,
|
|
497
500
|
runCommand,
|
package/commands/run.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Atris Run
|
|
2
|
+
* Atris Run - Auto-chain plan → do → review cycles
|
|
3
3
|
*
|
|
4
4
|
* The ignition switch. Reads inbox/backlog, loops autonomously
|
|
5
5
|
* until work is done or max cycles reached.
|
|
@@ -52,7 +52,7 @@ function getRunLogPath(runStamp, cycle) {
|
|
|
52
52
|
*/
|
|
53
53
|
function writePhaseToRunLog(runLogPath, cycle, phase, output, durationMs) {
|
|
54
54
|
const now = new Date().toISOString();
|
|
55
|
-
const header = `# Run Log
|
|
55
|
+
const header = `# Run Log - Cycle ${cycle}\n\n> Generated: ${now}\n\n`;
|
|
56
56
|
const phaseSection = `## ${phase.toUpperCase()} (${Math.round(durationMs / 1000)}s)\n\n${output || '(no output)'}\n\n---\n\n`;
|
|
57
57
|
|
|
58
58
|
if (!fs.existsSync(runLogPath)) {
|
|
@@ -115,7 +115,7 @@ function execPhaseCommandSync(cmd, opts = {}) {
|
|
|
115
115
|
* Build prompt for each phase with full context.
|
|
116
116
|
* If priorCycleReview is provided (from the previous cycle's review phase),
|
|
117
117
|
* it is injected into the plan prompt so the navigator can adjust based on
|
|
118
|
-
* what the validator found
|
|
118
|
+
* what the validator found - closing the try → notice → adjust loop.
|
|
119
119
|
*/
|
|
120
120
|
function buildRunPrompt(phase, context, priorCycleReview) {
|
|
121
121
|
const { mapPath, todoPath, personaPath, lessonsPath, journalPath } = context;
|
|
@@ -153,12 +153,12 @@ Workflow:
|
|
|
153
153
|
1. Read the journal's ## Inbox section for ideas/tasks
|
|
154
154
|
2. Read MAP.md for codebase navigation (file:line references)
|
|
155
155
|
3. Read lessons.md for past learnings (if it exists)
|
|
156
|
-
${(priorCycleReview && priorCycleReview.trim()) ? "4. Read the Previous Cycle's Review above
|
|
156
|
+
${(priorCycleReview && priorCycleReview.trim()) ? "4. Read the Previous Cycle's Review above - adjust planning based on what the validator found\n" : ""}5. For each inbox item, create a task in TODO.md under ## Backlog
|
|
157
157
|
Format: - **T#:** Description [execute]
|
|
158
158
|
6. Keep tasks small and specific (one function, one file, one fix)
|
|
159
159
|
7. Do NOT write code. Planning only.
|
|
160
160
|
|
|
161
|
-
If inbox is empty but TODO.md has backlog tasks, skip planning
|
|
161
|
+
If inbox is empty but TODO.md has backlog tasks, skip planning - tasks already exist.
|
|
162
162
|
If both inbox and backlog are empty, reply: [NOTHING_TO_DO]
|
|
163
163
|
|
|
164
164
|
Reply [PLAN_COMPLETE] when done.`;
|
|
@@ -171,7 +171,7 @@ Read these files first:
|
|
|
171
171
|
${readFiles}
|
|
172
172
|
|
|
173
173
|
Workflow:
|
|
174
|
-
1. Read TODO.md
|
|
174
|
+
1. Read TODO.md - pick the first task from ## Backlog
|
|
175
175
|
2. Move it to ## In Progress with: **Claimed by:** Executor at ${new Date().toISOString()}
|
|
176
176
|
3. Read MAP.md to find exact file:line locations
|
|
177
177
|
4. Implement the task step by step
|
|
@@ -191,7 +191,7 @@ Read these files first:
|
|
|
191
191
|
${readFiles}
|
|
192
192
|
|
|
193
193
|
Workflow:
|
|
194
|
-
1. Read TODO.md
|
|
194
|
+
1. Read TODO.md - find the task in ## In Progress
|
|
195
195
|
2. Review the implementation:
|
|
196
196
|
- Does it actually work? Test it if possible.
|
|
197
197
|
- Does it follow existing patterns? (check MAP.md)
|
|
@@ -257,29 +257,47 @@ function executePhase(phase, context, options = {}) {
|
|
|
257
257
|
/**
|
|
258
258
|
* Check if there's work to do (inbox items or backlog tasks)
|
|
259
259
|
*/
|
|
260
|
-
function
|
|
260
|
+
function hasInboxOrBacklogWork(atrisDir) {
|
|
261
261
|
// Check backlog tasks
|
|
262
262
|
const todoPath = path.join(atrisDir, 'TODO.md');
|
|
263
263
|
const todo = parseTodo(todoPath);
|
|
264
264
|
if (todo.backlog.length > 0 || todo.inProgress.length > 0) return true;
|
|
265
265
|
|
|
266
|
-
// Check inbox
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if (inboxMatch && inboxMatch[1].trim()) {
|
|
272
|
-
const items = inboxMatch[1].trim().split('\n').filter(l => {
|
|
273
|
-
const t = l.trim();
|
|
274
|
-
return t.startsWith('- ') && t.length > 2;
|
|
275
|
-
});
|
|
276
|
-
if (items.length > 0) return true;
|
|
277
|
-
}
|
|
278
|
-
}
|
|
266
|
+
// Check today's inbox through the ONE shared parser, so the gate, the seed
|
|
267
|
+
// picker, and the seeder all read the same file with the same section bounds.
|
|
268
|
+
try {
|
|
269
|
+
if (require('../lib/next-moves').todayInboxItems(process.cwd()).length > 0) return true;
|
|
270
|
+
} catch { /* best-effort */ }
|
|
279
271
|
|
|
280
272
|
return false;
|
|
281
273
|
}
|
|
282
274
|
|
|
275
|
+
// Raw count of unchecked ROADMAP open items. Utility for status/reporting; the
|
|
276
|
+
// work gate below does NOT use it (a raw count and the seed picker can disagree
|
|
277
|
+
// once items are handled/killed), it uses pickRoadmapSeed directly.
|
|
278
|
+
function roadmapOpenCount(atrisDir) {
|
|
279
|
+
try {
|
|
280
|
+
return require('../lib/next-moves').readRoadmapOpenItems(path.dirname(atrisDir)).length;
|
|
281
|
+
} catch {
|
|
282
|
+
return 0;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// A seedable ROADMAP item (the exact thing the idle loop would pull) counts as
|
|
287
|
+
// work. Using pickRoadmapSeed here means the work gate and the seed path are the
|
|
288
|
+
// SAME signal, so the loop never spins on an empty inbox with nothing to seed.
|
|
289
|
+
function hasSeedableRoadmapItem(atrisDir) {
|
|
290
|
+
try {
|
|
291
|
+
return require('../lib/next-moves').pickRoadmapSeed(path.dirname(atrisDir)) != null;
|
|
292
|
+
} catch {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function hasWork(atrisDir) {
|
|
298
|
+
return hasInboxOrBacklogWork(atrisDir) || hasSeedableRoadmapItem(atrisDir);
|
|
299
|
+
}
|
|
300
|
+
|
|
283
301
|
/**
|
|
284
302
|
* Append a durable Master-loop receipt for one cycle's review verdict.
|
|
285
303
|
*
|
|
@@ -331,7 +349,7 @@ function logRunCompletion(cycles, startTime, cycleTimings = []) {
|
|
|
331
349
|
timingLines = '\n' + timingLines;
|
|
332
350
|
}
|
|
333
351
|
|
|
334
|
-
const entry = `\n### Atris Run
|
|
352
|
+
const entry = `\n### Atris Run - ${new Date().toLocaleTimeString()}\n- Cycles: ${cycles}\n- Duration: ${duration}s${timingLines}\n`;
|
|
335
353
|
|
|
336
354
|
if (content.includes('## Notes')) {
|
|
337
355
|
content = content.replace(/(## Notes[^\n]*\n)/, `$1${entry}\n`);
|
|
@@ -343,7 +361,7 @@ function logRunCompletion(cycles, startTime, cycleTimings = []) {
|
|
|
343
361
|
}
|
|
344
362
|
|
|
345
363
|
/**
|
|
346
|
-
* Main run function
|
|
364
|
+
* Main run function - the ignition switch
|
|
347
365
|
*/
|
|
348
366
|
async function runAtris(options = {}) {
|
|
349
367
|
const {
|
|
@@ -374,7 +392,7 @@ async function runAtris(options = {}) {
|
|
|
374
392
|
console.log('');
|
|
375
393
|
if (verbose) {
|
|
376
394
|
console.log('┌─────────────────────────────────────────────────────────────┐');
|
|
377
|
-
console.log(`│ Atris Run v${pkg.version}
|
|
395
|
+
console.log(`│ Atris Run v${pkg.version} - autonomous plan → do → review │`);
|
|
378
396
|
console.log('└─────────────────────────────────────────────────────────────┘');
|
|
379
397
|
console.log('');
|
|
380
398
|
console.log(`Max cycles: ${cycles}`);
|
|
@@ -383,9 +401,9 @@ async function runAtris(options = {}) {
|
|
|
383
401
|
console.log(`Run logs: atris/logs/runs/`);
|
|
384
402
|
console.log('');
|
|
385
403
|
} else {
|
|
386
|
-
console.log(`atris run v${pkg.version}
|
|
404
|
+
console.log(`atris run v${pkg.version} - plan, do, review, repeat.`);
|
|
387
405
|
console.log(`i'll run up to ${cycles} cycle${cycles === 1 ? '' : 's'}, ${timeout / 1000}s per phase. next i'll check the backlog.`);
|
|
388
|
-
console.log(`phase reasoning will be saved to atris/logs/runs/
|
|
406
|
+
console.log(`phase reasoning will be saved to atris/logs/runs/ - you can read what i thought after.`);
|
|
389
407
|
console.log('');
|
|
390
408
|
}
|
|
391
409
|
|
|
@@ -410,7 +428,7 @@ async function runAtris(options = {}) {
|
|
|
410
428
|
const cycleTimings = [];
|
|
411
429
|
const writtenRunLogs = [];
|
|
412
430
|
let completedCycles = 0;
|
|
413
|
-
let lastReviewOutput = null; // Carried to next cycle's plan
|
|
431
|
+
let lastReviewOutput = null; // Carried to next cycle's plan - closes the loop
|
|
414
432
|
|
|
415
433
|
for (let cycle = 1; cycle <= cycles; cycle++) {
|
|
416
434
|
if (verbose) {
|
|
@@ -421,6 +439,28 @@ async function runAtris(options = {}) {
|
|
|
421
439
|
console.log(`\ncycle ${cycle} of ${cycles}.`);
|
|
422
440
|
}
|
|
423
441
|
|
|
442
|
+
// If there's no inbox/backlog work, pull the top open ROADMAP item into the
|
|
443
|
+
// inbox so this cycle pursues the goal. This is how the loop reads ROADMAP.
|
|
444
|
+
if (!hasInboxOrBacklogWork(atrisDir)) {
|
|
445
|
+
try {
|
|
446
|
+
const { pickRoadmapSeed, seedInboxFromMove, claimRoadmapItem } = require('../lib/next-moves');
|
|
447
|
+
const pick = pickRoadmapSeed(process.cwd());
|
|
448
|
+
if (pick) {
|
|
449
|
+
// Seed THEN claim. A crash after the seed leaves the item in the inbox
|
|
450
|
+
// (carried as recoverable work) rather than claimed-but-lost; the seed
|
|
451
|
+
// is idempotent by title so a retry will not duplicate it. The claim
|
|
452
|
+
// ('- [~]' in the Open loop items section) then drops it from the open
|
|
453
|
+
// set so the next idle cycle advances. ROADMAP state is the one source
|
|
454
|
+
// of truth the work gate and the seed picker both read.
|
|
455
|
+
seedInboxFromMove(process.cwd(), { title: pick.title, source: 'roadmap' });
|
|
456
|
+
claimRoadmapItem(process.cwd(), pick.title);
|
|
457
|
+
console.log(verbose
|
|
458
|
+
? `Seeded from ROADMAP: ${pick.title}`
|
|
459
|
+
: `no inbox or backlog work, so i pulled the top ROADMAP item: ${pick.title}`);
|
|
460
|
+
}
|
|
461
|
+
} catch { /* roadmap seeding is best-effort */ }
|
|
462
|
+
}
|
|
463
|
+
|
|
424
464
|
// Check if there's work
|
|
425
465
|
if (!hasWork(atrisDir)) {
|
|
426
466
|
console.log(verbose
|
|
@@ -435,7 +475,7 @@ async function runAtris(options = {}) {
|
|
|
435
475
|
try {
|
|
436
476
|
// PLAN
|
|
437
477
|
console.log(verbose
|
|
438
|
-
? '\n[1/3] PLAN
|
|
478
|
+
? '\n[1/3] PLAN - reading inbox, creating tasks...'
|
|
439
479
|
: 'planning… reading inbox, turning ideas into tasks.');
|
|
440
480
|
if (lastReviewOutput && verbose) {
|
|
441
481
|
console.log(' [loop] carrying previous review into plan prompt');
|
|
@@ -462,7 +502,7 @@ async function runAtris(options = {}) {
|
|
|
462
502
|
}
|
|
463
503
|
|
|
464
504
|
// DO
|
|
465
|
-
console.log(verbose ? '\n[2/3] DO
|
|
505
|
+
console.log(verbose ? '\n[2/3] DO - building task...' : 'building the top task now.');
|
|
466
506
|
phaseStart = Date.now();
|
|
467
507
|
const doOutput = executePhase('do', context, { verbose, timeout });
|
|
468
508
|
timing.do = Date.now() - phaseStart;
|
|
@@ -473,13 +513,13 @@ async function runAtris(options = {}) {
|
|
|
473
513
|
: `built in ${Math.round(timing.do / 1000)}s. next i'll review it.`);
|
|
474
514
|
|
|
475
515
|
// REVIEW
|
|
476
|
-
console.log(verbose ? '\n[3/3] REVIEW
|
|
516
|
+
console.log(verbose ? '\n[3/3] REVIEW - validating...' : 'reviewing the change against tests and validate.md.');
|
|
477
517
|
phaseStart = Date.now();
|
|
478
518
|
const reviewOutput = executePhase('review', context, { verbose, timeout });
|
|
479
519
|
timing.review = Date.now() - phaseStart;
|
|
480
520
|
writePhaseToRunLog(runLogPath, cycle, 'review', reviewOutput, timing.review);
|
|
481
521
|
|
|
482
|
-
// Carry the review output into the next cycle's plan
|
|
522
|
+
// Carry the review output into the next cycle's plan - closes the loop
|
|
483
523
|
lastReviewOutput = reviewOutput;
|
|
484
524
|
|
|
485
525
|
// Persist the verdict durably so the brain can see the review actually ran.
|
|
@@ -503,7 +543,7 @@ async function runAtris(options = {}) {
|
|
|
503
543
|
|
|
504
544
|
// Self-heal MAP.md refs after each cycle
|
|
505
545
|
console.log(verbose
|
|
506
|
-
? '\n[+] CLEAN
|
|
546
|
+
? '\n[+] CLEAN - healing MAP.md refs...'
|
|
507
547
|
: 'cleaning up drifted MAP.md refs.');
|
|
508
548
|
try {
|
|
509
549
|
cleanAtris({ dryRun: false });
|
|
@@ -513,7 +553,7 @@ async function runAtris(options = {}) {
|
|
|
513
553
|
|
|
514
554
|
// Auto-push if not disabled
|
|
515
555
|
if (push) {
|
|
516
|
-
console.log(verbose ? '\n[+] PUSH
|
|
556
|
+
console.log(verbose ? '\n[+] PUSH - pushing to remote...' : 'pushing to remote.');
|
|
517
557
|
try {
|
|
518
558
|
execSync('git push', { cwd: process.cwd(), encoding: 'utf8', stdio: 'pipe' });
|
|
519
559
|
console.log(verbose ? '✓ Pushed to remote' : 'pushed.');
|
|
@@ -653,7 +693,7 @@ function listRunLogs(args = []) {
|
|
|
653
693
|
const filePath = path.join(runsDir, file);
|
|
654
694
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
655
695
|
const lines = content.split('\n');
|
|
656
|
-
const cycleMatch = content.match(/# Run Log
|
|
696
|
+
const cycleMatch = content.match(/# Run Log - Cycle (\d+)/);
|
|
657
697
|
const phases = [...content.matchAll(/## (\w+)/g)].map(m => m[1]);
|
|
658
698
|
return {
|
|
659
699
|
file,
|
|
@@ -819,7 +859,7 @@ function searchRunLogs(args = []) {
|
|
|
819
859
|
}
|
|
820
860
|
|
|
821
861
|
console.log('');
|
|
822
|
-
console.log(`Search: "${keyword}"
|
|
862
|
+
console.log(`Search: "${keyword}" - ${results.length} match${results.length === 1 ? '' : 'es'} in ${files.length} run log${files.length === 1 ? '' : 's'}:`);
|
|
823
863
|
console.log('');
|
|
824
864
|
|
|
825
865
|
const shown = results.slice(0, limit);
|
|
@@ -860,7 +900,7 @@ function statsRunLogs() {
|
|
|
860
900
|
const filePath = path.join(runsDir, file);
|
|
861
901
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
862
902
|
|
|
863
|
-
const cycleMatch = content.match(/# Run Log
|
|
903
|
+
const cycleMatch = content.match(/# Run Log - Cycle (\d+)/);
|
|
864
904
|
if (cycleMatch) totalCycles++;
|
|
865
905
|
|
|
866
906
|
// Extract phase headers with durations: ## PLAN (3s)
|
|
@@ -885,7 +925,7 @@ function statsRunLogs() {
|
|
|
885
925
|
const count = phaseCounts[phase];
|
|
886
926
|
const durs = phaseDurations[phase] || [];
|
|
887
927
|
const avg = durs.length > 0 ? Math.round(durs.reduce((a, b) => a + b, 0) / durs.length) : 0;
|
|
888
|
-
console.log(` ${phase.padEnd(7)} │ ${String(count).padStart(5)} │ ${avg > 0 ? avg + 's' : '
|
|
928
|
+
console.log(` ${phase.padEnd(7)} │ ${String(count).padStart(5)} │ ${avg > 0 ? avg + 's' : '-'}`);
|
|
889
929
|
}
|
|
890
930
|
console.log('');
|
|
891
931
|
}
|
|
@@ -922,7 +962,7 @@ function exportRunLogs(args = []) {
|
|
|
922
962
|
logs: files.map(file => {
|
|
923
963
|
const filePath = path.join(runsDir, file);
|
|
924
964
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
925
|
-
const cycleMatch = content.match(/# Run Log
|
|
965
|
+
const cycleMatch = content.match(/# Run Log - Cycle (\d+)/);
|
|
926
966
|
const phases = [...content.matchAll(/## (\w+)\s*\((\d+)s\)/g)].map(m => ({
|
|
927
967
|
name: m[1],
|
|
928
968
|
duration_s: parseInt(m[2]),
|
|
@@ -1022,4 +1062,4 @@ function diffRunLogs(args = []) {
|
|
|
1022
1062
|
}
|
|
1023
1063
|
}
|
|
1024
1064
|
|
|
1025
|
-
module.exports = { runAtris, getRunLogDir, getRunLogPath, writePhaseToRunLog, appendMasterLoopReceipt, listRunLogs, pruneRunLogs, searchRunLogs, statsRunLogs, exportRunLogs, diffRunLogs, buildRunPrompt };
|
|
1065
|
+
module.exports = { runAtris, hasWork, hasInboxOrBacklogWork, roadmapOpenCount, getRunLogDir, getRunLogPath, writePhaseToRunLog, appendMasterLoopReceipt, listRunLogs, pruneRunLogs, searchRunLogs, statsRunLogs, exportRunLogs, diffRunLogs, buildRunPrompt };
|
package/commands/sync.js
CHANGED
|
@@ -52,6 +52,29 @@ function _templateTargetRelPath(relPath) {
|
|
|
52
52
|
return relPath === 'persona.md' ? 'PERSONA.md' : relPath;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
function ensureRealDirectory(dir) {
|
|
56
|
+
let stat = null;
|
|
57
|
+
try {
|
|
58
|
+
stat = fs.lstatSync(dir);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (!error || error.code !== 'ENOENT') throw error;
|
|
61
|
+
}
|
|
62
|
+
if (stat) {
|
|
63
|
+
if (stat.isDirectory()) return;
|
|
64
|
+
if (stat.isSymbolicLink()) {
|
|
65
|
+
try {
|
|
66
|
+
if (fs.statSync(dir).isDirectory()) return;
|
|
67
|
+
fs.unlinkSync(dir);
|
|
68
|
+
} catch (_) {
|
|
69
|
+
fs.unlinkSync(dir);
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
throw new Error(`${dir} exists and is not a directory`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
76
|
+
}
|
|
77
|
+
|
|
55
78
|
/**
|
|
56
79
|
* Sync the canonical skill set from atris-cli/atris/skills/* into a
|
|
57
80
|
* workspace's atris/skills/* (plus ensure .claude/skills/ symlinks).
|
|
@@ -70,8 +93,8 @@ function syncPackageSkills(targetAtrisDir, opts = {}) {
|
|
|
70
93
|
|
|
71
94
|
if (!fs.existsSync(packageSkillsDir)) return 0;
|
|
72
95
|
|
|
73
|
-
|
|
74
|
-
|
|
96
|
+
ensureRealDirectory(userSkillsDir);
|
|
97
|
+
ensureRealDirectory(claudeSkillsBaseDir);
|
|
75
98
|
|
|
76
99
|
const skillFolders = fs.readdirSync(packageSkillsDir).filter(f => {
|
|
77
100
|
try { return fs.statSync(path.join(packageSkillsDir, f)).isDirectory(); }
|
|
@@ -84,7 +107,7 @@ function syncPackageSkills(targetAtrisDir, opts = {}) {
|
|
|
84
107
|
const symlinkPath = path.join(claudeSkillsBaseDir, skill);
|
|
85
108
|
|
|
86
109
|
const syncRecursive = (src, dest, skillName, basePath = '') => {
|
|
87
|
-
|
|
110
|
+
ensureRealDirectory(dest);
|
|
88
111
|
for (const entry of fs.readdirSync(src)) {
|
|
89
112
|
const srcPath = path.join(src, entry);
|
|
90
113
|
const destPath = path.join(dest, entry);
|
|
@@ -112,7 +135,7 @@ function syncPackageSkills(targetAtrisDir, opts = {}) {
|
|
|
112
135
|
fs.symlinkSync(relativePath, symlinkPath);
|
|
113
136
|
if (verbose) console.log(`✓ Linked .claude/skills/${skill}`);
|
|
114
137
|
} catch (e) {
|
|
115
|
-
|
|
138
|
+
ensureRealDirectory(symlinkPath);
|
|
116
139
|
const skillFile = path.join(destSkillDir, 'SKILL.md');
|
|
117
140
|
if (fs.existsSync(skillFile)) {
|
|
118
141
|
fs.copyFileSync(skillFile, path.join(symlinkPath, 'SKILL.md'));
|
|
@@ -227,9 +250,13 @@ function renderBusinessAgentAdapter(bizMeta = {}, targetRoot = '.') {
|
|
|
227
250
|
'## Proof Loop',
|
|
228
251
|
'',
|
|
229
252
|
'```bash',
|
|
253
|
+
'atris sync --dry-run',
|
|
230
254
|
'atris business check',
|
|
231
255
|
'atris business record atris/reports/<recap>.md --outcome mixed --metric "operator speed"',
|
|
232
256
|
'atris business share --write',
|
|
257
|
+
'atris sync',
|
|
258
|
+
'# Optional during active collaboration:',
|
|
259
|
+
'atris sync --watch',
|
|
233
260
|
'```',
|
|
234
261
|
'',
|
|
235
262
|
`Workspace root at creation: ${rootHint}`,
|
|
@@ -370,7 +397,7 @@ function syncWorkspaceTemplate(targetRoot, bizMeta, options = {}) {
|
|
|
370
397
|
console.log(' ✓ Already up to date');
|
|
371
398
|
} else {
|
|
372
399
|
ensureWikiScaffold(targetRoot);
|
|
373
|
-
console.log(
|
|
400
|
+
console.log(' ✓ Local workspace updated. Run `atris sync` to push and pull with safety checks.');
|
|
374
401
|
}
|
|
375
402
|
|
|
376
403
|
return {
|
|
@@ -463,7 +490,6 @@ function syncAtris() {
|
|
|
463
490
|
|
|
464
491
|
const filesToSync = [
|
|
465
492
|
{ source: 'atris.md', target: 'atris.md' },
|
|
466
|
-
{ source: 'atris/atrisDev.md', target: 'atrisDev.md' },
|
|
467
493
|
{ source: 'PERSONA.md', target: 'PERSONA.md' },
|
|
468
494
|
{ source: 'GETTING_STARTED.md', target: 'GETTING_STARTED.md' },
|
|
469
495
|
{ source: 'atris/CLAUDE.md', target: 'CLAUDE.md' },
|
|
@@ -647,9 +673,7 @@ After displaying the boot output, respond to the user naturally.
|
|
|
647
673
|
*/
|
|
648
674
|
function syncRecursiveCount(src, dest, label, silent) {
|
|
649
675
|
let count = 0;
|
|
650
|
-
|
|
651
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
652
|
-
}
|
|
676
|
+
ensureRealDirectory(dest);
|
|
653
677
|
const entries = fs.readdirSync(src);
|
|
654
678
|
for (const entry of entries) {
|
|
655
679
|
const srcPath = path.join(src, entry);
|
|
@@ -732,12 +756,8 @@ function syncSkills({ silent = false } = {}) {
|
|
|
732
756
|
const userSkillsDir = path.join(targetDir, 'skills');
|
|
733
757
|
const claudeSkillsBaseDir = path.join(process.cwd(), '.claude', 'skills');
|
|
734
758
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
}
|
|
738
|
-
if (!fs.existsSync(claudeSkillsBaseDir)) {
|
|
739
|
-
fs.mkdirSync(claudeSkillsBaseDir, { recursive: true });
|
|
740
|
-
}
|
|
759
|
+
ensureRealDirectory(userSkillsDir);
|
|
760
|
+
ensureRealDirectory(claudeSkillsBaseDir);
|
|
741
761
|
|
|
742
762
|
for (const skill of skillFolders) {
|
|
743
763
|
const srcSkillDir = path.join(packageSkillsDir, skill);
|
|
@@ -756,7 +776,7 @@ function syncSkills({ silent = false } = {}) {
|
|
|
756
776
|
}
|
|
757
777
|
} catch (e) {
|
|
758
778
|
// Fallback: copy instead of symlink
|
|
759
|
-
|
|
779
|
+
ensureRealDirectory(symlinkPath);
|
|
760
780
|
const skillFile = path.join(destSkillDir, 'SKILL.md');
|
|
761
781
|
if (fs.existsSync(skillFile)) {
|
|
762
782
|
fs.copyFileSync(skillFile, path.join(symlinkPath, 'SKILL.md'));
|
|
@@ -808,7 +828,6 @@ function _findAtrisProjects(rootDir, maxDepth = 8) {
|
|
|
808
828
|
// Canonical files shipped from the package root. Must match syncAtris's filesToSync.
|
|
809
829
|
const SYNC_ALL_FILES = [
|
|
810
830
|
{ source: 'atris.md', target: 'atris.md' },
|
|
811
|
-
{ source: 'atris/atrisDev.md', target: 'atrisDev.md' },
|
|
812
831
|
{ source: 'PERSONA.md', target: 'PERSONA.md' },
|
|
813
832
|
{ source: 'GETTING_STARTED.md', target: 'GETTING_STARTED.md' },
|
|
814
833
|
{ source: 'atris/CLAUDE.md', target: 'CLAUDE.md' },
|