atris 3.15.56 → 3.16.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 +2 -2
- package/GETTING_STARTED.md +1 -1
- package/PERSONA.md +4 -4
- package/README.md +11 -11
- package/atris/skills/copy-editor/SKILL.md +30 -4
- package/atris/skills/improve/SKILL.md +18 -20
- package/atris/wiki/concepts/agent-activation-contract.md +5 -3
- package/atris/wiki/concepts/workspace-initialization-contract.md +4 -4
- package/atris/wiki/index.md +1 -0
- package/ax +522 -73
- package/bin/atris.js +32 -31
- package/commands/align.js +0 -14
- package/commands/apps.js +102 -1
- package/commands/autopilot.js +197 -22
- package/commands/brain.js +219 -34
- package/commands/brainstorm.js +0 -829
- package/commands/computer.js +45 -83
- package/commands/improve.js +501 -0
- package/commands/integrations.js +228 -0
- package/commands/lesson.js +44 -0
- package/commands/member.js +4498 -226
- package/commands/mission.js +302 -27
- package/commands/now.js +89 -1
- package/commands/radar.js +181 -56
- package/commands/skill.js +37 -6
- package/commands/soul.js +0 -4
- package/commands/task.js +5582 -517
- package/commands/terminal.js +14 -10
- package/commands/wiki.js +87 -1
- package/commands/workflow.js +288 -73
- package/commands/worktree.js +52 -15
- package/commands/xp.js +41 -65
- package/lib/auto-accept-certified.js +294 -0
- package/lib/file-ops.js +0 -184
- package/lib/member-alive.js +232 -0
- package/lib/policy-lessons.js +280 -0
- package/lib/receipt-evidence.js +64 -0
- package/lib/state-detection.js +34 -0
- package/lib/task-db.js +568 -16
- package/lib/task-proof.js +43 -0
- package/package.json +1 -1
- package/utils/auth.js +13 -4
- package/commands/research.js +0 -52
- package/lib/section-merge.js +0 -196
package/commands/xp.js
CHANGED
|
@@ -7,6 +7,7 @@ const crypto = require('crypto');
|
|
|
7
7
|
const { spawnSync } = require('child_process');
|
|
8
8
|
|
|
9
9
|
const DEFAULT_GRAPH_DAYS = 365;
|
|
10
|
+
const MAX_SYNC_GRAPH_DAYS = 370;
|
|
10
11
|
const INTENSITY_CHARS = [' ', '.', ':', '*', '#'];
|
|
11
12
|
const ROW_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
12
13
|
const TASK_EPISODES_FILE = path.join('.atris', 'state', 'task_episodes.jsonl');
|
|
@@ -29,7 +30,11 @@ const EARNING_MODEL = {
|
|
|
29
30
|
schema: 'atris.agentxp_earning_model.v1',
|
|
30
31
|
score_name: AGENT_XP_LABEL,
|
|
31
32
|
primary_public_source: 'accepted_task_receipt',
|
|
32
|
-
public_rule: 'No accepted proof-backed task episodes means no AgentXP leaderboard movement.',
|
|
33
|
+
public_rule: 'No human-accepted proof-backed task episodes means no AgentXP leaderboard movement.',
|
|
34
|
+
policies: {
|
|
35
|
+
task_done_with_proof: 'Records proof for review and RL context; emits no Career XP receipt until human accept.',
|
|
36
|
+
task_accept: 'Human accept with proof and positive reward is the Career XP receipt boundary.',
|
|
37
|
+
},
|
|
33
38
|
weights: [
|
|
34
39
|
{
|
|
35
40
|
id: 'accepted_task_receipt',
|
|
@@ -210,6 +215,38 @@ function combineAgentXpContributionGraphs(graphs, windowDays = DEFAULT_GRAPH_DAY
|
|
|
210
215
|
return graphFromDailyTotals(totals, windowDays);
|
|
211
216
|
}
|
|
212
217
|
|
|
218
|
+
function sanitizeContributionGraphForSync(graph) {
|
|
219
|
+
if (!graph || typeof graph !== 'object') return null;
|
|
220
|
+
const rawDays = Array.isArray(graph.days) ? graph.days : [];
|
|
221
|
+
const days = rawDays
|
|
222
|
+
.slice(-MAX_SYNC_GRAPH_DAYS)
|
|
223
|
+
.map((day) => {
|
|
224
|
+
const date = typeof day?.date === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(day.date)
|
|
225
|
+
? day.date
|
|
226
|
+
: null;
|
|
227
|
+
if (!date) return null;
|
|
228
|
+
const xp = Math.max(0, asNumber(day.xp ?? day.total_xp));
|
|
229
|
+
return {
|
|
230
|
+
date,
|
|
231
|
+
xp,
|
|
232
|
+
total_xp: Math.max(0, asNumber(day.total_xp, xp)),
|
|
233
|
+
intensity: Math.max(0, Math.min(4, asNumber(day.intensity))),
|
|
234
|
+
};
|
|
235
|
+
})
|
|
236
|
+
.filter(Boolean);
|
|
237
|
+
if (days.length === 0) return null;
|
|
238
|
+
return {
|
|
239
|
+
schema: 'atris.agent_xp_contribution_graph.v1',
|
|
240
|
+
metric_label: AGENT_XP_LABEL,
|
|
241
|
+
window_days: days.length,
|
|
242
|
+
total_xp: days.reduce((sum, day) => sum + day.xp, 0),
|
|
243
|
+
active_days: days.filter(day => day.xp > 0).length,
|
|
244
|
+
first_date: days[0]?.date || null,
|
|
245
|
+
last_date: days[days.length - 1]?.date || null,
|
|
246
|
+
days,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
213
250
|
function currentForm(payload) {
|
|
214
251
|
const arenas = payload.current_form_by_arena || {};
|
|
215
252
|
const local = arenas.local_workspace || {};
|
|
@@ -641,6 +678,7 @@ function buildAtrisActionSignalFromRows(rows = []) {
|
|
|
641
678
|
included_in_total_agent_xp: false,
|
|
642
679
|
public_leaderboard: false,
|
|
643
680
|
role: 'rl_routing_only',
|
|
681
|
+
note: 'Task done/finish proof can count here as non-public action context; Career XP still requires human accept.',
|
|
644
682
|
};
|
|
645
683
|
}
|
|
646
684
|
|
|
@@ -1348,10 +1386,6 @@ function readTaskProjectionState(workspace) {
|
|
|
1348
1386
|
}
|
|
1349
1387
|
}
|
|
1350
1388
|
|
|
1351
|
-
function readTaskProjection(workspace) {
|
|
1352
|
-
return readTaskProjectionState(workspace).tasks;
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
1389
|
function taskRef(task) {
|
|
1356
1390
|
return task?.display_id || task?.legacy_ref || task?.id || 'task';
|
|
1357
1391
|
}
|
|
@@ -1682,36 +1716,6 @@ function buildCareerXpSessionCapsule(args = []) {
|
|
|
1682
1716
|
return capsule;
|
|
1683
1717
|
}
|
|
1684
1718
|
|
|
1685
|
-
function normalizeLocalScore(score, workspace) {
|
|
1686
|
-
const card = score.profile_card || score.player_card || {};
|
|
1687
|
-
const integrity = score.integrity || {};
|
|
1688
|
-
const careerXp = asNumber(card.agent_xp ?? card.career_xp);
|
|
1689
|
-
const leaderboardEligible = Boolean(
|
|
1690
|
-
card.leaderboard_eligible ?? integrity.leaderboard_eligible
|
|
1691
|
-
);
|
|
1692
|
-
|
|
1693
|
-
return {
|
|
1694
|
-
metric_label: AGENT_XP_LABEL,
|
|
1695
|
-
agent_xp: careerXp,
|
|
1696
|
-
career_xp: careerXp,
|
|
1697
|
-
level: levelFromXp(careerXp),
|
|
1698
|
-
operator: score.operator || null,
|
|
1699
|
-
leaderboard_eligible: leaderboardEligible,
|
|
1700
|
-
source: 'local_contribution_score',
|
|
1701
|
-
workspace_root: score.workspace_root || workspace,
|
|
1702
|
-
current_form_by_arena: {
|
|
1703
|
-
local_workspace: {
|
|
1704
|
-
ovr: asNumber(card.ovr || card.current_form),
|
|
1705
|
-
current_form: asNumber(card.current_form || card.ovr),
|
|
1706
|
-
visible_stats: Array.isArray(card.visible_stats) ? card.visible_stats : [],
|
|
1707
|
-
leaderboard_eligible: leaderboardEligible,
|
|
1708
|
-
integrity_status: score.label || integrity.status || 'unknown',
|
|
1709
|
-
},
|
|
1710
|
-
},
|
|
1711
|
-
contribution_graph: score.contribution_graph || {},
|
|
1712
|
-
};
|
|
1713
|
-
}
|
|
1714
|
-
|
|
1715
1719
|
function renderContributionGraph(graph) {
|
|
1716
1720
|
if (!graph || !Array.isArray(graph.days)) return;
|
|
1717
1721
|
console.log('');
|
|
@@ -1729,36 +1733,6 @@ function renderLocalActivity(activity) {
|
|
|
1729
1733
|
);
|
|
1730
1734
|
}
|
|
1731
1735
|
|
|
1732
|
-
function loadLocalPayload(args) {
|
|
1733
|
-
const workspace = path.resolve(readFlag(args, '--workspace', process.cwd()));
|
|
1734
|
-
const operator = readFlag(
|
|
1735
|
-
args,
|
|
1736
|
-
'--operator',
|
|
1737
|
-
process.env.ATRIS_OPERATOR || process.env.USER || os.userInfo().username
|
|
1738
|
-
);
|
|
1739
|
-
const script = path.join(workspace, 'scripts', 'contribution_score.py');
|
|
1740
|
-
|
|
1741
|
-
if (!fs.existsSync(script)) {
|
|
1742
|
-
throw new Error(`No local contribution scorer found at ${path.relative(process.cwd(), script)}`);
|
|
1743
|
-
}
|
|
1744
|
-
|
|
1745
|
-
const result = spawnSync(
|
|
1746
|
-
process.env.PYTHON || 'python3',
|
|
1747
|
-
[script, '--workspace', workspace, '--operator', operator, '--json'],
|
|
1748
|
-
{ cwd: workspace, encoding: 'utf8', maxBuffer: 20 * 1024 * 1024 }
|
|
1749
|
-
);
|
|
1750
|
-
|
|
1751
|
-
if (result.error) {
|
|
1752
|
-
throw result.error;
|
|
1753
|
-
}
|
|
1754
|
-
if (result.status !== 0) {
|
|
1755
|
-
const detail = (result.stderr || result.stdout || '').trim();
|
|
1756
|
-
throw new Error(detail || `Local scorer exited ${result.status}`);
|
|
1757
|
-
}
|
|
1758
|
-
|
|
1759
|
-
return normalizeLocalScore(JSON.parse(result.stdout), workspace);
|
|
1760
|
-
}
|
|
1761
|
-
|
|
1762
1736
|
function earnedAgentXp(value) {
|
|
1763
1737
|
return Math.max(0, asNumber(value));
|
|
1764
1738
|
}
|
|
@@ -1945,6 +1919,7 @@ function buildAgentXpSyncPacket(args = []) {
|
|
|
1945
1919
|
const publicXp = earnedAgentXp(totalXp);
|
|
1946
1920
|
const currentForm = currentFormScore(totalXp);
|
|
1947
1921
|
const levelProgress = agentXpLevelProgress(publicXp);
|
|
1922
|
+
const contributionGraph = sanitizeContributionGraphForSync(projection.contribution_graph);
|
|
1948
1923
|
const workspaceRootHash = sha256(workspaces.map(item => item.workspace_root_hash || item.name).sort().join(':'));
|
|
1949
1924
|
const entry = {
|
|
1950
1925
|
user_id: player,
|
|
@@ -1964,6 +1939,7 @@ function buildAgentXpSyncPacket(args = []) {
|
|
|
1964
1939
|
lock_reason: eligible ? null : 'not_enough_trusted_proof',
|
|
1965
1940
|
public_adjustment: null,
|
|
1966
1941
|
next_move: eligible ? 'Play the next proof-backed AgentXP mission.' : 'Complete one proof-backed AgentXP rep.',
|
|
1942
|
+
contribution_graph: contributionGraph,
|
|
1967
1943
|
};
|
|
1968
1944
|
const packet = {
|
|
1969
1945
|
schema: 'atris.agentxp_sync_packet.v1',
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawnSync } = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { taskProofState } = require('./task-proof');
|
|
7
|
+
|
|
8
|
+
const AGENT_CERTIFICATION_REVIEW_PASSES = 2;
|
|
9
|
+
const AUTO_ACCEPT_HIGH_CONFIDENCE_PASSES = 3;
|
|
10
|
+
const DENIED_TAGS = new Set(['billing', 'deploy', 'feedback', 'voice', 'security', 'customer', 'external']);
|
|
11
|
+
|
|
12
|
+
const SIMPLE_VERIFY_TOKEN_RE = /^[a-zA-Z0-9_./:@=+-]+$/;
|
|
13
|
+
const GIT_WORKTREE_PATH_RE = /^[a-zA-Z0-9_./@=+-]+$/;
|
|
14
|
+
const GIT_REV_TOKEN_RE = /^[a-zA-Z0-9_./@=+~^-]+$/;
|
|
15
|
+
|
|
16
|
+
function hasUnsafePathSegment(token) {
|
|
17
|
+
const text = String(token || '');
|
|
18
|
+
return text.startsWith('/')
|
|
19
|
+
|| /^[a-zA-Z]:[\\/]/.test(text)
|
|
20
|
+
|| text.split(/[\\/]+/).includes('..');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function safeVerifyToken(token) {
|
|
24
|
+
return SIMPLE_VERIFY_TOKEN_RE.test(token) && !hasUnsafePathSegment(token);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function safeRelativePathToken(token) {
|
|
28
|
+
const text = String(token || '');
|
|
29
|
+
return Boolean(text)
|
|
30
|
+
&& !text.startsWith('-')
|
|
31
|
+
&& !text.includes(':')
|
|
32
|
+
&& safeVerifyToken(text);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function safeGitWorktreePathToken(token) {
|
|
36
|
+
const text = String(token || '');
|
|
37
|
+
return Boolean(text)
|
|
38
|
+
&& !text.startsWith('-')
|
|
39
|
+
&& !text.includes(':')
|
|
40
|
+
&& GIT_WORKTREE_PATH_RE.test(text)
|
|
41
|
+
&& !text.split(/[\\/]+/).includes('..');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function safeGitRevToken(token) {
|
|
45
|
+
const text = String(token || '');
|
|
46
|
+
return Boolean(text)
|
|
47
|
+
&& !text.startsWith('-')
|
|
48
|
+
&& !text.includes(':')
|
|
49
|
+
&& GIT_REV_TOKEN_RE.test(text)
|
|
50
|
+
&& !text.split(/[\\/]+/).includes('..');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function safeNodePathArgs(args) {
|
|
54
|
+
return args.every(token => safeRelativePathToken(token));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function safeGitDiffCheckArgs(args) {
|
|
58
|
+
return args.length === 0 || (args.length <= 2 && args.every(safeGitRevToken));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isAllowedGitDiffCheck(argv) {
|
|
62
|
+
const [bin, first, second, third, fourth] = argv;
|
|
63
|
+
if (bin !== 'git') return false;
|
|
64
|
+
if (first === 'diff' && second === '--check') {
|
|
65
|
+
return safeGitDiffCheckArgs(argv.slice(3));
|
|
66
|
+
}
|
|
67
|
+
if (first === '-C' && safeGitWorktreePathToken(second) && third === 'diff' && fourth === '--check') {
|
|
68
|
+
return safeGitDiffCheckArgs(argv.slice(5));
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isInsidePath(candidate, root) {
|
|
74
|
+
const relative = path.relative(root, candidate);
|
|
75
|
+
return relative === '' || (relative && !relative.startsWith('..') && !path.isAbsolute(relative));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function validateGitWorktreePath(argv, workspaceRoot) {
|
|
79
|
+
if (!(argv[0] === 'git' && argv[1] === '-C')) return { ok: true };
|
|
80
|
+
const workspace = path.resolve(workspaceRoot || process.cwd());
|
|
81
|
+
const target = path.resolve(workspace, argv[2]);
|
|
82
|
+
const allowedRoot = path.dirname(workspace);
|
|
83
|
+
if (!isInsidePath(target, workspace) && !isInsidePath(target, allowedRoot)) {
|
|
84
|
+
return { ok: false, reason: 'verify_command_not_allowed' };
|
|
85
|
+
}
|
|
86
|
+
if (!fs.existsSync(target)) return { ok: false, reason: 'verify_worktree_missing' };
|
|
87
|
+
return { ok: true };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function reviewPassCount(task) {
|
|
91
|
+
const metadata = task.metadata || {};
|
|
92
|
+
const review = task.review || {};
|
|
93
|
+
const count = Number(metadata.agent_review_pass_count ?? review.agent_review_pass_count);
|
|
94
|
+
return Number.isFinite(count) ? count : 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isAgentCertified(task) {
|
|
98
|
+
const metadata = task.metadata || {};
|
|
99
|
+
const review = task.review || {};
|
|
100
|
+
const handoff = review.handoff && typeof review.handoff === 'object' ? review.handoff : {};
|
|
101
|
+
return metadata.agent_certified === true
|
|
102
|
+
|| review.agent_certified === true
|
|
103
|
+
|| handoff.native_goal_status === 'agent_certified'
|
|
104
|
+
|| reviewPassCount(task) >= AGENT_CERTIFICATION_REVIEW_PASSES;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function latestProof(task) {
|
|
108
|
+
const metadata = task.metadata || {};
|
|
109
|
+
const review = task.review || {};
|
|
110
|
+
return String(metadata.latest_agent_proof || review.proof || '').trim();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function proofSentences(proof) {
|
|
114
|
+
return String(proof || '')
|
|
115
|
+
.replace(/([.!?])\s+/g, '$1\n')
|
|
116
|
+
.split(/[\n\r]+/)
|
|
117
|
+
.map((sentence) => sentence.trim())
|
|
118
|
+
.filter(Boolean);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function sentenceHasPullRequestReference(sentence) {
|
|
122
|
+
return /\bPR\s*#?\d+\b/i.test(sentence)
|
|
123
|
+
|| /\bpull request\s*#?\d+\b/i.test(sentence)
|
|
124
|
+
|| /\bgithub\.com\/\S+\/pull\/\d+\b/i.test(sentence)
|
|
125
|
+
|| /#\d+\b/.test(sentence);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function sentenceHasUnmergedPullRequestBoundary(sentence) {
|
|
129
|
+
return sentenceHasPullRequestReference(sentence)
|
|
130
|
+
&& (/\bopen\s+draft\b/i.test(sentence)
|
|
131
|
+
|| /\bopen\/draft\b/i.test(sentence)
|
|
132
|
+
|| /\bopen\s+and\s+draft\s*=\s*true\b/i.test(sentence)
|
|
133
|
+
|| /\bdraft\s*=\s*true\b/i.test(sentence)
|
|
134
|
+
|| /\bisDraft\s*[:=]\s*true\b/i.test(sentence)
|
|
135
|
+
|| /\bmergedAt\s*[:=]\s*null\b/i.test(sentence)
|
|
136
|
+
|| /\bclosed\s+(?:with\s+)?mergedAt\s*[:=]\s*null\b/i.test(sentence));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function proofHasUnmergedPullRequestBoundary(proof) {
|
|
140
|
+
return proofSentences(proof).some(sentenceHasUnmergedPullRequestBoundary);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function unmergedPullRequestBoundaryResult(ref, proof) {
|
|
144
|
+
return {
|
|
145
|
+
eligible: false,
|
|
146
|
+
ref,
|
|
147
|
+
reason: 'proof_unmerged_or_draft_pr_boundary',
|
|
148
|
+
next_action: 'revise the task out of Review or narrow the proof to draft/local-proof only before auto-accept',
|
|
149
|
+
proof,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function distinctReviewActors(task) {
|
|
154
|
+
const actors = new Set();
|
|
155
|
+
for (const event of task.events || []) {
|
|
156
|
+
if (!['proof_ready', 'reviewed'].includes(event.event_type)) continue;
|
|
157
|
+
const actor = event.actor || event.payload?.actor;
|
|
158
|
+
if (actor) actors.add(String(actor));
|
|
159
|
+
}
|
|
160
|
+
return actors;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function parseVerifyCommand(verify) {
|
|
164
|
+
const cmd = String(verify || '').trim();
|
|
165
|
+
if (!cmd) return { ok: false, reason: 'no_verify_command' };
|
|
166
|
+
if (/[;&|`$<>\n\r]/.test(cmd)) return { ok: false, reason: 'verify_command_not_allowed' };
|
|
167
|
+
const argv = cmd.split(/\s+/).filter(Boolean);
|
|
168
|
+
if (!argv.length || argv.some((token, index) => {
|
|
169
|
+
if (argv[0] === 'git' && argv[1] === '-C' && index === 2) {
|
|
170
|
+
return !safeGitWorktreePathToken(token);
|
|
171
|
+
}
|
|
172
|
+
if (argv[0] === 'git' && ['diff', '-C'].includes(argv[1]) && index >= (argv[1] === '-C' ? 5 : 3)) {
|
|
173
|
+
return !safeGitRevToken(token);
|
|
174
|
+
}
|
|
175
|
+
return !safeVerifyToken(token);
|
|
176
|
+
})) {
|
|
177
|
+
return { ok: false, reason: 'verify_command_not_allowed' };
|
|
178
|
+
}
|
|
179
|
+
const [bin, first, second] = argv;
|
|
180
|
+
const allowed = (bin === 'npm' && (
|
|
181
|
+
(first === 'test' && argv.length === 2)
|
|
182
|
+
|| (first === 'run' && Boolean(second) && !second.startsWith('-') && argv.length === 3)
|
|
183
|
+
))
|
|
184
|
+
|| (bin === 'node' && (
|
|
185
|
+
(first === '--test' && safeNodePathArgs(argv.slice(2)))
|
|
186
|
+
|| (first === '--check' && argv.length === 3 && safeRelativePathToken(second))
|
|
187
|
+
|| (/^scripts\/[a-zA-Z0-9_./-]+$/.test(first || '') && safeNodePathArgs(argv.slice(1)))
|
|
188
|
+
))
|
|
189
|
+
|| (bin === 'tsc' && argv.length === 1)
|
|
190
|
+
|| isAllowedGitDiffCheck(argv);
|
|
191
|
+
if (!allowed) return { ok: false, reason: 'verify_command_not_allowed' };
|
|
192
|
+
return { ok: true, argv };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function runVerifyCommand(verify, workspaceRoot) {
|
|
196
|
+
const parsed = parseVerifyCommand(verify);
|
|
197
|
+
if (!parsed.ok) return parsed;
|
|
198
|
+
const gitPathCheck = validateGitWorktreePath(parsed.argv, workspaceRoot);
|
|
199
|
+
if (!gitPathCheck.ok) return gitPathCheck;
|
|
200
|
+
const result = spawnSync(parsed.argv[0], parsed.argv.slice(1), {
|
|
201
|
+
cwd: workspaceRoot,
|
|
202
|
+
shell: false,
|
|
203
|
+
encoding: 'utf8',
|
|
204
|
+
timeout: 120000,
|
|
205
|
+
});
|
|
206
|
+
return {
|
|
207
|
+
ok: result.status === 0,
|
|
208
|
+
reason: result.status === 0 ? 'verify_passed' : 'verify_failed',
|
|
209
|
+
status: result.status,
|
|
210
|
+
stderr: String(result.stderr || '').slice(0, 400),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function strictVerifyMissingResult(ref) {
|
|
215
|
+
return {
|
|
216
|
+
eligible: false,
|
|
217
|
+
ref,
|
|
218
|
+
reason: 'strict_verify_missing',
|
|
219
|
+
next_action: 'rerun the review verifier and record a safe metadata.verify command before strict auto-accept',
|
|
220
|
+
review_chat_command: `atris task review-chat ${ref} --as codex-review`,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function evaluateAutoAccept(task, options = {}) {
|
|
225
|
+
const { strictVerify = false, minPasses = AGENT_CERTIFICATION_REVIEW_PASSES } = options;
|
|
226
|
+
const ref = task.display_id || task.legacy_ref || task.id;
|
|
227
|
+
if (task.status !== 'review') return { eligible: false, ref, reason: 'not_in_review' };
|
|
228
|
+
const metadata = task.metadata || {};
|
|
229
|
+
const review = task.review || {};
|
|
230
|
+
const approval = String(review.approval_status || metadata.approval_status || 'pending').toLowerCase();
|
|
231
|
+
if (approval && approval !== 'pending' && approval !== 'agent_certified') {
|
|
232
|
+
return { eligible: false, ref, reason: `approval_${approval}` };
|
|
233
|
+
}
|
|
234
|
+
if (metadata.auto_accepted_at) return { eligible: false, ref, reason: 'already_auto_accepted' };
|
|
235
|
+
|
|
236
|
+
const tag = String(task.tag || '').toLowerCase();
|
|
237
|
+
if (DENIED_TAGS.has(tag)) return { eligible: false, ref, reason: `denied_tag_${tag}` };
|
|
238
|
+
|
|
239
|
+
if (!isAgentCertified(task)) return { eligible: false, ref, reason: 'not_agent_certified' };
|
|
240
|
+
|
|
241
|
+
const passes = reviewPassCount(task);
|
|
242
|
+
if (passes < minPasses) return { eligible: false, ref, reason: 'insufficient_review_passes', passes };
|
|
243
|
+
|
|
244
|
+
const proof = latestProof(task);
|
|
245
|
+
if (proofHasUnmergedPullRequestBoundary(proof)) {
|
|
246
|
+
return unmergedPullRequestBoundaryResult(ref, proof);
|
|
247
|
+
}
|
|
248
|
+
const proofCheck = taskProofState(proof);
|
|
249
|
+
if (!proofCheck.ok) return { eligible: false, ref, reason: proofCheck.reason, proof };
|
|
250
|
+
|
|
251
|
+
const actors = distinctReviewActors(task);
|
|
252
|
+
const multiActor = actors.size >= 2;
|
|
253
|
+
const highConfidence = passes >= AUTO_ACCEPT_HIGH_CONFIDENCE_PASSES;
|
|
254
|
+
if (!multiActor && !highConfidence) {
|
|
255
|
+
return {
|
|
256
|
+
eligible: false,
|
|
257
|
+
ref,
|
|
258
|
+
reason: 'needs_second_reviewer_or_third_pass',
|
|
259
|
+
passes,
|
|
260
|
+
actors: [...actors],
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (strictVerify) {
|
|
265
|
+
const verify = metadata.verify;
|
|
266
|
+
if (!verify) return strictVerifyMissingResult(ref);
|
|
267
|
+
const workspaceRoot = task.workspace_root || process.cwd();
|
|
268
|
+
const verifyResult = runVerifyCommand(verify, workspaceRoot);
|
|
269
|
+
if (!verifyResult.ok) {
|
|
270
|
+
return { eligible: false, ref, reason: verifyResult.reason, verify, ...verifyResult };
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
eligible: true,
|
|
276
|
+
ref,
|
|
277
|
+
reason: strictVerify
|
|
278
|
+
? 'certified_strict_verify'
|
|
279
|
+
: (highConfidence ? 'certified_high_confidence' : 'certified_multi_actor'),
|
|
280
|
+
passes,
|
|
281
|
+
actors: [...actors],
|
|
282
|
+
proof,
|
|
283
|
+
policy: strictVerify ? 'strict_verify' : (highConfidence ? '3_passes' : '2_actors_2_passes'),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
module.exports = {
|
|
288
|
+
AGENT_CERTIFICATION_REVIEW_PASSES,
|
|
289
|
+
AUTO_ACCEPT_HIGH_CONFIDENCE_PASSES,
|
|
290
|
+
DENIED_TAGS,
|
|
291
|
+
evaluateAutoAccept,
|
|
292
|
+
parseVerifyCommand,
|
|
293
|
+
runVerifyCommand,
|
|
294
|
+
};
|
package/lib/file-ops.js
CHANGED
|
@@ -87,13 +87,6 @@ function createLogFile(logFile, dateFormatted) {
|
|
|
87
87
|
fs.writeFileSync(logFile, initialContent);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
function getTimeLabel() {
|
|
91
|
-
const now = new Date();
|
|
92
|
-
const hours = String(now.getHours()).padStart(2, '0');
|
|
93
|
-
const minutes = String(now.getMinutes()).padStart(2, '0');
|
|
94
|
-
return `${hours}:${minutes}`;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
90
|
// Inbox operations
|
|
98
91
|
function parseInboxItems(content) {
|
|
99
92
|
const match = content.match(/## Inbox\n([\s\S]*?)(?=\n##|\n---|$)/);
|
|
@@ -139,11 +132,6 @@ function addInboxItemToContent(content, id, summary) {
|
|
|
139
132
|
return replaceInboxSection(content, updatedItems);
|
|
140
133
|
}
|
|
141
134
|
|
|
142
|
-
function removeInboxItemFromContent(content, id) {
|
|
143
|
-
const items = parseInboxItems(content).filter((item) => item.id !== id);
|
|
144
|
-
return replaceInboxSection(content, items);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
135
|
function getNextInboxId(content) {
|
|
148
136
|
const items = parseInboxItems(content);
|
|
149
137
|
if (items.length === 0) return 1;
|
|
@@ -158,185 +146,13 @@ function addInboxIdea(logFile, summary) {
|
|
|
158
146
|
return nextId;
|
|
159
147
|
}
|
|
160
148
|
|
|
161
|
-
// Completion operations
|
|
162
|
-
function parseCompletionItems(content) {
|
|
163
|
-
const match = content.match(/## Completed ✅\n([\s\S]*?)(?=\n##|\n---|$)/);
|
|
164
|
-
if (!match) {
|
|
165
|
-
return [];
|
|
166
|
-
}
|
|
167
|
-
const body = match[1];
|
|
168
|
-
const lines = body.split('\n');
|
|
169
|
-
const items = [];
|
|
170
|
-
lines.forEach((line) => {
|
|
171
|
-
const trimmed = line.trim();
|
|
172
|
-
if (!trimmed) return;
|
|
173
|
-
if (trimmed.startsWith('(Empty')) return;
|
|
174
|
-
const parsed = trimmed.match(/^- \*\*C(\d+):\*\*\s*(.+)$|^- \*\*C(\d+):\s+(.+)$/);
|
|
175
|
-
if (parsed) {
|
|
176
|
-
const id = parseInt(parsed[1] || parsed[3], 10);
|
|
177
|
-
const text = parsed[2] || parsed[4];
|
|
178
|
-
items.push({ id, text, line: trimmed });
|
|
179
|
-
}
|
|
180
|
-
});
|
|
181
|
-
return items;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function replaceCompletedSection(content, items) {
|
|
185
|
-
const regex = /(## Completed ✅\n)([\s\S]*?)(\n---|\n##|$)/;
|
|
186
|
-
if (!regex.test(content)) {
|
|
187
|
-
const lines = items.length ? items.map((item) => item.line).join('\n') : '';
|
|
188
|
-
return `${content}\n\n## Completed ✅\n\n${lines}\n`;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return content.replace(regex, (match, header, body, suffix) => {
|
|
192
|
-
const inner = items.length
|
|
193
|
-
? `\n${items.map((item) => item.line).join('\n')}\n`
|
|
194
|
-
: '\n';
|
|
195
|
-
return `${header}${inner}${suffix}`;
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function addCompletionItemToContent(content, id, summary) {
|
|
200
|
-
const items = parseCompletionItems(content).filter((item) => item.id !== id);
|
|
201
|
-
const newItem = { id, text: summary, line: `- **C${id}:** ${summary}` };
|
|
202
|
-
const updatedItems = [...items, newItem];
|
|
203
|
-
return replaceCompletedSection(content, updatedItems);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function getNextCompletionId(content) {
|
|
207
|
-
const items = parseCompletionItems(content);
|
|
208
|
-
if (items.length === 0) return 1;
|
|
209
|
-
return items.reduce((max, item) => (item.id > max ? item.id : max), 0) + 1;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Notes operations
|
|
213
|
-
function insertIntoNotesSection(content, block) {
|
|
214
|
-
const regex = /(## Notes\n)([\s\S]*?)(\n---|\n##|$)/;
|
|
215
|
-
const match = content.match(regex);
|
|
216
|
-
if (!match) {
|
|
217
|
-
return `${content}\n\n## Notes\n\n${block}\n`;
|
|
218
|
-
}
|
|
219
|
-
const header = match[1];
|
|
220
|
-
const body = match[2];
|
|
221
|
-
const suffix = match[3];
|
|
222
|
-
const trimmedBody = body.replace(/\s*$/, '');
|
|
223
|
-
const newBody = trimmedBody
|
|
224
|
-
? `${trimmedBody}\n\n${block}\n`
|
|
225
|
-
: `\n${block}\n`;
|
|
226
|
-
return content.replace(regex, `${header}${newBody}${suffix}`);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function recordBrainstormSession(
|
|
230
|
-
logFile,
|
|
231
|
-
sourceLabel,
|
|
232
|
-
topic,
|
|
233
|
-
desiredOutcome,
|
|
234
|
-
keyQuestions,
|
|
235
|
-
focusAreas,
|
|
236
|
-
constraints,
|
|
237
|
-
references,
|
|
238
|
-
tonePreference,
|
|
239
|
-
nextSteps,
|
|
240
|
-
sessionSummary
|
|
241
|
-
) {
|
|
242
|
-
let content = fs.readFileSync(logFile, 'utf8');
|
|
243
|
-
const lines = [
|
|
244
|
-
`### Brainstorm Session — ${getTimeLabel()}`,
|
|
245
|
-
`**Source:** ${sourceLabel}`,
|
|
246
|
-
`**Topic:** ${topic}`,
|
|
247
|
-
];
|
|
248
|
-
if (desiredOutcome) {
|
|
249
|
-
lines.push(`**User Story / Desired Outcome:** ${desiredOutcome}`);
|
|
250
|
-
}
|
|
251
|
-
if (tonePreference) {
|
|
252
|
-
lines.push(`**Vibe / Feelings:** ${tonePreference}`);
|
|
253
|
-
}
|
|
254
|
-
if (keyQuestions && keyQuestions.length > 0) {
|
|
255
|
-
lines.push('**Key Questions:**');
|
|
256
|
-
keyQuestions.forEach((item) => lines.push(`- ${item}`));
|
|
257
|
-
}
|
|
258
|
-
if (focusAreas && focusAreas.length > 0) {
|
|
259
|
-
lines.push('**Focus Areas:**');
|
|
260
|
-
focusAreas.forEach((item) => lines.push(`- ${item}`));
|
|
261
|
-
}
|
|
262
|
-
if (constraints) {
|
|
263
|
-
lines.push(`**Constraints:** ${constraints}`);
|
|
264
|
-
}
|
|
265
|
-
if (references) {
|
|
266
|
-
lines.push(`**Context / References:** ${references}`);
|
|
267
|
-
}
|
|
268
|
-
if (sessionSummary) {
|
|
269
|
-
lines.push(`**Session Summary:** ${sessionSummary}`);
|
|
270
|
-
}
|
|
271
|
-
if (nextSteps && nextSteps.length > 0) {
|
|
272
|
-
lines.push('**Next Steps:**');
|
|
273
|
-
nextSteps.forEach((item) => lines.push(`- ${item}`));
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const block = lines.join('\n');
|
|
277
|
-
content = insertIntoNotesSection(content, block);
|
|
278
|
-
fs.writeFileSync(logFile, content);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function recordAutopilotVision(logFile, sourceLabel, summary, successCriteria, riskNotes) {
|
|
282
|
-
const content = fs.readFileSync(logFile, 'utf8');
|
|
283
|
-
const lines = [
|
|
284
|
-
`### Autopilot Vision — ${getTimeLabel()}`,
|
|
285
|
-
`**Source:** ${sourceLabel}`,
|
|
286
|
-
`**Summary:** ${summary}`,
|
|
287
|
-
'**Success Criteria:**',
|
|
288
|
-
...successCriteria.map((item) => `- ${item}`),
|
|
289
|
-
];
|
|
290
|
-
if (riskNotes && riskNotes.trim()) {
|
|
291
|
-
lines.push(`**Risks / Notes:** ${riskNotes}`);
|
|
292
|
-
}
|
|
293
|
-
const block = lines.join('\n');
|
|
294
|
-
const updated = insertIntoNotesSection(content, block);
|
|
295
|
-
fs.writeFileSync(logFile, updated);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function recordAutopilotIteration(logFile, iteration, result, notes) {
|
|
299
|
-
const content = fs.readFileSync(logFile, 'utf8');
|
|
300
|
-
const lines = [
|
|
301
|
-
`### Autopilot Iteration ${iteration} — ${getTimeLabel()}`,
|
|
302
|
-
`**Validator Result:** ${result}`,
|
|
303
|
-
];
|
|
304
|
-
if (notes && notes.trim()) {
|
|
305
|
-
lines.push(`**Notes:** ${notes}`);
|
|
306
|
-
}
|
|
307
|
-
const block = lines.join('\n');
|
|
308
|
-
const updated = insertIntoNotesSection(content, block);
|
|
309
|
-
fs.writeFileSync(logFile, updated);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function recordAutopilotSuccess(logFile, inboxId, summary) {
|
|
313
|
-
let content = fs.readFileSync(logFile, 'utf8');
|
|
314
|
-
if (typeof inboxId === 'number' && !Number.isNaN(inboxId)) {
|
|
315
|
-
content = removeInboxItemFromContent(content, inboxId);
|
|
316
|
-
}
|
|
317
|
-
const nextId = getNextCompletionId(content);
|
|
318
|
-
content = addCompletionItemToContent(content, nextId, `Autopilot — ${summary}`);
|
|
319
|
-
fs.writeFileSync(logFile, content);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
149
|
module.exports = {
|
|
323
150
|
getLogPath,
|
|
324
151
|
ensureLogDirectory,
|
|
325
152
|
createLogFile,
|
|
326
|
-
getTimeLabel,
|
|
327
153
|
parseInboxItems,
|
|
328
154
|
replaceInboxSection,
|
|
329
155
|
addInboxItemToContent,
|
|
330
|
-
removeInboxItemFromContent,
|
|
331
156
|
getNextInboxId,
|
|
332
157
|
addInboxIdea,
|
|
333
|
-
parseCompletionItems,
|
|
334
|
-
replaceCompletedSection,
|
|
335
|
-
addCompletionItemToContent,
|
|
336
|
-
getNextCompletionId,
|
|
337
|
-
insertIntoNotesSection,
|
|
338
|
-
recordBrainstormSession,
|
|
339
|
-
recordAutopilotVision,
|
|
340
|
-
recordAutopilotIteration,
|
|
341
|
-
recordAutopilotSuccess,
|
|
342
158
|
};
|