atris 3.63.0 → 3.65.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 +4 -0
- package/atris/skills/commerce/SKILL.md +24 -0
- package/atris/skills/x-search/SKILL.md +3 -1
- package/ax +4 -3
- package/bin/atris.js +28 -2
- package/cli/atris_code.py +25 -1
- package/commands/agent-spawn.js +1 -0
- package/commands/auth.js +79 -10
- package/commands/autoland.js +11 -1
- package/commands/browse.js +2 -2
- package/commands/business.js +3 -3
- package/commands/buy.js +3 -3
- package/commands/deck.js +2 -2
- package/commands/engine.js +292 -65
- package/commands/feedback.js +2 -2
- package/commands/fork.js +2 -2
- package/commands/gm.js +2 -1
- package/commands/hire.js +189 -0
- package/commands/improve.js +142 -8
- package/commands/integrations.js +4 -3
- package/commands/loops.js +2 -1
- package/commands/mail.js +163 -0
- package/commands/member.js +47 -9
- package/commands/mission.js +4 -3
- package/commands/pack-craft.js +2 -2
- package/commands/publish.js +2 -2
- package/commands/radar.js +3 -2
- package/commands/router.js +2 -2
- package/commands/stream.js +22 -1
- package/commands/task.js +84 -2
- package/commands/team.js +355 -67
- package/commands/transactions.js +3 -3
- package/commands/truth.js +2 -2
- package/commands/x-search.js +81 -10
- package/commands/youtube.js +2 -1
- package/lib/auto-accept-certified.js +83 -9
- package/lib/autoland.js +3 -2
- package/lib/ax-auto-lane.js +2 -2
- package/lib/brief-rules.js +11 -0
- package/lib/business-target.js +131 -0
- package/lib/chat-log-scan.js +2 -1
- package/lib/ci-runner.js +3 -2
- package/lib/codex-flight.js +2 -1
- package/lib/commerce.js +17 -2
- package/lib/deck-history.js +2 -2
- package/lib/deck-review.js +3 -3
- package/lib/design-api.js +2 -2
- package/lib/engine-processes.js +430 -0
- package/lib/engine-registry.js +101 -21
- package/lib/first-minute.js +2 -2
- package/lib/fleet.js +8 -36
- package/lib/git-spawn.js +5 -2
- package/lib/ignored-cache.js +51 -0
- package/lib/known-commands.js +1 -1
- package/lib/manifest.js +2 -2
- package/lib/map-refs.js +70 -4
- package/lib/member-identity.js +56 -0
- package/lib/pack-capabilities.js +2 -1
- package/lib/permission-grants.js +2 -2
- package/lib/pulse.js +5 -5
- package/lib/roster-models.js +11 -0
- package/lib/roster-runs.js +13 -2
- package/lib/roster-stale.js +241 -0
- package/lib/runner-command.js +22 -6
- package/lib/sandbox-pids.js +113 -0
- package/lib/secret-gateway.js +3 -13
- package/lib/state-home.js +106 -0
- package/lib/task-db.js +4 -3
- package/lib/task-prune.js +462 -0
- package/lib/team-lineup.js +10 -71
- package/lib/team-presence.js +224 -0
- package/lib/text-table.js +81 -0
- package/lib/x-post-check.js +497 -0
- package/package.json +1 -1
- package/scripts/det/data/xsearch-answer.json +24 -0
- package/scripts/det/data/xsearch-live-2026-09-29.json +93 -0
- package/scripts/det/xsearch-bench.js +307 -0
- package/scripts/det/ytnotes +61 -15
- package/scripts/det/ytrail-bench.js +2 -1
- package/utils/auth.js +3 -3
- package/utils/update-check.js +2 -2
package/commands/member.js
CHANGED
|
@@ -5443,7 +5443,12 @@ function isWikiMinerNoiseName(value) {
|
|
|
5443
5443
|
const name = compactSentence(value || '', 120);
|
|
5444
5444
|
if (!name) return true;
|
|
5445
5445
|
if (/^[A-Z]{2,}$/.test(name)) return false;
|
|
5446
|
-
return /^(?:i|me|my|mine|myself|we|us|our|ours|ourselves|you|your|yours|yourself|yourselves|he|him|his|himself|she|her|hers|herself|it|its|itself|they|them|their|theirs|themselves|this|that|these|those|the|wiki|mission)$/i.test(name);
|
|
5446
|
+
return /^(?:i|me|my|mine|myself|we|us|our|ours|ourselves|you|your|yours|yourself|yourselves|he|him|his|himself|she|her|hers|herself|it|its|itself|they|them|their|theirs|themselves|this|that|these|those|here|there|where|when|what|who|whom|which|why|how|everything|nothing|something|anything|everyone|anyone|someone|nobody|everybody|anybody|somebody|none|the|wiki|mission)$/i.test(name);
|
|
5447
|
+
}
|
|
5448
|
+
|
|
5449
|
+
function isWikiMinerSelfLoop(relationship) {
|
|
5450
|
+
const from = lowerCompact(relationship?.from);
|
|
5451
|
+
return from !== '' && from === lowerCompact(relationship?.to);
|
|
5447
5452
|
}
|
|
5448
5453
|
|
|
5449
5454
|
function normalizeWikiMinerExtraction(raw) {
|
|
@@ -5462,7 +5467,7 @@ function normalizeWikiMinerExtraction(raw) {
|
|
|
5462
5467
|
to: compactSentence(relationship?.to || '', 120),
|
|
5463
5468
|
type: allowedRelationTypes.has(String(relationship?.type || '').toLowerCase()) ? String(relationship.type).toLowerCase() : 'uses',
|
|
5464
5469
|
}))
|
|
5465
|
-
.filter((relationship) => !isWikiMinerNoiseName(relationship.from) && !isWikiMinerNoiseName(relationship.to));
|
|
5470
|
+
.filter((relationship) => !isWikiMinerNoiseName(relationship.from) && !isWikiMinerNoiseName(relationship.to) && !isWikiMinerSelfLoop(relationship));
|
|
5466
5471
|
return { entities, relationships };
|
|
5467
5472
|
}
|
|
5468
5473
|
|
|
@@ -5484,7 +5489,9 @@ function heuristicWikiMinerExtraction(pagePath, pageContent) {
|
|
|
5484
5489
|
}
|
|
5485
5490
|
const entities = [{ type: 'concept', name: title }];
|
|
5486
5491
|
for (const name of [...candidates].slice(0, 12)) entities.push({ type: /agent|api|sdk|cli|hub|system/i.test(name) ? 'system' : 'concept', name });
|
|
5487
|
-
const relationships = [...candidates].slice(0, 12)
|
|
5492
|
+
const relationships = [...candidates].slice(0, 12)
|
|
5493
|
+
.map((name) => ({ from: title, to: name, type: 'uses' }))
|
|
5494
|
+
.filter((relationship) => !isWikiMinerSelfLoop(relationship));
|
|
5488
5495
|
return { entities, relationships };
|
|
5489
5496
|
}
|
|
5490
5497
|
|
|
@@ -5538,11 +5545,12 @@ function mergeWikiGraph(graph, extraction, sourcePath) {
|
|
|
5538
5545
|
|
|
5539
5546
|
const relationshipMap = new Map();
|
|
5540
5547
|
for (const relationship of graph.relationships || []) {
|
|
5541
|
-
if (isWikiMinerNoiseName(relationship.from) || isWikiMinerNoiseName(relationship.to)) continue;
|
|
5548
|
+
if (isWikiMinerNoiseName(relationship.from) || isWikiMinerNoiseName(relationship.to) || isWikiMinerSelfLoop(relationship)) continue;
|
|
5542
5549
|
const key = `${lowerCompact(relationship.from)}|${String(relationship.type || 'uses').toLowerCase()}|${lowerCompact(relationship.to)}`;
|
|
5543
5550
|
relationshipMap.set(key, { ...relationship, sources: Array.isArray(relationship.sources) ? relationship.sources : [] });
|
|
5544
5551
|
}
|
|
5545
5552
|
for (const relationship of extraction.relationships || []) {
|
|
5553
|
+
if (isWikiMinerNoiseName(relationship.from) || isWikiMinerNoiseName(relationship.to) || isWikiMinerSelfLoop(relationship)) continue;
|
|
5546
5554
|
const key = `${lowerCompact(relationship.from)}|${relationship.type}|${lowerCompact(relationship.to)}`;
|
|
5547
5555
|
const existing = relationshipMap.get(key) || { from: relationship.from, to: relationship.to, type: relationship.type, sources: [] };
|
|
5548
5556
|
if (sourcePath && !existing.sources.includes(sourcePath)) existing.sources.push(sourcePath);
|
|
@@ -5819,7 +5827,6 @@ async function callSupervisorLlm(receipts, logs) {
|
|
|
5819
5827
|
const output = { isTTY: false, write() { return true; } };
|
|
5820
5828
|
const result = await postTurn(supervisorPrompt(receipts, logs), {
|
|
5821
5829
|
mode: process.env.ATRIS_SUPERVISOR_LLM_MODE || 'fast',
|
|
5822
|
-
route: 'local',
|
|
5823
5830
|
cwd: process.cwd(),
|
|
5824
5831
|
output,
|
|
5825
5832
|
color: false,
|
|
@@ -5905,11 +5912,21 @@ async function runSupervisorWake(name, paths, { execute = false } = {}) {
|
|
|
5905
5912
|
analysis = normalizeSupervisorAnalysis(llm.analysis, { status: 'ok', llm_source: llm.source });
|
|
5906
5913
|
if (!analysis) {
|
|
5907
5914
|
reason = 'llm_json_parse_failed';
|
|
5908
|
-
analysis =
|
|
5915
|
+
analysis = {
|
|
5916
|
+
...fallbackSupervisorAnalysis(receipts),
|
|
5917
|
+
status: 'parse_error',
|
|
5918
|
+
llm_source: llm.source,
|
|
5919
|
+
llm_error: 'invalid_analysis_shape',
|
|
5920
|
+
};
|
|
5909
5921
|
}
|
|
5910
5922
|
} else if (llm?.error) {
|
|
5911
5923
|
reason = llm.error === 'invalid_json' ? 'llm_json_parse_failed' : 'llm_analysis_failed';
|
|
5912
|
-
analysis =
|
|
5924
|
+
analysis = {
|
|
5925
|
+
...fallbackSupervisorAnalysis(receipts),
|
|
5926
|
+
status: llm.error === 'invalid_json' ? 'parse_error' : 'llm_error',
|
|
5927
|
+
llm_source: llm.source,
|
|
5928
|
+
llm_error: llm.error,
|
|
5929
|
+
};
|
|
5913
5930
|
} else {
|
|
5914
5931
|
reason = 'llm_not_configured';
|
|
5915
5932
|
analysis = fallbackSupervisorAnalysis(receipts);
|
|
@@ -7351,7 +7368,7 @@ async function runGeneralistWake(name, paths, { execute = false, domainInput = {
|
|
|
7351
7368
|
record: domain.ok && proof.status === 'ok',
|
|
7352
7369
|
libraryBefore: crossDomainLibraryBefore,
|
|
7353
7370
|
receiptPath,
|
|
7354
|
-
proofPath,
|
|
7371
|
+
proofPath: execute ? proofPath : null,
|
|
7355
7372
|
});
|
|
7356
7373
|
proof.learning = {
|
|
7357
7374
|
library_path: path.relative(root, learning.library_path),
|
|
@@ -7782,6 +7799,24 @@ function wakeRuleDirtyMemberScope(ctx) {
|
|
|
7782
7799
|
};
|
|
7783
7800
|
}
|
|
7784
7801
|
|
|
7802
|
+
// Rule: a goal pinned to a terminal mission must not mint another doomed tick.
|
|
7803
|
+
// The goal stays active until a human retires it or revives the mission, so the
|
|
7804
|
+
// wake surfaces that decision instead of proposing `member tick --goal` on a
|
|
7805
|
+
// dead spine. Unknown mission ids pass through: they may live in a worktree.
|
|
7806
|
+
function wakeRuleTerminalMissionGoal(ctx) {
|
|
7807
|
+
const mission = ctx.goalTerminalMission;
|
|
7808
|
+
if (!ctx.goal || !mission) return null;
|
|
7809
|
+
return {
|
|
7810
|
+
decision: 'ask',
|
|
7811
|
+
reason: `goal_mission_terminal:${mission.status}`,
|
|
7812
|
+
needs_user: true,
|
|
7813
|
+
ask: `Goal "${ctx.goal.title}" is pinned to mission ${mission.id} (${mission.status}). Retire the goal or revive the mission, then wake again.`,
|
|
7814
|
+
next_command: `atris member goal-from-mission ${ctx.name}`,
|
|
7815
|
+
goal: ctx.goal,
|
|
7816
|
+
current_experiment: ctx.current || null,
|
|
7817
|
+
};
|
|
7818
|
+
}
|
|
7819
|
+
|
|
7785
7820
|
// Rule: nothing blocks the member, so take the safe next bounded step.
|
|
7786
7821
|
function wakeRuleSafeBoundedTick(ctx) {
|
|
7787
7822
|
return {
|
|
@@ -7802,6 +7837,7 @@ const WAKE_DECISION_RULES = [
|
|
|
7802
7837
|
wakeRuleOpenReviewableProposal,
|
|
7803
7838
|
wakeRuleScoredNonTickCandidate,
|
|
7804
7839
|
wakeRuleDirtyMemberScope,
|
|
7840
|
+
wakeRuleTerminalMissionGoal,
|
|
7805
7841
|
wakeRuleSafeBoundedTick,
|
|
7806
7842
|
];
|
|
7807
7843
|
|
|
@@ -7815,6 +7851,8 @@ function wakeDecision(name, paths, { force = false, runtimeKind = memberRuntimeK
|
|
|
7815
7851
|
const steering = readSteeringMemory(paths, name);
|
|
7816
7852
|
const state = loadMemberGoals(name, paths);
|
|
7817
7853
|
const goal = activeGoal(state);
|
|
7854
|
+
const pinnedMission = goal?.mission_id ? memberRunMissionMap().get(String(goal.mission_id).trim()) || null : null;
|
|
7855
|
+
const goalTerminalMission = pinnedMission && MISSION_TERMINAL_STATUSES.has(lowerCompact(pinnedMission.status || '')) ? pinnedMission : null;
|
|
7818
7856
|
const current = memberOpenExperiment(state);
|
|
7819
7857
|
const generalistDecision = wakeRuleGeneralistDomainFile(name, { purpose, steering, state, goal, current });
|
|
7820
7858
|
if (generalistDecision) return generalistDecision;
|
|
@@ -7860,7 +7898,7 @@ function wakeDecision(name, paths, { force = false, runtimeKind = memberRuntimeK
|
|
|
7860
7898
|
next_command: wakeScores.selected.next_command,
|
|
7861
7899
|
} : null;
|
|
7862
7900
|
|
|
7863
|
-
const ctx = { name, force, purpose, nowFile, steering, state, goal, current, blocked, evidence, workspace, checks, wakeScores };
|
|
7901
|
+
const ctx = { name, force, purpose, nowFile, steering, state, goal, goalTerminalMission, current, blocked, evidence, workspace, checks, wakeScores };
|
|
7864
7902
|
for (const rule of WAKE_DECISION_RULES) {
|
|
7865
7903
|
const verdict = rule(ctx);
|
|
7866
7904
|
if (verdict) return composeWakeDecision(ctx, verdict);
|
package/commands/mission.js
CHANGED
|
@@ -7,6 +7,7 @@ const readline = require('readline');
|
|
|
7
7
|
const { spawn, spawnSync } = require('child_process');
|
|
8
8
|
const { hasFlag, readFlag, readIntFlag } = require('../lib/arg-parser');
|
|
9
9
|
const { memberProcessPrompt } = require('../lib/member-context');
|
|
10
|
+
const { memberIdentityLine, missionObjectiveLine } = require('../lib/member-identity');
|
|
10
11
|
const {
|
|
11
12
|
compactErrorPayload,
|
|
12
13
|
compactSuccessPayload,
|
|
@@ -570,7 +571,7 @@ function applyMissionRunnerProfile(runner) {
|
|
|
570
571
|
// it only ever takes an explicit pin (the mission's model or the roster
|
|
571
572
|
// line's); '' lets the engine ride its own default.
|
|
572
573
|
// A mission often still carries the claude model it was created with; an
|
|
573
|
-
// engine that runs only its own models (codex) drops it.
|
|
574
|
+
// engine that runs only its own models (codex, grok) drops it.
|
|
574
575
|
const CLAUDE_FAMILY_RUNNER_ENGINES = new Set(['claude', 'fable', 'haiku']);
|
|
575
576
|
|
|
576
577
|
function resolveMissionTickRunnerModel(mission) {
|
|
@@ -8634,7 +8635,7 @@ function buildTickPrompt(mission, tickIndex, maxTicks, frozen, pings = [], cwd =
|
|
|
8634
8635
|
`# Mission Tick ${tickIndex}/${maxTicks}`,
|
|
8635
8636
|
...pingLines,
|
|
8636
8637
|
``,
|
|
8637
|
-
|
|
8638
|
+
missionObjectiveLine(mission.objective),
|
|
8638
8639
|
`**Owner:** ${mission.owner}`,
|
|
8639
8640
|
`**Lane:** ${frozen.lane}`,
|
|
8640
8641
|
`**Cadence:** ${mission.cadence}`,
|
|
@@ -8648,7 +8649,7 @@ function buildTickPrompt(mission, tickIndex, maxTicks, frozen, pings = [], cwd =
|
|
|
8648
8649
|
``,
|
|
8649
8650
|
`## Your task`,
|
|
8650
8651
|
`Do ONE increment of work toward the stop condition. ONE. No more.`,
|
|
8651
|
-
`-
|
|
8652
|
+
`- ${memberIdentityLine(mission.owner)} Read atris/team/${mission.owner}/MEMBER.md (and SOUL.md if present) before acting, work in that identity, inside its scope and stop rules. After your work, append what you did and what you learned to atris/team/${mission.owner}/logs/<today's date>.md.`,
|
|
8652
8653
|
`- FIRST: inspect current mission/task state before acting. Read the relevant files, run \`atris mission status ${mission.id}\`, \`git status\`, or \`atris task list\` as needed so you know what's already done.`,
|
|
8653
8654
|
`- Pick the smallest concrete action that moves the mission forward.`,
|
|
8654
8655
|
`- Before acting, state your single next move in one sentence.`,
|
package/commands/pack-craft.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
|
-
const os = require('os');
|
|
5
4
|
const path = require('path');
|
|
5
|
+
const { stateHome } = require('../lib/state-home');
|
|
6
6
|
|
|
7
7
|
function slugify(value, fallback = 'atris-pack') {
|
|
8
8
|
return String(value || fallback)
|
|
@@ -69,7 +69,7 @@ function isNonEmptyTarget(targetDir) {
|
|
|
69
69
|
function defaultAuthor() {
|
|
70
70
|
try {
|
|
71
71
|
const creds = JSON.parse(
|
|
72
|
-
fs.readFileSync(path.join(
|
|
72
|
+
fs.readFileSync(path.join(stateHome(), '.atris', 'credentials.json'), 'utf8')
|
|
73
73
|
);
|
|
74
74
|
if (creds && typeof creds.email === 'string' && creds.email.trim()) return creds.email.trim();
|
|
75
75
|
} catch {
|
package/commands/publish.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const
|
|
3
|
+
const { stateHome } = require('../lib/state-home');
|
|
4
4
|
const { loadCredentials } = require('../utils/auth');
|
|
5
5
|
const { apiRequestJson } = require('../utils/api');
|
|
6
6
|
|
|
@@ -104,7 +104,7 @@ async function publishAtris() {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
// Save local copy
|
|
107
|
-
const localDir = path.join(
|
|
107
|
+
const localDir = path.join(stateHome(), '.atris', 'templates', name.toLowerCase().replace(/[^a-z0-9-]/g, '-'));
|
|
108
108
|
fs.mkdirSync(localDir, { recursive: true });
|
|
109
109
|
|
|
110
110
|
for (const f of files) {
|
package/commands/radar.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const path = require('path');
|
|
6
|
+
const { stateHome } = require('../lib/state-home');
|
|
6
7
|
const { execFileSync } = require('child_process');
|
|
7
8
|
const { parseScopeFlag, pathUnderRoot } = require('../lib/cli-scope');
|
|
8
9
|
|
|
@@ -346,7 +347,7 @@ function loadBusinessCollaboration(root, deps, team = {}, scorecardCount = count
|
|
|
346
347
|
const business = readJsonFile(path.join(root, '.atris', 'business.json'), deps, null);
|
|
347
348
|
const runtime = readJsonFile(path.join(root, '.atris', 'state', 'runtime.json'), deps, null);
|
|
348
349
|
const sync = readJsonFile(path.join(root, '.atris', 'state', '_sync.json'), deps, null);
|
|
349
|
-
const cache = readJsonFile(path.join(deps.homeDir ||
|
|
350
|
+
const cache = readJsonFile(path.join(deps.homeDir || stateHome(), '.atris', 'businesses.json'), deps, {}) || {};
|
|
350
351
|
const slug = business?.slug || sync?.workspace_slug || null;
|
|
351
352
|
const cacheEntry = slug && cache ? cache[slug] : null;
|
|
352
353
|
const hasAtris = deps.exists(path.join(root, 'atris'));
|
|
@@ -804,7 +805,7 @@ function collectRadar(options = {}) {
|
|
|
804
805
|
readlink: options.readlinkSync || fs.readlinkSync,
|
|
805
806
|
readdir: options.readdirSync || fs.readdirSync,
|
|
806
807
|
platform: options.platform || os.platform(),
|
|
807
|
-
homeDir: options.homeDir ||
|
|
808
|
+
homeDir: options.homeDir || stateHome(),
|
|
808
809
|
};
|
|
809
810
|
const nowMs = options.nowMs || Date.now();
|
|
810
811
|
const tasks = loadTasks(root, deps);
|
package/commands/router.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const crypto = require('node:crypto');
|
|
4
4
|
const fs = require('node:fs');
|
|
5
|
-
const os = require('node:os');
|
|
6
5
|
const path = require('node:path');
|
|
6
|
+
const { stateHome } = require('../lib/state-home');
|
|
7
7
|
const { spawnSync } = require('node:child_process');
|
|
8
8
|
const { loadOverrides, pickLane } = require('../lib/ax-auto-lane');
|
|
9
9
|
|
|
@@ -28,7 +28,7 @@ function readFirstNamedFlag(args, names) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function resolveRouterPaths(args = [], env = process.env) {
|
|
31
|
-
const home = env
|
|
31
|
+
const home = stateHome(env);
|
|
32
32
|
return {
|
|
33
33
|
picks: path.resolve(readFirstNamedFlag(args, ['--picks', '--picks-path'])
|
|
34
34
|
|| env.ATRIS_ROUTER_PICKS_PATH
|
package/commands/stream.js
CHANGED
|
@@ -589,12 +589,33 @@ function worktreeTask(root, deps) {
|
|
|
589
589
|
return worktreeConfig(root, deps).task;
|
|
590
590
|
}
|
|
591
591
|
|
|
592
|
+
// The branches a side copy starts from. A commit reachable from any of them
|
|
593
|
+
// was inherited, not made in the copy.
|
|
594
|
+
const WORKTREE_BASE_REFS = Object.freeze([
|
|
595
|
+
'refs/remotes/origin/HEAD',
|
|
596
|
+
'refs/remotes/origin/main',
|
|
597
|
+
'refs/remotes/origin/master',
|
|
598
|
+
'refs/heads/main',
|
|
599
|
+
'refs/heads/master',
|
|
600
|
+
]);
|
|
601
|
+
|
|
602
|
+
function worktreeBaseRefs(root, deps) {
|
|
603
|
+
const result = runGit(root, ['for-each-ref', '--format=%(refname)', ...WORKTREE_BASE_REFS], deps);
|
|
604
|
+
if (result.status !== 0) return [];
|
|
605
|
+
return String(result.stdout || '').split(/\r?\n/).map((line) => line.trim()).filter((ref) => WORKTREE_BASE_REFS.includes(ref));
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Only a commit the copy added on top of its base counts. A fresh copy cut
|
|
609
|
+
// from someone else's commit has none, so its owner shows no work. A repo
|
|
610
|
+
// with no main, master, or origin default keeps the newest commit.
|
|
592
611
|
function collectWorktreeActivityEvents(root, deps, events) {
|
|
593
612
|
const roots = collectWorkspaceRoots(root, deps);
|
|
594
613
|
for (const item of roots) {
|
|
595
614
|
const owner = worktreeOwner(item.root, deps);
|
|
596
615
|
if (!owner) continue;
|
|
597
|
-
const
|
|
616
|
+
const bases = worktreeBaseRefs(item.root, deps);
|
|
617
|
+
const range = bases.length ? ['HEAD', '--not', ...bases] : [];
|
|
618
|
+
const log = runGit(item.root, ['log', '-1', '--format=%cI%x09%an%x09%s', ...range], deps);
|
|
598
619
|
if (log.status !== 0 || !String(log.stdout || '').trim()) continue;
|
|
599
620
|
const [ts, author, subject] = String(log.stdout || '').trim().split('\t');
|
|
600
621
|
pushEvent(events, {
|
package/commands/task.js
CHANGED
|
@@ -253,6 +253,8 @@ atris task - durable local task state (SQLite, gitignored)
|
|
|
253
253
|
Sweep off-roadmap/duplicate work as archived (not failed);
|
|
254
254
|
--from-failed opts in to relabel a fail-closed row (never done)
|
|
255
255
|
atris task clear-done [--before <days>] [--dry-run] [--json] Archive completed rows, oldest first
|
|
256
|
+
atris task prune-test-junk [--yes] [--vacuum] [--json]
|
|
257
|
+
Count rows test runs left from temp folders; --yes backs up the db, then removes them
|
|
256
258
|
atris task keep [--json] Put away finished and untouched work, then refresh the list
|
|
257
259
|
atris task reap-mission-blockers [--json] Close blocker rows whose missions are complete or stopped
|
|
258
260
|
atris task relabel-archived [--dry-run|--apply]
|
|
@@ -1421,7 +1423,7 @@ function taskReviewSummary(task) {
|
|
|
1421
1423
|
const reviewPassCount = Number(metadata.agent_review_pass_count || payload.review_pass_count || 0);
|
|
1422
1424
|
const agentCertified = metadata.agent_certified === true
|
|
1423
1425
|
|| payload.agent_certified === true
|
|
1424
|
-
|| reviewPassCount >= AGENT_CERTIFICATION_REVIEW_PASSES;
|
|
1426
|
+
|| (reviewPassCount >= AGENT_CERTIFICATION_REVIEW_PASSES && reviewIntegrity.hasIndependentReview(task));
|
|
1425
1427
|
const reviewedEventHas = (key) => reviewed && reviewed.event_type === 'reviewed'
|
|
1426
1428
|
&& Object.prototype.hasOwnProperty.call(payload, key);
|
|
1427
1429
|
const clearedReviewFields = new Set(Array.isArray(payload.cleared_review_fields) ? payload.cleared_review_fields : []);
|
|
@@ -8841,7 +8843,7 @@ function taskAllowsReviewChat(task, { allowCertified = false } = {}) {
|
|
|
8841
8843
|
if (allowCertified) return true;
|
|
8842
8844
|
if (review.agent_certified === true || metadata.agent_certified === true) return false;
|
|
8843
8845
|
const reviewPassCount = Number(review.agent_review_pass_count || metadata.agent_review_pass_count || 0);
|
|
8844
|
-
if (reviewPassCount >= AGENT_CERTIFICATION_REVIEW_PASSES) return false;
|
|
8846
|
+
if (reviewPassCount >= AGENT_CERTIFICATION_REVIEW_PASSES && reviewIntegrity.hasIndependentReview(task)) return false;
|
|
8845
8847
|
const handoff = reviewHandoffForTask(task, { suppressExistingFollowUp: true });
|
|
8846
8848
|
return !(handoff && (handoff.next_action === 'continue_work' || handoff.next_action === 'human_accept_waiting'));
|
|
8847
8849
|
}
|
|
@@ -9814,6 +9816,78 @@ function cmdClearDone(args) {
|
|
|
9814
9816
|
console.log(`cleared ${candidates.length} completed task(s).`);
|
|
9815
9817
|
}
|
|
9816
9818
|
|
|
9819
|
+
function formatBytes(bytes) {
|
|
9820
|
+
const mb = Number(bytes || 0) / (1024 * 1024);
|
|
9821
|
+
return mb >= 1 ? `${mb.toFixed(1)} MB` : `${Math.round(Number(bytes || 0) / 1024)} KB`;
|
|
9822
|
+
}
|
|
9823
|
+
|
|
9824
|
+
// Test runs that forgot ATRIS_TASKS_DB left rows in the shared database
|
|
9825
|
+
// under temp folders. Default is a read-only count; --yes backs up, then
|
|
9826
|
+
// removes those rows and their history. Real project rows are never touched.
|
|
9827
|
+
function cmdPruneTestJunk(args) {
|
|
9828
|
+
const taskDb = getTaskDb();
|
|
9829
|
+
const prune = require('../lib/task-prune');
|
|
9830
|
+
const dbPath = taskDb.getDbPath();
|
|
9831
|
+
const apply = hasFlag(args, '--yes') && !hasFlag(args, '--dry-run');
|
|
9832
|
+
const vacuum = apply && hasFlag(args, '--vacuum');
|
|
9833
|
+
if (!apply) {
|
|
9834
|
+
const plan = prune.dryRun(dbPath);
|
|
9835
|
+
const { rows, ...summary } = plan;
|
|
9836
|
+
if (wantsJson(args)) {
|
|
9837
|
+
printJson({ ok: true, action: 'prune-test-junk', dry_run: true, ...summary });
|
|
9838
|
+
return;
|
|
9839
|
+
}
|
|
9840
|
+
if (plan.missing) {
|
|
9841
|
+
console.log(`no task database at ${dbPath}. nothing to prune.`);
|
|
9842
|
+
return;
|
|
9843
|
+
}
|
|
9844
|
+
const r = plan.remove;
|
|
9845
|
+
console.log('prune-test-junk dry run: nothing was changed.');
|
|
9846
|
+
console.log(`database: ${dbPath} (${formatBytes(plan.db_bytes)}, ${plan.total_tasks} tasks)`);
|
|
9847
|
+
console.log(`would remove ${r.tasks} test task(s) from temp folders, ${r.history_rows} history row(s), `
|
|
9848
|
+
+ `${r.stray_history_rows} stray history row(s), ${r.part_uses} part use row(s).`);
|
|
9849
|
+
for (const g of plan.groups) {
|
|
9850
|
+
console.log(` ${g.prefix.padEnd(22)} ${String(g.tasks).padStart(7)} tasks ${String(g.history_rows + g.stray_history_rows).padStart(8)} history rows`);
|
|
9851
|
+
}
|
|
9852
|
+
const k = plan.kept;
|
|
9853
|
+
console.log(`kept: ${k.folder_exists} temp task(s) whose folder still exists, ${k.recent} active in the last ${plan.grace_hours} hour(s), `
|
|
9854
|
+
+ `${k.held} with history from a real project, ${k.stray_held + k.stray_recent} leftover history set(s) that are recent or name a real project.`);
|
|
9855
|
+
if (r.tasks || r.stray_history_rows) {
|
|
9856
|
+
console.log('run with --yes to remove them. a backup copy is written next to the database first.');
|
|
9857
|
+
}
|
|
9858
|
+
return;
|
|
9859
|
+
}
|
|
9860
|
+
|
|
9861
|
+
if (vacuum && !wantsJson(args)) {
|
|
9862
|
+
console.log('note: --vacuum rewrites the whole file and needs every other atris process idle; it waits a few seconds, then gives up.');
|
|
9863
|
+
}
|
|
9864
|
+
let result;
|
|
9865
|
+
try {
|
|
9866
|
+
result = prune.applyPrune(dbPath, { vacuum });
|
|
9867
|
+
} catch (err) {
|
|
9868
|
+
failTask('atris task prune-test-junk', 'prune_failed', `prune-test-junk stopped: ${err && err.message || err}`, 1);
|
|
9869
|
+
}
|
|
9870
|
+
if (wantsJson(args)) {
|
|
9871
|
+
printJson({ ok: true, action: 'prune-test-junk', dry_run: false, ...result });
|
|
9872
|
+
return;
|
|
9873
|
+
}
|
|
9874
|
+
if (result.missing) {
|
|
9875
|
+
console.log(`no task database at ${dbPath}. nothing to prune.`);
|
|
9876
|
+
return;
|
|
9877
|
+
}
|
|
9878
|
+
const r = result.removed;
|
|
9879
|
+
console.log(`backup: ${result.backup_path} (${result.backup_tasks} tasks)`);
|
|
9880
|
+
console.log(`removed ${r.tasks} test task(s), ${r.history_rows} history row(s), `
|
|
9881
|
+
+ `${r.stray_history_rows} stray history row(s), ${r.part_uses} part use row(s).`);
|
|
9882
|
+
const sk = result.skipped;
|
|
9883
|
+
if (sk.held || sk.changed_since_backup || sk.recent || sk.folder_exists) {
|
|
9884
|
+
console.log(`left ${sk.changed_since_backup} that changed after the backup, ${sk.recent} that became active, `
|
|
9885
|
+
+ `${sk.folder_exists} whose folder came back, and ${sk.held} with real-project history.`);
|
|
9886
|
+
}
|
|
9887
|
+
console.log(`database now has ${result.total_tasks} tasks, ${formatBytes(result.bytes_after)} on disk`
|
|
9888
|
+
+ (result.vacuumed ? ' after vacuum.' : '. run again with --yes --vacuum to shrink the file.'));
|
|
9889
|
+
}
|
|
9890
|
+
|
|
9817
9891
|
function cmdReapMissionBlockers(args) {
|
|
9818
9892
|
const taskDb = getTaskDb();
|
|
9819
9893
|
const db = taskDb.open();
|
|
@@ -10468,6 +10542,13 @@ function stampReadyVerifyMetadata(taskDb, db, taskId, verify) {
|
|
|
10468
10542
|
if (!row) return;
|
|
10469
10543
|
const metadata = row.metadata && typeof row.metadata === 'object' ? { ...row.metadata } : {};
|
|
10470
10544
|
metadata.verify = verify;
|
|
10545
|
+
// A real verify stamped at ready means the creation-time degraded flag is
|
|
10546
|
+
// stale: clear it so `task show` stops warning about a missing check. A
|
|
10547
|
+
// diff-only verify keeps the flag; it still cannot fail on real breakage.
|
|
10548
|
+
if (verify.toLowerCase() !== 'git diff --check') {
|
|
10549
|
+
delete metadata.verification_status;
|
|
10550
|
+
delete metadata.verification_degraded_reason;
|
|
10551
|
+
}
|
|
10471
10552
|
db.prepare(`
|
|
10472
10553
|
UPDATE tasks
|
|
10473
10554
|
SET metadata = ?,
|
|
@@ -13374,6 +13455,7 @@ async function runTaskCommand(args) {
|
|
|
13374
13455
|
case 'fail': return cmdDone([...rest, '--failed']);
|
|
13375
13456
|
case 'archive': return cmdArchive(rest);
|
|
13376
13457
|
case 'clear-done': return cmdClearDone(rest);
|
|
13458
|
+
case 'prune-test-junk': return cmdPruneTestJunk(rest);
|
|
13377
13459
|
case 'keep': return cmdKeep(rest);
|
|
13378
13460
|
case 'reap-mission-blockers':
|
|
13379
13461
|
case 'reap-blockers':
|