atris 3.58.6 → 3.58.7
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 +8 -0
- package/atris/policies/engineering-principles.md +129 -0
- package/atris/policies/genesis.md +112 -0
- package/atris/policies/product-design-principles.md +100 -0
- package/atris/skills/design/SKILL.md +3 -1
- package/atris/skills/engines/SKILL.md +3 -3
- package/atris/skills/youtube/SKILL.md +11 -11
- package/bin/atris.js +56 -3
- package/commands/auth.js +8 -2
- package/commands/brain.js +1 -0
- package/commands/design.js +362 -0
- package/commands/doc-health.js +329 -0
- package/commands/drive.js +32 -0
- package/commands/improve.js +67 -1
- package/commands/land.js +144 -4
- package/commands/learn.js +6 -1
- package/commands/member.js +65 -11
- package/commands/mission.js +37 -7
- package/commands/pulse.js +38 -0
- package/commands/rsi.js +156 -0
- package/commands/task.js +41 -1
- package/commands/workflow.js +11 -6
- package/commands/youtube.js +76 -8
- package/lib/apply-gate.js +22 -4
- package/lib/daily-log.js +88 -0
- package/lib/design-api.js +130 -0
- package/lib/engine-ask.js +1 -1
- package/lib/first-minute.js +1 -6
- package/lib/known-commands.js +3 -3
- package/lib/member-context.js +42 -0
- package/lib/rsi-record.js +335 -0
- package/lib/state-detection.js +8 -8
- package/lib/task-db.js +71 -51
- package/lib/task-list-keeper.js +192 -0
- package/lib/todo-fallback.js +9 -3
- package/lib/todo.js +22 -10
- package/mcp/atris-mcp/index.mjs +174 -0
- package/package.json +8 -3
- package/scripts/det/ytnotes +43 -8
- package/utils/auth.js +62 -7
package/commands/drive.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
const fs = require('fs');
|
|
10
10
|
const path = require('path');
|
|
11
11
|
const { spawnSync } = require('child_process');
|
|
12
|
+
const rsi = require('../lib/rsi-record');
|
|
12
13
|
|
|
13
14
|
const BIN = path.join(__dirname, '..', 'bin', 'atris.js');
|
|
14
15
|
|
|
@@ -73,9 +74,31 @@ async function driveCommand(argv) {
|
|
|
73
74
|
return 0;
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
// Dream-RSI: one drive run is one bounded improvement attempt. Recorded
|
|
78
|
+
// only when the workspace has the recorder; a recording failure never
|
|
79
|
+
// changes the run or its exit code.
|
|
80
|
+
const rsiLog = (m) => process.stderr.write(`rsi: ${m}\n`);
|
|
81
|
+
const rsiAttempt = dryRun ? null : rsi.beginAttempt(cwd, { lane: rsi.IMPROVE_LANE, engine: 'claude', log: rsiLog });
|
|
82
|
+
const rsiBefore = rsiAttempt ? rsi.gitSnapshot(cwd) : null;
|
|
83
|
+
const rsiT0 = Date.now();
|
|
84
|
+
const rsiFinish = (outcome) => {
|
|
85
|
+
if (!rsiAttempt) return;
|
|
86
|
+
const delta = rsi.gitDelta(cwd, rsiBefore);
|
|
87
|
+
rsi.finishAttempt(cwd, rsiAttempt, {
|
|
88
|
+
commits: delta.commits,
|
|
89
|
+
files: delta.files,
|
|
90
|
+
elapsed_s: Math.round((Date.now() - rsiT0) / 100) / 10,
|
|
91
|
+
engine_calls: 1,
|
|
92
|
+
verify: 'skipped',
|
|
93
|
+
...outcome,
|
|
94
|
+
}, { log: rsiLog });
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
try {
|
|
76
98
|
const doctor = runAtris(['mission', 'doctor', '--json'], cwd);
|
|
77
99
|
const report = parseJsonLoose(doctor.stdout);
|
|
78
100
|
if (!report || !Array.isArray(report.findings)) {
|
|
101
|
+
rsiFinish({ status: 'failed', reason: 'mission doctor returned no parseable findings' });
|
|
79
102
|
console.error('drive: mission doctor returned no parseable findings.');
|
|
80
103
|
if (doctor.stderr) console.error(doctor.stderr.slice(0, 500));
|
|
81
104
|
return 1;
|
|
@@ -167,6 +190,11 @@ async function driveCommand(argv) {
|
|
|
167
190
|
};
|
|
168
191
|
if (!dryRun) appendState(cwd, record);
|
|
169
192
|
|
|
193
|
+
rsiFinish({
|
|
194
|
+
status: fixed.length ? 'shipped' : 'nothing',
|
|
195
|
+
reason: `${fixed.length} auto-fixed, ${disengagements.length} still need a human`.slice(0, 200),
|
|
196
|
+
});
|
|
197
|
+
|
|
170
198
|
if (json) { console.log(JSON.stringify({ ok: true, ...record }, null, 2)); return 0; }
|
|
171
199
|
|
|
172
200
|
console.log(`drive tick: ${report.checked_count} missions checked, ${report.findings.length} findings`);
|
|
@@ -182,6 +210,10 @@ async function driveCommand(argv) {
|
|
|
182
210
|
}
|
|
183
211
|
console.log(` next: atris drive status · fix the ✋ list · re-run atris drive`);
|
|
184
212
|
return disengagements.length > 0 ? 0 : 0;
|
|
213
|
+
} catch (err) {
|
|
214
|
+
rsiFinish({ status: 'failed', reason: String(err && err.message ? err.message : err).slice(0, 200) });
|
|
215
|
+
throw err;
|
|
216
|
+
}
|
|
185
217
|
}
|
|
186
218
|
|
|
187
219
|
module.exports = { driveCommand };
|
package/commands/improve.js
CHANGED
|
@@ -30,6 +30,7 @@ const close = require('./close');
|
|
|
30
30
|
const { readUsage } = require('../lib/usage');
|
|
31
31
|
const { knownCommands } = require('../lib/known-commands');
|
|
32
32
|
const { treeHashFor } = require('../lib/tree-hash');
|
|
33
|
+
const rsi = require('../lib/rsi-record');
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
36
|
* Expand a leading `~` to the real home directory for LOCAL filesystem
|
|
@@ -735,6 +736,68 @@ function runLocalFallback(opts = {}) {
|
|
|
735
736
|
};
|
|
736
737
|
}
|
|
737
738
|
|
|
739
|
+
// --- Dream-RSI attempt recording -----------------------------------------
|
|
740
|
+
// A shipping tick (mode full, not dry-run) is one bounded attempt at
|
|
741
|
+
// improving the workspace. When the workspace has the recorder
|
|
742
|
+
// (backend/scripts/rsi/record.py), wrap the tick in open -> finish so the
|
|
743
|
+
// attempt lands in .atris/state/rsi/attempts.jsonl. Recording never changes
|
|
744
|
+
// the tick's result or exit code.
|
|
745
|
+
|
|
746
|
+
function improveAttemptOutcome(result, { workspace, before, elapsedMs }) {
|
|
747
|
+
const s = (result && result.summary) || {};
|
|
748
|
+
const delta = rsi.gitDelta(workspace, before);
|
|
749
|
+
const files = [...new Set([...(Array.isArray(s.files) ? s.files : []), ...delta.files])].slice(0, 200);
|
|
750
|
+
const landed = files.length > 0 || delta.commits > 0 || Boolean(s.shipped);
|
|
751
|
+
const verify = s.verify === true ? 'pass' : s.verify === false ? 'fail' : 'skipped';
|
|
752
|
+
let status;
|
|
753
|
+
if (!result || !result.ok || s.error || verify === 'fail') status = 'failed';
|
|
754
|
+
else if (landed) status = 'shipped';
|
|
755
|
+
else status = 'nothing';
|
|
756
|
+
const reason = status === 'failed'
|
|
757
|
+
? String(result.error || s.error || 'tick failed').slice(-200)
|
|
758
|
+
: String(s.shipped || result.reason || 'nothing to do').slice(0, 200);
|
|
759
|
+
return {
|
|
760
|
+
status,
|
|
761
|
+
verify,
|
|
762
|
+
commits: delta.commits,
|
|
763
|
+
files,
|
|
764
|
+
elapsed_s: Math.round(elapsedMs / 100) / 10,
|
|
765
|
+
engine_calls: 1,
|
|
766
|
+
reason,
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
async function runImprove(opts = {}, deps = {}) {
|
|
771
|
+
const workspace = opts.workspace || process.cwd();
|
|
772
|
+
const log = deps.log || (() => {});
|
|
773
|
+
// Local receipt writes expand a leading ~; the recorder check must too, or
|
|
774
|
+
// a `~/...` workspace would never see its own backend/scripts/rsi/record.py.
|
|
775
|
+
const rsiRoot = expandHome(workspace);
|
|
776
|
+
const shippingTick = (opts.mode || 'full') === 'full' && !opts.dryRun;
|
|
777
|
+
const attempt = shippingTick
|
|
778
|
+
? rsi.beginAttempt(rsiRoot, { lane: rsi.IMPROVE_LANE, engine: rsi.engineFromModel(opts.model) || 'claude', log })
|
|
779
|
+
: null;
|
|
780
|
+
if (!attempt) return runImproveCore(opts, deps);
|
|
781
|
+
const startedMs = Date.now();
|
|
782
|
+
const before = rsi.gitSnapshot(rsiRoot);
|
|
783
|
+
try {
|
|
784
|
+
const result = await runImproveCore(opts, deps);
|
|
785
|
+
rsi.finishAttempt(rsiRoot, attempt, improveAttemptOutcome(result, { workspace: rsiRoot, before, elapsedMs: Date.now() - startedMs }), { log });
|
|
786
|
+
return result;
|
|
787
|
+
} catch (err) {
|
|
788
|
+
rsi.finishAttempt(rsiRoot, attempt, {
|
|
789
|
+
status: 'failed',
|
|
790
|
+
verify: 'skipped',
|
|
791
|
+
commits: 0,
|
|
792
|
+
files: [],
|
|
793
|
+
elapsed_s: Math.round((Date.now() - startedMs) / 100) / 10,
|
|
794
|
+
engine_calls: 1,
|
|
795
|
+
reason: String(err && err.message || err).slice(-200),
|
|
796
|
+
}, { log });
|
|
797
|
+
throw err;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
738
801
|
/**
|
|
739
802
|
* Run one improvement tick. Dependency-injected so tests can fake the
|
|
740
803
|
* network (apiRequestJson), auth (loadCredentials), the local fallback,
|
|
@@ -743,7 +806,7 @@ function runLocalFallback(opts = {}) {
|
|
|
743
806
|
* Returns a structured result:
|
|
744
807
|
* { ok, source: 'api'|'local'|'none', reason, summary?, scorecardPath?, local?, apiResult?, error? }
|
|
745
808
|
*/
|
|
746
|
-
async function
|
|
809
|
+
async function runImproveCore(opts = {}, deps = {}) {
|
|
747
810
|
const apiFn = deps.apiRequestJson || apiRequestJson;
|
|
748
811
|
const loadCreds = deps.loadCredentials || loadCredentials;
|
|
749
812
|
const localFn = deps.runLocalFallback || runLocalFallback;
|
|
@@ -989,6 +1052,8 @@ function isRevisionSignalFile(file) {
|
|
|
989
1052
|
const REVISION_WINDOW_MS = REVISION_WINDOW_HOURS * 60 * 60 * 1000;
|
|
990
1053
|
const AGENT_TRAILER_MARKERS = [
|
|
991
1054
|
'atris-builder[bot]',
|
|
1055
|
+
'night@atris.ai',
|
|
1056
|
+
'devin',
|
|
992
1057
|
'claude',
|
|
993
1058
|
'cursor',
|
|
994
1059
|
'codex',
|
|
@@ -1406,6 +1471,7 @@ async function run(argv = [], deps = {}) {
|
|
|
1406
1471
|
module.exports = {
|
|
1407
1472
|
run,
|
|
1408
1473
|
runImprove,
|
|
1474
|
+
runImproveCore,
|
|
1409
1475
|
parseImproveArgs,
|
|
1410
1476
|
buildImprovePayload,
|
|
1411
1477
|
summarizeImproveResponse,
|
package/commands/land.js
CHANGED
|
@@ -58,6 +58,102 @@ function worktreeWithinReapGrace(worktreePath, now = Date.now()) {
|
|
|
58
58
|
return typeof mtime === 'number' && now - mtime < WORKTREE_REAP_GRACE_MS;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
function canonicalPath(p) {
|
|
62
|
+
try {
|
|
63
|
+
return fs.realpathSync(p);
|
|
64
|
+
} catch {
|
|
65
|
+
return path.resolve(p);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function gitCommonDir(root) {
|
|
70
|
+
for (const args of [['rev-parse', '--path-format=absolute', '--git-common-dir'], ['rev-parse', '--git-common-dir']]) {
|
|
71
|
+
const res = runGit(args, { cwd: root, check: false });
|
|
72
|
+
if (res.status === 0 && res.stdout.trim()) return path.resolve(root, res.stdout.trim());
|
|
73
|
+
}
|
|
74
|
+
return '';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function headRefName(headFile) {
|
|
78
|
+
try {
|
|
79
|
+
const m = /^ref: refs\/heads\/(.+)$/.exec(fs.readFileSync(headFile, 'utf8').trim());
|
|
80
|
+
return m ? m[1] : '';
|
|
81
|
+
} catch {
|
|
82
|
+
return '';
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Branch checkouts straight from .git/worktrees/<id>/HEAD plus the main
|
|
87
|
+
// checkout's HEAD — the live truth, not the board snapshot. A `worktree add`
|
|
88
|
+
// still in flight (or a half-registered entry) can leave a branch checked out
|
|
89
|
+
// in a directory the snapshot never saw, and git's own `branch -D` refusal
|
|
90
|
+
// only consults registrations whose gitdir link is already written.
|
|
91
|
+
// pendingFresh marks a fresh registration whose HEAD file does not exist yet:
|
|
92
|
+
// an add mid-flight that has not named its branch.
|
|
93
|
+
function liveCheckouts(root, now = Date.now()) {
|
|
94
|
+
const bound = new Map();
|
|
95
|
+
let pendingFresh = false;
|
|
96
|
+
const common = gitCommonDir(root);
|
|
97
|
+
if (!common) return { bound, pendingFresh };
|
|
98
|
+
if (path.basename(common) === '.git') {
|
|
99
|
+
const name = headRefName(path.join(common, 'HEAD'));
|
|
100
|
+
const mainPath = path.dirname(common);
|
|
101
|
+
if (name && fs.existsSync(mainPath)) bound.set(name, { path: mainPath, adminDir: common });
|
|
102
|
+
}
|
|
103
|
+
let ids = [];
|
|
104
|
+
try {
|
|
105
|
+
ids = fs.readdirSync(path.join(common, 'worktrees'));
|
|
106
|
+
} catch {
|
|
107
|
+
return { bound, pendingFresh };
|
|
108
|
+
}
|
|
109
|
+
for (const id of ids) {
|
|
110
|
+
const adminDir = path.join(common, 'worktrees', id);
|
|
111
|
+
let stat;
|
|
112
|
+
try {
|
|
113
|
+
stat = fs.statSync(adminDir);
|
|
114
|
+
} catch {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (!stat.isDirectory()) continue;
|
|
118
|
+
const headFile = path.join(adminDir, 'HEAD');
|
|
119
|
+
const name = headRefName(headFile);
|
|
120
|
+
if (!name) {
|
|
121
|
+
if (!fs.existsSync(headFile) && now - stat.mtimeMs < WORKTREE_REAP_GRACE_MS) pendingFresh = true;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
let wtPath = null;
|
|
125
|
+
try {
|
|
126
|
+
wtPath = path.dirname(fs.readFileSync(path.join(adminDir, 'gitdir'), 'utf8').trim());
|
|
127
|
+
} catch {
|
|
128
|
+
// gitdir unwritten or lost: cannot prove which directory is the
|
|
129
|
+
// checkout, so the branch stays claimed — fail safe, prune's job.
|
|
130
|
+
}
|
|
131
|
+
if (wtPath && !fs.existsSync(wtPath)) continue;
|
|
132
|
+
bound.set(name, { path: wtPath, adminDir });
|
|
133
|
+
}
|
|
134
|
+
return { bound, pendingFresh };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// The branch's own reflog records when `worktree add -b` (or `git branch`)
|
|
138
|
+
// created it. While an add is in flight a just-created target branch is very
|
|
139
|
+
// likely the checkout being wired up right now — keep it for this pass.
|
|
140
|
+
function branchCreatedWithinGrace(root, name, now = Date.now()) {
|
|
141
|
+
const res = runGit(['reflog', 'show', '--date=unix', name], { cwd: root, check: false });
|
|
142
|
+
if (res.status !== 0) return false;
|
|
143
|
+
const lines = res.stdout.split(/\r?\n/).filter(Boolean);
|
|
144
|
+
const m = /@\{(\d+)\}/.exec(lines[lines.length - 1] || '');
|
|
145
|
+
return Boolean(m) && now - Number(m[1]) * 1000 < WORKTREE_REAP_GRACE_MS;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// One receipt line for a branch kept because a live checkout claims it:
|
|
149
|
+
// fresh checkouts name the grace, older ones name where the branch is in use.
|
|
150
|
+
function checkoutKeepLine(name, bound, now) {
|
|
151
|
+
if (worktreeWithinReapGrace(bound.path || bound.adminDir, now)) {
|
|
152
|
+
return bound.path ? `${bound.path} (fresh_worktree_grace)` : `branch ${name} (fresh_worktree_grace)`;
|
|
153
|
+
}
|
|
154
|
+
return `branch ${name} (checked out in ${bound.path || 'a worktree'})`;
|
|
155
|
+
}
|
|
156
|
+
|
|
61
157
|
function listBranches(root, base = '') {
|
|
62
158
|
// With a base, ask git for ahead counts in the same single spawn
|
|
63
159
|
// (%(ahead-behind:) needs git >= 2.41; on failure we retry without it and
|
|
@@ -151,7 +247,12 @@ function collectBoard(root, { ttlDays = DEFAULT_TTL_DAYS, staleHours = DEFAULT_S
|
|
|
151
247
|
const worktrees = [];
|
|
152
248
|
const all = listWorktrees(root);
|
|
153
249
|
for (const wt of all.slice(1)) {
|
|
154
|
-
const
|
|
250
|
+
const rawBranch = (wt.branch || '').replace(/^refs\/heads\//, '');
|
|
251
|
+
// 'detached' is a marker, not a name: left as-is it reads as a real branch
|
|
252
|
+
// name (hiding the worktree's own commits and, in reap, pushing the word
|
|
253
|
+
// "detached" into the delete list). Treat it as no branch so the worktree
|
|
254
|
+
// is asked directly what it holds.
|
|
255
|
+
const branch = rawBranch === 'detached' ? '' : rawBranch;
|
|
155
256
|
// light mode skips the full `git status` per worktree, the banner summary
|
|
156
257
|
// never reads dirty counts, only worktree mtimes for staleness.
|
|
157
258
|
const counts = (light ? null : statusCounts(wt.path)) || { staged: 0, unstaged: 0, untracked: 0 };
|
|
@@ -376,8 +477,25 @@ function reap(root, { ttlDays = DEFAULT_TTL_DAYS, staleHours = DEFAULT_STALE_HOU
|
|
|
376
477
|
};
|
|
377
478
|
if (targetNames.size === 0 && worktreeTargets.length === 0) return receipt;
|
|
378
479
|
if (dryRun) {
|
|
480
|
+
// The preview must match the real pass: a branch still checked out in a
|
|
481
|
+
// worktree the board missed is kept, not listed as deleted. A checkout
|
|
482
|
+
// that is itself a removal target would be gone first, so it still reads
|
|
483
|
+
// as deletable here.
|
|
484
|
+
const live = liveCheckouts(root, now);
|
|
485
|
+
const targetPaths = new Set(worktreeTargets.map((w) => canonicalPath(w.path)));
|
|
379
486
|
receipt.removedWorktrees = worktreeTargets.map((w) => w.path);
|
|
380
|
-
receipt.deletedBranches = [...targetNames]
|
|
487
|
+
receipt.deletedBranches = [...targetNames].filter((name) => {
|
|
488
|
+
const bound = live.bound.get(name);
|
|
489
|
+
if (bound && !(bound.path && targetPaths.has(canonicalPath(bound.path)))) {
|
|
490
|
+
receipt.keptWorktrees.push(checkoutKeepLine(name, bound, now));
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
if (!bound && live.pendingFresh && branchCreatedWithinGrace(root, name, now)) {
|
|
494
|
+
receipt.keptWorktrees.push(`branch ${name} (fresh_worktree_grace)`);
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
return true;
|
|
498
|
+
});
|
|
381
499
|
return receipt;
|
|
382
500
|
}
|
|
383
501
|
|
|
@@ -406,9 +524,17 @@ function reap(root, { ttlDays = DEFAULT_TTL_DAYS, staleHours = DEFAULT_STALE_HOU
|
|
|
406
524
|
(w) => targetNames.has(w.branch) || (includeDetached && w.state === 'detached')
|
|
407
525
|
);
|
|
408
526
|
for (const w of survivingWorktreeTargets) {
|
|
527
|
+
// The grace check ran at scan time, and a bundle build can sit between it
|
|
528
|
+
// and removal — an engine that booted mid-sweep refreshes the directory
|
|
529
|
+
// mtime in that gap. Stat once more at removal time, the cheapest place
|
|
530
|
+
// to never be wrong about freshness.
|
|
531
|
+
if (worktreeWithinReapGrace(w.path)) {
|
|
532
|
+
receipt.keptWorktrees.push(`${w.path} (fresh_worktree_grace)`);
|
|
533
|
+
if (w.branch) targetNames.delete(w.branch);
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
409
536
|
// Salvage-then-remove, never keep-because-dirty: patches + untracked
|
|
410
|
-
// copies bank everything force-remove would destroy.
|
|
411
|
-
// grace was already applied when candidates were selected.
|
|
537
|
+
// copies bank everything force-remove would destroy.
|
|
412
538
|
if (w.dirty > 0 && !salvageWorktree(w, dir, receipt)) {
|
|
413
539
|
// could not fully back up what force-remove would destroy, keep it,
|
|
414
540
|
// and say why: a bare path in the receipt reads as an unexplained
|
|
@@ -430,7 +556,21 @@ function reap(root, { ttlDays = DEFAULT_TTL_DAYS, staleHours = DEFAULT_STALE_HOU
|
|
|
430
556
|
}
|
|
431
557
|
}
|
|
432
558
|
|
|
559
|
+
// Re-scan after removals: worktrees just removed freed their branches, and
|
|
560
|
+
// anything still bound is checked out in a directory that exists — the
|
|
561
|
+
// board snapshot may have missed it entirely (a `worktree add` mid-flight),
|
|
562
|
+
// and git's own -D refusal only reads fully wired registrations.
|
|
563
|
+
const live = liveCheckouts(root, now);
|
|
433
564
|
for (const name of targetNames) {
|
|
565
|
+
const bound = live.bound.get(name);
|
|
566
|
+
if (bound) {
|
|
567
|
+
receipt.keptWorktrees.push(checkoutKeepLine(name, bound, now));
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
if (live.pendingFresh && branchCreatedWithinGrace(root, name, now)) {
|
|
571
|
+
receipt.keptWorktrees.push(`branch ${name} (fresh_worktree_grace)`);
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
434
574
|
const entry = board.branches.find((b) => b.name === name);
|
|
435
575
|
// the board is a snapshot; an agent may have committed since it was
|
|
436
576
|
// taken. A branch that moved is left alone, the next reap sees the
|
package/commands/learn.js
CHANGED
|
@@ -420,6 +420,10 @@ function logDirect(jsonStr, deps = {}) {
|
|
|
420
420
|
}
|
|
421
421
|
}
|
|
422
422
|
|
|
423
|
+
function leftoverClaimableInsight(insight) {
|
|
424
|
+
return /^\[claimable\]/i.test(String(insight || '').trim());
|
|
425
|
+
}
|
|
426
|
+
|
|
423
427
|
/**
|
|
424
428
|
* Harvest learnings from journal Notes sections.
|
|
425
429
|
* Scans recent journals for lines that look like insights.
|
|
@@ -450,13 +454,14 @@ function harvestFromJournals(deps = {}) {
|
|
|
450
454
|
// Scan last 7 journals for Notes section entries
|
|
451
455
|
const candidates = [];
|
|
452
456
|
for (const logPath of allLogs.slice(0, 7)) {
|
|
453
|
-
const content = fs.readFileSync(logPath, 'utf8');
|
|
457
|
+
const content = fs.readFileSync(logPath, 'utf8').replace(/\r\n/g, '\n');
|
|
454
458
|
const notesMatch = content.match(/## Notes\n([\s\S]*?)(?=\n## |$)/);
|
|
455
459
|
if (notesMatch && notesMatch[1].trim()) {
|
|
456
460
|
const lines = notesMatch[1].trim().split('\n').filter(l => l.startsWith('- '));
|
|
457
461
|
for (const line of lines) {
|
|
458
462
|
// Strip bullet and optional timestamp prefix
|
|
459
463
|
const insight = line.replace(/^- (\d{2}:\d{2} \u2014 )?/, '').trim();
|
|
464
|
+
if (leftoverClaimableInsight(insight)) continue;
|
|
460
465
|
if (insight.length > 10) {
|
|
461
466
|
candidates.push({ insight, source: path.basename(logPath) });
|
|
462
467
|
}
|
package/commands/member.js
CHANGED
|
@@ -10,6 +10,7 @@ const { defaultObjectiveRunner } = require('../lib/default-runner');
|
|
|
10
10
|
const { readJson, writeJson } = require('../lib/json-file');
|
|
11
11
|
const { hasFlag, readFlag, readNumberFlag } = require('../lib/arg-parser');
|
|
12
12
|
const { ensureMemberBundle, memberBundlePresent } = require('../lib/member-scaffold');
|
|
13
|
+
const { memberProcessPrompt, MEMBER_PROCESS_PATH } = require('../lib/member-context');
|
|
13
14
|
|
|
14
15
|
function findWorkspaceBusinessId(startDir = process.cwd()) {
|
|
15
16
|
let dir = path.resolve(startDir);
|
|
@@ -462,6 +463,7 @@ function missionPurpose(paths) {
|
|
|
462
463
|
}
|
|
463
464
|
|
|
464
465
|
const MEMBER_RUN_RUNNABLE_STATUSES = new Set(['planning', 'running', 'ready']);
|
|
466
|
+
const MISSION_TERMINAL_STATUSES = new Set(['complete', 'stopped', 'failed']);
|
|
465
467
|
|
|
466
468
|
function memberRunMissionMap() {
|
|
467
469
|
try {
|
|
@@ -928,12 +930,11 @@ function memberPing(name, ...args) {
|
|
|
928
930
|
process.exit(2);
|
|
929
931
|
}
|
|
930
932
|
const missionMod = require('./mission');
|
|
931
|
-
const terminal = new Set(['complete', 'stopped', 'failed']);
|
|
932
933
|
const candidates = [
|
|
933
934
|
...missionMod.listMissions(process.cwd()),
|
|
934
935
|
...missionMod.listWorktreeRollupMissions(process.cwd()),
|
|
935
936
|
]
|
|
936
|
-
.filter((m) => m && m.owner === name && !
|
|
937
|
+
.filter((m) => m && m.owner === name && !MISSION_TERMINAL_STATUSES.has(m.status))
|
|
937
938
|
.sort((a, b) => String(b.updated_at || b.created_at || '').localeCompare(String(a.updated_at || a.created_at || '')));
|
|
938
939
|
|
|
939
940
|
const taskNote = pingClaimedTaskDialogue(name, text, from);
|
|
@@ -4216,6 +4217,7 @@ function memberActivate(name) {
|
|
|
4216
4217
|
|
|
4217
4218
|
const content = fs.readFileSync(activePath, 'utf8');
|
|
4218
4219
|
const fm = parseFrontmatter(content) || {};
|
|
4220
|
+
const sharedProcess = memberProcessPrompt(process.cwd());
|
|
4219
4221
|
|
|
4220
4222
|
console.log('');
|
|
4221
4223
|
console.log(`Activating: ${fm.name || name} (${fm.role || 'no role'})`);
|
|
@@ -4298,7 +4300,8 @@ function memberActivate(name) {
|
|
|
4298
4300
|
|
|
4299
4301
|
console.log('');
|
|
4300
4302
|
console.log(`Member "${fm.name || name}" activated.`);
|
|
4301
|
-
|
|
4303
|
+
const identityPath = path.relative(process.cwd(), activePath);
|
|
4304
|
+
console.log(`Tell your agent: "You are the ${fm.role || name}. Read ${sharedProcess ? `${MEMBER_PROCESS_PATH}, then ` : ''}${identityPath}. Stay inside this member's permissions."`);
|
|
4302
4305
|
}
|
|
4303
4306
|
|
|
4304
4307
|
// --- UPGRADE subcommand ---
|
|
@@ -4707,6 +4710,49 @@ function memberGoalFromMission(name, ...args) {
|
|
|
4707
4710
|
);
|
|
4708
4711
|
return;
|
|
4709
4712
|
}
|
|
4713
|
+
if (MISSION_TERMINAL_STATUSES.has(lowerCompact(runtime.status || ''))) {
|
|
4714
|
+
// A finished mission must not mint or re-point an active goal; that recreated the
|
|
4715
|
+
// stale-goal defect class (OBL-1961, OBL-2212, OBL-2232) after every retire. --force
|
|
4716
|
+
// cannot bypass this, it only widens the existing-goal match on a living mission.
|
|
4717
|
+
const staleGoals = state.goals.filter((goal) => (
|
|
4718
|
+
goal.source === 'mission' && goal.status === 'active'
|
|
4719
|
+
&& runtime.id && goal.mission_id === runtime.id
|
|
4720
|
+
));
|
|
4721
|
+
const staleList = staleGoals.map((goal) => goal.title || goal.id).join('; ');
|
|
4722
|
+
const ask = staleGoals.length
|
|
4723
|
+
? `Mission ${runtime.id || compactSentence(runtimeFocus, 88)} is ${runtime.status}, so its active goal is stale. Retire ${compactSentence(staleList, 140)} by setting its status in goals.json, or start a living mission and run: atris member goal-from-mission ${name} --force`
|
|
4724
|
+
: `Mission ${runtime.id || compactSentence(runtimeFocus, 88)} is ${runtime.status}. Start a living mission, then run: atris member goal-from-mission ${name} --force`;
|
|
4725
|
+
const logPath = appendMemberGoalLog(paths.memberDir, name, 'Member goal-from-mission blocked', {
|
|
4726
|
+
ask,
|
|
4727
|
+
mission_id: runtime.id || '',
|
|
4728
|
+
mission_status: runtime.status || '',
|
|
4729
|
+
stale_goals: staleGoals.map((goal) => goal.id),
|
|
4730
|
+
});
|
|
4731
|
+
printJsonOrText(
|
|
4732
|
+
{
|
|
4733
|
+
ok: true,
|
|
4734
|
+
action: 'needs_user',
|
|
4735
|
+
member: name,
|
|
4736
|
+
needs_user: true,
|
|
4737
|
+
ask,
|
|
4738
|
+
stale_goals: staleGoals,
|
|
4739
|
+
mission: {
|
|
4740
|
+
north_star: purpose.northStar,
|
|
4741
|
+
runtime_id: runtime.id || null,
|
|
4742
|
+
runtime_status: runtime.status || null,
|
|
4743
|
+
runtime_next: runtime.next || null,
|
|
4744
|
+
},
|
|
4745
|
+
mission_file: paths.missionFile,
|
|
4746
|
+
log_path: logPath,
|
|
4747
|
+
},
|
|
4748
|
+
[
|
|
4749
|
+
`Blocked for ${name}: mission ${runtime.id || 'in now.md'} is ${runtime.status}, a finished mission cannot drive an active goal.`,
|
|
4750
|
+
`Ask: ${ask}`,
|
|
4751
|
+
],
|
|
4752
|
+
asJson,
|
|
4753
|
+
);
|
|
4754
|
+
return;
|
|
4755
|
+
}
|
|
4710
4756
|
// The title IS the mission focus, no boilerplate prefix; the acceptance list already
|
|
4711
4757
|
// says "one bounded step" and the why carries the full sentence.
|
|
4712
4758
|
const title = compactSentence(runtimeFocus, 96);
|
|
@@ -4977,7 +5023,8 @@ function fallbackProposalForGoal(goal, context = {}) {
|
|
|
4977
5023
|
};
|
|
4978
5024
|
}
|
|
4979
5025
|
|
|
4980
|
-
function proposalPromptForGoal(goal, context = {}) {
|
|
5026
|
+
function proposalPromptForGoal(goal, context = {}, cwd = process.cwd()) {
|
|
5027
|
+
const sharedProcess = memberProcessPrompt(cwd);
|
|
4981
5028
|
const files = (context?.evidence?.goal_files?.files || [])
|
|
4982
5029
|
.filter((file) => file.exists && file.excerpt)
|
|
4983
5030
|
.slice(0, 4)
|
|
@@ -5005,6 +5052,7 @@ function proposalPromptForGoal(goal, context = {}) {
|
|
|
5005
5052
|
},
|
|
5006
5053
|
};
|
|
5007
5054
|
return [
|
|
5055
|
+
...(sharedProcess ? [sharedProcess, ''] : []),
|
|
5008
5056
|
'You generate the next bounded Atris member experiment.',
|
|
5009
5057
|
'Read the JSON context and return only JSON with keys: title, proof_target, next_step, verifier, stop_rule.',
|
|
5010
5058
|
'The next_step must be adaptive to the goal/evidence, concrete, receipt-backed, and safe for one bounded tick.',
|
|
@@ -5058,10 +5106,11 @@ async function callAtris2ProposalLlm(goal, context = {}) {
|
|
|
5058
5106
|
const injected = injectedLlmProposal();
|
|
5059
5107
|
if (injected) return injected;
|
|
5060
5108
|
if (process.env.ATRIS_MEMBER_PROPOSAL_LLM !== '1') return null;
|
|
5109
|
+
const prompt = proposalPromptForGoal(goal, context, process.cwd());
|
|
5061
5110
|
try {
|
|
5062
5111
|
const { postTurn } = require('../ax');
|
|
5063
5112
|
const output = { isTTY: false, write() { return true; } };
|
|
5064
|
-
const result = await postTurn(
|
|
5113
|
+
const result = await postTurn(prompt, {
|
|
5065
5114
|
mode: process.env.ATRIS_MEMBER_PROPOSAL_LLM_MODE || 'fast',
|
|
5066
5115
|
route: 'local',
|
|
5067
5116
|
cwd: process.cwd(),
|
|
@@ -5761,6 +5810,7 @@ function emptyObjectiveGeneratorProposal(extra = {}) {
|
|
|
5761
5810
|
updated_at: stampIso(),
|
|
5762
5811
|
status: extra.status || 'empty',
|
|
5763
5812
|
advisory_only: true,
|
|
5813
|
+
auto_task_eligible: extra.auto_task_eligible !== false,
|
|
5764
5814
|
world_model_used: false,
|
|
5765
5815
|
llm_source: extra.llm_source || null,
|
|
5766
5816
|
llm_error: extra.llm_error || null,
|
|
@@ -5893,8 +5943,8 @@ function fallbackObjectiveGeneratorProposal(graph, recommendations, transferPatt
|
|
|
5893
5943
|
),
|
|
5894
5944
|
suggested_member: member,
|
|
5895
5945
|
suggested_patterns: objectivePatternMatches(transferPatterns, proposedObjective),
|
|
5896
|
-
}, { status: 'ok', llm_error: 'llm_not_configured', world_model_used: true });
|
|
5897
|
-
return proposal || emptyObjectiveGeneratorProposal({ status: 'llm_not_configured', llm_error: 'llm_not_configured' });
|
|
5946
|
+
}, { status: 'ok', llm_error: 'llm_not_configured', world_model_used: true, auto_task_eligible: false });
|
|
5947
|
+
return proposal || emptyObjectiveGeneratorProposal({ status: 'llm_not_configured', llm_error: 'llm_not_configured', auto_task_eligible: false });
|
|
5898
5948
|
}
|
|
5899
5949
|
|
|
5900
5950
|
function objectiveGeneratorPrompt(graph, recommendations, transferPatterns = []) {
|
|
@@ -6056,7 +6106,7 @@ async function runObjectiveGeneratorWake(name, paths, { execute = false } = {})
|
|
|
6056
6106
|
proposal = emptyObjectiveGeneratorProposal({ status: llm.error === 'invalid_json' ? 'parse_error' : 'llm_error', llm_source: llm.source, llm_error: llm.error });
|
|
6057
6107
|
proposal.world_model_used = true;
|
|
6058
6108
|
} else {
|
|
6059
|
-
reason = 'heuristic_objective_proposal_written';
|
|
6109
|
+
reason = execute ? 'heuristic_objective_proposal_written' : 'heuristic_objective_proposal_dry_run';
|
|
6060
6110
|
proposal = fallbackObjectiveGeneratorProposal(graph, recommendations, transferPatterns);
|
|
6061
6111
|
}
|
|
6062
6112
|
}
|
|
@@ -6065,7 +6115,7 @@ async function runObjectiveGeneratorWake(name, paths, { execute = false } = {})
|
|
|
6065
6115
|
proposal.suggested_patterns = objectivePatternMatches(transferPatterns, proposal.proposed_objective);
|
|
6066
6116
|
}
|
|
6067
6117
|
|
|
6068
|
-
if (execute && proposal?.status === 'ok' && Number(proposal.overall_score) > 7) {
|
|
6118
|
+
if (execute && proposal?.status === 'ok' && proposal.auto_task_eligible !== false && Number(proposal.overall_score) > 7) {
|
|
6069
6119
|
createdTask = createAutoObjectiveTask(proposal);
|
|
6070
6120
|
proposal.created_task = createdTask.ok ? {
|
|
6071
6121
|
id: createdTask.task_id || null,
|
|
@@ -6081,7 +6131,8 @@ async function runObjectiveGeneratorWake(name, paths, { execute = false } = {})
|
|
|
6081
6131
|
}
|
|
6082
6132
|
|
|
6083
6133
|
const proposalsPath = objectiveGeneratorProposalsPath(root);
|
|
6084
|
-
|
|
6134
|
+
const proposalsWritten = Boolean(execute);
|
|
6135
|
+
if (proposalsWritten) {
|
|
6085
6136
|
fs.mkdirSync(path.dirname(proposalsPath), { recursive: true });
|
|
6086
6137
|
fs.writeFileSync(proposalsPath, JSON.stringify(proposal, null, 2) + '\n', 'utf8');
|
|
6087
6138
|
}
|
|
@@ -6111,6 +6162,7 @@ async function runObjectiveGeneratorWake(name, paths, { execute = false } = {})
|
|
|
6111
6162
|
llm_successful: Boolean(llm?.source && llm?.proposal && proposal.status === 'ok'),
|
|
6112
6163
|
llm_error: llm?.error || proposal.llm_error || null,
|
|
6113
6164
|
proposals_path: path.relative(root, proposalsPath),
|
|
6165
|
+
proposals_written: proposalsWritten,
|
|
6114
6166
|
task_creation_threshold: 7,
|
|
6115
6167
|
task_created: Boolean(createdTask?.ok),
|
|
6116
6168
|
created_task: proposal.created_task,
|
|
@@ -6128,7 +6180,7 @@ async function runObjectiveGeneratorWake(name, paths, { execute = false } = {})
|
|
|
6128
6180
|
score: proposal.overall_score || '',
|
|
6129
6181
|
task: proposal.created_task?.ref || '',
|
|
6130
6182
|
receipt: path.relative(root, receiptPath),
|
|
6131
|
-
output: path.relative(root, proposalsPath),
|
|
6183
|
+
output: proposalsWritten ? path.relative(root, proposalsPath) : '',
|
|
6132
6184
|
});
|
|
6133
6185
|
|
|
6134
6186
|
return {
|
|
@@ -7817,6 +7869,7 @@ const WAKE_REASON_TEXT = {
|
|
|
7817
7869
|
auto_improver_task_create_failed: 'it found an improvement but could not put the task on the board',
|
|
7818
7870
|
heuristic_cross_domain_proof_written: 'it wrote a cross-domain proof using its built-in heuristics',
|
|
7819
7871
|
heuristic_objective_proposal_written: 'it drafted an objective proposal using its built-in heuristics',
|
|
7872
|
+
heuristic_objective_proposal_dry_run: 'it previewed an objective proposal using its built-in heuristics, without writing it',
|
|
7820
7873
|
install_requires_clean_git: 'installing needs a clean git tree first',
|
|
7821
7874
|
insufficient_world_model_data: 'its world model is too thin to act on yet',
|
|
7822
7875
|
llm_json_parse_failed: 'the model reply did not parse, so it stopped rather than act on garbage',
|
|
@@ -9232,6 +9285,7 @@ async function memberCommand(subcommand, ...args) {
|
|
|
9232
9285
|
}
|
|
9233
9286
|
|
|
9234
9287
|
module.exports = {
|
|
9288
|
+
proposalPromptForGoal,
|
|
9235
9289
|
memberCommand,
|
|
9236
9290
|
findAllMembers,
|
|
9237
9291
|
findWorkspaceBusinessId,
|