atris 3.57.4 → 3.58.6
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 +12 -1
- package/atris/skills/clawhub/member-runtime/SKILL.md +9 -9
- package/atris/skills/create-member/SKILL.md +16 -13
- package/atris/skills/design/SKILL.md +27 -4
- package/atris/skills/engines/SKILL.md +55 -12
- package/atris/skills/x-search/SKILL.md +8 -6
- package/atris/skills/youtube/SKILL.md +66 -21
- package/atris/team/brainstormer/MEMBER.md +2 -2
- package/atris/team/executor/MEMBER.md +3 -3
- package/atris/team/launcher/MEMBER.md +4 -4
- package/atris/team/navigator/MEMBER.md +3 -3
- package/atris/team/researcher/MEMBER.md +2 -2
- package/atris/team/validator/MEMBER.md +1 -1
- package/atris.md +14 -0
- package/ax +153 -23
- package/bin/atris.js +78 -30
- package/commands/aeo.js +4 -4
- package/commands/agents.js +1 -1
- package/commands/align.js +12 -12
- package/commands/analytics.js +3 -3
- package/commands/app.js +5 -5
- package/commands/ask.js +175 -0
- package/commands/auth.js +59 -31
- package/commands/autoland.js +29 -5
- package/commands/autopilot-front.js +6 -6
- package/commands/autopilot.js +90 -90
- package/commands/avail.js +5 -5
- package/commands/bench.js +63 -1
- package/commands/browse.js +2 -2
- package/commands/business.js +12 -12
- package/commands/card.js +2 -2
- package/commands/clean.js +13 -13
- package/commands/close.js +3 -3
- package/commands/compile.js +13 -13
- package/commands/computer.js +94 -27
- package/commands/console.js +5 -5
- package/commands/decide.js +1 -1
- package/commands/deck.js +9 -9
- package/commands/drive.js +4 -4
- package/commands/engine.js +173 -19
- package/commands/errors.js +3 -3
- package/commands/experiments.js +18 -1
- package/commands/feed.js +2 -2
- package/commands/feedback.js +4 -4
- package/commands/fleet.js +12 -12
- package/commands/game.js +1 -1
- package/commands/guide.js +49 -0
- package/commands/improve.js +19 -17
- package/commands/init.js +67 -19
- package/commands/interview.js +7 -7
- package/commands/land.js +40 -15
- package/commands/learn.js +216 -50
- package/commands/lesson.js +2 -2
- package/commands/log.js +1 -1
- package/commands/loops.js +4 -4
- package/commands/member.js +75 -50
- package/commands/mission.js +90 -82
- package/commands/one-lap.js +1 -1
- package/commands/pack.js +74 -8
- package/commands/plugin.js +6 -6
- package/commands/probe.js +9 -9
- package/commands/pull.js +35 -35
- package/commands/pulse.js +1 -1
- package/commands/push.js +21 -21
- package/commands/rainmaker.js +1 -1
- package/commands/recap.js +7 -7
- package/commands/reel.js +2 -2
- package/commands/release.js +3 -3
- package/commands/report.js +1 -1
- package/commands/review.js +5 -5
- package/commands/revisions.js +1 -1
- package/commands/run.js +1 -1
- package/commands/scout.js +3 -3
- package/commands/security-review.js +7 -7
- package/commands/serve.js +9 -9
- package/commands/setup.js +1 -1
- package/commands/sign.js +4 -4
- package/commands/signup.js +4 -4
- package/commands/site.js +8 -3
- package/commands/skill.js +6 -6
- package/commands/slop.js +18 -18
- package/commands/social.js +3 -3
- package/commands/soul.js +14 -14
- package/commands/spaceship.js +1 -1
- package/commands/status.js +2 -2
- package/commands/strings.js +13 -13
- package/commands/study.js +4 -4
- package/commands/sync.js +8 -8
- package/commands/task.js +50 -25
- package/commands/teach.js +2 -2
- package/commands/team.js +1 -1
- package/commands/theme.js +2 -2
- package/commands/tree.js +34 -0
- package/commands/truth.js +7 -7
- package/commands/verify.js +12 -12
- package/commands/visualize.js +2 -2
- package/commands/workflow.js +101 -48
- package/commands/worktree.js +1 -1
- package/commands/write.js +9 -9
- package/commands/x-search.js +93 -22
- package/commands/xp.js +6 -3
- package/commands/youtube.js +1019 -123
- package/lib/accept-verify-gate.js +3 -3
- package/lib/activity-stream.js +1 -1
- package/lib/auto-accept-certified.js +28 -12
- package/lib/autoland.js +6 -6
- package/lib/bench/engines.js +59 -8
- package/lib/bench/report.js +1 -1
- package/lib/bench/runner.js +204 -5
- package/lib/bench/tree-render.js +76 -0
- package/lib/card.js +1 -1
- package/lib/chat-log-scan.js +1 -1
- package/lib/claude-boot-block.js +2 -0
- package/lib/deck-compose.js +1 -1
- package/lib/deck-history.js +2 -2
- package/lib/deck-layout.js +1 -1
- package/lib/deck-review.js +5 -5
- package/lib/deck-schema.js +1 -1
- package/lib/default-verifier.js +1 -1
- package/lib/developer-api.js +3 -0
- package/lib/engine-ask.js +45 -7
- package/lib/engine-registry.js +15 -2
- package/lib/file-ops.js +1 -1
- package/lib/fleet.js +71 -35
- package/lib/html-render.js +1 -1
- package/lib/intents.js +172 -0
- package/lib/journal.js +1 -1
- package/lib/known-commands.js +2 -2
- package/lib/land-green.js +364 -0
- package/lib/lesson-contradiction.js +2 -2
- package/lib/lesson-preflight.js +1 -1
- package/lib/manifest.js +5 -5
- package/lib/member-alive.js +9 -5
- package/lib/memory-view.js +3 -3
- package/lib/mission-protected-lane.js +1 -1
- package/lib/mission-root.js +2 -2
- package/lib/orb-context.js +10 -10
- package/lib/pack-capabilities.js +861 -3
- package/lib/permission-grants.js +2 -2
- package/lib/policy-lessons.js +6 -6
- package/lib/pulse.js +8 -8
- package/lib/receipt-evidence.js +1 -1
- package/lib/reel.js +1 -1
- package/lib/review-integrity.js +1 -1
- package/lib/runner-command.js +35 -5
- package/lib/scorecard.js +4 -4
- package/lib/security-scan.js +5 -5
- package/lib/self-drive.js +1 -1
- package/lib/site-publish.js +372 -0
- package/lib/site.js +1 -1
- package/lib/slides-deck.js +2 -2
- package/lib/state-detection.js +2 -2
- package/lib/sync-telemetry.js +2 -2
- package/lib/task-db.js +17 -10
- package/lib/task-explanation.js +1 -1
- package/lib/task-proof.js +2 -2
- package/lib/task-receipt.js +3 -3
- package/lib/theme.js +12 -12
- package/lib/todo-fallback.js +3 -3
- package/lib/todo-sections.js +1 -1
- package/lib/tree-hash.js +79 -0
- package/lib/usage.js +1 -1
- package/lib/wish-delegate.js +1 -1
- package/lib/workspace-safety.js +4 -4
- package/lib/workspace-scaffold.js +3 -3
- package/package.json +1 -1
- package/scripts/det/concierge-bakeoff.sh +26 -0
- package/scripts/det/ytnotes +82 -5
- package/scripts/det/ytquote-repair.js +0 -1
- package/scripts/det/ytrail-race.js +0 -5
- package/scripts/member-operate.mjs +52 -14
- package/scripts/outbound-artifact-gate.js +0 -3
- package/utils/auth.js +47 -9
- package/utils/config.js +0 -1
- package/scripts/det/checklist-score.js +0 -191
|
@@ -57,7 +57,7 @@ function placeholderIssue(command) {
|
|
|
57
57
|
|
|
58
58
|
function nonFalsifyingIssue(command) {
|
|
59
59
|
if (NON_FALSIFYING_COMMANDS.some((re) => re.test(command.trim()))) {
|
|
60
|
-
return `\`${command.trim()}\` cannot fail for the reason this task exists
|
|
60
|
+
return `\`${command.trim()}\` cannot fail for the reason this task exists, it passes on a commit that deletes the work`;
|
|
61
61
|
}
|
|
62
62
|
return '';
|
|
63
63
|
}
|
|
@@ -83,8 +83,8 @@ function evaluateAcceptVerify(task, workspaceRoot, { cache = null } = {}) {
|
|
|
83
83
|
// Two different questions got fused here at first: "can this fail?" and "is this
|
|
84
84
|
// safe to execute at accept time?". Only the first is this gate's business.
|
|
85
85
|
// Refusing everything outside the execution allow-list flagged 455 accepted tasks,
|
|
86
|
-
// and sampling them found ordinary verifiers
|
|
87
|
-
// run`
|
|
86
|
+
// and sampling them found ordinary verifiers: `npm run type-check`, `npx vitest
|
|
87
|
+
// run` sitting beside real prose. A gate that blocks real work gets switched off.
|
|
88
88
|
// So: refuse what provably cannot fail, run what can be run safely, and mark the
|
|
89
89
|
// rest unchecked rather than pretend a judgment we cannot make.
|
|
90
90
|
const parsed = parseVerifyCommand(command);
|
package/lib/activity-stream.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Activity stream: normalize the workspace's receipt channels into ONE
|
|
4
4
|
// time-ordered feed of what the agent actually did, plus a heartbeat summary.
|
|
5
5
|
// The task board shows a static inventory of tasks; this turns the same data
|
|
6
|
-
// into "what happened, in order"
|
|
6
|
+
// into "what happened, in order": the stream you watch for hours.
|
|
7
7
|
|
|
8
8
|
const TS_KEYS = ['ts', 'at', 'created_at', 'accepted_at', 'timestamp', 'updated_at'];
|
|
9
9
|
|
|
@@ -11,6 +11,18 @@ const { policyLessonsForFiles, readPolicyLessons } = require('./policy-lessons')
|
|
|
11
11
|
const { skillEvalGate } = require('./skill-eval-gate');
|
|
12
12
|
|
|
13
13
|
const AGENT_CERTIFICATION_REVIEW_PASSES = 2;
|
|
14
|
+
|
|
15
|
+
// Verifier wall clock. The old flat 120s ceiling was measured against this
|
|
16
|
+
// workspace's real suites: the largest single test file needs ~123s and
|
|
17
|
+
// `npm run test` needs ~268s, so honest full-suite proofs could never land.
|
|
18
|
+
// Default is now 10 minutes with an ATRIS_VERIFY_TIMEOUT_MS override; agents
|
|
19
|
+
// should still prefer fast single-file verifiers, but a slow honest check
|
|
20
|
+
// must time out as "too slow", never silently fail as "unproven".
|
|
21
|
+
const DEFAULT_VERIFY_TIMEOUT_MS = 10 * 60 * 1000;
|
|
22
|
+
function verifyTimeoutMs() {
|
|
23
|
+
const raw = Number(process.env.ATRIS_VERIFY_TIMEOUT_MS);
|
|
24
|
+
return Number.isFinite(raw) && raw > 0 ? Math.min(raw, 60 * 60 * 1000) : DEFAULT_VERIFY_TIMEOUT_MS;
|
|
25
|
+
}
|
|
14
26
|
// Kept for compat with older callers/tests; the pass-count landing lane it
|
|
15
27
|
// once powered is gone. Passes alone never land work, an independent
|
|
16
28
|
// reviewer does.
|
|
@@ -451,7 +463,7 @@ function isAutoCertifyVerifyCommandAllowed(verify) {
|
|
|
451
463
|
|
|
452
464
|
// Re-entrancy guard: every verify child carries ATRIS_VERIFY_IN_PROGRESS=1
|
|
453
465
|
// (see verifyCommandEnv). A verify that shells back into the CLI and reaches
|
|
454
|
-
// another verify would otherwise recurse without bound
|
|
466
|
+
// another verify would otherwise recurse without bound, that cycle
|
|
455
467
|
// fork-bombed the fleet on 2026-07-29 when a stored pytest called
|
|
456
468
|
// `atris task status` and the read path re-ran the verify. Refuse loudly.
|
|
457
469
|
const VERIFY_IN_PROGRESS_ENV = 'ATRIS_VERIFY_IN_PROGRESS';
|
|
@@ -497,7 +509,7 @@ function runVerifyCommand(verify, workspaceRoot) {
|
|
|
497
509
|
env: verifyCommandEnv(parsed.env),
|
|
498
510
|
shell: false,
|
|
499
511
|
encoding: 'utf8',
|
|
500
|
-
timeout:
|
|
512
|
+
timeout: verifyTimeoutMs(),
|
|
501
513
|
});
|
|
502
514
|
// "I could not run the check" is not "the check failed". Collapsing the two
|
|
503
515
|
// let a broken harness read as broken code: under cron's PATH every
|
|
@@ -526,7 +538,7 @@ function runVerifyCommand(verify, workspaceRoot) {
|
|
|
526
538
|
}
|
|
527
539
|
|
|
528
540
|
// Verifiers run with shell:false, so the child gets no PATH resolution beyond
|
|
529
|
-
// whatever the parent inherited. Cron has no Homebrew PATH
|
|
541
|
+
// whatever the parent inherited. Cron has no Homebrew PATH, that is why every
|
|
530
542
|
// cron entry hardcodes an absolute node path. `npm`, `npx` and friends live in
|
|
531
543
|
// the same bin directory as the node binary currently executing, so seeding
|
|
532
544
|
// PATH with it makes recorded checks runnable from cron with zero config: no
|
|
@@ -540,11 +552,15 @@ function verifyCommandEnv(extraEnv) {
|
|
|
540
552
|
base.PATH = current ? `${binDir}${path.delimiter}${current}` : binDir;
|
|
541
553
|
}
|
|
542
554
|
base[VERIFY_IN_PROGRESS_ENV] = '1';
|
|
555
|
+
// A verifier that inherits a test-runner context joins the outer runner as
|
|
556
|
+
// a child and reports instant exit 0 without running anything. Strip it so
|
|
557
|
+
// recorded checks are real runs, not phantom passes.
|
|
558
|
+
delete base.NODE_TEST_CONTEXT;
|
|
543
559
|
return base;
|
|
544
560
|
}
|
|
545
561
|
|
|
546
|
-
// ENOENT: the binary is not on PATH at all. ETIMEDOUT: the
|
|
547
|
-
// off before it could reach a verdict. Neither is evidence about the diff.
|
|
562
|
+
// ENOENT: the binary is not on PATH at all. ETIMEDOUT: the verify ceiling cut
|
|
563
|
+
// it off before it could reach a verdict. Neither is evidence about the diff.
|
|
548
564
|
function unrunnableVerifyReason(result) {
|
|
549
565
|
const code = result && result.error ? result.error.code : null;
|
|
550
566
|
if (code === 'ENOENT') return 'command_not_found';
|
|
@@ -567,7 +583,7 @@ function runVerifyCommandCached(verify, workspaceRoot, cache = null) {
|
|
|
567
583
|
|
|
568
584
|
// Pre-land hygiene: dead exports used to surface only AFTER landing, when the
|
|
569
585
|
// full suite's repo-hygiene ratchet went red on master (lesson:
|
|
570
|
-
// engine-dead-exports
|
|
586
|
+
// engine-dead-exports: engines export every internal helper, the task's own
|
|
571
587
|
// verify command stays green, and the breakage lands). In repos that carry the
|
|
572
588
|
// ratchet (test/repo-hygiene.test.js), run the same detector before landing so
|
|
573
589
|
// the gate refuses the work instead of master discovering it. Memoized per
|
|
@@ -602,7 +618,7 @@ function repoHygieneGate(workspaceRoot) {
|
|
|
602
618
|
}
|
|
603
619
|
: { ok: true };
|
|
604
620
|
} catch (err) {
|
|
605
|
-
// "I could not run the detector" is not a verdict on the work
|
|
621
|
+
// "I could not run the detector" is not a verdict on the work, same
|
|
606
622
|
// rule as verify_unrunnable, but hygiene is a repo-wide ratchet the
|
|
607
623
|
// suite still enforces, so failing open here only delays the red.
|
|
608
624
|
result = { ok: true, skipped: true, error: String((err && err.message) || err).slice(0, 200) };
|
|
@@ -948,7 +964,7 @@ function receiptEvidenceBlock(task, ref) {
|
|
|
948
964
|
}
|
|
949
965
|
|
|
950
966
|
// Authors declare protected lanes in the task text, not the tag field. Match
|
|
951
|
-
// only explicit, deliberate declarations
|
|
967
|
+
// only explicit, deliberate declarations, an unanchored "security" would
|
|
952
968
|
// snag every task that merely mentions the word and wedge the loop shut,
|
|
953
969
|
// which is its own failure mode. Kept narrow on purpose.
|
|
954
970
|
const PROTECTED_LANE_PHRASES = [
|
|
@@ -979,7 +995,7 @@ function declaredProtectedLane(task) {
|
|
|
979
995
|
|
|
980
996
|
// Only consulted when a task carries no tag at all, so this never overrides an
|
|
981
997
|
// author who did classify their work. Terms are concrete nouns from the denied
|
|
982
|
-
// lanes
|
|
998
|
+
// lanes: things you can only be touching on purpose. Deliberately excludes
|
|
983
999
|
// broad words like "auth", "user", or "api" that appear in ordinary frontend
|
|
984
1000
|
// work and would hold the whole queue.
|
|
985
1001
|
const PROTECTED_LANE_TERMS = [
|
|
@@ -1051,12 +1067,12 @@ function evaluateAutoAccept(task, options = {}) {
|
|
|
1051
1067
|
}
|
|
1052
1068
|
|
|
1053
1069
|
// accept-all: the protected lanes above are the only human gate. No
|
|
1054
|
-
// certification, pass-count, reviewer, or proof-quality bar
|
|
1070
|
+
// certification, pass-count, reviewer, or proof-quality bar, but work
|
|
1055
1071
|
// is never marked done against evidence it isn't: a proof naming an
|
|
1056
1072
|
// unmerged draft PR still blocks, and a recorded check that FAILS still
|
|
1057
1073
|
// blocks (absence of a check does not).
|
|
1058
1074
|
if (acceptAll) {
|
|
1059
|
-
// In this lane the tag IS the safety system
|
|
1075
|
+
// In this lane the tag IS the safety system, the only human gate. But
|
|
1060
1076
|
// most rows carry no tag at all (13 of 16 in the 2026-07-25 sweep), so
|
|
1061
1077
|
// refusing every untagged task would wedge the loop shut, which is the
|
|
1062
1078
|
// same outage as failing open, just quieter. Instead: when there is no
|
|
@@ -1096,7 +1112,7 @@ function evaluateAutoAccept(task, options = {}) {
|
|
|
1096
1112
|
if (verify && executeVerify) {
|
|
1097
1113
|
const verifyResult = runVerifyCommandCached(verify, task.workspace_root || process.cwd(), verifyCache);
|
|
1098
1114
|
// A check that runs and fails blocks. So does a check whose worktree
|
|
1099
|
-
// is gone
|
|
1115
|
+
// is gone, otherwise the daily reap converts "has a failing check"
|
|
1100
1116
|
// into "lands unchecked" the morning after it clears the worktree.
|
|
1101
1117
|
// A check that merely isn't in the runnable allowlist counts as no
|
|
1102
1118
|
// check at all.
|
package/lib/autoland.js
CHANGED
|
@@ -69,7 +69,7 @@ function writeJson(file, value) {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
// Default is ON: agents are good enough that a growing review queue is
|
|
72
|
-
// latency, not safety
|
|
72
|
+
// latency, not safety. The denied tags keep the irreversible lanes human.
|
|
73
73
|
// A missing policy file means "on, owner inferred"; only an explicit
|
|
74
74
|
// `atris autoland off` (enabled: false) turns it off.
|
|
75
75
|
function inferOwner(root) {
|
|
@@ -157,7 +157,7 @@ function heartbeatStatusText(root, policy) {
|
|
|
157
157
|
: ageHours < 1 ? 'under an hour' : `${Math.floor(ageHours)}h`;
|
|
158
158
|
// Evidence first: a receipt from the last couple of hours proves the loop is
|
|
159
159
|
// alive no matter what the policy file remembers. The hourly cron makes two
|
|
160
|
-
// missed hours an outage rather than jitter
|
|
160
|
+
// missed hours an outage rather than jitter, and a heartbeat that has gone
|
|
161
161
|
// quiet is the single most useful thing this line can say, because that is
|
|
162
162
|
// the state nobody notices.
|
|
163
163
|
if (ageHours !== null && ageHours <= HEARTBEAT_LIVE_HOURS) return `running hourly (last tick ${ageText} ago)`;
|
|
@@ -236,7 +236,7 @@ function waitingHours(task, now = Date.now()) {
|
|
|
236
236
|
return Math.floor((now - certifiedAtMs(task)) / 3600000);
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
-
// Tasks that are certified and pending
|
|
239
|
+
// Tasks that are certified and pending, finished work sitting on the
|
|
240
240
|
// human side of the fence, whatever the reason.
|
|
241
241
|
function waitingOnHuman(tasks, now = Date.now()) {
|
|
242
242
|
return (tasks || [])
|
|
@@ -508,7 +508,7 @@ function dejargon(line, options = {}) {
|
|
|
508
508
|
// The clarity gate in one call: strip what an operator can't act on (flag
|
|
509
509
|
// dashes, task ids, snake_case) and close the sentence on a whole clause, so
|
|
510
510
|
// any title or line borrowed from the queue reaches a human actionable at a
|
|
511
|
-
// glance. One sentence in, one operator-ready sentence out
|
|
511
|
+
// glance. One sentence in, one operator-ready sentence out. Content always
|
|
512
512
|
// ships, the gate only changes how it reads.
|
|
513
513
|
function clarify(text, max = 160) {
|
|
514
514
|
return finishThought(dejargon(text), max);
|
|
@@ -538,7 +538,7 @@ function memberTrustLabel(member, root) {
|
|
|
538
538
|
|
|
539
539
|
// Short enough for one iMessage, plain enough to read walking to the car.
|
|
540
540
|
// Every line answers: what happened, how big, how we know. It names the work
|
|
541
|
-
// because "7 things landed" tells the score, not the story
|
|
541
|
+
// because "7 things landed" tells the score, not the story, and it always
|
|
542
542
|
// ends with what's waiting and what to do next, because those are the only
|
|
543
543
|
// lines that ask anything of the reader.
|
|
544
544
|
function composeDigest({ accepted, waiting, protectedWaiting = [], waitingWishes = [], landed, project, nextMoves = [], acceptAll = false, reapError = null, janitor = null, landingSweep = null, fullStory = false, root = process.cwd() }) {
|
|
@@ -620,7 +620,7 @@ function composeDigest({ accepted, waiting, protectedWaiting = [], waitingWishes
|
|
|
620
620
|
}
|
|
621
621
|
// nextMoves: array of {title, owner}, or {moves, unexplained} where
|
|
622
622
|
// unexplained counts queue items whose sentence carries no operator why.
|
|
623
|
-
// Those are counted, not shown
|
|
623
|
+
// Those are counted, not shown. A raw title the reader can't act on is
|
|
624
624
|
// noise here and a writing bug at its source.
|
|
625
625
|
const nextInfo = Array.isArray(nextMoves)
|
|
626
626
|
? { moves: nextMoves, unexplained: 0 }
|
package/lib/bench/engines.js
CHANGED
|
@@ -9,6 +9,9 @@ const ENGINE_NAMES = Object.freeze([
|
|
|
9
9
|
'cursor',
|
|
10
10
|
'claude',
|
|
11
11
|
'atris-fast',
|
|
12
|
+
'agy',
|
|
13
|
+
'opencode',
|
|
14
|
+
'devin',
|
|
12
15
|
'null',
|
|
13
16
|
'solution',
|
|
14
17
|
]);
|
|
@@ -18,8 +21,13 @@ const ENGINE_BINS = Object.freeze({
|
|
|
18
21
|
cursor: 'cursor-agent',
|
|
19
22
|
claude: 'claude',
|
|
20
23
|
'atris-fast': 'atris',
|
|
24
|
+
agy: 'agy',
|
|
25
|
+
opencode: 'opencode',
|
|
26
|
+
devin: 'devin',
|
|
21
27
|
});
|
|
22
28
|
|
|
29
|
+
const AGY_HEADLESS_NOTE = 'You are running headless with edit permission already granted. Apply changes directly and never ask for confirmation; finish the task fully, then report.';
|
|
30
|
+
|
|
23
31
|
function normalizeEngineName(name) {
|
|
24
32
|
const normalized = String(name || '').trim();
|
|
25
33
|
if (!normalized) return '';
|
|
@@ -29,23 +37,30 @@ function normalizeEngineName(name) {
|
|
|
29
37
|
return normalized;
|
|
30
38
|
}
|
|
31
39
|
|
|
32
|
-
function commandAvailable(command) {
|
|
40
|
+
function commandAvailable(command, env = process.env) {
|
|
33
41
|
const result = spawnSync('/bin/sh', ['-c', `command -v ${command}`], {
|
|
34
42
|
encoding: 'utf8',
|
|
35
|
-
env
|
|
43
|
+
env,
|
|
36
44
|
});
|
|
37
45
|
return result.status === 0 && Boolean(String(result.stdout || '').trim());
|
|
38
46
|
}
|
|
39
47
|
|
|
40
48
|
function spawnEngine(command, args, workspaceDir, timeoutMs, options = {}) {
|
|
49
|
+
// Two hermetic-workspace traps (verified live 2026-09-04): engines that
|
|
50
|
+
// read PWD from the environment (opencode) edited the real repo fixture
|
|
51
|
+
// instead of the temp workspace, and engines that read a piped stdin as
|
|
52
|
+
// the prompt (devin) exited 0 in 3s having done nothing. Point PWD at the
|
|
53
|
+
// workspace and close stdin unless the caller supplies input.
|
|
41
54
|
const result = spawnSync(command, args, {
|
|
42
55
|
cwd: workspaceDir,
|
|
43
56
|
input: options.input,
|
|
57
|
+
stdio: options.input === undefined ? ['ignore', 'pipe', 'pipe'] : undefined,
|
|
44
58
|
encoding: 'utf8',
|
|
45
59
|
timeout: Number(timeoutMs || 300000),
|
|
46
60
|
maxBuffer: Number(options.maxBuffer || 16 * 1024 * 1024),
|
|
47
61
|
env: {
|
|
48
62
|
...process.env,
|
|
63
|
+
PWD: workspaceDir,
|
|
49
64
|
...(options.env || {}),
|
|
50
65
|
},
|
|
51
66
|
});
|
|
@@ -53,14 +68,15 @@ function spawnEngine(command, args, workspaceDir, timeoutMs, options = {}) {
|
|
|
53
68
|
status: result.status === null ? 1 : result.status,
|
|
54
69
|
stdout: result.stdout || '',
|
|
55
70
|
stderr: result.error ? String(result.error.message || result.error) : (result.stderr || ''),
|
|
71
|
+
timedOut: Boolean(result.error && result.error.code === 'ETIMEDOUT'),
|
|
56
72
|
};
|
|
57
73
|
}
|
|
58
74
|
|
|
59
75
|
function realEngineAdapter(name, bin, runArgs) {
|
|
60
76
|
return {
|
|
61
77
|
name,
|
|
62
|
-
available(workspaceDir) {
|
|
63
|
-
if (!commandAvailable(bin)) {
|
|
78
|
+
available(workspaceDir, env) {
|
|
79
|
+
if (!commandAvailable(bin, env)) {
|
|
64
80
|
return { available: false, reason: `${bin} not found` };
|
|
65
81
|
}
|
|
66
82
|
if (name === 'atris-fast' && !fs.existsSync(path.join(workspaceDir, 'atris'))) {
|
|
@@ -68,8 +84,8 @@ function realEngineAdapter(name, bin, runArgs) {
|
|
|
68
84
|
}
|
|
69
85
|
return { available: true, reason: 'available' };
|
|
70
86
|
},
|
|
71
|
-
run(promptText, workspaceDir, timeoutMs) {
|
|
72
|
-
return spawnEngine(bin, runArgs(promptText, workspaceDir), workspaceDir, timeoutMs);
|
|
87
|
+
run(promptText, workspaceDir, timeoutMs, env) {
|
|
88
|
+
return spawnEngine(bin, runArgs(promptText, workspaceDir), workspaceDir, timeoutMs, { env });
|
|
73
89
|
},
|
|
74
90
|
};
|
|
75
91
|
}
|
|
@@ -95,14 +111,15 @@ function solutionEngineAdapter(solutionPath) {
|
|
|
95
111
|
}
|
|
96
112
|
return { available: true, reason: 'available' };
|
|
97
113
|
},
|
|
98
|
-
run(_promptText, workspaceDir, timeoutMs) {
|
|
99
|
-
return spawnEngine('/bin/sh', [solutionPath], workspaceDir, timeoutMs);
|
|
114
|
+
run(_promptText, workspaceDir, timeoutMs, env) {
|
|
115
|
+
return spawnEngine('/bin/sh', [solutionPath], workspaceDir, timeoutMs, { env });
|
|
100
116
|
},
|
|
101
117
|
};
|
|
102
118
|
}
|
|
103
119
|
|
|
104
120
|
function getEngineAdapter(name, options = {}) {
|
|
105
121
|
const normalized = normalizeEngineName(name);
|
|
122
|
+
const model = options.model ? String(options.model).trim() : '';
|
|
106
123
|
if (normalized === 'null') return nullEngineAdapter();
|
|
107
124
|
if (normalized === 'solution') return solutionEngineAdapter(options.solutionPath);
|
|
108
125
|
if (normalized === 'codex') {
|
|
@@ -124,6 +141,40 @@ function getEngineAdapter(name, options = {}) {
|
|
|
124
141
|
'--dangerously-skip-permissions',
|
|
125
142
|
]);
|
|
126
143
|
}
|
|
144
|
+
if (normalized === 'agy') {
|
|
145
|
+
// --add-dir is mandatory: without it agy edits its own scratch folder.
|
|
146
|
+
// The headless note is mandatory too: with every auto-approve flag on,
|
|
147
|
+
// gemini still stops to ask "may I edit?" and exits 0 having changed
|
|
148
|
+
// nothing (verified on 3.7 and 3.8, 2026-09-04). One line fixes it.
|
|
149
|
+
return realEngineAdapter('agy', ENGINE_BINS.agy, (promptText, workspaceDir) => [
|
|
150
|
+
'--mode',
|
|
151
|
+
'accept-edits',
|
|
152
|
+
'--dangerously-skip-permissions',
|
|
153
|
+
'--add-dir',
|
|
154
|
+
workspaceDir,
|
|
155
|
+
...(model ? ['--model', model] : []),
|
|
156
|
+
'-p',
|
|
157
|
+
`${AGY_HEADLESS_NOTE}\n\n${promptText}`,
|
|
158
|
+
]);
|
|
159
|
+
}
|
|
160
|
+
if (normalized === 'opencode') {
|
|
161
|
+
return realEngineAdapter('opencode', ENGINE_BINS.opencode, (promptText) => [
|
|
162
|
+
'run',
|
|
163
|
+
'--auto',
|
|
164
|
+
...(model ? ['-m', model] : []),
|
|
165
|
+
promptText,
|
|
166
|
+
]);
|
|
167
|
+
}
|
|
168
|
+
if (normalized === 'devin') {
|
|
169
|
+
return realEngineAdapter('devin', ENGINE_BINS.devin, (promptText) => [
|
|
170
|
+
'-p',
|
|
171
|
+
'--permission-mode',
|
|
172
|
+
'dangerous',
|
|
173
|
+
...(model ? ['--model', model] : []),
|
|
174
|
+
'--',
|
|
175
|
+
promptText,
|
|
176
|
+
]);
|
|
177
|
+
}
|
|
127
178
|
if (normalized === 'atris-fast') {
|
|
128
179
|
return realEngineAdapter('atris-fast', ENGINE_BINS['atris-fast'], (promptText) => [
|
|
129
180
|
'chat',
|
package/lib/bench/report.js
CHANGED
|
@@ -64,7 +64,7 @@ function buildBenchReport(options = {}) {
|
|
|
64
64
|
|
|
65
65
|
const byEngine = new Map();
|
|
66
66
|
for (const record of records) {
|
|
67
|
-
const engineName = record.engine || 'unknown';
|
|
67
|
+
const engineName = (record.engine || 'unknown') + (record.model ? `@${record.model}` : '');
|
|
68
68
|
if (!byEngine.has(engineName)) byEngine.set(engineName, []);
|
|
69
69
|
byEngine.get(engineName).push(record);
|
|
70
70
|
}
|