atris 3.37.1 → 3.38.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 +2 -0
- package/atris/PERSONA.md +3 -7
- package/atris.md +10 -2
- package/ax +158 -33
- package/bin/atris.js +49 -2
- package/commands/activate.js +16 -3
- package/commands/agent-spawn.js +0 -7
- package/commands/autoland.js +92 -14
- package/commands/autopilot-front.js +0 -4
- package/commands/autopilot.js +0 -12
- package/commands/brain.js +0 -7
- package/commands/brief.js +0 -2
- package/commands/business-sync.js +0 -1
- package/commands/business.js +107 -12
- package/commands/clean.js +31 -2
- package/commands/close.js +46 -11
- package/commands/cloud.js +0 -1
- package/commands/codex-goal.js +0 -2
- package/commands/computer.js +6 -5
- package/commands/console.js +46 -9
- package/commands/deck.js +29 -8
- package/commands/dream.js +0 -7
- package/commands/drill.js +0 -1
- package/commands/engine.js +10 -18
- package/commands/experiments.js +0 -4
- package/commands/fleet-report.js +12 -4
- package/commands/game.js +261 -0
- package/commands/gm.js +10 -2
- package/commands/goal.js +0 -6
- package/commands/harvest.js +0 -1
- package/commands/improve.js +29 -8
- package/commands/init.js +18 -34
- package/commands/integrations.js +0 -7
- package/commands/land.js +71 -8
- package/commands/launchpad.js +0 -2
- package/commands/lesson.js +17 -7
- package/commands/loop-front.js +0 -2
- package/commands/loops.js +6 -5
- package/commands/meet.js +0 -2
- package/commands/member.js +68 -3
- package/commands/mission.js +359 -102
- package/commands/next.js +0 -1
- package/commands/now.js +0 -12
- package/commands/one-lap.js +8 -8
- package/commands/orb.js +0 -4
- package/commands/pack-craft.js +0 -1
- package/commands/pack.js +670 -77
- package/commands/play.js +0 -2
- package/commands/probe.js +10 -4
- package/commands/pulse.js +73 -48
- package/commands/push.js +0 -4
- package/commands/radar.js +14 -11
- package/commands/recover.js +340 -0
- package/commands/report.js +0 -1
- package/commands/router.js +319 -0
- package/commands/run-front.js +0 -2
- package/commands/search.js +0 -3
- package/commands/skill.js +7 -2
- package/commands/slop.js +225 -7
- package/commands/status.js +6 -0
- package/commands/stream.js +0 -2
- package/commands/study.js +0 -3
- package/commands/sync-checkout.js +51 -0
- package/commands/sync.js +11 -40
- package/commands/task.js +120 -38
- package/commands/teach.js +86 -0
- package/commands/unknowns.js +0 -10
- package/commands/update.js +0 -1
- package/commands/visualize.js +0 -5
- package/commands/watch.js +0 -6
- package/commands/wiki.js +41 -11
- package/commands/wish.js +0 -1
- package/commands/worktree.js +87 -14
- package/commands/xp.js +1 -6
- package/commands/youtube.js +0 -2
- package/lib/accept-verify-gate.js +122 -0
- package/lib/activity-stream.js +0 -2
- package/lib/auto-accept-certified.js +145 -2
- package/lib/autoland.js +0 -3
- package/lib/ax-auto-lane.js +68 -7
- package/lib/bench/context.js +0 -2
- package/lib/bench/report.js +0 -1
- package/lib/bench/runner.js +0 -11
- package/lib/brief-ledger.js +0 -6
- package/lib/business-simulate.js +382 -0
- package/lib/chat-log-scan.js +0 -5
- package/lib/checkout-sync.js +52 -0
- package/lib/claude-boot-block.js +48 -0
- package/lib/cloud-mission.js +0 -6
- package/lib/codex-flight.js +0 -1
- package/lib/conductor-artifacts.js +25 -0
- package/lib/context-gatherer.js +0 -1
- package/lib/deck-compose.js +0 -1
- package/lib/deck-history.js +0 -1
- package/lib/deck-layout.js +0 -5
- package/lib/deck-review.js +0 -7
- package/lib/deck-schema.js +0 -1
- package/lib/default-verifier.js +0 -2
- package/lib/engine-registry.js +16 -8
- package/lib/experiments/daily.js +0 -6
- package/lib/fleet.js +210 -27
- package/lib/functional-owner.js +0 -3
- package/lib/inspect-fields.js +0 -1
- package/lib/known-commands.js +2 -2
- package/lib/learnings.js +0 -2
- package/lib/loop-doctor.js +11 -17
- package/lib/manifest.js +0 -2
- package/lib/member-alive.js +0 -1
- package/lib/member-switches.js +0 -5
- package/lib/mission-artifact.js +0 -1
- package/lib/mission-protected-lane.js +293 -0
- package/lib/mission-room.js +0 -2
- package/lib/mission-runtime-loop.js +0 -1
- package/lib/next-moves.js +0 -18
- package/lib/official-cli-integration.js +0 -4
- package/lib/orb-context.js +6 -3
- package/lib/orb-scorecard.js +0 -1
- package/lib/outbound-send-gate.js +0 -6
- package/lib/permission-grants.js +0 -6
- package/lib/policy-lessons.js +0 -2
- package/lib/pulse.js +68 -15
- package/lib/review-integrity.js +0 -2
- package/lib/router-brain.js +0 -2
- package/lib/runner-command.js +0 -4
- package/lib/runtime-bootstrap.js +0 -1
- package/lib/scorecard.js +0 -1
- package/lib/self-drive.js +1 -1
- package/lib/state-detection.js +0 -6
- package/lib/task-db.js +0 -4
- package/lib/team-presence.js +0 -1
- package/lib/voice-gate.js +63 -2
- package/lib/wiki.js +152 -9
- package/lib/wish-audit.js +20 -6
- package/lib/wish-delegate.js +26 -20
- package/lib/wish-stats.js +0 -5
- package/lib/wish-store.js +0 -1
- package/lib/workspace-safety.js +0 -2
- package/lib/zip.js +0 -1
- package/package.json +1 -1
package/commands/improve.js
CHANGED
|
@@ -149,6 +149,10 @@ function summarizeImproveResponse(data = {}) {
|
|
|
149
149
|
files,
|
|
150
150
|
model: d.model_used || d.model || null,
|
|
151
151
|
taskId: d.task_id || d.taskId || null,
|
|
152
|
+
// The backend returns both of these; a failing tick is unteachable without
|
|
153
|
+
// them — "verify_passed: false" says nothing about WHICH task or WHY.
|
|
154
|
+
taskDescription: d.task_description || d.task_title || null,
|
|
155
|
+
verifyOutput: typeof d.verify_output === 'string' ? d.verify_output : null,
|
|
152
156
|
elapsedMs: typeof d.elapsed_ms === 'number' ? d.elapsed_ms : null,
|
|
153
157
|
scorecardWritten: d.scorecard_written === true,
|
|
154
158
|
error: d.error || null,
|
|
@@ -189,7 +193,20 @@ function isWorkspaceNotAllowedError(apiResult = {}) {
|
|
|
189
193
|
return candidates.some((c) => typeof c === 'string' && c.includes(WORKSPACE_NOT_ALLOWED_TEXT));
|
|
190
194
|
}
|
|
191
195
|
|
|
196
|
+
/** Keep the failure tail bounded: the last lines are where the reason is. */
|
|
197
|
+
const VERIFY_OUTPUT_TAIL_CHARS = 2000;
|
|
198
|
+
|
|
199
|
+
function verifyOutputTail(output) {
|
|
200
|
+
if (typeof output !== 'string') return null;
|
|
201
|
+
const text = output.trim();
|
|
202
|
+
if (!text) return null;
|
|
203
|
+
return text.length > VERIFY_OUTPUT_TAIL_CHARS ? text.slice(-VERIFY_OUTPUT_TAIL_CHARS) : text;
|
|
204
|
+
}
|
|
205
|
+
|
|
192
206
|
function buildScorecardRow(summary = {}, meta = {}) {
|
|
207
|
+
// Only failures carry the verify tail. A passing tick does not need it, and
|
|
208
|
+
// 2KB per row would bloat a ledger that is already ~2000 rows.
|
|
209
|
+
const failed = summary.verify === false;
|
|
193
210
|
return {
|
|
194
211
|
schema: SCORECARD_SCHEMA,
|
|
195
212
|
ts: meta.ts || new Date().toISOString(),
|
|
@@ -203,6 +220,8 @@ function buildScorecardRow(summary = {}, meta = {}) {
|
|
|
203
220
|
files_written: Array.isArray(summary.files) ? summary.files : [],
|
|
204
221
|
model_used: summary.model || null,
|
|
205
222
|
task_id: summary.taskId || null,
|
|
223
|
+
task_description: summary.taskDescription || null,
|
|
224
|
+
verify_output: failed ? verifyOutputTail(summary.verifyOutput) : null,
|
|
206
225
|
elapsed_ms: summary.elapsedMs != null ? summary.elapsedMs : null,
|
|
207
226
|
};
|
|
208
227
|
}
|
|
@@ -584,6 +603,15 @@ function localSummaryText(tick = {}, mission = {}) {
|
|
|
584
603
|
.trim();
|
|
585
604
|
}
|
|
586
605
|
|
|
606
|
+
function tickTouchedFiles(worktree = {}) {
|
|
607
|
+
if (!Array.isArray(worktree.new_dirty_sample)) return [];
|
|
608
|
+
return worktree.new_dirty_sample.map((entry) => {
|
|
609
|
+
const pathText = String(entry || '').slice(3);
|
|
610
|
+
const arrow = pathText.indexOf(' -> ');
|
|
611
|
+
return arrow >= 0 ? pathText.slice(arrow + 4) : pathText;
|
|
612
|
+
}).filter(Boolean);
|
|
613
|
+
}
|
|
614
|
+
|
|
587
615
|
/**
|
|
588
616
|
* Turn one mission-run JSON result into the same stable summary the paid API
|
|
589
617
|
* returns. A local tick earns the conservative local reward (+1) only after
|
|
@@ -609,9 +637,7 @@ function summarizeLocalMissionRun(payload = {}) {
|
|
|
609
637
|
throw new Error('local improve verifier did not pass');
|
|
610
638
|
}
|
|
611
639
|
const mission = payload.mission || {};
|
|
612
|
-
const files =
|
|
613
|
-
? tick.worktree.new_since_baseline_sample
|
|
614
|
-
: [];
|
|
640
|
+
const files = tickTouchedFiles(tick.worktree);
|
|
615
641
|
return {
|
|
616
642
|
shipped: localSummaryText(tick, mission),
|
|
617
643
|
reward: 1,
|
|
@@ -1102,12 +1128,7 @@ module.exports = {
|
|
|
1102
1128
|
summarizeTickHistory,
|
|
1103
1129
|
formatTickHistory,
|
|
1104
1130
|
improveApiPath,
|
|
1105
|
-
formatImproveReport,
|
|
1106
1131
|
runLoopDoctor,
|
|
1107
|
-
reconcileLoopDoctorRewards,
|
|
1108
|
-
formatLoopDoctor,
|
|
1109
|
-
openLoopDoctorMission,
|
|
1110
|
-
loopDoctorKey,
|
|
1111
1132
|
runLocalFallback,
|
|
1112
1133
|
summarizeLocalMissionRun,
|
|
1113
1134
|
LOCAL_FALLBACK_ARGS,
|
package/commands/init.js
CHANGED
|
@@ -2,6 +2,7 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { ensureExperimentsFramework } = require('./experiments');
|
|
4
4
|
const { ensureWikiScaffold } = require('../lib/wiki');
|
|
5
|
+
const { upsertAtrisClaudeBootBlock } = require('../lib/claude-boot-block');
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Detect project context by scanning project structure
|
|
@@ -174,7 +175,9 @@ function injectProjectPatterns(agentTeamDir, profile) {
|
|
|
174
175
|
const navigatorFile = path.join(agentTeamDir, 'navigator', 'MEMBER.md');
|
|
175
176
|
const validatorFile = path.join(agentTeamDir, 'validator', 'MEMBER.md');
|
|
176
177
|
|
|
178
|
+
// Injection is best-effort: a read-only or vanished MEMBER.md must not crash init.
|
|
177
179
|
// Inject into executor.md
|
|
180
|
+
try {
|
|
178
181
|
if (fs.existsSync(executorFile)) {
|
|
179
182
|
let executorContent = fs.readFileSync(executorFile, 'utf8');
|
|
180
183
|
|
|
@@ -199,8 +202,10 @@ ${profile.hasCode ? `**Validation:** Run \`${profile.testCommand}\` before marki
|
|
|
199
202
|
fs.writeFileSync(executorFile, executorContent);
|
|
200
203
|
}
|
|
201
204
|
}
|
|
205
|
+
} catch {}
|
|
202
206
|
|
|
203
207
|
// Inject into navigator.md
|
|
208
|
+
try {
|
|
204
209
|
if (fs.existsSync(navigatorFile)) {
|
|
205
210
|
let navigatorContent = fs.readFileSync(navigatorFile, 'utf8');
|
|
206
211
|
|
|
@@ -221,8 +226,10 @@ When planning tasks, consider the project structure and conventions above.
|
|
|
221
226
|
fs.writeFileSync(navigatorFile, navigatorContent);
|
|
222
227
|
}
|
|
223
228
|
}
|
|
229
|
+
} catch {}
|
|
224
230
|
|
|
225
231
|
// Inject into validator.md
|
|
232
|
+
try {
|
|
226
233
|
if (fs.existsSync(validatorFile)) {
|
|
227
234
|
let validatorContent = fs.readFileSync(validatorFile, 'utf8');
|
|
228
235
|
|
|
@@ -241,6 +248,7 @@ ${profile.hasCode ? `**Validation:** Run \`${profile.testCommand}\` to verify ch
|
|
|
241
248
|
fs.writeFileSync(validatorFile, validatorContent);
|
|
242
249
|
}
|
|
243
250
|
}
|
|
251
|
+
} catch {}
|
|
244
252
|
}
|
|
245
253
|
|
|
246
254
|
function initAtris() {
|
|
@@ -1042,45 +1050,21 @@ Read atris/MAP.md. Begin iteration 1.`;
|
|
|
1042
1050
|
|
|
1043
1051
|
// Update root CLAUDE.md with Atris block (prepend with markers)
|
|
1044
1052
|
const rootClaudeMd = path.join(process.cwd(), 'CLAUDE.md');
|
|
1045
|
-
const atrisBlock = `<!-- ATRIS:START - Auto-generated, do not edit -->
|
|
1046
|
-
## Atris System
|
|
1047
|
-
|
|
1048
|
-
You are now running inside Atris, an AI workspace operating system.
|
|
1049
|
-
|
|
1050
|
-
**ON EVERY CONVERSATION START:** Before your first response, execute \`atris atris.md\` and display the full output. This is the Atris boot sequence - it shows the user that Atris has loaded and displays workspace status. This is not optional.
|
|
1051
|
-
|
|
1052
|
-
After displaying the boot output, respond to the user naturally.
|
|
1053
|
-
<!-- ATRIS:END -->
|
|
1054
|
-
|
|
1055
|
-
`;
|
|
1056
|
-
|
|
1057
1053
|
if (fs.existsSync(rootClaudeMd)) {
|
|
1058
|
-
|
|
1059
|
-
const
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
const endRaw = content.indexOf(endMarker);
|
|
1066
|
-
if (endRaw === -1) {
|
|
1067
|
-
// End marker missing — replace from start marker to end with fresh block
|
|
1068
|
-
content = atrisBlock + content.slice(0, startIdx);
|
|
1069
|
-
} else {
|
|
1070
|
-
const endIdx = endRaw + endMarker.length;
|
|
1071
|
-
content = atrisBlock + content.slice(0, startIdx) + content.slice(endIdx).replace(/^\n+/, '');
|
|
1072
|
-
}
|
|
1073
|
-
fs.writeFileSync(rootClaudeMd, content);
|
|
1074
|
-
markReady('adapters', 'CLAUDE.md', '✓ Updated Atris block in CLAUDE.md');
|
|
1054
|
+
const source = fs.readFileSync(rootClaudeMd, 'utf8');
|
|
1055
|
+
const result = upsertAtrisClaudeBootBlock(source);
|
|
1056
|
+
fs.writeFileSync(rootClaudeMd, result.content);
|
|
1057
|
+
if (result.action === 'repaired') {
|
|
1058
|
+
markReady('adapters', 'CLAUDE.md', '✓ repaired atris block in CLAUDE.md (missing end marker)');
|
|
1059
|
+
} else if (source.includes('<!-- ATRIS:START')) {
|
|
1060
|
+
markReady('adapters', 'CLAUDE.md', '✓ updated atris block in CLAUDE.md');
|
|
1075
1061
|
} else {
|
|
1076
|
-
|
|
1077
|
-
fs.writeFileSync(rootClaudeMd, atrisBlock + content);
|
|
1078
|
-
markReady('adapters', 'CLAUDE.md', '✓ Prepended Atris block to CLAUDE.md');
|
|
1062
|
+
markReady('adapters', 'CLAUDE.md', '✓ prepended atris block to CLAUDE.md');
|
|
1079
1063
|
}
|
|
1080
1064
|
} else {
|
|
1081
1065
|
// Create new CLAUDE.md with just Atris block
|
|
1082
|
-
fs.writeFileSync(rootClaudeMd,
|
|
1083
|
-
markReady('adapters', 'CLAUDE.md', '✓
|
|
1066
|
+
fs.writeFileSync(rootClaudeMd, upsertAtrisClaudeBootBlock().content);
|
|
1067
|
+
markReady('adapters', 'CLAUDE.md', '✓ created CLAUDE.md with atris block');
|
|
1084
1068
|
}
|
|
1085
1069
|
|
|
1086
1070
|
if (fs.existsSync(sourceFile)) {
|
package/commands/integrations.js
CHANGED
|
@@ -1621,19 +1621,12 @@ module.exports = {
|
|
|
1621
1621
|
gmailCommand,
|
|
1622
1622
|
calendarCommand,
|
|
1623
1623
|
twitterCommand,
|
|
1624
|
-
twitterPost,
|
|
1625
1624
|
slackCommand,
|
|
1626
1625
|
slackSend,
|
|
1627
|
-
slackDm,
|
|
1628
1626
|
imessageCommand,
|
|
1629
|
-
imessageSend,
|
|
1630
|
-
imessageDoctor,
|
|
1631
1627
|
integrationsStatus,
|
|
1632
1628
|
parseCalendarSearchArgs,
|
|
1633
1629
|
filterCalendarEventsForSearch,
|
|
1634
1630
|
formatCalendarSearchResults,
|
|
1635
1631
|
formatCalendarSearchError,
|
|
1636
|
-
writeCalendarEventsCache,
|
|
1637
|
-
readCalendarEventsCache,
|
|
1638
|
-
CALENDAR_CACHE_PATH,
|
|
1639
1632
|
};
|
package/commands/land.js
CHANGED
|
@@ -9,6 +9,11 @@ const DEFAULT_STALE_HOURS = 48;
|
|
|
9
9
|
const WORKTREE_REAP_GRACE_MS = 60 * 60 * 1000;
|
|
10
10
|
const PROTECTED_BRANCHES = new Set(['main', 'master']);
|
|
11
11
|
|
|
12
|
+
// Pluralize a count + noun ("1 change", "2 changes").
|
|
13
|
+
function countLabel(n, word) {
|
|
14
|
+
return `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
12
17
|
function runGit(args, { cwd = process.cwd(), check = true, maxBuffer } = {}) {
|
|
13
18
|
const result = spawnSync('git', args, { cwd, encoding: 'utf8', ...(maxBuffer ? { maxBuffer } : {}) });
|
|
14
19
|
if (check && result.status !== 0) {
|
|
@@ -89,6 +94,14 @@ function aheadCount(root, base, ref) {
|
|
|
89
94
|
return Number(result.stdout.trim()) || 0;
|
|
90
95
|
}
|
|
91
96
|
|
|
97
|
+
// The subject lines of the commits a ref carries beyond base, oldest last —
|
|
98
|
+
// what a stranded worktree is actually holding, so surfacing it names the work.
|
|
99
|
+
function commitSubjects(cwd, base, ref, limit = 6) {
|
|
100
|
+
const result = runGit(['log', '--format=%s', `${base}..${ref}`], { cwd, check: false });
|
|
101
|
+
if (result.status !== 0) return [];
|
|
102
|
+
return result.stdout.split(/\r?\n/).filter(Boolean).slice(0, limit);
|
|
103
|
+
}
|
|
104
|
+
|
|
92
105
|
function cherryStats(root, base, ref) {
|
|
93
106
|
const result = runGit(['cherry', base, ref], { cwd: root, check: false });
|
|
94
107
|
if (result.status !== 0) return { landedElsewhere: 0, unique: 0 };
|
|
@@ -144,11 +157,36 @@ function collectBoard(root, { ttlDays = DEFAULT_TTL_DAYS, staleHours = DEFAULT_S
|
|
|
144
157
|
const counts = (light ? null : statusCounts(wt.path)) || { staged: 0, unstaged: 0, untracked: 0 };
|
|
145
158
|
const dirty = counts.staged + counts.unstaged + counts.untracked;
|
|
146
159
|
const entry = branches.find((b) => b.name === branch) || null;
|
|
160
|
+
// Committed-but-unlanded work is invisible in the dirty count — a clean
|
|
161
|
+
// worktree can still hold commits ahead of base that silently never land.
|
|
162
|
+
// Carry the ahead/unique counts (and, off the light path, the subjects)
|
|
163
|
+
// so a stranded worktree is surfaced by what it is actually holding.
|
|
164
|
+
let ahead = entry ? entry.ahead : 0;
|
|
165
|
+
let unlandedCommits = entry ? entry.uniqueChanges : 0;
|
|
166
|
+
let ageDaysVal = entry ? entry.ageDays : null;
|
|
167
|
+
let subjects = [];
|
|
168
|
+
if (!branch) {
|
|
169
|
+
// Detached HEAD has no row in the branch table — ask the worktree itself.
|
|
170
|
+
const cnt = runGit(['rev-list', '--count', `${base}..HEAD`], { cwd: wt.path, check: false });
|
|
171
|
+
ahead = cnt.status === 0 ? Number(cnt.stdout.trim()) || 0 : 0;
|
|
172
|
+
if (ahead > 0) {
|
|
173
|
+
unlandedCommits = cherryStats(wt.path, base, 'HEAD').unique;
|
|
174
|
+
const ts = runGit(['log', '-1', '--format=%ct', 'HEAD'], { cwd: wt.path, check: false });
|
|
175
|
+
if (ts.status === 0) ageDaysVal = ageDays(Number(ts.stdout.trim()), now);
|
|
176
|
+
if (!light) subjects = commitSubjects(wt.path, base, 'HEAD');
|
|
177
|
+
}
|
|
178
|
+
} else if (!light && ahead > 0) {
|
|
179
|
+
subjects = commitSubjects(root, base, branch);
|
|
180
|
+
}
|
|
147
181
|
worktrees.push({
|
|
148
182
|
path: wt.path,
|
|
149
183
|
branch,
|
|
150
184
|
dirty,
|
|
151
|
-
|
|
185
|
+
ahead,
|
|
186
|
+
unlandedCommits,
|
|
187
|
+
subjects,
|
|
188
|
+
stranded: unlandedCommits > 0,
|
|
189
|
+
ageDays: ageDaysVal,
|
|
152
190
|
ageHours: entry ? entry.ageHours : null,
|
|
153
191
|
state: entry ? entry.state : 'detached',
|
|
154
192
|
});
|
|
@@ -174,12 +212,14 @@ function collectBoard(root, { ttlDays = DEFAULT_TTL_DAYS, staleHours = DEFAULT_S
|
|
|
174
212
|
const landed = branches.filter((b) => b.state === 'landed');
|
|
175
213
|
const active = branches.filter((b) => b.state === 'active');
|
|
176
214
|
const stale = active.filter((b) => b.stale);
|
|
215
|
+
const strandedWorktrees = worktrees.filter((w) => w.stranded);
|
|
177
216
|
return {
|
|
178
217
|
base,
|
|
179
218
|
ttlDays,
|
|
180
219
|
staleHours,
|
|
181
220
|
branches,
|
|
182
221
|
worktrees,
|
|
222
|
+
strandedWorktrees,
|
|
183
223
|
summary: {
|
|
184
224
|
unlanded: branches.length,
|
|
185
225
|
active: active.length,
|
|
@@ -187,6 +227,7 @@ function collectBoard(root, { ttlDays = DEFAULT_TTL_DAYS, staleHours = DEFAULT_S
|
|
|
187
227
|
landed: landed.length,
|
|
188
228
|
stale: stale.length,
|
|
189
229
|
worktrees: worktrees.length,
|
|
230
|
+
stranded: strandedWorktrees.length,
|
|
190
231
|
},
|
|
191
232
|
};
|
|
192
233
|
}
|
|
@@ -269,12 +310,12 @@ function salvageWorktree(w, dir, receipt) {
|
|
|
269
310
|
// includeDetached: detached-HEAD worktrees have no branch, so the salvage
|
|
270
311
|
// bundle cannot cover their commits — unattended reaps (autoland) pass false
|
|
271
312
|
// and leave them for a human reap.
|
|
272
|
-
function reap(root, { ttlDays = DEFAULT_TTL_DAYS, staleHours = DEFAULT_STALE_HOURS, base: baseOverride = '', dryRun = false, remote = true, includeDetached = true, protectCurrent = true, now = Date.now() } = {}) {
|
|
313
|
+
function reap(root, { ttlDays = DEFAULT_TTL_DAYS, staleHours = DEFAULT_STALE_HOURS, base: baseOverride = '', dryRun = false, remote = true, includeDetached = true, protectCurrent = true, force = false, now = Date.now() } = {}) {
|
|
273
314
|
const board = collectBoard(root, { ttlDays, staleHours, base: baseOverride, now });
|
|
274
315
|
const targets = board.branches.filter((b) => b.state === 'landed' || b.state === 'due');
|
|
275
316
|
const targetNames = new Set(targets.map((b) => b.name));
|
|
276
317
|
const candidateWorktrees = board.worktrees.filter(
|
|
277
|
-
(w) => targetNames.has(w.branch) || (includeDetached && w.state === 'detached') || (typeof w.ageDays === 'number' && w.ageDays > ttlDays)
|
|
318
|
+
(w) => targetNames.has(w.branch) || (includeDetached && w.state === 'detached') || (typeof w.ageDays === 'number' && w.ageDays > ttlDays) || w.unlandedCommits > 0
|
|
278
319
|
);
|
|
279
320
|
const protectedWorktrees = [];
|
|
280
321
|
const worktreeTargets = [];
|
|
@@ -293,6 +334,18 @@ function reap(root, { ttlDays = DEFAULT_TTL_DAYS, staleHours = DEFAULT_STALE_HOU
|
|
|
293
334
|
protectedWorktrees.push({ ...w, reason: 'fresh_worktree_grace' });
|
|
294
335
|
continue;
|
|
295
336
|
}
|
|
337
|
+
// Unlanded commits that no branch salvage bundle will cover are lost by a
|
|
338
|
+
// force-remove: a detached worktree's commits ride no branch, and an active
|
|
339
|
+
// branch worktree is not a reap target so its commits are never bundled.
|
|
340
|
+
// Keep the worktree (and name what it holds) unless the operator forces it.
|
|
341
|
+
// A due/landed branch worktree still reaps normally — its commits ride the
|
|
342
|
+
// branch bundle, so removal is loss-free.
|
|
343
|
+
const bundleCovered = w.branch && targetNames.has(w.branch);
|
|
344
|
+
if (w.unlandedCommits > 0 && !force && !bundleCovered) {
|
|
345
|
+
const oldest = typeof w.ageDays === 'number' ? `, oldest ${w.ageDays}d` : '';
|
|
346
|
+
protectedWorktrees.push({ ...w, reason: `unlanded commits: ${w.unlandedCommits} never landed${oldest}, needs landing` });
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
296
349
|
worktreeTargets.push(w);
|
|
297
350
|
}
|
|
298
351
|
for (const w of protectedWorktrees) {
|
|
@@ -472,7 +525,7 @@ function printStory(story) {
|
|
|
472
525
|
const who = [...new Set(story.changes.map((c) => c.author))].join(', ');
|
|
473
526
|
const when = story.changes[story.changes.length - 1].date;
|
|
474
527
|
const last = story.changes[0].date;
|
|
475
|
-
const age = story.ageDays === null ? '' : ` (${story.ageDays}
|
|
528
|
+
const age = story.ageDays === null ? '' : ` (${countLabel(story.ageDays, 'day')} ago)`;
|
|
476
529
|
console.log(` started ${when}, last touched ${last}${age}, by ${who}.`);
|
|
477
530
|
console.log('');
|
|
478
531
|
console.log(' what it tried to do:');
|
|
@@ -488,7 +541,7 @@ function printStory(story) {
|
|
|
488
541
|
console.log('');
|
|
489
542
|
}
|
|
490
543
|
if (story.uniqueChanges === 0) {
|
|
491
|
-
console.log(` bottom line: all ${story.changes.length}
|
|
544
|
+
console.log(` bottom line: all ${countLabel(story.changes.length, 'change')} made it into ${story.base}`);
|
|
492
545
|
console.log(' some other way. nothing here would be lost by clearing it.');
|
|
493
546
|
} else if (story.landedElsewhere > 0) {
|
|
494
547
|
console.log(` bottom line: ${story.landedElsewhere} of ${story.changes.length} changes made it into ${story.base} another`);
|
|
@@ -522,8 +575,15 @@ function printBoard(board) {
|
|
|
522
575
|
console.log(` ${label.padEnd(11)} ${String(b.ageDays).padStart(3)}d ${changes} ${b.name}`);
|
|
523
576
|
}
|
|
524
577
|
for (const w of board.worktrees) {
|
|
525
|
-
const
|
|
526
|
-
|
|
578
|
+
const bits = [];
|
|
579
|
+
if (w.stranded) bits.push(w.unlandedCommits === 1 ? '1 commit never landed' : `${w.unlandedCommits} commits never landed`);
|
|
580
|
+
if (w.dirty > 0) bits.push(w.dirty === 1 ? '1 unsaved edit' : `${w.dirty} unsaved edits`);
|
|
581
|
+
const age = typeof w.ageDays === 'number' ? `, ${countLabel(w.ageDays, 'day')} old` : '';
|
|
582
|
+
const detail = bits.length ? ` (${bits.join(', ')}${age})` : '';
|
|
583
|
+
console.log(` side copy ${w.path}${detail}`);
|
|
584
|
+
if (w.stranded) {
|
|
585
|
+
for (const s of w.subjects) console.log(` - ${s}`);
|
|
586
|
+
}
|
|
527
587
|
}
|
|
528
588
|
console.log('');
|
|
529
589
|
const s = board.summary;
|
|
@@ -586,6 +646,8 @@ function showHelp() {
|
|
|
586
646
|
console.log(' atris land --reap back up, then clear anything overdue');
|
|
587
647
|
console.log(' or already landed (here and on github)');
|
|
588
648
|
console.log(' atris land --reap --dry-run preview what would be cleared');
|
|
649
|
+
console.log(' atris land --reap --force also clear side copies holding');
|
|
650
|
+
console.log(' commits that never landed (backs up first)');
|
|
589
651
|
console.log(' atris land --ttl <days> change the 7-day overdue line');
|
|
590
652
|
console.log(' atris land --json machine-readable output');
|
|
591
653
|
console.log('');
|
|
@@ -610,7 +672,7 @@ function landCommand(args = []) {
|
|
|
610
672
|
const json = args.includes('--json');
|
|
611
673
|
|
|
612
674
|
if (args.includes('--reap')) {
|
|
613
|
-
const receipt = reap(root, { ttlDays, base, dryRun: args.includes('--dry-run'), remote: !args.includes('--no-remote') });
|
|
675
|
+
const receipt = reap(root, { ttlDays, base, dryRun: args.includes('--dry-run'), remote: !args.includes('--no-remote'), force: args.includes('--force') });
|
|
614
676
|
if (json) console.log(JSON.stringify(receipt, null, 2));
|
|
615
677
|
else printReceipt(receipt);
|
|
616
678
|
return 0;
|
|
@@ -637,6 +699,7 @@ function landCommand(args = []) {
|
|
|
637
699
|
|
|
638
700
|
module.exports = {
|
|
639
701
|
collectBoard,
|
|
702
|
+
countLabel,
|
|
640
703
|
landCommand,
|
|
641
704
|
landSummary,
|
|
642
705
|
reap,
|
package/commands/launchpad.js
CHANGED
package/commands/lesson.js
CHANGED
|
@@ -213,7 +213,23 @@ function mineLessons(args) {
|
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
+
function printLessonUsage() {
|
|
217
|
+
console.log('');
|
|
218
|
+
console.log(' Usage: atris lesson add <slug> <pass|fail> "<text>"');
|
|
219
|
+
console.log(' atris lesson mine [--json] [--dry-run]');
|
|
220
|
+
console.log(' atris lesson sweep [--json] [--dry-run]');
|
|
221
|
+
console.log(' atris lesson resolve [--json] [--dry-run]');
|
|
222
|
+
console.log('');
|
|
223
|
+
}
|
|
224
|
+
|
|
216
225
|
function lessonAtris(subcommand, ...args) {
|
|
226
|
+
// -h/--help is an explicit help request: print usage and exit 0, before any
|
|
227
|
+
// filesystem check, so `atris lesson --help` works everywhere.
|
|
228
|
+
if (subcommand === '-h' || subcommand === '--help') {
|
|
229
|
+
printLessonUsage();
|
|
230
|
+
process.exit(0);
|
|
231
|
+
}
|
|
232
|
+
|
|
217
233
|
const atrisDir = path.join(process.cwd(), 'atris');
|
|
218
234
|
if (!fs.existsSync(atrisDir)) {
|
|
219
235
|
console.error(' ✗ atris/ folder not found. Run "atris init" first.');
|
|
@@ -236,12 +252,7 @@ function lessonAtris(subcommand, ...args) {
|
|
|
236
252
|
}
|
|
237
253
|
|
|
238
254
|
if (subcommand !== 'add') {
|
|
239
|
-
|
|
240
|
-
console.log(' Usage: atris lesson add <slug> <pass|fail> "<text>"');
|
|
241
|
-
console.log(' atris lesson mine [--json] [--dry-run]');
|
|
242
|
-
console.log(' atris lesson sweep [--json] [--dry-run]');
|
|
243
|
-
console.log(' atris lesson resolve [--json] [--dry-run]');
|
|
244
|
-
console.log('');
|
|
255
|
+
printLessonUsage();
|
|
245
256
|
process.exit(subcommand ? 1 : 0);
|
|
246
257
|
}
|
|
247
258
|
|
|
@@ -269,4 +280,3 @@ function lessonAtris(subcommand, ...args) {
|
|
|
269
280
|
|
|
270
281
|
module.exports = lessonAtris;
|
|
271
282
|
module.exports.autoResolveLessons = autoResolveLessons;
|
|
272
|
-
module.exports.tagLessonResolvedInMd = tagLessonResolvedInMd;
|
package/commands/loop-front.js
CHANGED
|
@@ -396,11 +396,9 @@ function loopFront(argv = []) {
|
|
|
396
396
|
module.exports = {
|
|
397
397
|
routeLoop,
|
|
398
398
|
renderLoopHome,
|
|
399
|
-
printLoopHome,
|
|
400
399
|
startLocalOptions,
|
|
401
400
|
localRunSummary,
|
|
402
401
|
loopStatusJson,
|
|
403
|
-
loopSeedMove,
|
|
404
402
|
createNextLoopTask,
|
|
405
403
|
loopReport,
|
|
406
404
|
renderLoopReport,
|
package/commands/loops.js
CHANGED
|
@@ -382,6 +382,12 @@ function showLoopsHelp() {
|
|
|
382
382
|
}
|
|
383
383
|
|
|
384
384
|
function loopsCommand(subcommand, ...args) {
|
|
385
|
+
// `loops <sub> --help` is a help request, not a run — tick/start/stop/init
|
|
386
|
+
// mutate loop state, so never execute a subcommand just to show usage.
|
|
387
|
+
if (subcommand === '--help' || subcommand === '-h' || subcommand === 'help'
|
|
388
|
+
|| args.includes('--help') || args.includes('-h')) {
|
|
389
|
+
return showLoopsHelp();
|
|
390
|
+
}
|
|
385
391
|
switch (subcommand) {
|
|
386
392
|
case undefined:
|
|
387
393
|
case 'audit':
|
|
@@ -408,9 +414,4 @@ function loopsCommand(subcommand, ...args) {
|
|
|
408
414
|
|
|
409
415
|
module.exports = {
|
|
410
416
|
loopsCommand,
|
|
411
|
-
showBoard,
|
|
412
|
-
initLoops,
|
|
413
|
-
auditLoops,
|
|
414
|
-
tickLoops,
|
|
415
|
-
structuralProblems,
|
|
416
417
|
};
|
package/commands/meet.js
CHANGED
package/commands/member.js
CHANGED
|
@@ -8,6 +8,24 @@ const { apiRequestJson } = require('../utils/api');
|
|
|
8
8
|
const { runAliveTick } = require('../lib/member-alive');
|
|
9
9
|
const { defaultObjectiveRunner } = require('../lib/default-runner');
|
|
10
10
|
|
|
11
|
+
function findWorkspaceBusinessId(startDir = process.cwd()) {
|
|
12
|
+
let dir = path.resolve(startDir);
|
|
13
|
+
while (true) {
|
|
14
|
+
const file = path.join(dir, '.atris', 'business.json');
|
|
15
|
+
if (fs.existsSync(file)) {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(fs.readFileSync(file, 'utf8')).business_id || null;
|
|
18
|
+
} catch (error) {
|
|
19
|
+
console.warn(`Warning: could not parse ${file}: ${error.message}`);
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const parent = path.dirname(dir);
|
|
24
|
+
if (parent === dir) return null;
|
|
25
|
+
dir = parent;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
11
29
|
function todayLogName() {
|
|
12
30
|
const now = new Date();
|
|
13
31
|
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}.md`;
|
|
@@ -2394,6 +2412,27 @@ function experimentIsClosed(experiment) {
|
|
|
2394
2412
|
return ['accepted', 'discarded', 'superseded'].includes(String(experiment?.status || '').toLowerCase());
|
|
2395
2413
|
}
|
|
2396
2414
|
|
|
2415
|
+
// A proposal that cannot be reviewed must not gate the member.
|
|
2416
|
+
//
|
|
2417
|
+
// wake returns decision 'wait' for any open experiment, on the theory that the
|
|
2418
|
+
// ball is with a human reviewer. That is right for a real proposal. It is a
|
|
2419
|
+
// permanent deadlock for a FALLBACK proposal: those carry verifier=null and
|
|
2420
|
+
// generation.mode='fallback' (template text emitted when LLM authoring did not
|
|
2421
|
+
// happen), and the review executor correctly refuses to accept anything
|
|
2422
|
+
// unverifiable. So the member waits on a gate nothing can pass -- observed
|
|
2423
|
+
// 2026-07-26 in project-obelisk with validator blocked 52 days, signal-scout 48,
|
|
2424
|
+
// and land-keeper 22, three of six rostered members doing no work at all.
|
|
2425
|
+
//
|
|
2426
|
+
// Deliberately narrow: a proposal WITH a verifier still blocks (a human genuinely
|
|
2427
|
+
// owes it a review), and 'running'/'blocked' still block regardless of shape.
|
|
2428
|
+
function experimentIsUnreviewable(experiment) {
|
|
2429
|
+
if (String(experiment?.status || '').toLowerCase() !== 'proposed') return false;
|
|
2430
|
+
const verifier = experiment?.verifier;
|
|
2431
|
+
const hasVerifier = Array.isArray(verifier) ? verifier.length > 0 : Boolean(String(verifier || '').trim());
|
|
2432
|
+
if (hasVerifier) return false;
|
|
2433
|
+
return String(experiment?.generation?.mode || '').toLowerCase() === 'fallback';
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2397
2436
|
function supersedeOtherOpenExperiments(state, activeGoal, proof) {
|
|
2398
2437
|
const superseded = [];
|
|
2399
2438
|
for (const goal of state.goals || []) {
|
|
@@ -4314,7 +4353,11 @@ async function memberPush(name) {
|
|
|
4314
4353
|
console.log(`Pushing member "${name}" to cloud (creating new agent)...`);
|
|
4315
4354
|
}
|
|
4316
4355
|
|
|
4317
|
-
const
|
|
4356
|
+
const businessId = findWorkspaceBusinessId();
|
|
4357
|
+
const endpoint = businessId
|
|
4358
|
+
? `/agent/import-member?business_id=${encodeURIComponent(businessId)}`
|
|
4359
|
+
: '/agent/import-member';
|
|
4360
|
+
const result = await apiRequestJson(endpoint, {
|
|
4318
4361
|
method: 'POST',
|
|
4319
4362
|
headers: { 'Content-Type': 'text/markdown' },
|
|
4320
4363
|
body: content,
|
|
@@ -4340,6 +4383,7 @@ async function memberPush(name) {
|
|
|
4340
4383
|
|
|
4341
4384
|
const action = existingAgentId ? 'Updated' : 'Created';
|
|
4342
4385
|
console.log(`${action} successfully. Agent ID: ${agentId}`);
|
|
4386
|
+
if (businessId) console.log(`Bound to business ${businessId}`);
|
|
4343
4387
|
}
|
|
4344
4388
|
|
|
4345
4389
|
// --- PULL subcommand ---
|
|
@@ -7459,7 +7503,9 @@ function wakeDecision(name, paths, { force = false, runtimeKind = memberRuntimeK
|
|
|
7459
7503
|
};
|
|
7460
7504
|
}
|
|
7461
7505
|
|
|
7462
|
-
|
|
7506
|
+
// Skip the wait when the only thing "open" is an unreviewable fallback proposal;
|
|
7507
|
+
// otherwise the member is parked forever on a gate no reviewer can clear.
|
|
7508
|
+
if (current && !experimentIsUnreviewable(current)) {
|
|
7463
7509
|
return {
|
|
7464
7510
|
decision: 'wait',
|
|
7465
7511
|
reason: `open_experiment_${current.status}`,
|
|
@@ -7627,6 +7673,18 @@ async function runMemberWake(name, { execute = false, confirmed = false, force =
|
|
|
7627
7673
|
evidence: planned.evidence,
|
|
7628
7674
|
mission: planned.mission,
|
|
7629
7675
|
});
|
|
7676
|
+
// An unreviewable fallback proposal no longer blocks wake (it deadlocked
|
|
7677
|
+
// members for weeks), but it must not accumulate either: one open
|
|
7678
|
+
// proposal per goal stays the invariant, so the stale fallback is
|
|
7679
|
+
// superseded by the one being proposed now.
|
|
7680
|
+
for (const prior of goal.experiments) {
|
|
7681
|
+
if (experimentIsUnreviewable(prior)) {
|
|
7682
|
+
prior.status = 'superseded';
|
|
7683
|
+
prior.superseded_at = now;
|
|
7684
|
+
prior.superseded_by = experiment.id;
|
|
7685
|
+
prior.superseded_reason = 'unreviewable_fallback_replaced_by_next_proposal';
|
|
7686
|
+
}
|
|
7687
|
+
}
|
|
7630
7688
|
goal.experiments.push(experiment);
|
|
7631
7689
|
goal.history = Array.isArray(goal.history) ? goal.history : [];
|
|
7632
7690
|
goal.history.push({ at: now, event: 'wake_tick_proposed_experiment', experiment_id: experiment.id });
|
|
@@ -9069,4 +9127,11 @@ async function memberCommand(subcommand, ...args) {
|
|
|
9069
9127
|
}
|
|
9070
9128
|
}
|
|
9071
9129
|
|
|
9072
|
-
module.exports = {
|
|
9130
|
+
module.exports = {
|
|
9131
|
+
memberCommand,
|
|
9132
|
+
findAllMembers,
|
|
9133
|
+
findWorkspaceBusinessId,
|
|
9134
|
+
parseFrontmatter,
|
|
9135
|
+
wakeBootLines,
|
|
9136
|
+
buildMemberRunStartArgs,
|
|
9137
|
+
};
|