atris 3.37.1 → 3.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/atris/PERSONA.md +3 -7
- package/atris.md +10 -2
- package/ax +158 -33
- package/bin/atris.js +49 -2
- package/commands/activate.js +16 -3
- package/commands/agent-spawn.js +0 -7
- package/commands/autoland.js +92 -14
- package/commands/autopilot-front.js +0 -4
- package/commands/autopilot.js +0 -12
- package/commands/brain.js +0 -7
- package/commands/brief.js +0 -2
- package/commands/business-sync.js +0 -1
- package/commands/business.js +107 -12
- package/commands/clean.js +31 -2
- package/commands/close.js +46 -11
- package/commands/cloud.js +0 -1
- package/commands/codex-goal.js +0 -2
- package/commands/computer.js +6 -5
- package/commands/console.js +46 -9
- package/commands/deck.js +29 -8
- package/commands/dream.js +0 -7
- package/commands/drill.js +0 -1
- package/commands/engine.js +10 -18
- package/commands/experiments.js +0 -4
- package/commands/fleet-report.js +12 -4
- package/commands/game.js +261 -0
- package/commands/gm.js +10 -2
- package/commands/goal.js +0 -6
- package/commands/harvest.js +0 -1
- package/commands/improve.js +29 -8
- package/commands/init.js +18 -34
- package/commands/integrations.js +0 -7
- package/commands/land.js +71 -8
- package/commands/launchpad.js +0 -2
- package/commands/lesson.js +17 -7
- package/commands/loop-front.js +0 -2
- package/commands/loops.js +6 -5
- package/commands/meet.js +0 -2
- package/commands/member.js +68 -3
- package/commands/mission.js +359 -102
- package/commands/next.js +0 -1
- package/commands/now.js +0 -12
- package/commands/one-lap.js +8 -8
- package/commands/orb.js +0 -4
- package/commands/pack-craft.js +0 -1
- package/commands/pack.js +670 -77
- package/commands/play.js +0 -2
- package/commands/probe.js +10 -4
- package/commands/pulse.js +73 -48
- package/commands/push.js +0 -4
- package/commands/radar.js +14 -11
- package/commands/recover.js +340 -0
- package/commands/report.js +0 -1
- package/commands/router.js +319 -0
- package/commands/run-front.js +0 -2
- package/commands/search.js +0 -3
- package/commands/skill.js +7 -2
- package/commands/slop.js +225 -7
- package/commands/status.js +6 -0
- package/commands/stream.js +0 -2
- package/commands/study.js +0 -3
- package/commands/sync-checkout.js +51 -0
- package/commands/sync.js +11 -40
- package/commands/task.js +120 -38
- package/commands/teach.js +86 -0
- package/commands/unknowns.js +0 -10
- package/commands/update.js +0 -1
- package/commands/visualize.js +0 -5
- package/commands/watch.js +0 -6
- package/commands/wiki.js +41 -11
- package/commands/wish.js +0 -1
- package/commands/worktree.js +87 -14
- package/commands/xp.js +1 -6
- package/commands/youtube.js +0 -2
- package/lib/accept-verify-gate.js +122 -0
- package/lib/activity-stream.js +0 -2
- package/lib/auto-accept-certified.js +145 -2
- package/lib/autoland.js +0 -3
- package/lib/ax-auto-lane.js +68 -7
- package/lib/bench/context.js +0 -2
- package/lib/bench/report.js +0 -1
- package/lib/bench/runner.js +0 -11
- package/lib/brief-ledger.js +0 -6
- package/lib/business-simulate.js +382 -0
- package/lib/chat-log-scan.js +0 -5
- package/lib/checkout-sync.js +52 -0
- package/lib/claude-boot-block.js +48 -0
- package/lib/cloud-mission.js +0 -6
- package/lib/codex-flight.js +0 -1
- package/lib/conductor-artifacts.js +25 -0
- package/lib/context-gatherer.js +0 -1
- package/lib/deck-compose.js +0 -1
- package/lib/deck-history.js +0 -1
- package/lib/deck-layout.js +0 -5
- package/lib/deck-review.js +0 -7
- package/lib/deck-schema.js +0 -1
- package/lib/default-verifier.js +0 -2
- package/lib/engine-registry.js +16 -8
- package/lib/experiments/daily.js +0 -6
- package/lib/fleet.js +210 -27
- package/lib/functional-owner.js +0 -3
- package/lib/inspect-fields.js +0 -1
- package/lib/known-commands.js +2 -2
- package/lib/learnings.js +0 -2
- package/lib/loop-doctor.js +11 -17
- package/lib/manifest.js +0 -2
- package/lib/member-alive.js +0 -1
- package/lib/member-switches.js +0 -5
- package/lib/mission-artifact.js +0 -1
- package/lib/mission-protected-lane.js +293 -0
- package/lib/mission-room.js +0 -2
- package/lib/mission-runtime-loop.js +0 -1
- package/lib/next-moves.js +0 -18
- package/lib/official-cli-integration.js +0 -4
- package/lib/orb-context.js +6 -3
- package/lib/orb-scorecard.js +0 -1
- package/lib/outbound-send-gate.js +0 -6
- package/lib/permission-grants.js +0 -6
- package/lib/policy-lessons.js +0 -2
- package/lib/pulse.js +68 -15
- package/lib/review-integrity.js +0 -2
- package/lib/router-brain.js +0 -2
- package/lib/runner-command.js +0 -4
- package/lib/runtime-bootstrap.js +0 -1
- package/lib/scorecard.js +0 -1
- package/lib/self-drive.js +1 -1
- package/lib/state-detection.js +0 -6
- package/lib/task-db.js +0 -4
- package/lib/team-presence.js +0 -1
- package/lib/voice-gate.js +63 -2
- package/lib/wiki.js +152 -9
- package/lib/wish-audit.js +20 -6
- package/lib/wish-delegate.js +26 -20
- package/lib/wish-stats.js +0 -5
- package/lib/wish-store.js +0 -1
- package/lib/workspace-safety.js +0 -2
- package/lib/zip.js +0 -1
- package/package.json +1 -1
package/commands/autoland.js
CHANGED
|
@@ -470,9 +470,12 @@ function showStatus(root, args) {
|
|
|
470
470
|
const blocked = results.filter((r) => r.action !== 'would_accept');
|
|
471
471
|
const tasks = readProjection(root);
|
|
472
472
|
const waiting = autoland.waitingOnHuman(tasks);
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
473
|
+
// Ask the crontab, not the policy file. The cached flag is written once at
|
|
474
|
+
// `autoland on` time, so it reports "unknown" for a cron that has been
|
|
475
|
+
// running hourly for weeks, and would keep reporting "running" for one
|
|
476
|
+
// someone deleted. A loop that cannot confirm its own liveness from
|
|
477
|
+
// evidence cannot be trusted to say it is healthy.
|
|
478
|
+
const heartbeatInstalled = heartbeatLiveness(root, policy);
|
|
476
479
|
|
|
477
480
|
if (json) {
|
|
478
481
|
console.log(JSON.stringify({
|
|
@@ -498,12 +501,7 @@ function showStatus(root, args) {
|
|
|
498
501
|
: 'off - everything waits for you';
|
|
499
502
|
console.log(` policy: ${policyText}`);
|
|
500
503
|
if (enabled && acceptAll) console.log(' bar: everything lands except the protected lanes (money, deploys, security, customer, outward)');
|
|
501
|
-
|
|
502
|
-
? 'running hourly'
|
|
503
|
-
: heartbeatInstalled === false
|
|
504
|
-
? 'not installed'
|
|
505
|
-
: 'unknown - run atris autoland on to check and repair';
|
|
506
|
-
console.log(` heartbeat: ${heartbeatText}`);
|
|
504
|
+
console.log(` heartbeat: ${heartbeatStatusText(root, policy)}`);
|
|
507
505
|
if (policy && policy.imessage_to) console.log(` daily message: ${policy.imessage_to} at ${policy.digest_hour ?? autoland.DEFAULT_DIGEST_HOUR}:00`);
|
|
508
506
|
const reapTrouble = autoland.readState(root).last_reap_error;
|
|
509
507
|
if (reapTrouble) console.log(` cleanup trouble: landing sweep failed on ${reapTrouble.date} (${reapTrouble.error}) - run: atris land --reap`);
|
|
@@ -696,6 +694,68 @@ function persistTickReceipt(root, receipt) {
|
|
|
696
694
|
return receipt.receipt_path;
|
|
697
695
|
}
|
|
698
696
|
|
|
697
|
+
// A tick that scans a backlog, lands none of it, and prints only zeros is
|
|
698
|
+
// indistinguishable from a tick with no work. Say the standing numbers out
|
|
699
|
+
// loud, and call out the two states that mean the loop is wedged rather than
|
|
700
|
+
// idle: work blocked behind a check the harness could not execute, and work
|
|
701
|
+
// that passed every gate and still did not land.
|
|
702
|
+
// Installed and alive are different claims, and only one of them is worth
|
|
703
|
+
// printing. Deliberately does NOT shell out to crontab: `autoland status` is
|
|
704
|
+
// read-only and must stay fast (a stalled crontab would block it for the full
|
|
705
|
+
// 10s read timeout — see 'autoland status never probes a stalled crontab').
|
|
706
|
+
// Tick receipts are local file reads and are stronger evidence anyway: a
|
|
707
|
+
// crontab line proves configuration, a recent receipt proves the loop ran.
|
|
708
|
+
function heartbeatLiveness(root, policy) {
|
|
709
|
+
return typeof policy?.heartbeat_installed === 'boolean' ? policy.heartbeat_installed : null;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function lastTickAgeHours(root) {
|
|
713
|
+
try {
|
|
714
|
+
const runsDir = path.join(root, 'atris', 'runs');
|
|
715
|
+
const newest = fs.readdirSync(runsDir)
|
|
716
|
+
.filter((f) => f.startsWith('autoland-tick-') && f.endsWith('.json'))
|
|
717
|
+
.sort()
|
|
718
|
+
.pop();
|
|
719
|
+
if (!newest) return null;
|
|
720
|
+
const stamp = fs.statSync(path.join(runsDir, newest)).mtimeMs;
|
|
721
|
+
return (Date.now() - stamp) / 3_600_000;
|
|
722
|
+
} catch {
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function heartbeatStatusText(root, policy) {
|
|
728
|
+
const ageHours = lastTickAgeHours(root);
|
|
729
|
+
const ageText = ageHours === null
|
|
730
|
+
? null
|
|
731
|
+
: ageHours < 1 ? 'under an hour' : `${Math.floor(ageHours)}h`;
|
|
732
|
+
// Evidence first: a receipt from the last couple of hours proves the loop is
|
|
733
|
+
// alive no matter what the policy file remembers. The hourly cron makes two
|
|
734
|
+
// missed hours an outage rather than jitter — and a heartbeat that has gone
|
|
735
|
+
// quiet is the single most useful thing this line can say, because that is
|
|
736
|
+
// the state nobody notices.
|
|
737
|
+
if (ageHours !== null && ageHours <= 2) return `running hourly (last tick ${ageText} ago)`;
|
|
738
|
+
if (ageHours !== null) return `SILENT - last tick ${ageText} ago; run atris autoland tick`;
|
|
739
|
+
const installed = heartbeatLiveness(root, policy);
|
|
740
|
+
if (installed === false) return 'not installed - run atris autoland on';
|
|
741
|
+
if (installed === true) return 'installed, but no tick has ever run - run atris autoland tick';
|
|
742
|
+
return 'unknown - run atris autoland on to check and repair';
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function stuckBacklogNote(receipt) {
|
|
746
|
+
const parts = [];
|
|
747
|
+
const scanned = Number(receipt.scanned || 0);
|
|
748
|
+
const certified = Number(receipt.certified || 0);
|
|
749
|
+
if (scanned > 0 && receipt.landed.length === 0) {
|
|
750
|
+
parts.push(`${certified}/${scanned} certified in review, none landed`);
|
|
751
|
+
}
|
|
752
|
+
const unrunnable = Array.isArray(receipt.unrunnable_verifiers) ? receipt.unrunnable_verifiers.length : 0;
|
|
753
|
+
if (unrunnable > 0) {
|
|
754
|
+
parts.push(`${unrunnable} blocked by a check this machine could not run`);
|
|
755
|
+
}
|
|
756
|
+
return parts.length ? `, ${parts.join(', ')}` : '';
|
|
757
|
+
}
|
|
758
|
+
|
|
699
759
|
function digestTickStatus(receipt) {
|
|
700
760
|
if (receipt.digest_sent) return 'sent';
|
|
701
761
|
if (receipt.digest_due) return 'saved';
|
|
@@ -734,6 +794,23 @@ function runTickBody(root, { json, policy, receipt }) {
|
|
|
734
794
|
.filter((r) => r.action === 'skipped' && r.proof_state === 'suite_green_citation_required')
|
|
735
795
|
.map((r) => ({ ref: r.ref, reason: r.reason }));
|
|
736
796
|
if (citationBlocks.length) receipt.citation_blocks = citationBlocks;
|
|
797
|
+
// A verifier the machine cannot execute is a broken harness, not a verdict
|
|
798
|
+
// on the diff. It must never sit quietly in the skipped pile: surface it
|
|
799
|
+
// as an alarm so a wedged loop is visible on the first tick, not the 480th.
|
|
800
|
+
// Not filtered by action: an unrunnable check surfaces as `skipped` from
|
|
801
|
+
// the eligibility gate and as `revised`/`revise_failed` from the landing
|
|
802
|
+
// gate. The reason is the signal, not the lane it came through.
|
|
803
|
+
const unrunnable = results
|
|
804
|
+
.filter((r) => r && r.action !== 'accepted' && r.reason === 'verify_unrunnable')
|
|
805
|
+
.map((r) => ({
|
|
806
|
+
ref: r.ref,
|
|
807
|
+
cause: r.unrunnable_cause || 'unknown',
|
|
808
|
+
command: r.command || r.verify || null,
|
|
809
|
+
}));
|
|
810
|
+
if (unrunnable.length) {
|
|
811
|
+
receipt.unrunnable_verifiers = unrunnable;
|
|
812
|
+
receipt.alarms = Number(receipt.alarms || 0) + unrunnable.length;
|
|
813
|
+
}
|
|
737
814
|
receipt.certified = parsed.certified ?? null;
|
|
738
815
|
receipt.scanned = parsed.scanned ?? null;
|
|
739
816
|
receipt.revised = parsed.revised ?? null;
|
|
@@ -956,7 +1033,12 @@ function runTickBody(root, { json, policy, receipt }) {
|
|
|
956
1033
|
const heldNote = receipt.missions_held ? `, held ${receipt.missions_held} human-blocked mission${receipt.missions_held === 1 ? '' : 's'}` : '';
|
|
957
1034
|
const wishNote = `, ${wishSweepSummaryLine(receipt.wish_dispatch, receipt.wish_dispatch_error)}`;
|
|
958
1035
|
const receiptNote = receipt.receipt_path ? `, receipt ${receipt.receipt_path}` : '';
|
|
959
|
-
|
|
1036
|
+
// Deltas alone ("0 certified, 0 landed") are true and useless: they read
|
|
1037
|
+
// as "nothing to do" whether the queue is empty or 19-deep and wedged.
|
|
1038
|
+
// Always print the standing backlog, and name work that cleared every
|
|
1039
|
+
// gate and still did not land — that is the shape of a stuck loop.
|
|
1040
|
+
const stuckNote = stuckBacklogNote(receipt);
|
|
1041
|
+
const summary = `autoland tick: ${receipt.reviews_certified ?? 0} reviews certified, ${receipt.landed.length} landed${receipt.landed.length ? ` (${receipt.landed.join(', ')})` : ''}, ${receipt.alarms} alarms${stuckNote}, digest ${digestTickStatus(receipt)}${reapNote}${janitorNote}${heldNote}${wishNote}${receiptNote}`;
|
|
960
1042
|
console.log(gateForHuman(summary).text);
|
|
961
1043
|
for (const fulfilled of receipt.wish_dispatch?.fulfilled_results || []) {
|
|
962
1044
|
if (fulfilled.review_ask) console.log(fulfilled.review_ask);
|
|
@@ -1014,18 +1096,14 @@ function autolandCommand(args = []) {
|
|
|
1014
1096
|
|
|
1015
1097
|
module.exports = {
|
|
1016
1098
|
autolandCommand,
|
|
1017
|
-
attachedTasksForMission,
|
|
1018
1099
|
digestNextMoves,
|
|
1019
1100
|
digestStoryRows,
|
|
1020
1101
|
digestTickStatus,
|
|
1021
1102
|
explainResult,
|
|
1022
|
-
missionReadyForClosedTaskVerify,
|
|
1023
1103
|
operatorReady,
|
|
1024
1104
|
readAcceptedTaskHistory,
|
|
1025
1105
|
hasAgentJargon,
|
|
1026
1106
|
runTickBody,
|
|
1027
|
-
persistTickReceipt,
|
|
1028
1107
|
sweepLanding,
|
|
1029
1108
|
verifyClosedTaskMissions,
|
|
1030
|
-
wishSweepSummaryLine,
|
|
1031
1109
|
};
|
package/commands/autopilot.js
CHANGED
|
@@ -3825,7 +3825,6 @@ async function autopilotFromTodo(options = {}) {
|
|
|
3825
3825
|
}
|
|
3826
3826
|
|
|
3827
3827
|
module.exports = {
|
|
3828
|
-
appendTickSummary,
|
|
3829
3828
|
snapshotRepoHeads,
|
|
3830
3829
|
diffAdvancedRepoHeads,
|
|
3831
3830
|
reconcileTimedOutTick,
|
|
@@ -3833,10 +3832,7 @@ module.exports = {
|
|
|
3833
3832
|
findCompletionReceipt,
|
|
3834
3833
|
isDoPhaseTimeoutMessage,
|
|
3835
3834
|
validateVerifyCommandShape,
|
|
3836
|
-
askHuman,
|
|
3837
|
-
askModel,
|
|
3838
3835
|
autopilotAtris,
|
|
3839
|
-
autopilotFromTodo,
|
|
3840
3836
|
buildPrompt,
|
|
3841
3837
|
pendingExperimentTargets,
|
|
3842
3838
|
isLessonResolved,
|
|
@@ -3847,7 +3843,6 @@ module.exports = {
|
|
|
3847
3843
|
parseLessons,
|
|
3848
3844
|
pickUnresolvedFailLesson,
|
|
3849
3845
|
runLessonDetector,
|
|
3850
|
-
isStillTrue,
|
|
3851
3846
|
getTaskAgeDays,
|
|
3852
3847
|
getIdleTickCount,
|
|
3853
3848
|
getRecentSignals,
|
|
@@ -3865,14 +3860,8 @@ module.exports = {
|
|
|
3865
3860
|
renderHumanSuggestion,
|
|
3866
3861
|
renderHumanTickIntro,
|
|
3867
3862
|
proposeCandidateHorizons,
|
|
3868
|
-
recordTickCommit,
|
|
3869
|
-
regressionCheck,
|
|
3870
|
-
repoMapAuditReportsClean,
|
|
3871
|
-
isCleanMapBrokenRefFailLesson,
|
|
3872
|
-
inlinePythonVerifyFailureNowPasses,
|
|
3873
3863
|
runPlanReview,
|
|
3874
3864
|
runTaskOnce,
|
|
3875
|
-
buildPlanReviewPrompt,
|
|
3876
3865
|
parseVerdict,
|
|
3877
3866
|
scoreEndgameCandidates,
|
|
3878
3867
|
suggestNextTask,
|
|
@@ -3882,7 +3871,6 @@ module.exports = {
|
|
|
3882
3871
|
isPhaseKillError,
|
|
3883
3872
|
execPhaseCommandSync,
|
|
3884
3873
|
executePhaseDetailed,
|
|
3885
|
-
autopilotGlassLogDir,
|
|
3886
3874
|
buildAutopilotGlassLog,
|
|
3887
3875
|
writeAutopilotGlassLog,
|
|
3888
3876
|
lessonSlug
|
package/commands/brain.js
CHANGED
|
@@ -1714,17 +1714,10 @@ function brainCommand(args = process.argv.slice(3)) {
|
|
|
1714
1714
|
module.exports = {
|
|
1715
1715
|
brainCommand,
|
|
1716
1716
|
collectState,
|
|
1717
|
-
prepareBrainState,
|
|
1718
1717
|
renderStatus,
|
|
1719
|
-
renderLedger,
|
|
1720
|
-
renderActivationCard,
|
|
1721
|
-
renderActivationGallery,
|
|
1722
1718
|
recordTaskEpisodeScorecards,
|
|
1723
|
-
recordFeedback,
|
|
1724
|
-
recordApproval,
|
|
1725
1719
|
verifyActivationCard,
|
|
1726
1720
|
verifyActivationGallery,
|
|
1727
|
-
verifyBrain,
|
|
1728
1721
|
parseContributionCard,
|
|
1729
1722
|
escapeRegExp,
|
|
1730
1723
|
countTodoItems,
|
package/commands/brief.js
CHANGED
package/commands/business.js
CHANGED
|
@@ -6,6 +6,12 @@ const { apiRequestJson } = require('../utils/api');
|
|
|
6
6
|
const { syncBusinessCanonical, ensureWorkspaceStateFiles, ensureBusinessRootAgentAdapters } = require('./sync');
|
|
7
7
|
const { ensureContextScaffold, writeWikiStatus, appendWikiLog } = require('../lib/wiki');
|
|
8
8
|
const { writeRuntimeReceipt } = require('../lib/runtime-bootstrap');
|
|
9
|
+
const {
|
|
10
|
+
SIMULATE_USAGE,
|
|
11
|
+
buildSimulationPlan,
|
|
12
|
+
parseSimulationArgs,
|
|
13
|
+
renderDryRun,
|
|
14
|
+
} = require('../lib/business-simulate');
|
|
9
15
|
|
|
10
16
|
function getBusinessConfigPath() {
|
|
11
17
|
const home = require('os').homedir();
|
|
@@ -2386,6 +2392,7 @@ async function createBusinessInternal(name, flags = [], mode = 'auto') {
|
|
|
2386
2392
|
options.here ||
|
|
2387
2393
|
Boolean(options.root)
|
|
2388
2394
|
);
|
|
2395
|
+
let workspaceRoot = null;
|
|
2389
2396
|
|
|
2390
2397
|
// Scaffold legacy local directory if in an atris project
|
|
2391
2398
|
const atrisDir = !shouldCreateCanonicalWorkspace ? findAtrisDir() : null;
|
|
@@ -2405,7 +2412,7 @@ async function createBusinessInternal(name, flags = [], mode = 'auto') {
|
|
|
2405
2412
|
console.log(` Local scaffold: ${bizDir}/`);
|
|
2406
2413
|
}
|
|
2407
2414
|
} else if (shouldCreateCanonicalWorkspace) {
|
|
2408
|
-
|
|
2415
|
+
workspaceRoot = resolveWorkspaceRoot(biz.slug, options);
|
|
2409
2416
|
const scaffold = createCanonicalBusinessWorkspace(workspaceRoot, {
|
|
2410
2417
|
business_id: biz.id,
|
|
2411
2418
|
workspace_id: biz.workspace_id,
|
|
@@ -2446,10 +2453,10 @@ async function createBusinessInternal(name, flags = [], mode = 'auto') {
|
|
|
2446
2453
|
console.log(` Agent: ${biz.agent_id || '(none)'}`);
|
|
2447
2454
|
console.log(` Dashboard: https://atris.ai/dashboard/gm/${biz.id}`);
|
|
2448
2455
|
if (shouldCreateCanonicalWorkspace) {
|
|
2449
|
-
const workspaceRoot = resolveWorkspaceRoot(biz.slug, options);
|
|
2450
2456
|
console.log(renderBusinessCreatedNextSteps(biz, workspaceRoot));
|
|
2451
2457
|
}
|
|
2452
2458
|
console.log('');
|
|
2459
|
+
return { business: biz, workspaceRoot };
|
|
2453
2460
|
}
|
|
2454
2461
|
|
|
2455
2462
|
async function createBusiness(name, ...flags) {
|
|
@@ -2875,6 +2882,96 @@ function findAtrisDir() {
|
|
|
2875
2882
|
return null;
|
|
2876
2883
|
}
|
|
2877
2884
|
|
|
2885
|
+
function printBusinessSimulateHelp() {
|
|
2886
|
+
console.log(SIMULATE_USAGE);
|
|
2887
|
+
console.log('');
|
|
2888
|
+
console.log('Creates a business workspace, four simulation members, their missions, and atris/ENDGAME.md.');
|
|
2889
|
+
console.log('Use --dry-run to preview every simulation file without cloud calls or writes.');
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
async function simulateBusiness(args = [], dependencies = {}) {
|
|
2893
|
+
const options = parseSimulationArgs(args);
|
|
2894
|
+
const createdAt = new Date().toISOString();
|
|
2895
|
+
const preview = buildSimulationPlan({
|
|
2896
|
+
idea: options.idea,
|
|
2897
|
+
businessName: options.businessName,
|
|
2898
|
+
roleNames: options.roleNames,
|
|
2899
|
+
createdAt,
|
|
2900
|
+
});
|
|
2901
|
+
|
|
2902
|
+
if (options.dryRun) {
|
|
2903
|
+
console.log(renderDryRun(preview));
|
|
2904
|
+
return preview;
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
const init = dependencies.initBusinessWorkspace || initBusinessWorkspace;
|
|
2908
|
+
const runMemberCommand = dependencies.memberCommand || require('./member').memberCommand;
|
|
2909
|
+
const initialized = await init(options.businessName);
|
|
2910
|
+
const business = initialized?.business || {};
|
|
2911
|
+
const workspaceRoot = initialized?.workspaceRoot
|
|
2912
|
+
|| resolveWorkspaceRoot(business.slug || options.businessSlug, {});
|
|
2913
|
+
const plan = buildSimulationPlan({
|
|
2914
|
+
idea: options.idea,
|
|
2915
|
+
businessName: business.name || options.businessName,
|
|
2916
|
+
businessSlug: business.slug || options.businessSlug,
|
|
2917
|
+
roleNames: options.roleNames,
|
|
2918
|
+
createdAt,
|
|
2919
|
+
workspacePath: workspaceRoot,
|
|
2920
|
+
});
|
|
2921
|
+
|
|
2922
|
+
const businessJson = path.join(workspaceRoot, '.atris', 'business.json');
|
|
2923
|
+
if (!fs.existsSync(businessJson)) {
|
|
2924
|
+
throw new Error(`Business init did not create the expected workspace: ${workspaceRoot}`);
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2927
|
+
const originalCwd = process.cwd();
|
|
2928
|
+
try {
|
|
2929
|
+
process.chdir(workspaceRoot);
|
|
2930
|
+
|
|
2931
|
+
const endgame = plan.files.find((file) => file.kind === 'endgame');
|
|
2932
|
+
fs.writeFileSync(path.join(workspaceRoot, endgame.path), endgame.content, 'utf8');
|
|
2933
|
+
|
|
2934
|
+
for (const role of plan.roles) {
|
|
2935
|
+
const memberDir = path.join(workspaceRoot, 'atris', 'team', role.name);
|
|
2936
|
+
const memberFile = path.join(memberDir, 'MEMBER.md');
|
|
2937
|
+
if (!fs.existsSync(memberFile)) {
|
|
2938
|
+
await runMemberCommand(
|
|
2939
|
+
'create',
|
|
2940
|
+
role.name,
|
|
2941
|
+
`--role=${role.title}`,
|
|
2942
|
+
`--description=${role.description}`,
|
|
2943
|
+
);
|
|
2944
|
+
} else {
|
|
2945
|
+
console.log(`Using existing team/${role.name}/ scaffold from business init.`);
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2948
|
+
for (const directory of ['skills', 'tools', 'context', 'logs']) {
|
|
2949
|
+
fs.mkdirSync(path.join(memberDir, directory), { recursive: true });
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2952
|
+
const roleFiles = plan.files.filter((file) => file.role?.name === role.name);
|
|
2953
|
+
for (const file of roleFiles) {
|
|
2954
|
+
fs.writeFileSync(path.join(workspaceRoot, file.path), file.content, 'utf8');
|
|
2955
|
+
}
|
|
2956
|
+
|
|
2957
|
+
await runMemberCommand('push', role.name);
|
|
2958
|
+
}
|
|
2959
|
+
} finally {
|
|
2960
|
+
process.chdir(originalCwd);
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
console.log('');
|
|
2964
|
+
console.log('business simulation ready');
|
|
2965
|
+
console.log(`workspace: ${workspaceRoot}`);
|
|
2966
|
+
console.log('roster:');
|
|
2967
|
+
for (const role of plan.roles) {
|
|
2968
|
+
console.log(` - ${role.name}: ${role.role}`);
|
|
2969
|
+
}
|
|
2970
|
+
console.log('next: open an agent session in the workspace and run the loop against atris/ENDGAME.md');
|
|
2971
|
+
console.log('');
|
|
2972
|
+
return plan;
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2878
2975
|
|
|
2879
2976
|
async function quickstart() {
|
|
2880
2977
|
console.log(`
|
|
@@ -2945,6 +3042,7 @@ function printBusinessHelp() {
|
|
|
2945
3042
|
console.log(' init <name> RECOMMENDED: create a business environment (cloud + local)');
|
|
2946
3043
|
console.log(' workspace <name> Alias for init');
|
|
2947
3044
|
console.log(' create <name> Cloud-only business record; add --workspace to also scaffold local');
|
|
3045
|
+
console.log(' simulate <idea> Create a business, four-role team, missions, and endgame loop');
|
|
2948
3046
|
console.log(' add <slug> Register an existing cloud business');
|
|
2949
3047
|
console.log(' list Show registered businesses');
|
|
2950
3048
|
console.log(' team [slug] Show members, roles, and admin access');
|
|
@@ -2973,6 +3071,10 @@ async function businessCommand(subcommand, ...args) {
|
|
|
2973
3071
|
printBusinessHelp();
|
|
2974
3072
|
return;
|
|
2975
3073
|
}
|
|
3074
|
+
if (subcommand === 'simulate' && args.some(isHelpToken)) {
|
|
3075
|
+
printBusinessSimulateHelp();
|
|
3076
|
+
return;
|
|
3077
|
+
}
|
|
2976
3078
|
const nameTakingSubcommands = new Set(['create', 'new', 'init', 'workspace']);
|
|
2977
3079
|
if (nameTakingSubcommands.has(subcommand) && args.some(isHelpToken)) {
|
|
2978
3080
|
printBusinessHelp();
|
|
@@ -2995,6 +3097,9 @@ async function businessCommand(subcommand, ...args) {
|
|
|
2995
3097
|
case 'workspace':
|
|
2996
3098
|
await initBusinessWorkspace(args[0], ...args.slice(1));
|
|
2997
3099
|
break;
|
|
3100
|
+
case 'simulate':
|
|
3101
|
+
await simulateBusiness(args);
|
|
3102
|
+
break;
|
|
2998
3103
|
case 'list':
|
|
2999
3104
|
case 'ls': {
|
|
3000
3105
|
const opts = {};
|
|
@@ -3072,26 +3177,16 @@ async function businessCommand(subcommand, ...args) {
|
|
|
3072
3177
|
|
|
3073
3178
|
module.exports = {
|
|
3074
3179
|
businessCommand,
|
|
3075
|
-
businessHealth,
|
|
3076
|
-
businessAudit,
|
|
3077
|
-
businessDoctor,
|
|
3078
|
-
businessTeam,
|
|
3079
3180
|
loadBusinesses,
|
|
3080
3181
|
saveBusinesses,
|
|
3081
|
-
getBusinessConfigPath,
|
|
3082
3182
|
businessMatchesSlug,
|
|
3083
3183
|
isAccidentalHelpBusiness,
|
|
3084
3184
|
validateBusinessCreateName,
|
|
3085
3185
|
analyzeBusinessDoctor,
|
|
3086
|
-
readBusinessFolderBindings,
|
|
3087
3186
|
createCanonicalBusinessWorkspace,
|
|
3088
|
-
initBusinessWorkspace,
|
|
3089
3187
|
onboardBusiness,
|
|
3090
|
-
startBusinessWorkspace,
|
|
3091
3188
|
shareBusinessWorkspace,
|
|
3092
3189
|
collectBusinessShareState,
|
|
3093
3190
|
renderBusinessCreatedNextSteps,
|
|
3094
|
-
renderBusinessShareHandoff,
|
|
3095
|
-
renderBusinessStartCard,
|
|
3096
3191
|
recordBusinessRun,
|
|
3097
3192
|
};
|
package/commands/clean.js
CHANGED
|
@@ -2,6 +2,7 @@ const fs = require('fs');
|
|
|
2
2
|
const os = require('os');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const escapeRegExp = require('../lib/escape-regexp');
|
|
5
|
+
const { wikiMetabolismNudge } = require('../lib/wiki');
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* atris clean - Workspace housekeeping with auto-heal
|
|
@@ -60,6 +61,14 @@ function cleanAtris(options = {}) {
|
|
|
60
61
|
const stalePages = findStalePages(cwd, atrisDir);
|
|
61
62
|
results.stalePages = stalePages;
|
|
62
63
|
|
|
64
|
+
// 6. Dead code report (report-only: deletion is a human/reviewed call)
|
|
65
|
+
try {
|
|
66
|
+
const { findDeadCode } = require('./slop');
|
|
67
|
+
const dead = findDeadCode(cwd);
|
|
68
|
+
results.deadFiles = dead.dead.map((f) => path.relative(cwd, f));
|
|
69
|
+
results.testOnlyFiles = dead.testOnly.map((f) => path.relative(cwd, f));
|
|
70
|
+
} catch { results.deadFiles = []; results.testOnlyFiles = []; }
|
|
71
|
+
|
|
63
72
|
if (options.json) {
|
|
64
73
|
console.log(JSON.stringify(cleanResultPayload(results, options, cwd), null, 2));
|
|
65
74
|
return results;
|
|
@@ -75,6 +84,12 @@ function cleanAtris(options = {}) {
|
|
|
75
84
|
console.log('Results:');
|
|
76
85
|
console.log('');
|
|
77
86
|
|
|
87
|
+
const wikiNudge = wikiMetabolismNudge(cwd);
|
|
88
|
+
if (wikiNudge) {
|
|
89
|
+
console.log(wikiNudge);
|
|
90
|
+
console.log('');
|
|
91
|
+
}
|
|
92
|
+
|
|
78
93
|
// Stale tasks
|
|
79
94
|
if (staleTasks.length > 0) {
|
|
80
95
|
console.log(`⚠ ${staleTasks.length} stale ${staleTasks.length === 1 ? 'task' : 'tasks'} (claimed >3 days, not completed):`);
|
|
@@ -140,6 +155,16 @@ function cleanAtris(options = {}) {
|
|
|
140
155
|
console.log('✓ No stale wiki pages');
|
|
141
156
|
}
|
|
142
157
|
|
|
158
|
+
// Dead code
|
|
159
|
+
if ((results.deadFiles || []).length > 0) {
|
|
160
|
+
console.log(`⚠ ${results.deadFiles.length} dead ${results.deadFiles.length === 1 ? 'file' : 'files'} (unreachable and unreferenced — atris slop dead for detail):`);
|
|
161
|
+
results.deadFiles.slice(0, 5).forEach((f) => console.log(` • ${f}`));
|
|
162
|
+
if (results.deadFiles.length > 5) console.log(` ... and ${results.deadFiles.length - 5} more`);
|
|
163
|
+
console.log('');
|
|
164
|
+
} else {
|
|
165
|
+
console.log('✓ No dead code');
|
|
166
|
+
}
|
|
167
|
+
|
|
143
168
|
const hasIssues = staleTasks.length > 0 || unhealable.length > 0 || stalePages.length > 0;
|
|
144
169
|
if (hasIssues) {
|
|
145
170
|
console.log('Manual action needed:');
|
|
@@ -165,6 +190,7 @@ function cleanResultPayload(results, options = {}, cwd = process.cwd()) {
|
|
|
165
190
|
if (results.staleTasks.length > 0) manualAction.push('Delete stale tasks or finish them');
|
|
166
191
|
if (results.unhealableRefs.length > 0) manualAction.push('Manually fix unhealable MAP.md refs');
|
|
167
192
|
if (results.stalePages.length > 0) manualAction.push('Re-compile stale pages');
|
|
193
|
+
if ((results.deadFiles || []).length > 0) manualAction.push('Delete dead files (atris slop dead)');
|
|
168
194
|
return {
|
|
169
195
|
ok: manualAction.length === 0,
|
|
170
196
|
action: 'clean',
|
|
@@ -196,6 +222,11 @@ function cleanResultPayload(results, options = {}, cwd = process.cwd()) {
|
|
|
196
222
|
stale_source: page.staleSource,
|
|
197
223
|
})),
|
|
198
224
|
},
|
|
225
|
+
dead_code: {
|
|
226
|
+
count: (results.deadFiles || []).length,
|
|
227
|
+
items: results.deadFiles || [],
|
|
228
|
+
test_only: results.testOnlyFiles || [],
|
|
229
|
+
},
|
|
199
230
|
},
|
|
200
231
|
manual_action: manualAction,
|
|
201
232
|
};
|
|
@@ -659,8 +690,6 @@ module.exports = {
|
|
|
659
690
|
cleanResultPayload,
|
|
660
691
|
findStaleTasks,
|
|
661
692
|
healBrokenMapRefs,
|
|
662
|
-
archiveOldJournals,
|
|
663
|
-
cleanEmptySections,
|
|
664
693
|
findStalePages,
|
|
665
694
|
checkPageStaleness
|
|
666
695
|
};
|
package/commands/close.js
CHANGED
|
@@ -517,14 +517,27 @@ function pulseLivenessState(cwd, now) {
|
|
|
517
517
|
// pulse liveness check only sees silence; this sees a run of negative reward.
|
|
518
518
|
// Sums reward over the last N finished ticks; unhealthy when the trend is net
|
|
519
519
|
// negative, healthy once it climbs back to zero or better.
|
|
520
|
+
//
|
|
521
|
+
// Recency guard: this sensor only speaks to a *live* run of ticks. A
|
|
522
|
+
// decommissioned or intermittent loop leaves stale receipts frozen; without a
|
|
523
|
+
// guard the last N finished ticks stay net-negative forever and the flag can
|
|
524
|
+
// never clear (no fresh positive receipt arrives). It also must not treat a
|
|
525
|
+
// scattered set — e.g. one tick today plus five from days ago — as a "run":
|
|
526
|
+
// summing rewards across a multi-day gap is meaningless. So we only consider
|
|
527
|
+
// finished ticks from the last few days and require a full window of them; a
|
|
528
|
+
// silent or sparse loop is the pulse-liveness sensor's concern, and here it
|
|
529
|
+
// reports healthy so any open reward flag auto-closes.
|
|
520
530
|
const REWARD_REGRESSION_WINDOW = 6;
|
|
531
|
+
const REWARD_REGRESSION_RECENCY_MS = 2 * DAY_MS;
|
|
521
532
|
|
|
522
533
|
function rewardRegressionState(cwd, now) {
|
|
523
534
|
const receipts = pulse.readJsonl(pulse.pulseReceiptsPath(cwd));
|
|
524
535
|
const finished = receipts
|
|
525
536
|
.filter((r) => r && r.phase === 'finished' && parseDate(r.ts || r.at) !== null)
|
|
537
|
+
.filter((r) => now.getTime() - parseDate(r.ts || r.at) <= REWARD_REGRESSION_RECENCY_MS)
|
|
526
538
|
.sort((a, b) => parseDate(a.ts || a.at) - parseDate(b.ts || b.at));
|
|
527
|
-
|
|
539
|
+
// Not enough recent signal to call a degradation run: leave it to pulse liveness.
|
|
540
|
+
if (finished.length < REWARD_REGRESSION_WINDOW) return { healthy: true, unhealthy: false, sparse: true };
|
|
528
541
|
const window = finished.slice(-REWARD_REGRESSION_WINDOW);
|
|
529
542
|
const sum = window.reduce((acc, r) => acc + (Number(r.reward) || 0), 0);
|
|
530
543
|
return { healthy: sum >= 0, unhealthy: sum < 0, sum };
|
|
@@ -782,13 +795,14 @@ function appendOpenedFromSource({ what, owner = 'operator', lane = 'code', ttlDa
|
|
|
782
795
|
return event;
|
|
783
796
|
}
|
|
784
797
|
|
|
785
|
-
function appendClosedFromSource(flag, cwd, now) {
|
|
798
|
+
function appendClosedFromSource(flag, cwd, now, note) {
|
|
786
799
|
const event = {
|
|
787
800
|
kind: 'closed',
|
|
788
801
|
at: now.toISOString(),
|
|
789
802
|
id: flag.id,
|
|
790
803
|
proof: RESOLVED_IN_SOURCE_PROOF,
|
|
791
804
|
};
|
|
805
|
+
if (note) event.note = note;
|
|
792
806
|
appendEvent(event, cwd);
|
|
793
807
|
return event;
|
|
794
808
|
}
|
|
@@ -809,12 +823,20 @@ function scanState(cwd = process.cwd(), options = {}) {
|
|
|
809
823
|
const lessonFlags = lessonFlagsState(cwd, now);
|
|
810
824
|
const openBySource = new Map();
|
|
811
825
|
const dissolvedSources = new Set();
|
|
826
|
+
const recentlyClosedSources = new Set();
|
|
812
827
|
const autoClosed = [];
|
|
813
828
|
|
|
814
829
|
// Dissolve is a durable verdict: a source the operator declared dead on
|
|
815
|
-
// purpose must never re-open on a later scan.
|
|
830
|
+
// purpose must never re-open on a later scan. Recent closes get a 30-day
|
|
831
|
+
// quiet period so a resolved source cannot immediately breed a new flag.
|
|
816
832
|
for (const flag of foldEvents(readEvents(cwd), { now })) {
|
|
817
833
|
if (flag.status === 'dissolved' && flag.source) dissolvedSources.add(flag.source);
|
|
834
|
+
const closedAt = parseDate(flag.closed_at);
|
|
835
|
+
const closedAge = closedAt === null ? null : now.getTime() - closedAt;
|
|
836
|
+
if (flag.status === 'closed' && flag.source && closedAge !== null
|
|
837
|
+
&& closedAge >= 0 && closedAge <= 30 * DAY_MS) {
|
|
838
|
+
recentlyClosedSources.add(flag.source);
|
|
839
|
+
}
|
|
818
840
|
}
|
|
819
841
|
|
|
820
842
|
const failedTasks = Array.from(tasksById.values())
|
|
@@ -827,17 +849,22 @@ function scanState(cwd = process.cwd(), options = {}) {
|
|
|
827
849
|
if (flag.source) openBySource.set(flag.source, flag);
|
|
828
850
|
}
|
|
829
851
|
|
|
830
|
-
const closeFlag = (flag) => {
|
|
831
|
-
const event = appendClosedFromSource(flag, cwd, now);
|
|
852
|
+
const closeFlag = (flag, note) => {
|
|
853
|
+
const event = appendClosedFromSource(flag, cwd, now, note);
|
|
832
854
|
autoClosed.push({ flag, event });
|
|
833
855
|
openBySource.delete(flag.source);
|
|
856
|
+
recentlyClosedSources.add(flag.source);
|
|
834
857
|
};
|
|
835
858
|
|
|
836
859
|
for (const flag of Array.from(openBySource.values())) {
|
|
837
860
|
if (flag.source.startsWith('task:')) {
|
|
838
861
|
const task = tasksById.get(flag.source.slice('task:'.length));
|
|
839
862
|
const status = normalizeSpaces(task && task.status).toLowerCase();
|
|
840
|
-
if (task
|
|
863
|
+
if (!task) {
|
|
864
|
+
closeFlag(flag, 'task no longer in projection (done/archived)');
|
|
865
|
+
} else if (!taskWaitingOnHumanAccept(task) || taskResolved(task) || status === 'failed') {
|
|
866
|
+
closeFlag(flag);
|
|
867
|
+
}
|
|
841
868
|
continue;
|
|
842
869
|
}
|
|
843
870
|
if (flag.source.startsWith(FAILED_TASK_BATCH_PREFIX)) {
|
|
@@ -881,7 +908,8 @@ function scanState(cwd = process.cwd(), options = {}) {
|
|
|
881
908
|
|
|
882
909
|
const opened = [];
|
|
883
910
|
const openCandidate = (candidate) => {
|
|
884
|
-
if (!candidate.source || openBySource.has(candidate.source)
|
|
911
|
+
if (!candidate.source || openBySource.has(candidate.source)
|
|
912
|
+
|| dissolvedSources.has(candidate.source) || recentlyClosedSources.has(candidate.source)) return null;
|
|
885
913
|
const event = appendOpenedFromSource(candidate, cwd, now);
|
|
886
914
|
opened.push(event);
|
|
887
915
|
openBySource.set(candidate.source, { ...event, status: 'open' });
|
|
@@ -889,10 +917,11 @@ function scanState(cwd = process.cwd(), options = {}) {
|
|
|
889
917
|
};
|
|
890
918
|
|
|
891
919
|
for (const task of tasks) {
|
|
892
|
-
const
|
|
893
|
-
const
|
|
920
|
+
const currentTask = tasksById.get(normalizeSpaces(task && task.id));
|
|
921
|
+
const source = taskSource(currentTask);
|
|
922
|
+
const ref = taskRef(currentTask);
|
|
894
923
|
if (!source || !ref) continue;
|
|
895
|
-
if (taskWaitingOnHumanAccept(
|
|
924
|
+
if (taskWaitingOnHumanAccept(currentTask) && isOlderThan(currentTask.updated_at, 5, now)) {
|
|
896
925
|
openCandidate({
|
|
897
926
|
what: `task ${ref} has waited in review too long, accept it or send it back`,
|
|
898
927
|
lane: 'code',
|
|
@@ -1054,6 +1083,13 @@ function run(args = [], context = {}) {
|
|
|
1054
1083
|
printHelp();
|
|
1055
1084
|
return 0;
|
|
1056
1085
|
}
|
|
1086
|
+
// `close <sub> --help` is a help request, not a run — sweep and scan mutate
|
|
1087
|
+
// the ledger (escalations, auto-closes), so never execute a subcommand just
|
|
1088
|
+
// to show usage.
|
|
1089
|
+
if (rest.includes('--help') || rest.includes('-h')) {
|
|
1090
|
+
printHelp();
|
|
1091
|
+
return 0;
|
|
1092
|
+
}
|
|
1057
1093
|
if (subcommand === 'add') return commandAdd(rest, context);
|
|
1058
1094
|
if (subcommand === 'list') return commandList(rest, context);
|
|
1059
1095
|
if (subcommand === 'done') return commandDone(rest, context);
|
|
@@ -1078,6 +1114,5 @@ module.exports = {
|
|
|
1078
1114
|
openFlags,
|
|
1079
1115
|
ledgerPath,
|
|
1080
1116
|
readEvents,
|
|
1081
|
-
listLine,
|
|
1082
1117
|
sweepLine,
|
|
1083
1118
|
};
|