atris 3.37.1 → 3.40.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 +25 -6
- package/README.md +2 -0
- package/atris/PERSONA.md +11 -11
- package/atris.md +17 -2
- package/ax +160 -34
- package/bin/atris.js +78 -6
- package/commands/activate.js +16 -3
- package/commands/agent-spawn.js +13 -18
- package/commands/autoland.js +76 -47
- package/commands/autopilot-front.js +0 -4
- package/commands/autopilot.js +0 -12
- package/commands/bench.js +10 -12
- package/commands/brain.js +0 -7
- package/commands/brief.js +0 -2
- package/commands/business-sync.js +0 -1
- package/commands/business.js +452 -12
- package/commands/chat-scan.js +5 -7
- package/commands/clean.js +31 -2
- package/commands/close.js +46 -11
- package/commands/cloud.js +0 -1
- package/commands/codex-goal.js +8 -12
- package/commands/computer.js +6 -5
- package/commands/console.js +64 -11
- package/commands/decide.js +166 -0
- package/commands/deck.js +30 -12
- package/commands/dream.js +0 -7
- package/commands/drill.js +14 -25
- package/commands/engine.js +206 -26
- package/commands/experiments.js +0 -4
- package/commands/fleet-report.js +12 -4
- package/commands/game.js +261 -0
- package/commands/gm.js +17 -7
- package/commands/goal.js +0 -6
- package/commands/harvest.js +1 -5
- package/commands/improve.js +29 -8
- package/commands/init.js +41 -37
- package/commands/integrations.js +0 -7
- package/commands/land.js +77 -20
- package/commands/launchpad.js +1 -16
- package/commands/lesson.js +17 -7
- package/commands/lifecycle.js +5 -5
- package/commands/log.js +55 -5
- package/commands/loop-front.js +0 -2
- package/commands/loops.js +6 -5
- package/commands/meet.js +0 -2
- package/commands/member.js +622 -573
- package/commands/mission.js +790 -314
- 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 +3599 -424
- package/commands/play.js +6 -6
- package/commands/probe.js +12 -6
- package/commands/pulse.js +88 -64
- package/commands/push.js +0 -4
- package/commands/radar.js +14 -11
- package/commands/recover.js +340 -0
- package/commands/release.js +10 -9
- package/commands/report.js +0 -1
- package/commands/router.js +320 -0
- package/commands/run-front.js +0 -2
- package/commands/search.js +0 -3
- package/commands/site-deploy.js +870 -0
- package/commands/site.js +11 -2
- package/commands/skill.js +7 -2
- package/commands/slop.js +237 -7
- package/commands/status.js +6 -0
- package/commands/stream.js +4 -20
- package/commands/study.js +0 -3
- package/commands/sync-checkout.js +51 -0
- package/commands/sync.js +11 -40
- package/commands/task.js +999 -595
- package/commands/taste.js +101 -0
- package/commands/teach.js +86 -0
- package/commands/team.js +83 -3
- package/commands/unknowns.js +0 -10
- package/commands/update.js +0 -1
- package/commands/vercel.js +4 -2
- package/commands/visualize.js +0 -5
- package/commands/voice.js +195 -0
- package/commands/watch.js +1 -28
- package/commands/wiki.js +42 -15
- package/commands/wish.js +0 -1
- package/commands/workflow.js +2 -2
- package/commands/worktree.js +88 -28
- package/commands/xp.js +28 -30
- package/commands/youtube.js +0 -2
- package/lib/accept-verify-gate.js +126 -0
- package/lib/activity-stream.js +0 -2
- package/lib/arg-parser.js +41 -0
- package/lib/auto-accept-certified.js +261 -3
- package/lib/autoland.js +66 -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 +18 -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 +7 -2
- 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 +157 -28
- package/lib/experiments/daily.js +0 -6
- package/lib/falsifier-probe.js +84 -0
- package/lib/fleet.js +275 -42
- package/lib/functional-owner.js +0 -3
- package/lib/git-spawn.js +15 -0
- package/lib/inspect-fields.js +0 -1
- package/lib/json-file.js +37 -0
- package/lib/known-commands.js +2 -2
- package/lib/learnings.js +0 -2
- package/lib/lesson-preflight.js +146 -0
- package/lib/loop-doctor.js +11 -19
- 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-human-asks.js +28 -0
- package/lib/mission-protected-lane.js +296 -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 +47 -6
- package/lib/orb-context.js +14 -4
- package/lib/orb-scorecard.js +0 -1
- package/lib/outbound-send-gate.js +0 -6
- package/lib/pack-capabilities.js +685 -0
- 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 +51 -3
- package/lib/runner-command.js +0 -10
- package/lib/runtime-bootstrap.js +0 -1
- package/lib/scorecard.js +0 -1
- package/lib/self-drive.js +45 -14
- package/lib/state-detection.js +0 -6
- package/lib/task-db.js +137 -7
- package/lib/task-decision.js +50 -0
- package/lib/taste-lessons.js +153 -0
- package/lib/team-presence.js +0 -1
- package/lib/tool-result-encode.js +17 -1
- package/lib/voice-gate.js +129 -2
- package/lib/wiki.js +152 -9
- package/lib/wish-audit.js +21 -7
- package/lib/wish-delegate.js +27 -21
- 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 +95 -8
- package/package.json +2 -1
- package/templates/business-starter/persona.md +9 -0
package/lib/policy-lessons.js
CHANGED
|
@@ -320,12 +320,10 @@ function syncLessonsMd(root, mined) {
|
|
|
320
320
|
}
|
|
321
321
|
|
|
322
322
|
module.exports = {
|
|
323
|
-
AGENT_ACTOR_PATTERN,
|
|
324
323
|
VERIFY_COMMAND_PATTERN,
|
|
325
324
|
loadHistory,
|
|
326
325
|
mineProofPolicy,
|
|
327
326
|
policyHintsForProof,
|
|
328
|
-
policyLessonsPath,
|
|
329
327
|
readPolicyLessons,
|
|
330
328
|
writePolicyLessons,
|
|
331
329
|
syncLessonsMd,
|
package/lib/pulse.js
CHANGED
|
@@ -288,8 +288,16 @@ function shouldFallbackToAutopilot({ missionReason, autopilotFallback = true, no
|
|
|
288
288
|
// actual delta (new commit or newly-dirtied files), never the whole dirty tree —
|
|
289
289
|
// crediting pre-existing dirt re-rewards the same change every tick (the reward
|
|
290
290
|
// inflation bug). The caller computes producedWork from a before/after snapshot.
|
|
291
|
-
|
|
291
|
+
//
|
|
292
|
+
// A crashed engine must also score -1. Before this, a hard `mission error` tick
|
|
293
|
+
// produced no work and ran no verifier, so it scored 0 — indistinguishable from
|
|
294
|
+
// a healthy idle tick. The 2026-07-16 heartbeat death proved the cost: 25
|
|
295
|
+
// consecutive error ticks left reward_sum flat, nothing escalated, and the loop
|
|
296
|
+
// stopped with a ledger that still read green. `actorReason: 'error'` is a
|
|
297
|
+
// failure, not a rest.
|
|
298
|
+
function scoreTick({ verifyPassed, producedWork, actorOk, actorReason } = {}) {
|
|
292
299
|
if (verifyPassed === false) return -1;
|
|
300
|
+
if (actorOk === false && actorReason === 'error') return -1;
|
|
293
301
|
if (!producedWork) return 0;
|
|
294
302
|
return verifyPassed === true ? 1 : 0;
|
|
295
303
|
}
|
|
@@ -575,7 +583,7 @@ function releaseLock(root) {
|
|
|
575
583
|
} catch {}
|
|
576
584
|
}
|
|
577
585
|
|
|
578
|
-
// ---
|
|
586
|
+
// --- scheduled tick script + launchd plist (pure string generation) ---
|
|
579
587
|
|
|
580
588
|
// Minimal shell wrapper modeled on the proven commander tick.sh: deadline
|
|
581
589
|
// self-removal, then hand off to `atris pulse tick` (all real logic lives in JS,
|
|
@@ -593,6 +601,8 @@ function buildTickScript(opts = {}) {
|
|
|
593
601
|
runnerCommandTemplate = '',
|
|
594
602
|
verifyCmd = 'npm test',
|
|
595
603
|
pathDirs = [],
|
|
604
|
+
launchLabel = '',
|
|
605
|
+
launchPlistPath = '',
|
|
596
606
|
} = opts;
|
|
597
607
|
if (!root) throw new Error('buildTickScript: root is required');
|
|
598
608
|
if (!stateHome) throw new Error('buildTickScript: stateHome is required');
|
|
@@ -636,16 +646,26 @@ STATE="${stateHome}"
|
|
|
636
646
|
LOG_DIR="$STATE/logs"
|
|
637
647
|
DEADLINE_EPOCH="${deadlineEpoch}"
|
|
638
648
|
MARKER="${marker}"
|
|
649
|
+
LAUNCH_LABEL="${launchLabel}"
|
|
650
|
+
LAUNCH_PLIST="${launchPlistPath}"
|
|
651
|
+
|
|
652
|
+
# Claude Max subscription auth comes from the macOS keychain. An API key would
|
|
653
|
+
# override it and route autonomous work onto metered API billing.
|
|
654
|
+
unset ANTHROPIC_API_KEY
|
|
639
655
|
|
|
640
|
-
#
|
|
656
|
+
# launchd has a minimal PATH; restore the dirs the engine's bare-name spawns need.
|
|
641
657
|
${pathExport}
|
|
642
658
|
|
|
643
659
|
mkdir -p "$LOG_DIR"
|
|
644
660
|
|
|
645
661
|
now="$(date +%s)"
|
|
646
662
|
if [ "$now" -ge "$DEADLINE_EPOCH" ]; then
|
|
663
|
+
if [ -n "$LAUNCH_LABEL" ]; then
|
|
664
|
+
launchctl bootout "gui/$(id -u)/$LAUNCH_LABEL" 2>/dev/null || true
|
|
665
|
+
[ -n "$LAUNCH_PLIST" ] && rm -f "$LAUNCH_PLIST"
|
|
666
|
+
fi
|
|
647
667
|
crontab -l 2>/dev/null | grep -v "$MARKER" | crontab - 2>/dev/null || true
|
|
648
|
-
echo "$(date -Iseconds) pulse expired; removed
|
|
668
|
+
echo "$(date -Iseconds) pulse expired; removed scheduler" >> "$LOG_DIR/control.log"
|
|
649
669
|
expired_at="$(date -Iseconds)"
|
|
650
670
|
journal_year="$(date +%Y)"
|
|
651
671
|
journal_day="$(date +%F)"
|
|
@@ -693,6 +713,48 @@ fi
|
|
|
693
713
|
`;
|
|
694
714
|
}
|
|
695
715
|
|
|
716
|
+
function xmlEscape(value) {
|
|
717
|
+
return String(value)
|
|
718
|
+
.replace(/&/g, '&')
|
|
719
|
+
.replace(/</g, '<')
|
|
720
|
+
.replace(/>/g, '>')
|
|
721
|
+
.replace(/"/g, '"')
|
|
722
|
+
.replace(/'/g, ''');
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function launchIntervalFromCadence({ cadenceMinutes, cron } = {}) {
|
|
726
|
+
const minutes = Number(cadenceMinutes);
|
|
727
|
+
if (Number.isInteger(minutes) && minutes > 0) return { seconds: minutes * 60, fallback: false };
|
|
728
|
+
const normalized = normalizeCronCadence(cron || DEFAULT_CADENCE_CRON);
|
|
729
|
+
const everyMinutes = normalized.match(/^\*\/(\d+) \* \* \* \*$/);
|
|
730
|
+
if (everyMinutes) return { seconds: Number(everyMinutes[1]) * 60, fallback: false };
|
|
731
|
+
if (/^\d+ \* \* \* \*$/.test(normalized)) return { seconds: 3600, fallback: false };
|
|
732
|
+
return { seconds: 3600, fallback: true };
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function buildLaunchAgentPlist(opts = {}) {
|
|
736
|
+
const { label, scriptPath, stateHome } = opts;
|
|
737
|
+
if (!label) throw new Error('buildLaunchAgentPlist: label is required');
|
|
738
|
+
if (!scriptPath) throw new Error('buildLaunchAgentPlist: scriptPath is required');
|
|
739
|
+
if (!stateHome) throw new Error('buildLaunchAgentPlist: stateHome is required');
|
|
740
|
+
const interval = launchIntervalFromCadence(opts).seconds;
|
|
741
|
+
const logPath = path.join(stateHome, 'logs', 'launchd.log');
|
|
742
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
743
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
744
|
+
<plist version="1.0">
|
|
745
|
+
<dict>
|
|
746
|
+
<key>Label</key><string>${xmlEscape(label)}</string>
|
|
747
|
+
<key>ProgramArguments</key>
|
|
748
|
+
<array><string>/bin/bash</string><string>${xmlEscape(scriptPath)}</string></array>
|
|
749
|
+
<key>StartInterval</key><integer>${interval}</integer>
|
|
750
|
+
<key>AbandonProcessGroup</key><true/>
|
|
751
|
+
<key>StandardOutPath</key><string>${xmlEscape(logPath)}</string>
|
|
752
|
+
<key>StandardErrorPath</key><string>${xmlEscape(logPath)}</string>
|
|
753
|
+
</dict>
|
|
754
|
+
</plist>
|
|
755
|
+
`;
|
|
756
|
+
}
|
|
757
|
+
|
|
696
758
|
function buildCrontabLine(opts = {}) {
|
|
697
759
|
const { cron = DEFAULT_CADENCE_CRON, scriptPath, marker = PULSE_MARKER } = opts;
|
|
698
760
|
if (!scriptPath) throw new Error('buildCrontabLine: scriptPath is required');
|
|
@@ -703,28 +765,17 @@ module.exports = {
|
|
|
703
765
|
PULSE_RECEIPT_SCHEMA,
|
|
704
766
|
SCORECARD_SCHEMA,
|
|
705
767
|
PULSE_MARKER,
|
|
706
|
-
LEGACY_STATE_DIRNAME,
|
|
707
768
|
DEFAULT_CADENCE_CRON,
|
|
708
769
|
STALE_TICK_MS,
|
|
709
|
-
LIVENESS_STALE_MS,
|
|
710
770
|
stateDir,
|
|
711
|
-
pulseRepoSlug,
|
|
712
|
-
pulseStateHome,
|
|
713
|
-
pulseMarker,
|
|
714
771
|
legacyPulseStateHome,
|
|
715
|
-
readTickScriptRoot,
|
|
716
|
-
legacyPulseStateMatchesRoot,
|
|
717
772
|
resolvePulseSlot,
|
|
718
773
|
pulseReceiptsPath,
|
|
719
774
|
scorecardsPath,
|
|
720
|
-
pulseCounterPath,
|
|
721
|
-
pulseLockDir,
|
|
722
775
|
buildPulseReceipt,
|
|
723
776
|
buildPulseScorecardRow,
|
|
724
|
-
buildOrbFailureScorecardRow,
|
|
725
777
|
buildInterruptedPulseReceipt,
|
|
726
778
|
ingestOrbScorecard,
|
|
727
|
-
orbRunKey,
|
|
728
779
|
scoreTick,
|
|
729
780
|
fsSnapshot,
|
|
730
781
|
diffFsSnapshots,
|
|
@@ -744,6 +795,8 @@ module.exports = {
|
|
|
744
795
|
acquireLock,
|
|
745
796
|
releaseLock,
|
|
746
797
|
buildTickScript,
|
|
798
|
+
buildLaunchAgentPlist,
|
|
799
|
+
launchIntervalFromCadence,
|
|
747
800
|
buildCrontabLine,
|
|
748
801
|
normalizeCronCadence,
|
|
749
802
|
};
|
package/lib/review-integrity.js
CHANGED
package/lib/router-brain.js
CHANGED
|
@@ -6,6 +6,8 @@ const path = require('node:path');
|
|
|
6
6
|
const DEFAULT_MIN_RECEIPTS = 3;
|
|
7
7
|
const DEFAULT_HALF_LIFE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
8
8
|
const DURATION_SCALE_MS = 60 * 1000;
|
|
9
|
+
const STRETCH_ZONE_MIN = 0.6;
|
|
10
|
+
const STRETCH_ZONE_MAX = 0.85;
|
|
9
11
|
|
|
10
12
|
function objectValue(value) {
|
|
11
13
|
return value && typeof value === 'object' ? value : null;
|
|
@@ -313,15 +315,56 @@ function rankEnginesDetailed(candidates, options = {}) {
|
|
|
313
315
|
|| fallbackOrder(left.candidate, left.index) - fallbackOrder(right.candidate, right.index)
|
|
314
316
|
|| candidateId(left.candidate).localeCompare(candidateId(right.candidate)))
|
|
315
317
|
.map((entry) => entry.candidate);
|
|
318
|
+
|
|
319
|
+
const stretchPick = stretchZonePick(ranked.map((candidate) => ({
|
|
320
|
+
candidate,
|
|
321
|
+
predicted: statsByEngine.get(candidateId(candidate))?.verified_pass_rate ?? null,
|
|
322
|
+
})), options);
|
|
323
|
+
const finalRanked = stretchPick
|
|
324
|
+
? [stretchPick, ...ranked.filter((candidate) => candidate !== stretchPick)]
|
|
325
|
+
: ranked;
|
|
316
326
|
return {
|
|
317
|
-
candidates:
|
|
327
|
+
candidates: finalRanked,
|
|
318
328
|
stats,
|
|
319
329
|
task_type: taskType,
|
|
320
330
|
used_track_record: true,
|
|
321
331
|
thin_engines: [],
|
|
332
|
+
stretch_zone_pick: stretchPick ? candidateId(stretchPick) : null,
|
|
322
333
|
};
|
|
323
334
|
}
|
|
324
335
|
|
|
336
|
+
function engineCost(candidate) {
|
|
337
|
+
const value = Number(candidate && typeof candidate === 'object' ? candidate.cost : NaN);
|
|
338
|
+
return Number.isFinite(value) && value >= 0 ? value : Number.POSITIVE_INFINITY;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// stretch zone rule for low-stakes lanes: land the cheapest engine whose
|
|
342
|
+
// predicted success rate sits in the learnable band. entries are
|
|
343
|
+
// { candidate, predicted } rows where predicted is a 0..1 rate or null.
|
|
344
|
+
// returns the winning candidate, or null when nobody is in the band; the
|
|
345
|
+
// caller then keeps the normal score order, whose top is the strongest
|
|
346
|
+
// engine, which is the escalation path when every engine sits below the
|
|
347
|
+
// band floor.
|
|
348
|
+
function stretchZonePick(entries, options = {}) {
|
|
349
|
+
const lowStakes = options.lowStakes === true || normalizedLabel(options.stakes) === 'low';
|
|
350
|
+
if (!lowStakes) return null;
|
|
351
|
+
const rows = (Array.isArray(entries) ? entries : [])
|
|
352
|
+
.map((entry, index) => ({
|
|
353
|
+
candidate: entry && typeof entry === 'object' && 'candidate' in entry ? entry.candidate : entry,
|
|
354
|
+
predicted: entry && typeof entry === 'object' ? finiteNonnegative(entry.predicted) : null,
|
|
355
|
+
index,
|
|
356
|
+
}))
|
|
357
|
+
.filter((row) => row.candidate);
|
|
358
|
+
const inBand = rows
|
|
359
|
+
.filter((row) => row.predicted !== null && row.predicted <= 1
|
|
360
|
+
&& row.predicted >= STRETCH_ZONE_MIN && row.predicted <= STRETCH_ZONE_MAX)
|
|
361
|
+
.sort((left, right) => engineCost(left.candidate) - engineCost(right.candidate)
|
|
362
|
+
|| right.predicted - left.predicted
|
|
363
|
+
|| fallbackOrder(left.candidate, left.index) - fallbackOrder(right.candidate, right.index)
|
|
364
|
+
|| candidateId(left.candidate).localeCompare(candidateId(right.candidate)));
|
|
365
|
+
return inBand.length ? inBand[0].candidate : null;
|
|
366
|
+
}
|
|
367
|
+
|
|
325
368
|
function rankEngines(candidates, options = {}) {
|
|
326
369
|
return rankEnginesDetailed(candidates, options).candidates;
|
|
327
370
|
}
|
|
@@ -335,6 +378,10 @@ function routerPickExplanation(decision) {
|
|
|
335
378
|
return `router picked ${engine} because ${taskType} track records are thin, so fallback order applies.`;
|
|
336
379
|
}
|
|
337
380
|
const stats = decision.stats.find((row) => row.engine === engine && row.task_type === taskType);
|
|
381
|
+
if (decision.stretch_zone_pick === engine) {
|
|
382
|
+
const predicted = stats ? stats.verified_pass_rate.toFixed(2) : 'unknown';
|
|
383
|
+
return `router picked ${engine} because the lane is low stakes and its predicted pass rate ${predicted} sits in the ${STRETCH_ZONE_MIN.toFixed(2)} to ${STRETCH_ZONE_MAX.toFixed(2)} stretch zone at the lowest cost.`;
|
|
384
|
+
}
|
|
338
385
|
if (!stats) return `router picked ${engine} because it has the strongest ${taskType} track record.`;
|
|
339
386
|
const passRate = (stats.verified_pass_rate * 100).toFixed(1);
|
|
340
387
|
const duration = stats.median_duration_ms === null ? 'no duration' : `${Math.round(stats.median_duration_ms)} ms median`;
|
|
@@ -342,11 +389,12 @@ function routerPickExplanation(decision) {
|
|
|
342
389
|
}
|
|
343
390
|
|
|
344
391
|
module.exports = {
|
|
345
|
-
|
|
346
|
-
|
|
392
|
+
STRETCH_ZONE_MIN,
|
|
393
|
+
STRETCH_ZONE_MAX,
|
|
347
394
|
loadRouterHistory,
|
|
348
395
|
computeEngineTaskStats,
|
|
349
396
|
rankEngines,
|
|
350
397
|
rankEnginesDetailed,
|
|
351
398
|
routerPickExplanation,
|
|
399
|
+
stretchZonePick,
|
|
352
400
|
};
|
package/lib/runner-command.js
CHANGED
|
@@ -67,11 +67,6 @@ const RUNNER_PROFILE_DEFS = Object.freeze({
|
|
|
67
67
|
model: 'grok-4.5',
|
|
68
68
|
commandTemplate: '{bin} --always-approve -p {prompt}',
|
|
69
69
|
}),
|
|
70
|
-
hermes: Object.freeze({
|
|
71
|
-
bin: 'hermes',
|
|
72
|
-
model: '',
|
|
73
|
-
commandTemplate: '{bin} -p -- {prompt}',
|
|
74
|
-
}),
|
|
75
70
|
droid: Object.freeze({
|
|
76
71
|
bin: 'droid',
|
|
77
72
|
model: '',
|
|
@@ -85,7 +80,6 @@ const RUNNER_PROFILE_DEFS = Object.freeze({
|
|
|
85
80
|
const RUNNER_PROFILE_ALIASES = Object.freeze({
|
|
86
81
|
'atris2-fast': 'atris-fast',
|
|
87
82
|
'atris-2-fast': 'atris-fast',
|
|
88
|
-
'hermes-agent': 'hermes',
|
|
89
83
|
});
|
|
90
84
|
|
|
91
85
|
// Back-compat surface: RUNNER_PROFILES still resolves every accepted name
|
|
@@ -230,11 +224,7 @@ module.exports = {
|
|
|
230
224
|
RUNNER_PROFILE_DEFS,
|
|
231
225
|
RUNNER_PROFILE_ALIASES,
|
|
232
226
|
RUNNER_PROFILE_NAMES,
|
|
233
|
-
resolveRunnerProfileName,
|
|
234
227
|
resolveRunnerProfile,
|
|
235
|
-
resolveRunnerModel: resolveClaudeRunnerModel,
|
|
236
|
-
resolveRunnerBin: resolveClaudeRunnerBin,
|
|
237
|
-
resolveRunnerCommandTemplate: resolveClaudeRunnerCommandTemplate,
|
|
238
228
|
resolveClaudeRunnerModel,
|
|
239
229
|
resolveClaudeRunnerBin,
|
|
240
230
|
resolveClaudeRunnerCommandTemplate,
|
package/lib/runtime-bootstrap.js
CHANGED
package/lib/scorecard.js
CHANGED
package/lib/self-drive.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const { openHumanAsks } = require('./mission-human-asks');
|
|
5
6
|
|
|
6
7
|
const HUMAN_BLOCKING_REASONS = new Set(['auth-required', 'model-unavailable', 'rate-limit-exceeded-wall']);
|
|
7
8
|
const OPEN_TASK_STATUSES = new Set(['open', 'claimed', 'review']);
|
|
@@ -154,10 +155,10 @@ function projectTasks(taskDb, db, workspaceRoot) {
|
|
|
154
155
|
function handleMissionBlocker({ mission, stopReason, workspaceRoot, appendEvent }, injected = {}) {
|
|
155
156
|
if (!mission || !mission.id) return { taskId: null, dispatched: false, engine: null, reason: 'missing mission' };
|
|
156
157
|
const blockerClass = reasonClass(stopReason || mission.stop_reason || mission.last_tick_reason);
|
|
157
|
-
if (HUMAN_BLOCKING_REASONS.has(blockerClass) || (
|
|
158
|
+
if (HUMAN_BLOCKING_REASONS.has(blockerClass) || openHumanAsks(mission.human_asks).length) {
|
|
158
159
|
return { taskId: null, dispatched: false, engine: null, reason: 'human-blocking' };
|
|
159
160
|
}
|
|
160
|
-
if (['ready', 'complete'].includes(String(mission.status || '').toLowerCase())) {
|
|
161
|
+
if (['ready', 'complete', 'stopped'].includes(String(mission.status || '').toLowerCase())) {
|
|
161
162
|
return { taskId: null, dispatched: false, engine: null, reason: 'stop condition met' };
|
|
162
163
|
}
|
|
163
164
|
|
|
@@ -175,23 +176,53 @@ function handleMissionBlocker({ mission, stopReason, workspaceRoot, appendEvent
|
|
|
175
176
|
return { taskId: taskRef(task), dispatched: false, engine: null, reason: 'existing blocker task' };
|
|
176
177
|
}
|
|
177
178
|
|
|
179
|
+
const evidence = String(mission.receipt_path || mission.last_tick_reason || stopReason || blockerClass).trim();
|
|
180
|
+
const objective = String(mission.objective || mission.id).trim();
|
|
181
|
+
const fullStopReason = String(stopReason || blockerClass).trim();
|
|
182
|
+
const blockerMetadata = {
|
|
183
|
+
mission_id: mission.id,
|
|
184
|
+
mission_objective: objective,
|
|
185
|
+
mission_blocker_class: blockerClass,
|
|
186
|
+
stop_reason: fullStopReason,
|
|
187
|
+
evidence,
|
|
188
|
+
};
|
|
189
|
+
const latestClosed = matching
|
|
190
|
+
.filter(row => !OPEN_TASK_STATUSES.has(row.status))
|
|
191
|
+
.sort((a, b) => Number(b.updated_at || b.created_at || 0) - Number(a.updated_at || a.created_at || 0))[0];
|
|
192
|
+
if (latestClosed && typeof deps.taskDb.reopenTask === 'function') {
|
|
193
|
+
const reopened = deps.taskDb.reopenTask(db, {
|
|
194
|
+
id: latestClosed.id,
|
|
195
|
+
actor: 'self-drive',
|
|
196
|
+
reason: `mission blocker fired again: ${blockerClass}`,
|
|
197
|
+
metadata: blockerMetadata,
|
|
198
|
+
});
|
|
199
|
+
if (reopened.reopened) {
|
|
200
|
+
const refreshed = deps.taskDb.withTaskDisplayRefs(deps.taskDb.listTasks(db, { workspaceRoot: scopedRoot }));
|
|
201
|
+
task = refreshed.find(row => row.id === latestClosed.id) || reopened.row;
|
|
202
|
+
if (task && typeof deps.taskDb.noteTask === 'function') {
|
|
203
|
+
deps.taskDb.noteTask(db, {
|
|
204
|
+
id: task.id,
|
|
205
|
+
actor: 'self-drive',
|
|
206
|
+
content: `Mission blocker reopened.\nMission objective: ${objective}\nStop reason: ${fullStopReason}\nEvidence: ${evidence}`,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
projectTasks(deps.taskDb, db, scopedRoot);
|
|
210
|
+
appendEvent?.('mission_blocker_task_reopened', {
|
|
211
|
+
task_id: taskRef(task),
|
|
212
|
+
stop_reason: fullStopReason,
|
|
213
|
+
blocker_class: blockerClass,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
178
218
|
if (!task) {
|
|
179
|
-
const evidence = String(mission.receipt_path || mission.last_tick_reason || stopReason || blockerClass).trim();
|
|
180
|
-
const objective = String(mission.objective || mission.id).trim();
|
|
181
|
-
const fullStopReason = String(stopReason || blockerClass).trim();
|
|
182
219
|
const title = missionBlockerTitle(objective, fullStopReason);
|
|
183
220
|
const created = deps.taskDb.addTask(db, {
|
|
184
221
|
title,
|
|
185
222
|
tag: 'mission-blocker',
|
|
186
223
|
workspaceRoot: scopedRoot,
|
|
187
|
-
sourceKey: `mission-blocker:${mission.id}:${blockerClass}
|
|
188
|
-
metadata:
|
|
189
|
-
mission_id: mission.id,
|
|
190
|
-
mission_objective: objective,
|
|
191
|
-
mission_blocker_class: blockerClass,
|
|
192
|
-
stop_reason: fullStopReason,
|
|
193
|
-
evidence,
|
|
194
|
-
},
|
|
224
|
+
sourceKey: `mission-blocker:${mission.id}:${blockerClass}`,
|
|
225
|
+
metadata: blockerMetadata,
|
|
195
226
|
});
|
|
196
227
|
const refreshed = deps.taskDb.withTaskDisplayRefs(deps.taskDb.listTasks(db, { workspaceRoot: scopedRoot }));
|
|
197
228
|
task = refreshed.find((row) => row.id === created.id) || deps.taskDb.getTask(db, created.id);
|
|
@@ -255,4 +286,4 @@ function handleMissionBlocker({ mission, stopReason, workspaceRoot, appendEvent
|
|
|
255
286
|
}
|
|
256
287
|
}
|
|
257
288
|
|
|
258
|
-
module.exports = { handleMissionBlocker };
|
|
289
|
+
module.exports = { handleMissionBlocker, reasonClass, HUMAN_BLOCKING_REASONS };
|
package/lib/state-detection.js
CHANGED
|
@@ -406,11 +406,5 @@ function loadContext(workspaceDir) {
|
|
|
406
406
|
module.exports = {
|
|
407
407
|
detectWorkspaceState,
|
|
408
408
|
loadContext,
|
|
409
|
-
getTodayInboxItems,
|
|
410
|
-
getTodayLogPath,
|
|
411
|
-
getBacklogTasks,
|
|
412
|
-
getInProgressTasks,
|
|
413
|
-
getCompletedTasks,
|
|
414
|
-
getTaskCounts,
|
|
415
409
|
getTaskGlance
|
|
416
410
|
};
|
package/lib/task-db.js
CHANGED
|
@@ -28,6 +28,8 @@ const os = require('os');
|
|
|
28
28
|
const crypto = require('crypto');
|
|
29
29
|
const { DatabaseSync } = require('node:sqlite');
|
|
30
30
|
const reviewIntegrity = require('./review-integrity');
|
|
31
|
+
const { isDecisionTask } = require('./task-decision');
|
|
32
|
+
const { parseVerifyCommand } = require('./auto-accept-certified');
|
|
31
33
|
|
|
32
34
|
const DEFAULT_DB_PATH = path.join(os.homedir(), '.atris', 'tasks.db');
|
|
33
35
|
const TASK_EPISODES_FILE = path.join('.atris', 'state', 'task_episodes.jsonl');
|
|
@@ -38,6 +40,8 @@ const PROJECTION_EVENT_LIMIT = 8;
|
|
|
38
40
|
const PROJECTION_MESSAGE_LIMIT = 6;
|
|
39
41
|
const PROJECTION_PAYLOAD_TEXT_LIMIT = 1000;
|
|
40
42
|
const AGENT_CERTIFICATION_REVIEW_PASSES = 2;
|
|
43
|
+
const OPEN_TASK_STATUSES = new Set(['open', 'claimed', 'review']);
|
|
44
|
+
const TERMINAL_MISSION_STATUSES = new Set(['complete', 'stopped']);
|
|
41
45
|
const TASK_REF_GENERIC_TOKENS = new Set(['app', 'atris', 'atrisos', 'project', 'repo', 'workspace']);
|
|
42
46
|
const TASK_PLAN_TAGS = new Set([
|
|
43
47
|
'agent',
|
|
@@ -380,6 +384,16 @@ function taskDisplayRefMap(rows) {
|
|
|
380
384
|
return map;
|
|
381
385
|
}
|
|
382
386
|
|
|
387
|
+
function taskCreationMetadata(metadata) {
|
|
388
|
+
const next = metadata && typeof metadata === 'object' ? { ...metadata } : {};
|
|
389
|
+
const verify = typeof next.verify === 'string' ? next.verify.trim() : '';
|
|
390
|
+
if (!verify || verify.toLowerCase() === 'git diff --check') {
|
|
391
|
+
next.verification_status = 'degraded';
|
|
392
|
+
next.verification_degraded_reason = verify ? 'diff_only_verify' : 'missing_verify';
|
|
393
|
+
}
|
|
394
|
+
return next;
|
|
395
|
+
}
|
|
396
|
+
|
|
383
397
|
function addTask(db, { title, tag, workspaceRoot: ws, sourceKey: sk, metadata, status, claimedBy }) {
|
|
384
398
|
if (!title || !String(title).trim()) throw new Error('title required');
|
|
385
399
|
const now = Date.now();
|
|
@@ -394,6 +408,7 @@ function addTask(db, { title, tag, workspaceRoot: ws, sourceKey: sk, metadata, s
|
|
|
394
408
|
).get(ws, sk);
|
|
395
409
|
if (existing) return { id: existing.id, inserted: false };
|
|
396
410
|
}
|
|
411
|
+
const taskMetadata = taskCreationMetadata(metadata);
|
|
397
412
|
withBusyRetry(() => db.prepare(`
|
|
398
413
|
INSERT INTO tasks (id, title, status, tag, workspace_root, source_key,
|
|
399
414
|
claimed_by, claimed_at, created_at, updated_at, metadata)
|
|
@@ -409,7 +424,7 @@ function addTask(db, { title, tag, workspaceRoot: ws, sourceKey: sk, metadata, s
|
|
|
409
424
|
claimedAt,
|
|
410
425
|
now,
|
|
411
426
|
now,
|
|
412
|
-
|
|
427
|
+
JSON.stringify(taskMetadata),
|
|
413
428
|
));
|
|
414
429
|
appendTaskEvent(db, {
|
|
415
430
|
taskId: id,
|
|
@@ -421,7 +436,7 @@ function addTask(db, { title, tag, workspaceRoot: ws, sourceKey: sk, metadata, s
|
|
|
421
436
|
tag: tag || null,
|
|
422
437
|
status: taskStatus,
|
|
423
438
|
source_key: sk || null,
|
|
424
|
-
metadata:
|
|
439
|
+
metadata: taskMetadata,
|
|
425
440
|
},
|
|
426
441
|
});
|
|
427
442
|
return { id, inserted: true };
|
|
@@ -531,6 +546,72 @@ function releaseTask(db, { id, actor }) {
|
|
|
531
546
|
return { released: true, row: updated, event };
|
|
532
547
|
}
|
|
533
548
|
|
|
549
|
+
function reopenTask(db, { id, actor, reason, metadata: metadataPatch } = {}) {
|
|
550
|
+
if (!id) throw new Error('id required');
|
|
551
|
+
const row = getTask(db, id);
|
|
552
|
+
if (!row) return { reopened: false, reason: 'not_found' };
|
|
553
|
+
if (OPEN_TASK_STATUSES.has(row.status)) return { reopened: false, reason: `already_${row.status}`, row };
|
|
554
|
+
if (!['done', 'failed', 'archived'].includes(row.status)) {
|
|
555
|
+
return { reopened: false, reason: `not_reopenable_${row.status}` };
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const now = Math.max(Date.now(), Number(row.updated_at || 0) + 1);
|
|
559
|
+
const actorText = actor || process.env.ATRIS_AGENT_ID || process.env.USER || null;
|
|
560
|
+
const reasonText = String(reason || 'reopened').trim();
|
|
561
|
+
const metadata = {
|
|
562
|
+
...(row.metadata && typeof row.metadata === 'object' ? row.metadata : {}),
|
|
563
|
+
...(metadataPatch && typeof metadataPatch === 'object' ? metadataPatch : {}),
|
|
564
|
+
reopened_at: new Date(now).toISOString(),
|
|
565
|
+
reopened_by: actorText,
|
|
566
|
+
reopened_from: row.status,
|
|
567
|
+
reopen_reason: reasonText,
|
|
568
|
+
};
|
|
569
|
+
for (const key of [
|
|
570
|
+
'accepted_at',
|
|
571
|
+
'accepted_by',
|
|
572
|
+
'agent_certification_policy',
|
|
573
|
+
'agent_certified',
|
|
574
|
+
'agent_certified_at',
|
|
575
|
+
'agent_certified_by',
|
|
576
|
+
'agent_review_actors',
|
|
577
|
+
'agent_review_pass_count',
|
|
578
|
+
'agent_reviewed_at',
|
|
579
|
+
'agent_reviewed_by',
|
|
580
|
+
'approval_status',
|
|
581
|
+
'archived_at',
|
|
582
|
+
'archived_by',
|
|
583
|
+
'archived_from',
|
|
584
|
+
'archived_reason',
|
|
585
|
+
'independent_review_by',
|
|
586
|
+
'latest_agent_lesson',
|
|
587
|
+
'latest_agent_next_task',
|
|
588
|
+
'latest_agent_proof',
|
|
589
|
+
]) delete metadata[key];
|
|
590
|
+
|
|
591
|
+
const result = withBusyRetry(() => db.prepare(`
|
|
592
|
+
UPDATE tasks
|
|
593
|
+
SET status = 'open',
|
|
594
|
+
claimed_by = NULL,
|
|
595
|
+
claimed_at = NULL,
|
|
596
|
+
done_at = NULL,
|
|
597
|
+
updated_at = ?,
|
|
598
|
+
metadata = ?
|
|
599
|
+
WHERE id = ?
|
|
600
|
+
AND status = ?
|
|
601
|
+
AND updated_at = ?
|
|
602
|
+
`).run(now, JSON.stringify(metadata), id, row.status, row.updated_at));
|
|
603
|
+
if (result.changes !== 1) return { reopened: false, reason: 'stale_task_state' };
|
|
604
|
+
const updated = getTask(db, id);
|
|
605
|
+
const event = appendTaskEvent(db, {
|
|
606
|
+
taskId: id,
|
|
607
|
+
workspaceRoot: updated.workspace_root,
|
|
608
|
+
actor: actorText,
|
|
609
|
+
eventType: 'reopened',
|
|
610
|
+
payload: { previous_status: row.status, reason: reasonText },
|
|
611
|
+
});
|
|
612
|
+
return { reopened: true, event, row: updated };
|
|
613
|
+
}
|
|
614
|
+
|
|
534
615
|
function doneTask(db, { id, status, actor, allowReview = false, action, proof, autoAccepted = false } = {}) {
|
|
535
616
|
if (!id) throw new Error('id required');
|
|
536
617
|
const final = status || 'done';
|
|
@@ -570,6 +651,39 @@ function doneTask(db, { id, status, actor, allowReview = false, action, proof, a
|
|
|
570
651
|
return { updated: false };
|
|
571
652
|
}
|
|
572
653
|
|
|
654
|
+
function reapMissionBlockerTasks(db, { workspaceRoot: ws, missions = [], actor } = {}) {
|
|
655
|
+
const missionById = new Map((Array.isArray(missions) ? missions : [])
|
|
656
|
+
.filter(mission => mission && mission.id)
|
|
657
|
+
.map(mission => [String(mission.id), mission]));
|
|
658
|
+
const candidates = listTasks(db, { workspaceRoot: ws || null, limit: null })
|
|
659
|
+
.filter(row => OPEN_TASK_STATUSES.has(row.status))
|
|
660
|
+
.filter(row => row.tag === 'mission-blocker')
|
|
661
|
+
.filter(row => row.metadata && row.metadata.mission_id && row.metadata.mission_blocker_class)
|
|
662
|
+
.map(row => ({ row, mission: missionById.get(String(row.metadata.mission_id)) }))
|
|
663
|
+
.filter(({ mission }) => mission && TERMINAL_MISSION_STATUSES.has(String(mission.status || '').toLowerCase()));
|
|
664
|
+
const closed = [];
|
|
665
|
+
for (const { row, mission } of candidates) {
|
|
666
|
+
const missionStatus = String(mission.status).toLowerCase();
|
|
667
|
+
const reason = `mission ${mission.id} is ${missionStatus}`;
|
|
668
|
+
const result = archiveTask(db, {
|
|
669
|
+
id: row.id,
|
|
670
|
+
actor,
|
|
671
|
+
reason,
|
|
672
|
+
});
|
|
673
|
+
if (!result.archived) continue;
|
|
674
|
+
closed.push({
|
|
675
|
+
task_id: row.id,
|
|
676
|
+
title: row.title,
|
|
677
|
+
previous_status: row.status,
|
|
678
|
+
mission_id: mission.id,
|
|
679
|
+
mission_status: missionStatus,
|
|
680
|
+
blocker_class: row.metadata.mission_blocker_class,
|
|
681
|
+
reason,
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
return { closed, count: closed.length };
|
|
685
|
+
}
|
|
686
|
+
|
|
573
687
|
// Distinct terminal status for housekeeping sweeps (duplicate loop-ticks,
|
|
574
688
|
// off-roadmap backlog resets, synthetic-test cleanup). Never conflate this
|
|
575
689
|
// with `failed` — 'failed' means the work itself did not succeed, 'archived'
|
|
@@ -1556,6 +1670,21 @@ function reviewTask(db, { id, actor, reward, lesson, nextTask, proof, verify, ca
|
|
|
1556
1670
|
metadata.agent_reviewed_by = reviewer;
|
|
1557
1671
|
if (proofText) metadata.latest_agent_proof = proofText;
|
|
1558
1672
|
if (verifyText) {
|
|
1673
|
+
// Gate at the write path, not just cmdReview: a verify the strict parser
|
|
1674
|
+
// rejects can never be run by the hourly autoland recheck, so storing it
|
|
1675
|
+
// silently parks the task on a human forever (observed 2026-07-31: five
|
|
1676
|
+
// certified tasks stalled for days on unrunnable verifies stored by
|
|
1677
|
+
// fleet lanes that bypass the command-layer gate).
|
|
1678
|
+
const parsedVerify = parseVerifyCommand(verifyText);
|
|
1679
|
+
if (!parsedVerify.ok) {
|
|
1680
|
+
const error = new Error(
|
|
1681
|
+
`verify command is not runnable by the hourly recheck (${parsedVerify.reason || 'verify_command_not_allowed'}); `
|
|
1682
|
+
+ 'use an allowlisted shape: cd backend && ../venv/bin/python -m pytest <file> -q, '
|
|
1683
|
+
+ 'test -s <artifact>, npm test, node --test <file>, tsc, or git diff --check',
|
|
1684
|
+
);
|
|
1685
|
+
error.reason = parsedVerify.reason || 'verify_command_not_allowed';
|
|
1686
|
+
throw error;
|
|
1687
|
+
}
|
|
1559
1688
|
metadata.verify = verifyText;
|
|
1560
1689
|
metadata.latest_agent_verify = verifyText;
|
|
1561
1690
|
}
|
|
@@ -1997,8 +2126,11 @@ function appendSection(lines, name, rows) {
|
|
|
1997
2126
|
const tag = [...new Set(tags.filter(Boolean).map(value => String(value).trim()).filter(Boolean))]
|
|
1998
2127
|
.map(value => ` [${value}]`)
|
|
1999
2128
|
.join('');
|
|
2129
|
+
// Decision holds live in metadata.tags (or the primary tag). Surface them
|
|
2130
|
+
// so a human judgment row never blends into ordinary work on the board.
|
|
2131
|
+
const decision = isDecisionTask(row) ? ' [decision]' : '';
|
|
2000
2132
|
const displayRef = meta.todo_id || row.display_id || row.id;
|
|
2001
|
-
lines.push(`- **[${displayRef}]** ${row.title}${tag}`);
|
|
2133
|
+
lines.push(`- **[${displayRef}]** ${row.title}${tag}${decision}`);
|
|
2002
2134
|
if (row.claimed_by && row.status === 'claimed') lines.push(` **Claimed by:** ${row.claimed_by}`);
|
|
2003
2135
|
if (meta.verify) lines.push(` **Verify:** ${meta.verify}`);
|
|
2004
2136
|
}
|
|
@@ -2046,12 +2178,13 @@ module.exports = {
|
|
|
2046
2178
|
listTasks,
|
|
2047
2179
|
claimTask,
|
|
2048
2180
|
releaseTask,
|
|
2181
|
+
reopenTask,
|
|
2049
2182
|
backlogTask,
|
|
2050
2183
|
clearPlanTasks,
|
|
2051
2184
|
doneTask,
|
|
2185
|
+
reapMissionBlockerTasks,
|
|
2052
2186
|
archiveTask,
|
|
2053
2187
|
relabelArchivedTasks,
|
|
2054
|
-
isJune10BacklogResetMarker,
|
|
2055
2188
|
readyTask,
|
|
2056
2189
|
setTaskResult,
|
|
2057
2190
|
reviseTask,
|
|
@@ -2065,10 +2198,7 @@ module.exports = {
|
|
|
2065
2198
|
taskProjection,
|
|
2066
2199
|
renderTodoMarkdown,
|
|
2067
2200
|
normalizeTaskRef,
|
|
2068
|
-
shortestUniqueTaskRef,
|
|
2069
2201
|
taskDisplayRefMap,
|
|
2070
2202
|
withTaskDisplayRefs,
|
|
2071
2203
|
newId,
|
|
2072
|
-
// Test surface
|
|
2073
|
-
_SCHEMA: SCHEMA,
|
|
2074
2204
|
};
|