atris 3.58.8 → 3.59.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +3 -2
- package/atris/skills/engines/SKILL.md +28 -1
- package/bin/atris.js +3 -0
- package/commands/autopilot.js +75 -5
- package/commands/clean.js +17 -0
- package/commands/doc-health.js +11 -6
- package/commands/engine.js +384 -4
- package/commands/host.js +53 -0
- package/commands/member.js +57 -6
- package/commands/mission.js +72 -8
- package/commands/one-lap.js +58 -3
- package/commands/run.js +4 -1
- package/commands/scout.js +1 -1
- package/commands/search.js +5 -3
- package/commands/unknowns.js +5 -0
- package/commands/verify.js +12 -4
- package/commands/visualize.js +20 -3
- package/commands/workflow.js +17 -6
- package/lib/dispatch-scout.js +31 -22
- package/lib/engine-registry.js +1482 -13
- package/lib/feedback-triage.js +2 -0
- package/lib/fleet.js +107 -11
- package/lib/host.js +336 -0
- package/lib/known-commands.js +1 -1
- package/lib/map-refs.js +44 -1
- package/lib/member-engine.js +253 -0
- package/lib/roster-markdown.js +206 -0
- package/lib/roster-models.js +179 -0
- package/lib/runner-command.js +94 -14
- package/lib/self-drive.js +9 -1
- package/lib/wish-audit.js +10 -6
- package/lib/wish-delegate.js +10 -0
- package/package.json +1 -1
- package/templates/members/host/MEMBER.md +23 -0
- package/templates/members/host/MISSION.md +11 -0
- package/templates/members/host/SOUL.md +7 -0
- package/templates/members/host/context/why.md +3 -0
- package/templates/members/host/goals.json +5 -0
- package/templates/members/host/goals.md +3 -0
- package/templates/members/host/skills/host/SKILL.md +13 -0
package/commands/mission.js
CHANGED
|
@@ -21,6 +21,7 @@ const {
|
|
|
21
21
|
} = require('../lib/brief-ledger');
|
|
22
22
|
const {
|
|
23
23
|
resolveClaudeRunnerModel,
|
|
24
|
+
resolvePinnedRunnerModel,
|
|
24
25
|
resolveClaudeRunnerBin,
|
|
25
26
|
resolveClaudeRunnerCommandTemplate,
|
|
26
27
|
buildRunnerCommand,
|
|
@@ -385,13 +386,59 @@ function resolveMissionRunnerSelection(value, options = {}) {
|
|
|
385
386
|
exitMissionError(`Unknown ${noun} "${raw}". ${knownMissionRunnerText()}.`, 2, asJson);
|
|
386
387
|
}
|
|
387
388
|
|
|
388
|
-
|
|
389
|
+
// The roster line's effort and time cap for this tick, when it sets them.
|
|
390
|
+
function rosterTickPins(engine) {
|
|
391
|
+
return {
|
|
392
|
+
...(engine && engine.roster_effort ? { roster_effort: engine.roster_effort } : {}),
|
|
393
|
+
...(engine && engine.roster_max_seconds ? { roster_max_seconds: engine.roster_max_seconds } : {}),
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// A tick's time limit: the time left on the mission, capped by the roster
|
|
398
|
+
// line's "max 20 min" when it sets one, else by the default tick cap.
|
|
399
|
+
function missionTickTimeoutMs(mission, remainingMs) {
|
|
400
|
+
const cap = Number(mission && mission.roster_max_seconds) > 0
|
|
401
|
+
? Number(mission.roster_max_seconds) * 1000
|
|
402
|
+
: MISSION_RUN_DEFAULTS.claudeTimeoutMs;
|
|
403
|
+
return clampTimeoutMs(remainingMs, cap);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function resolveMissionTickRunner(mission, root = process.cwd(), options = {}) {
|
|
389
407
|
if (String(mission && mission.runner || '').trim().toLowerCase() !== MISSION_AUTO_RUNNER) {
|
|
390
408
|
return { mission, engine_id: null, requested_engine: null, engine_fallback_reason: null };
|
|
391
409
|
}
|
|
392
|
-
|
|
410
|
+
// A mission owned by a team member runs on that member's roster pick. An
|
|
411
|
+
// explicit preferred engine (or a run's --engine) still wins, and with no
|
|
412
|
+
// roster line behind the member, routing stays exactly as it was.
|
|
413
|
+
if (!String(mission.preferred_engine || '').trim() && mission.owner) {
|
|
414
|
+
const { memberRosterEngine } = require('../lib/member-engine');
|
|
415
|
+
const member = memberRosterEngine(mission.owner, root, options);
|
|
416
|
+
if (member) {
|
|
417
|
+
if (process.env.ATRIS_ROUTER_EXPLAIN !== '0') console.error(member.reason);
|
|
418
|
+
return {
|
|
419
|
+
mission: {
|
|
420
|
+
...mission,
|
|
421
|
+
runner: member.engine.id,
|
|
422
|
+
runner_kind: 'engine',
|
|
423
|
+
...(!mission.model && member.model ? { model: member.model } : {}),
|
|
424
|
+
...rosterTickPins(member.engine),
|
|
425
|
+
},
|
|
426
|
+
engine_id: member.engine.id,
|
|
427
|
+
requested_engine: null,
|
|
428
|
+
engine_fallback_reason: null,
|
|
429
|
+
member_engine_reason: member.reason,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
const resolved = resolveEngineForRoleWithPreference('executor', root, mission.preferred_engine, options);
|
|
393
434
|
return {
|
|
394
|
-
mission: resolved.engine ? {
|
|
435
|
+
mission: resolved.engine ? {
|
|
436
|
+
...mission,
|
|
437
|
+
runner: resolved.engine.id,
|
|
438
|
+
runner_kind: 'engine',
|
|
439
|
+
...(!mission.model && resolved.engine.roster_model ? { model: resolved.engine.roster_model } : {}),
|
|
440
|
+
...rosterTickPins(resolved.engine),
|
|
441
|
+
} : mission,
|
|
395
442
|
engine_id: resolved.engine ? resolved.engine.id : null,
|
|
396
443
|
requested_engine: resolved.requested_engine,
|
|
397
444
|
engine_fallback_reason: resolved.engine_fallback_reason,
|
|
@@ -444,6 +491,20 @@ function applyMissionRunnerProfile(runner) {
|
|
|
444
491
|
};
|
|
445
492
|
}
|
|
446
493
|
|
|
494
|
+
// Claude-family engines share the claude CLI, so they keep the resolved
|
|
495
|
+
// default model pin. Every other engine's CLI rejects a claude model name, so
|
|
496
|
+
// it only ever takes an explicit pin (the mission's model or the roster
|
|
497
|
+
// line's); '' lets the engine ride its own default.
|
|
498
|
+
const CLAUDE_FAMILY_RUNNER_ENGINES = new Set(['claude', 'fable', 'haiku']);
|
|
499
|
+
|
|
500
|
+
function resolveMissionTickRunnerModel(mission) {
|
|
501
|
+
const engine = canonicalEngineName(mission && mission.runner);
|
|
502
|
+
if (!engine || CLAUDE_FAMILY_RUNNER_ENGINES.has(engine)) {
|
|
503
|
+
return resolveClaudeRunnerModel(mission);
|
|
504
|
+
}
|
|
505
|
+
return resolvePinnedRunnerModel(mission);
|
|
506
|
+
}
|
|
507
|
+
|
|
447
508
|
function exitMissionError(message, code = 1, asJson = false) {
|
|
448
509
|
if (asJson) {
|
|
449
510
|
printCliJson(
|
|
@@ -8745,7 +8806,7 @@ function spawnGenericRunnerTick(mission, opts) {
|
|
|
8745
8806
|
let briefId = null;
|
|
8746
8807
|
try {
|
|
8747
8808
|
promptFile = writeRunnerPromptFile(cwd, mission.id, prompt);
|
|
8748
|
-
cmd = buildRunnerCommand({ promptFile, model });
|
|
8809
|
+
cmd = buildRunnerCommand({ promptFile, model, ...(mission.roster_effort ? { effort: mission.roster_effort } : {}) });
|
|
8749
8810
|
const engine = canonicalEngineName(mission.runner);
|
|
8750
8811
|
if (engine) {
|
|
8751
8812
|
const record = appendBriefRecord(cwd, {
|
|
@@ -8922,6 +8983,7 @@ function spawnClaudeTick(mission, opts) {
|
|
|
8922
8983
|
'--include-partial-messages',
|
|
8923
8984
|
];
|
|
8924
8985
|
if (model) args.push('--model', model);
|
|
8986
|
+
if (mission.roster_effort) args.push('--effort', mission.roster_effort);
|
|
8925
8987
|
if (sessionMode === 'set') args.push('--session-id', sessionId);
|
|
8926
8988
|
else if (sessionMode === 'resume') args.push('--resume', sessionId);
|
|
8927
8989
|
|
|
@@ -9088,7 +9150,7 @@ async function runEngineVerifier(mission, options = {}) {
|
|
|
9088
9150
|
signal: options.signal,
|
|
9089
9151
|
timeoutMs: ENGINE_VERIFY_TIMEOUT_MS,
|
|
9090
9152
|
prompt: buildEngineVerifyPrompt(mission, options.tickIndex || 1),
|
|
9091
|
-
model:
|
|
9153
|
+
model: resolveMissionTickRunnerModel(verifyMission),
|
|
9092
9154
|
});
|
|
9093
9155
|
} catch (error) {
|
|
9094
9156
|
engineResult = { ok: false, error: error.message };
|
|
@@ -9668,12 +9730,12 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
9668
9730
|
const runClaudeSession = () => spawnClaudeTick(tickRuntimeMission, {
|
|
9669
9731
|
sessionMode, sessionId: useId, cwd, signal: controller.signal,
|
|
9670
9732
|
missionLock: lock,
|
|
9671
|
-
timeoutMs:
|
|
9733
|
+
timeoutMs: missionTickTimeoutMs(
|
|
9734
|
+
tickRuntimeMission,
|
|
9672
9735
|
(maxWallSeconds - ((Date.now() - startedAt) / 1000)) * 1000,
|
|
9673
|
-
MISSION_RUN_DEFAULTS.claudeTimeoutMs,
|
|
9674
9736
|
),
|
|
9675
9737
|
prompt,
|
|
9676
|
-
model:
|
|
9738
|
+
model: resolveMissionTickRunnerModel(tickRuntimeMission),
|
|
9677
9739
|
});
|
|
9678
9740
|
try {
|
|
9679
9741
|
claudeResult = await runClaudeSession();
|
|
@@ -11726,6 +11788,8 @@ module.exports = {
|
|
|
11726
11788
|
missionHumanStatusText,
|
|
11727
11789
|
resolveMissionRunnerSelection,
|
|
11728
11790
|
resolveMissionTickRunner,
|
|
11791
|
+
resolveMissionTickRunnerModel,
|
|
11792
|
+
missionTickTimeoutMs,
|
|
11729
11793
|
engineFailureHealthStatus,
|
|
11730
11794
|
recordMissionEngineTickOutcome,
|
|
11731
11795
|
tickMadeProgress,
|
package/commands/one-lap.js
CHANGED
|
@@ -9,7 +9,10 @@ const { readWishes } = require('../lib/wish-store');
|
|
|
9
9
|
const {
|
|
10
10
|
engineRegistryView,
|
|
11
11
|
resolveEngineForRole,
|
|
12
|
+
resolveEngineForRoleRanked,
|
|
12
13
|
resolveRegisteredEngine,
|
|
14
|
+
rosterPinFor,
|
|
15
|
+
rosterDecided,
|
|
13
16
|
} = require('../lib/engine-registry');
|
|
14
17
|
const { parseVerifyCommand } = require('../lib/auto-accept-certified');
|
|
15
18
|
const fleet = require('../lib/fleet');
|
|
@@ -155,7 +158,8 @@ function readyExecutor(root, preferred = '') {
|
|
|
155
158
|
if (!selected.roles.includes('executor')) throw new Error(`engine ${selected.id} is not an executor`);
|
|
156
159
|
if (!selected.health || selected.health.status !== 'ready') throw new Error(`engine ${selected.id} is not ready`);
|
|
157
160
|
if (!fleet.FLEET_CAPABLE.includes(selected.id)) throw new Error(`engine ${selected.id} cannot build headlessly`);
|
|
158
|
-
|
|
161
|
+
const pin = rosterPinFor('executor', selected.id, root);
|
|
162
|
+
return Object.keys(pin).length ? { ...selected, ...pin } : selected;
|
|
159
163
|
}
|
|
160
164
|
const routed = resolveEngineForRole('executor', root);
|
|
161
165
|
if (routed && fleet.FLEET_CAPABLE.includes(routed.id)) return routed;
|
|
@@ -168,16 +172,30 @@ function readyExecutor(root, preferred = '') {
|
|
|
168
172
|
|
|
169
173
|
function readyValidators(root, preferred = '', exclude = '') {
|
|
170
174
|
const blocked = String(exclude || '').trim();
|
|
175
|
+
const rosterRanked = resolveEngineForRoleRanked('validator', root);
|
|
176
|
+
const fromRoster = rosterDecided(rosterRanked.source);
|
|
177
|
+
const rosterOrder = !preferred && fromRoster
|
|
178
|
+
? rosterRanked.ranked.map((engine) => engine.id)
|
|
179
|
+
: [];
|
|
180
|
+
const pinned = fromRoster && rosterRanked.engine
|
|
181
|
+
&& (rosterRanked.engine.roster_model || rosterRanked.engine.roster_effort || rosterRanked.engine.roster_max_seconds)
|
|
182
|
+
? rosterRanked.engine
|
|
183
|
+
: null;
|
|
184
|
+
// A roster pick can name an engine that reviews only when picked (codex),
|
|
185
|
+
// so the roster's list joins even when an engine lacks the role.
|
|
186
|
+
const rosterPicked = new Set(fromRoster ? rosterRanked.ranked.map((engine) => engine.id) : []);
|
|
171
187
|
const candidates = engineRegistryView(root)
|
|
172
|
-
.filter((engine) => engine.roles.includes('validator'))
|
|
188
|
+
.filter((engine) => engine.roles.includes('validator') || rosterPicked.has(engine.id))
|
|
173
189
|
.filter((engine) => engine.id !== blocked)
|
|
174
190
|
.sort((a, b) => {
|
|
175
191
|
const aPreferred = preferred && a.id === preferred ? 0 : 1;
|
|
176
192
|
const bPreferred = preferred && b.id === preferred ? 0 : 1;
|
|
177
193
|
return aPreferred - bPreferred
|
|
194
|
+
|| (rosterOrder.length ? (rosterOrder.indexOf(a.id) - rosterOrder.indexOf(b.id)) : 0)
|
|
178
195
|
|| Number(a.fallback_order) - Number(b.fallback_order)
|
|
179
196
|
|| String(a.id).localeCompare(String(b.id));
|
|
180
|
-
})
|
|
197
|
+
})
|
|
198
|
+
.map((engine) => (pinned && engine.id === pinned.id ? { ...engine, ...pinFields(pinned) } : engine));
|
|
181
199
|
const ready = candidates.filter((engine) => engine.health && engine.health.status === 'ready');
|
|
182
200
|
// Route-time determinism: which validator binaries exist on this machine
|
|
183
201
|
// must not change the lap's route. When none are ready, dispatch with the
|
|
@@ -187,6 +205,39 @@ function readyValidators(root, preferred = '', exclude = '') {
|
|
|
187
205
|
return ready.length ? ready : candidates;
|
|
188
206
|
}
|
|
189
207
|
|
|
208
|
+
function pinFields(engine) {
|
|
209
|
+
const pin = {};
|
|
210
|
+
for (const field of ['roster_model', 'roster_effort', 'roster_max_seconds']) {
|
|
211
|
+
if (engine && engine[field]) pin[field] = engine[field];
|
|
212
|
+
}
|
|
213
|
+
return pin;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// The roster's pins ride into the flight: the builder's model, effort, and
|
|
217
|
+
// time cap for the builder, and each reviewer's for that reviewer only. A
|
|
218
|
+
// reviewer with only a model keeps the plain model string.
|
|
219
|
+
function lapModelPins(executor, validators = []) {
|
|
220
|
+
const validatorModels = {};
|
|
221
|
+
for (const engine of validators) {
|
|
222
|
+
if (!engine) continue;
|
|
223
|
+
if (engine.roster_effort || engine.roster_max_seconds) {
|
|
224
|
+
validatorModels[engine.id] = {
|
|
225
|
+
model: engine.roster_model || '',
|
|
226
|
+
effort: engine.roster_effort || '',
|
|
227
|
+
max_seconds: engine.roster_max_seconds || 0,
|
|
228
|
+
};
|
|
229
|
+
} else if (engine.roster_model) {
|
|
230
|
+
validatorModels[engine.id] = engine.roster_model;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
model: executor && executor.roster_model || '',
|
|
235
|
+
...(executor && executor.roster_effort ? { effort: executor.roster_effort } : {}),
|
|
236
|
+
...(executor && executor.roster_max_seconds ? { maxSeconds: executor.roster_max_seconds } : {}),
|
|
237
|
+
validatorModels: Object.keys(validatorModels).length ? validatorModels : null,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
190
241
|
function taskById(runCli, taskId) {
|
|
191
242
|
const result = runCli(['task', 'show', taskId, '--json']);
|
|
192
243
|
if (!result || result.status !== 0) return null;
|
|
@@ -656,6 +707,7 @@ async function runOneLap(ask, options = {}) {
|
|
|
656
707
|
reviewOnly: true,
|
|
657
708
|
verifierCommand: verifier,
|
|
658
709
|
validatorEngines: validators.map((entry) => entry.id),
|
|
710
|
+
...lapModelPins(executor, validators),
|
|
659
711
|
actor: mission.owner || 'mission-lead',
|
|
660
712
|
installedEngines: engineRegistryView(root)
|
|
661
713
|
.filter((entry) => entry.health && entry.health.status === 'ready')
|
|
@@ -773,4 +825,7 @@ async function runOneLap(ask, options = {}) {
|
|
|
773
825
|
module.exports = {
|
|
774
826
|
oneLapSafetyIssue,
|
|
775
827
|
runOneLap,
|
|
828
|
+
readyExecutor,
|
|
829
|
+
readyValidators,
|
|
830
|
+
lapModelPins,
|
|
776
831
|
};
|
package/commands/run.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
const fs = require('fs');
|
|
11
11
|
const path = require('path');
|
|
12
|
+
const { mapNotesPath } = require('../lib/map-refs');
|
|
12
13
|
const { execSync, spawnSync } = require('child_process');
|
|
13
14
|
const { getLogPath, ensureLogDirectory, createLogFile } = require('../lib/journal');
|
|
14
15
|
const { parseTodo } = require('../lib/todo');
|
|
@@ -118,11 +119,12 @@ function execPhaseCommandSync(cmd, opts = {}) {
|
|
|
118
119
|
* what the validator found - closing the try → notice → adjust loop.
|
|
119
120
|
*/
|
|
120
121
|
function buildRunPrompt(phase, context, priorCycleReview) {
|
|
121
|
-
const { mapPath, todoPath, personaPath, lessonsPath, journalPath } = context;
|
|
122
|
+
const { mapPath, notesPath, todoPath, personaPath, lessonsPath, journalPath } = context;
|
|
122
123
|
|
|
123
124
|
const readFiles = [
|
|
124
125
|
personaPath && `- ${personaPath}`,
|
|
125
126
|
mapPath && `- ${mapPath}`,
|
|
127
|
+
notesPath && `- ${notesPath}`,
|
|
126
128
|
todoPath && `- ${todoPath}`,
|
|
127
129
|
lessonsPath && `- ${lessonsPath}`,
|
|
128
130
|
journalPath && `- ${journalPath}`,
|
|
@@ -413,6 +415,7 @@ async function runAtris(options = {}) {
|
|
|
413
415
|
// Build context paths
|
|
414
416
|
const context = {
|
|
415
417
|
mapPath: fs.existsSync(path.join(atrisDir, 'MAP.md')) ? 'atris/MAP.md' : null,
|
|
418
|
+
notesPath: mapNotesPath(path.dirname(atrisDir)),
|
|
416
419
|
todoPath: fs.existsSync(path.join(atrisDir, 'TODO.md')) ? 'atris/TODO.md' : null,
|
|
417
420
|
personaPath: fs.existsSync(path.join(atrisDir, 'PERSONA.md')) ? 'atris/PERSONA.md' : null,
|
|
418
421
|
lessonsPath: fs.existsSync(path.join(atrisDir, 'lessons.md')) ? 'atris/lessons.md' : null,
|
package/commands/scout.js
CHANGED
|
@@ -53,7 +53,7 @@ const ENGINES = {
|
|
|
53
53
|
function scoutPrompt(question, dir) {
|
|
54
54
|
return [
|
|
55
55
|
`You are a read-only scout in ${dir}. You NEVER write, edit, or run mutating commands.`,
|
|
56
|
-
`Search order: index files first (atris/MAP.md, atris/TODO.md, README) via targeted reads, then grep. Cheap reads only.`,
|
|
56
|
+
`Search order: index files first (atris/MAP.md, atris/refs/MAP-NOTES.md if present for exact file:line refs, atris/TODO.md, README) via targeted reads, then grep. Cheap reads only.`,
|
|
57
57
|
``,
|
|
58
58
|
`Question: ${question}`,
|
|
59
59
|
``,
|
package/commands/search.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const { existingMapDocs } = require('../lib/map-refs');
|
|
5
6
|
|
|
6
7
|
const MEMORY_LAYERS = [
|
|
7
8
|
{ key: 'features', dir: path.join('atris', 'features') },
|
|
@@ -226,10 +227,11 @@ function scanLayer(root, layer, terms) {
|
|
|
226
227
|
return scanFiles(root, layer.key, layer.dir.split(path.sep).join('/'), listMarkdownFiles(dir), terms);
|
|
227
228
|
}
|
|
228
229
|
|
|
230
|
+
// The map layer covers the boot map and the notes file that holds its deep refs.
|
|
229
231
|
function scanMap(root, terms) {
|
|
230
|
-
const
|
|
231
|
-
if (!
|
|
232
|
-
return scanFiles(root, 'map', 'atris/MAP.md',
|
|
232
|
+
const files = existingMapDocs(root).map((doc) => path.join(root, doc));
|
|
233
|
+
if (!files.length) return emptyLayerResult('map', 'atris/MAP.md');
|
|
234
|
+
return scanFiles(root, 'map', 'atris/MAP.md', files, terms);
|
|
233
235
|
}
|
|
234
236
|
|
|
235
237
|
function scanSource(root, terms) {
|
package/commands/unknowns.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const { mapNotesPath } = require('../lib/map-refs');
|
|
5
6
|
const { spawnSync } = require('child_process');
|
|
6
7
|
const taskDb = require('../lib/task-db');
|
|
7
8
|
const {
|
|
@@ -167,6 +168,8 @@ function formatTaskEvents(rows) {
|
|
|
167
168
|
|
|
168
169
|
function gatherTerritoryContext(root, db) {
|
|
169
170
|
const map = readFileBestEffort(path.join(root, 'atris', 'MAP.md'), { lines: 100 });
|
|
171
|
+
const notesPath = mapNotesPath(root);
|
|
172
|
+
const notes = notesPath ? readFileBestEffort(path.join(root, notesPath), { lines: 100 }) : '';
|
|
170
173
|
const lessonsMd = readFileBestEffort(path.join(root, 'atris', 'lessons.md'), { maxChars: READ_LIMIT });
|
|
171
174
|
const lessonsJson = readFileBestEffort(path.join(root, 'atris', 'lessons.json'), { maxChars: READ_LIMIT });
|
|
172
175
|
const journals = readRecentJournals(root);
|
|
@@ -176,6 +179,7 @@ function gatherTerritoryContext(root, db) {
|
|
|
176
179
|
return [
|
|
177
180
|
section('workspace_root', root),
|
|
178
181
|
section('atris/MAP.md first 100 lines', map),
|
|
182
|
+
...(notesPath ? [section(`${notesPath} first 100 lines`, notes)] : []),
|
|
179
183
|
section('atris/lessons.md', lessonsMd),
|
|
180
184
|
section('atris/lessons.json', lessonsJson),
|
|
181
185
|
section('last 3 daily journals', journals),
|
|
@@ -614,4 +618,5 @@ module.exports = {
|
|
|
614
618
|
unknownsCommand,
|
|
615
619
|
showHelp,
|
|
616
620
|
fallbackPayload,
|
|
621
|
+
gatherTerritoryContext,
|
|
617
622
|
};
|
package/commands/verify.js
CHANGED
|
@@ -2,6 +2,7 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { spawnSync } = require('child_process');
|
|
4
4
|
const escapeRegExp = require('../lib/escape-regexp');
|
|
5
|
+
const { existingMapDocs } = require('../lib/map-refs');
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* atris verify [task] - Validate work is actually done
|
|
@@ -397,13 +398,13 @@ function checkDocsVsChanges(cwd, atrisDir) {
|
|
|
397
398
|
return result;
|
|
398
399
|
}
|
|
399
400
|
|
|
400
|
-
// Check if significant files are in MAP.md
|
|
401
|
+
// Check if significant files are in MAP.md or the notes file it points to
|
|
401
402
|
const mapFile = path.join(atrisDir, 'MAP.md');
|
|
402
403
|
if (!fs.existsSync(mapFile)) {
|
|
403
404
|
return result;
|
|
404
405
|
}
|
|
405
406
|
|
|
406
|
-
const mapContent =
|
|
407
|
+
const mapContent = mapDocsText(atrisDir);
|
|
407
408
|
|
|
408
409
|
const significantExtensions = ['.js', '.ts', '.py', '.go', '.rs', '.rb', '.java'];
|
|
409
410
|
const significantChanges = changedFiles.filter(f =>
|
|
@@ -547,6 +548,12 @@ function verifyChange(cwd, change) {
|
|
|
547
548
|
};
|
|
548
549
|
}
|
|
549
550
|
|
|
551
|
+
// A file counts as documented when the boot map or atris/refs/MAP-NOTES.md names it.
|
|
552
|
+
function mapDocsText(atrisDir) {
|
|
553
|
+
const root = path.dirname(atrisDir);
|
|
554
|
+
return existingMapDocs(root).map((doc) => fs.readFileSync(path.join(root, doc), 'utf8')).join('\n');
|
|
555
|
+
}
|
|
556
|
+
|
|
550
557
|
/**
|
|
551
558
|
* Check if MAP.md documents the expected files
|
|
552
559
|
*/
|
|
@@ -556,7 +563,7 @@ function checkMapForFiles(atrisDir, changes) {
|
|
|
556
563
|
return { documented: false };
|
|
557
564
|
}
|
|
558
565
|
|
|
559
|
-
const content =
|
|
566
|
+
const content = mapDocsText(atrisDir);
|
|
560
567
|
const fileChanges = changes.filter(c => c.type === 'file');
|
|
561
568
|
|
|
562
569
|
if (fileChanges.length === 0) {
|
|
@@ -725,5 +732,6 @@ module.exports = {
|
|
|
725
732
|
escapeRegExp,
|
|
726
733
|
mapCoverage,
|
|
727
734
|
isPathCovered,
|
|
728
|
-
normalizeMapPath
|
|
735
|
+
normalizeMapPath,
|
|
736
|
+
checkMapForFiles,
|
|
729
737
|
};
|
package/commands/visualize.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const { mapNotesPath } = require('../lib/map-refs');
|
|
3
4
|
const { spawnSync } = require('child_process');
|
|
4
5
|
const { getLogPath } = require('../lib/journal');
|
|
5
6
|
const { ensureValidCredentials } = require('../utils/auth');
|
|
@@ -153,14 +154,16 @@ function readBusinessMeta(cwd = process.cwd()) {
|
|
|
153
154
|
}
|
|
154
155
|
}
|
|
155
156
|
|
|
157
|
+
function promptWords(prompt) {
|
|
158
|
+
return new Set(String(prompt || '').toLowerCase().split(/[^a-z0-9]+/).filter(w => w.length >= 4));
|
|
159
|
+
}
|
|
160
|
+
|
|
156
161
|
function findRelevantContextFiles(cwd, prompt) {
|
|
157
162
|
const roots = [
|
|
158
163
|
path.join(cwd, 'atris', 'context'),
|
|
159
164
|
path.join(cwd, 'atris', 'wiki'),
|
|
160
165
|
];
|
|
161
|
-
const words =
|
|
162
|
-
prompt.toLowerCase().split(/[^a-z0-9]+/).filter(w => w.length >= 4)
|
|
163
|
-
);
|
|
166
|
+
const words = promptWords(prompt);
|
|
164
167
|
const files = [];
|
|
165
168
|
|
|
166
169
|
function walk(dir) {
|
|
@@ -192,6 +195,17 @@ function findRelevantContextFiles(cwd, prompt) {
|
|
|
192
195
|
.slice(0, 4);
|
|
193
196
|
}
|
|
194
197
|
|
|
198
|
+
// The deep map is large: keep only the lines that share a word with the prompt.
|
|
199
|
+
function mapNotesExcerpt(cwd, prompt, maxChars) {
|
|
200
|
+
const notesPath = mapNotesPath(cwd);
|
|
201
|
+
if (!notesPath) return '';
|
|
202
|
+
const words = promptWords(prompt);
|
|
203
|
+
if (!words.size) return '';
|
|
204
|
+
const lines = fs.readFileSync(path.join(cwd, notesPath), 'utf8').split(/\r?\n/)
|
|
205
|
+
.filter(line => [...words].some(word => line.toLowerCase().includes(word)));
|
|
206
|
+
return lines.join('\n').slice(0, maxChars);
|
|
207
|
+
}
|
|
208
|
+
|
|
195
209
|
function collectWorkspaceContext(prompt, cwd = process.cwd()) {
|
|
196
210
|
const business = readBusinessMeta(cwd);
|
|
197
211
|
const chunks = [];
|
|
@@ -201,6 +215,8 @@ function collectWorkspaceContext(prompt, cwd = process.cwd()) {
|
|
|
201
215
|
|
|
202
216
|
const mapSnippet = readTextIfExists(path.join(cwd, 'atris', 'MAP.md'), 1400);
|
|
203
217
|
if (mapSnippet) chunks.push(`MAP excerpt:\n${mapSnippet}`);
|
|
218
|
+
const notesSnippet = mapNotesExcerpt(cwd, prompt, 1400);
|
|
219
|
+
if (notesSnippet) chunks.push(`MAP notes excerpt:\n${notesSnippet}`);
|
|
204
220
|
|
|
205
221
|
const todoSnippet = readTextIfExists(path.join(cwd, 'atris', 'TODO.md'), 1000);
|
|
206
222
|
if (todoSnippet) chunks.push(`TODO excerpt:\n${todoSnippet}`);
|
|
@@ -389,4 +405,5 @@ async function visualizeAtris(args = process.argv.slice(3)) {
|
|
|
389
405
|
|
|
390
406
|
module.exports = {
|
|
391
407
|
visualizeAtris,
|
|
408
|
+
collectWorkspaceContext,
|
|
392
409
|
};
|
package/commands/workflow.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const { mapNotesPath } = require('../lib/map-refs');
|
|
3
4
|
const { getLogPath } = require('../lib/journal');
|
|
4
5
|
const {
|
|
5
6
|
buildFirstMinute,
|
|
@@ -491,6 +492,19 @@ async function runAtris2Local(userInput, atris2Mode) {
|
|
|
491
492
|
}
|
|
492
493
|
}
|
|
493
494
|
|
|
495
|
+
// Where the agent finds code. When the short routing map has a notes file,
|
|
496
|
+
// point line lookups there so the agent does not hunt for refs the map lacks.
|
|
497
|
+
function mapPromptSection(targetDir, cwd = process.cwd()) {
|
|
498
|
+
const mapFile = path.join(targetDir, 'MAP.md');
|
|
499
|
+
if (!fs.existsSync(mapFile)) return '';
|
|
500
|
+
const mapRef = path.relative(cwd, mapFile);
|
|
501
|
+
const notes = mapNotesPath(path.dirname(targetDir));
|
|
502
|
+
if (!notes) return `## MAP.md\nRead this file for file:line references: ${mapRef}\n\n`;
|
|
503
|
+
const notesRef = path.relative(cwd, path.join(path.dirname(targetDir), notes));
|
|
504
|
+
return `## MAP.md\nRead this file to find where work lives: ${mapRef}\n\n`
|
|
505
|
+
+ `## MAP-NOTES.md\nRead this file for exact file:line references: ${notesRef}\n\n`;
|
|
506
|
+
}
|
|
507
|
+
|
|
494
508
|
async function planAtris(userInput = null) {
|
|
495
509
|
const { loadConfig } = require('../utils/config');
|
|
496
510
|
const { loadCredentials, ensureValidCredentials } = require('../utils/auth');
|
|
@@ -773,9 +787,7 @@ async function planAtris(userInput = null) {
|
|
|
773
787
|
systemPrompt += '## PERSONA.md\n' + fs.readFileSync(personaPath, 'utf8') + '\n\n';
|
|
774
788
|
}
|
|
775
789
|
|
|
776
|
-
|
|
777
|
-
systemPrompt += `## MAP.md\nRead this file for file:line references: ${mapFileRef}\n\n`;
|
|
778
|
-
}
|
|
790
|
+
systemPrompt += mapPromptSection(targetDir);
|
|
779
791
|
|
|
780
792
|
// Build user prompt with context
|
|
781
793
|
let userPrompt = `You are the Navigator. Take ideas from Inbox → break them down into perfect, manageable tasks.\n\n`;
|
|
@@ -1253,9 +1265,7 @@ async function doAtris() {
|
|
|
1253
1265
|
if (persona) {
|
|
1254
1266
|
systemPrompt += '## PERSONA.md\n' + persona + '\n\n';
|
|
1255
1267
|
}
|
|
1256
|
-
|
|
1257
|
-
systemPrompt += `## MAP.md\nRead this file for file:line references: ${mapPath}\n\n`;
|
|
1258
|
-
}
|
|
1268
|
+
systemPrompt += mapPromptSection(targetDir);
|
|
1259
1269
|
if (profile) {
|
|
1260
1270
|
systemPrompt += `## PROJECT CONTEXT\nType: ${context}\nProfile: ${JSON.stringify(profile, null, 2)}\n\n`;
|
|
1261
1271
|
}
|
|
@@ -1898,6 +1908,7 @@ async function executeAgentSDKFast(userInput) {
|
|
|
1898
1908
|
}
|
|
1899
1909
|
|
|
1900
1910
|
module.exports = {
|
|
1911
|
+
mapPromptSection,
|
|
1901
1912
|
planAtris,
|
|
1902
1913
|
doAtris,
|
|
1903
1914
|
reviewAtris,
|
package/lib/dispatch-scout.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { spawnSync } = require('child_process');
|
|
6
|
+
const { MAP_REF_DOCS, existingMapDocs } = require('./map-refs');
|
|
6
7
|
const {
|
|
7
8
|
buildReadOnlyEngineInvocation,
|
|
8
9
|
runAskProcess,
|
|
@@ -91,9 +92,8 @@ function seedScoutContext({ task, worktreePath }) {
|
|
|
91
92
|
const trackedList = trackedFiles(worktreePath);
|
|
92
93
|
const tracked = new Set(trackedList);
|
|
93
94
|
const keywords = titleKeywords(task && task.title);
|
|
94
|
-
const mapPath =
|
|
95
|
-
|
|
96
|
-
try { mapText = fs.readFileSync(path.join(worktreePath, mapPath), 'utf8'); } catch {}
|
|
95
|
+
const mapPath = MAP_REF_DOCS[0];
|
|
96
|
+
const mapTexts = readMapTexts(worktreePath);
|
|
97
97
|
|
|
98
98
|
const scores = new Map();
|
|
99
99
|
const gotchaCandidates = [];
|
|
@@ -103,18 +103,21 @@ function seedScoutContext({ task, worktreePath }) {
|
|
|
103
103
|
};
|
|
104
104
|
if (tracked.has(mapPath)) addScore(mapPath, 1);
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
for (
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
106
|
+
// The boot map routes; the notes file holds the deep file:line refs. Read both.
|
|
107
|
+
for (const [doc, mapText] of mapTexts) {
|
|
108
|
+
const mapLines = mapText.split(/\r?\n/);
|
|
109
|
+
for (let index = 0; index < mapLines.length; index += 1) {
|
|
110
|
+
const line = mapLines[index];
|
|
111
|
+
const lower = line.toLowerCase();
|
|
112
|
+
const matches = keywords.filter((keyword) => lower.includes(keyword));
|
|
113
|
+
const refs = mapPathRefs(line, tracked);
|
|
114
|
+
for (const ref of refs) {
|
|
115
|
+
const pathMatches = keywords.filter((keyword) => ref.toLowerCase().includes(keyword)).length;
|
|
116
|
+
if (matches.length || pathMatches) addScore(ref, 80 + (matches.length * 5) + (pathMatches * 8));
|
|
117
|
+
}
|
|
118
|
+
if (matches.length && line.trim() && line.trim().length <= 300 && gotchaCandidates.length < 16) {
|
|
119
|
+
gotchaCandidates.push({ ...(doc === mapPath ? {} : { file: doc }), line: index + 1, text: line.trim() });
|
|
120
|
+
}
|
|
118
121
|
}
|
|
119
122
|
}
|
|
120
123
|
|
|
@@ -268,15 +271,22 @@ function verifyHit(hit, { worktreePath, tracked, allowed }) {
|
|
|
268
271
|
return { path: citedPath, line: nearest.line, excerpt, why };
|
|
269
272
|
}
|
|
270
273
|
|
|
271
|
-
|
|
274
|
+
// Map text by doc, boot map first. A missing doc is simply absent.
|
|
275
|
+
function readMapTexts(root) {
|
|
276
|
+
return new Map(existingMapDocs(root).map((doc) => [doc, fs.readFileSync(path.join(root, doc), 'utf8')]));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function verifyMapGotcha(gotcha, mapTexts) {
|
|
272
280
|
if (!gotcha || typeof gotcha !== 'object' || Array.isArray(gotcha)) return null;
|
|
273
281
|
const text = String(gotcha.text || '').trim();
|
|
274
282
|
const citedLine = Number(gotcha.line);
|
|
283
|
+
const file = gotcha.file === undefined ? MAP_REF_DOCS[0] : String(gotcha.file);
|
|
275
284
|
if (!text || text.length > 300 || !Number.isInteger(citedLine) || citedLine < 1) return null;
|
|
276
|
-
|
|
285
|
+
if (!mapTexts.has(file)) return null;
|
|
286
|
+
const lines = mapTexts.get(file).split(/\r?\n/);
|
|
277
287
|
const actualLine = lines.findIndex((line) => line.trim() === text) + 1;
|
|
278
288
|
if (!actualLine) return null;
|
|
279
|
-
return { line: actualLine, text };
|
|
289
|
+
return { ...(file === MAP_REF_DOCS[0] ? {} : { file }), line: actualLine, text };
|
|
280
290
|
}
|
|
281
291
|
|
|
282
292
|
function verifyScoutPack(rawPack, { task, worktreePath, allowedFiles, expectedCommit = '' }) {
|
|
@@ -301,11 +311,10 @@ function verifyScoutPack(rawPack, { task, worktreePath, allowedFiles, expectedCo
|
|
|
301
311
|
});
|
|
302
312
|
if (hits.length < 2) return null;
|
|
303
313
|
|
|
304
|
-
|
|
305
|
-
try { mapText = fs.readFileSync(path.join(worktreePath, 'atris', 'MAP.md'), 'utf8'); } catch {}
|
|
314
|
+
const mapTexts = readMapTexts(worktreePath);
|
|
306
315
|
const mapGotchas = (Array.isArray(rawPack.map_gotchas) ? rawPack.map_gotchas : [])
|
|
307
316
|
.slice(0, SCOUT_MAX_MAP_GOTCHAS)
|
|
308
|
-
.map((gotcha) => verifyMapGotcha(gotcha,
|
|
317
|
+
.map((gotcha) => verifyMapGotcha(gotcha, mapTexts))
|
|
309
318
|
.filter(Boolean);
|
|
310
319
|
const notChecked = (Array.isArray(rawPack.not_checked) ? rawPack.not_checked : [])
|
|
311
320
|
.map((entry) => String(entry || '').trim())
|
|
@@ -360,7 +369,7 @@ function renderVerifiedScoutBlock(pack, { worktreePath }) {
|
|
|
360
369
|
}
|
|
361
370
|
if (pack.map_gotchas.length) {
|
|
362
371
|
lines.push('MAP gotchas:');
|
|
363
|
-
lines.push(...pack.map_gotchas.map((gotcha) => `- MAP line ${gotcha.line}: ${gotcha.text}`));
|
|
372
|
+
lines.push(...pack.map_gotchas.map((gotcha) => `- ${gotcha.file || 'MAP'} line ${gotcha.line}: ${gotcha.text}`));
|
|
364
373
|
}
|
|
365
374
|
if (pack.not_checked.length) lines.push(`Not checked: ${pack.not_checked.join('; ')}`);
|
|
366
375
|
return lines.join('\n');
|