atris 3.53.0 → 3.56.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/FOR_AGENTS.md +7 -0
- package/README.md +1 -1
- package/atris/policies/ANTISLOP.md +2 -1
- package/atris/skills/copy-editor/SKILL.md +2 -1
- package/atris/skills/design/SKILL.md +1 -1
- package/atris/skills/x-search/SKILL.md +24 -22
- package/atris/skills/youtube/SKILL.md +82 -33
- package/atris/team/_template/SOUL.md +40 -0
- package/atris/team/validator/MEMBER.md +1 -2
- package/bin/atris +21 -0
- package/bin/atris.js +563 -373
- package/commands/api-key.js +170 -0
- package/commands/auth.js +453 -43
- package/commands/autoland.js +3 -1
- package/commands/autopilot-front.js +58 -11
- package/commands/avail.js +5 -4
- package/commands/balance.js +55 -0
- package/commands/bench.js +29 -1
- package/commands/brain.js +63 -12
- package/commands/brainstorm.js +18 -0
- package/commands/business.js +149 -1
- package/commands/chat-scan.js +17 -4
- package/commands/close.js +5 -1
- package/commands/doctor.js +174 -0
- package/commands/dream.js +7 -0
- package/commands/drill.js +34 -2
- package/commands/engine.js +47 -35
- package/commands/errors.js +58 -8
- package/commands/experiments.js +14 -0
- package/commands/feedback.js +61 -2
- package/commands/fleet-report.js +27 -7
- package/commands/fleet.js +98 -11
- package/commands/founder.js +60 -21
- package/commands/human-missions.js +79 -4
- package/commands/improve.js +54 -4
- package/commands/init.js +68 -92
- package/commands/install.js +40 -0
- package/commands/integrations.js +565 -30
- package/commands/interview.js +8 -0
- package/commands/land.js +48 -17
- package/commands/learn.js +29 -4
- package/commands/log.js +51 -0
- package/commands/member.js +68 -10
- package/commands/mission.js +287 -50
- package/commands/now.js +15 -3
- package/commands/orb.js +97 -2
- package/commands/pack.js +83 -9
- package/commands/playbook.js +381 -0
- package/commands/plugin.js +16 -0
- package/commands/radar.js +31 -7
- package/commands/recap.js +128 -21
- package/commands/release.js +102 -2
- package/commands/review.js +5 -10
- package/commands/revisions.js +55 -0
- package/commands/run-front.js +12 -4
- package/commands/scout.js +12 -1
- package/commands/search.js +123 -28
- package/commands/sign.js +2 -2
- package/commands/signup.js +12 -3
- package/commands/site-deploy.js +17 -4
- package/commands/site.js +2 -2
- package/commands/skill-eval.js +289 -0
- package/commands/skill.js +56 -6
- package/commands/social.js +603 -0
- package/commands/spaceship.js +69 -6
- package/commands/stream.js +8 -1
- package/commands/study.js +32 -19
- package/commands/sync.js +28 -22
- package/commands/task.js +435 -60
- package/commands/teach.js +72 -63
- package/commands/terminal.js +25 -9
- package/commands/topup.js +88 -0
- package/commands/truth.js +52 -6
- package/commands/usage.js +91 -0
- package/commands/verify.js +3 -2
- package/commands/version.js +3 -1
- package/commands/voice.js +5 -1
- package/commands/who.js +69 -11
- package/commands/wiki.js +7 -4
- package/commands/wish.js +76 -5
- package/commands/workflow.js +327 -145
- package/commands/worktree.js +1 -0
- package/commands/write.js +5 -0
- package/commands/x-search.js +340 -0
- package/commands/xp.js +5 -1
- package/commands/youtube.js +551 -25
- package/lib/account-bound.js +47 -0
- package/lib/auto-accept-certified.js +8 -1
- package/lib/auto-lessons.js +186 -0
- package/lib/autoland.js +4 -2
- package/lib/chat-log-scan.js +7 -4
- package/lib/cli-json.js +77 -0
- package/lib/cli-scope.js +26 -0
- package/lib/codex-flight.js +1 -0
- package/lib/conductor-artifacts.js +1 -1
- package/lib/context-gatherer.js +11 -0
- package/lib/developer-api.js +116 -0
- package/lib/feedback-triage.js +274 -0
- package/lib/first-minute.js +398 -0
- package/lib/fleet.js +115 -34
- package/lib/functional-owner.js +22 -0
- package/lib/known-commands.js +31 -5
- package/lib/member-scaffold.js +197 -0
- package/lib/mission-root.js +4 -2
- package/lib/noninteractive.js +88 -0
- package/lib/policy-lessons.js +4 -1
- package/lib/revision-metric.js +279 -0
- package/lib/scratch-root.js +48 -0
- package/lib/skill-eval-gate.js +249 -0
- package/lib/task-db.js +4 -0
- package/lib/task-proof.js +69 -8
- package/lib/verifier-quality.js +69 -0
- package/lib/wish-audit.js +2 -2
- package/lib/wish-delegate.js +10 -1
- package/lib/workspace-scaffold.js +270 -0
- package/package.json +3 -2
- package/scripts/det/checklist-score.js +191 -0
- package/scripts/det/ytsearch +31 -0
- package/utils/update-check.js +15 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { spawnSync } = require('node:child_process');
|
|
6
|
+
|
|
7
|
+
const SKILL_EVAL_SCHEMA = 'atris.skill_eval.v1';
|
|
8
|
+
const SCORECARDS_PATH = path.join('.atris', 'state', 'scorecards.jsonl');
|
|
9
|
+
const BASE_REFS = Object.freeze(['origin/master', 'origin/main', 'master', 'main']);
|
|
10
|
+
|
|
11
|
+
function runGit(root, args) {
|
|
12
|
+
try {
|
|
13
|
+
return spawnSync('git', args, {
|
|
14
|
+
cwd: root,
|
|
15
|
+
encoding: 'utf8',
|
|
16
|
+
timeout: 10000,
|
|
17
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
18
|
+
});
|
|
19
|
+
} catch (error) {
|
|
20
|
+
return { status: 1, stdout: '', stderr: String(error && error.message ? error.message : error) };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizedRelativePath(root, value) {
|
|
25
|
+
const text = String(value || '').trim().replace(/\\/g, '/');
|
|
26
|
+
if (!text || text.includes('\0') || path.isAbsolute(text)) return null;
|
|
27
|
+
const absolute = path.resolve(root, text);
|
|
28
|
+
const relative = path.relative(root, absolute).replace(/\\/g, '/');
|
|
29
|
+
if (!relative || relative === '..' || relative.startsWith('../')) return null;
|
|
30
|
+
return relative;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function nulTerminatedPaths(result, root) {
|
|
34
|
+
if (!result || result.status !== 0) return [];
|
|
35
|
+
return String(result.stdout || '')
|
|
36
|
+
.split('\0')
|
|
37
|
+
.filter(Boolean)
|
|
38
|
+
.map((entry) => normalizedRelativePath(root, entry))
|
|
39
|
+
.filter(Boolean);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function resolveBaseRef(root) {
|
|
43
|
+
const head = runGit(root, ['rev-parse', '--verify', 'HEAD^{commit}']);
|
|
44
|
+
if (head.status !== 0) return null;
|
|
45
|
+
for (const ref of BASE_REFS) {
|
|
46
|
+
const candidate = runGit(root, ['rev-parse', '--verify', `${ref}^{commit}`]);
|
|
47
|
+
if (candidate.status === 0) return ref;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function indexMtime(root) {
|
|
53
|
+
const result = runGit(root, ['rev-parse', '--git-path', 'index']);
|
|
54
|
+
if (result.status !== 0) return 0;
|
|
55
|
+
const value = String(result.stdout || '').trim();
|
|
56
|
+
const file = path.isAbsolute(value) ? value : path.resolve(root, value);
|
|
57
|
+
try {
|
|
58
|
+
return fs.statSync(file).mtimeMs;
|
|
59
|
+
} catch {
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function fileChangeMtime(root, skillPath) {
|
|
65
|
+
const absolute = path.join(root, skillPath);
|
|
66
|
+
try {
|
|
67
|
+
return fs.statSync(absolute).mtimeMs;
|
|
68
|
+
} catch {
|
|
69
|
+
try {
|
|
70
|
+
return fs.statSync(path.dirname(absolute)).mtimeMs;
|
|
71
|
+
} catch {
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function committedChangeMtime(root, baseRef, skillPath) {
|
|
78
|
+
if (!baseRef) return 0;
|
|
79
|
+
const result = runGit(root, [
|
|
80
|
+
'log',
|
|
81
|
+
'-1',
|
|
82
|
+
'--format=%cI',
|
|
83
|
+
`${baseRef}..HEAD`,
|
|
84
|
+
'--',
|
|
85
|
+
skillPath,
|
|
86
|
+
]);
|
|
87
|
+
if (result.status !== 0) return 0;
|
|
88
|
+
const parsed = Date.parse(String(result.stdout || '').trim());
|
|
89
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function changedSkillFiles(workspaceRoot = process.cwd()) {
|
|
93
|
+
const root = path.resolve(workspaceRoot);
|
|
94
|
+
const inside = runGit(root, ['rev-parse', '--is-inside-work-tree']);
|
|
95
|
+
if (inside.status !== 0 || String(inside.stdout || '').trim() !== 'true') return [];
|
|
96
|
+
|
|
97
|
+
const changes = new Map();
|
|
98
|
+
const add = (source, paths) => {
|
|
99
|
+
for (const relative of paths) {
|
|
100
|
+
if (path.posix.basename(relative) !== 'SKILL.md') continue;
|
|
101
|
+
const current = changes.get(relative) || new Set();
|
|
102
|
+
current.add(source);
|
|
103
|
+
changes.set(relative, current);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
add('worktree', nulTerminatedPaths(runGit(root, ['diff', '--name-only', '-z']), root));
|
|
108
|
+
add('staged', nulTerminatedPaths(runGit(root, ['diff', '--cached', '--name-only', '-z']), root));
|
|
109
|
+
add('untracked', nulTerminatedPaths(runGit(root, ['ls-files', '--others', '--exclude-standard', '-z']), root));
|
|
110
|
+
|
|
111
|
+
const baseRef = resolveBaseRef(root);
|
|
112
|
+
if (baseRef) {
|
|
113
|
+
add('committed', nulTerminatedPaths(
|
|
114
|
+
runGit(root, ['diff', '--name-only', '-z', `${baseRef}...HEAD`]),
|
|
115
|
+
root,
|
|
116
|
+
));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const stagedAt = indexMtime(root);
|
|
120
|
+
return [...changes.entries()]
|
|
121
|
+
.map(([skillPath, sources]) => {
|
|
122
|
+
let changedAtMs = 0;
|
|
123
|
+
if (sources.has('worktree') || sources.has('untracked')) {
|
|
124
|
+
changedAtMs = Math.max(changedAtMs, fileChangeMtime(root, skillPath));
|
|
125
|
+
}
|
|
126
|
+
if (sources.has('staged')) changedAtMs = Math.max(changedAtMs, stagedAt);
|
|
127
|
+
if (sources.has('committed')) {
|
|
128
|
+
changedAtMs = Math.max(changedAtMs, committedChangeMtime(root, baseRef, skillPath));
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
path: skillPath,
|
|
132
|
+
changed_at_ms: changedAtMs,
|
|
133
|
+
changed_at: changedAtMs > 0 ? new Date(changedAtMs).toISOString() : null,
|
|
134
|
+
sources: [...sources].sort(),
|
|
135
|
+
};
|
|
136
|
+
})
|
|
137
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function readSkillEvalReceipts(workspaceRoot = process.cwd()) {
|
|
141
|
+
const root = path.resolve(workspaceRoot);
|
|
142
|
+
const file = path.join(root, SCORECARDS_PATH);
|
|
143
|
+
if (!fs.existsSync(file)) return [];
|
|
144
|
+
const rows = [];
|
|
145
|
+
for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
|
|
146
|
+
const trimmed = line.trim();
|
|
147
|
+
if (!trimmed) continue;
|
|
148
|
+
try {
|
|
149
|
+
const row = JSON.parse(trimmed);
|
|
150
|
+
if (row && row.schema === SKILL_EVAL_SCHEMA) rows.push(row);
|
|
151
|
+
} catch {
|
|
152
|
+
// Foreign and malformed scorecard rows do not prove a skill evaluation.
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return rows;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function hasRubricScores(value) {
|
|
159
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
160
|
+
const scores = Object.values(value);
|
|
161
|
+
return scores.length > 0 && scores.every((score) => Number.isFinite(score));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function normalizedIdentity(value) {
|
|
165
|
+
return String(value || '').trim().toLowerCase();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function validateSkillEvalReceipt(receipt, skill) {
|
|
169
|
+
if (!receipt || receipt.schema !== SKILL_EVAL_SCHEMA) {
|
|
170
|
+
return { ok: false, reason: 'wrong_schema' };
|
|
171
|
+
}
|
|
172
|
+
if (String(receipt.skill_path || '').replace(/\\/g, '/') !== skill.path) {
|
|
173
|
+
return { ok: false, reason: 'wrong_skill_path' };
|
|
174
|
+
}
|
|
175
|
+
const receiptAtMs = Date.parse(String(receipt.ts || ''));
|
|
176
|
+
if (!Number.isFinite(receiptAtMs) || receiptAtMs <= Number(skill.changed_at_ms || 0)) {
|
|
177
|
+
return { ok: false, reason: 'stale_receipt' };
|
|
178
|
+
}
|
|
179
|
+
if (receipt.passed !== true) return { ok: false, reason: 'eval_did_not_pass' };
|
|
180
|
+
if (!hasRubricScores(receipt.rubric_scores)) {
|
|
181
|
+
return { ok: false, reason: 'rubric_scores_missing' };
|
|
182
|
+
}
|
|
183
|
+
const worker = normalizedIdentity(receipt.worker_model);
|
|
184
|
+
const judge = normalizedIdentity(receipt.judge_identity);
|
|
185
|
+
if (!worker || !judge) return { ok: false, reason: 'judge_or_worker_missing' };
|
|
186
|
+
if (worker === judge) return { ok: false, reason: 'judge_matches_worker' };
|
|
187
|
+
return { ok: true, receipt_at_ms: receiptAtMs };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function skillEvalGate(workspaceRoot = process.cwd()) {
|
|
191
|
+
const root = path.resolve(workspaceRoot);
|
|
192
|
+
const skills = changedSkillFiles(root);
|
|
193
|
+
if (!skills.length) return { ok: true, skipped: true, skills: [] };
|
|
194
|
+
|
|
195
|
+
const receipts = readSkillEvalReceipts(root);
|
|
196
|
+
const missing = [];
|
|
197
|
+
const accepted = [];
|
|
198
|
+
for (const skill of skills) {
|
|
199
|
+
const candidates = receipts.filter((receipt) => (
|
|
200
|
+
String(receipt.skill_path || '').replace(/\\/g, '/') === skill.path
|
|
201
|
+
));
|
|
202
|
+
const valid = candidates
|
|
203
|
+
.map((receipt) => ({ receipt, result: validateSkillEvalReceipt(receipt, skill) }))
|
|
204
|
+
.filter((entry) => entry.result.ok)
|
|
205
|
+
.sort((a, b) => b.result.receipt_at_ms - a.result.receipt_at_ms)[0];
|
|
206
|
+
if (valid) {
|
|
207
|
+
accepted.push({ path: skill.path, receipt_ts: valid.receipt.ts });
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const latest = candidates
|
|
211
|
+
.map((receipt) => ({ receipt, parsed: Date.parse(String(receipt.ts || '')) }))
|
|
212
|
+
.filter((entry) => Number.isFinite(entry.parsed))
|
|
213
|
+
.sort((a, b) => b.parsed - a.parsed)[0];
|
|
214
|
+
const validation = latest ? validateSkillEvalReceipt(latest.receipt, skill) : null;
|
|
215
|
+
missing.push({
|
|
216
|
+
path: skill.path,
|
|
217
|
+
changed_at: skill.changed_at,
|
|
218
|
+
reason: validation ? validation.reason : 'receipt_missing',
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!missing.length) return { ok: true, skills, receipts: accepted };
|
|
223
|
+
const commands = missing.map((entry) => `atris skill eval ${entry.path}`);
|
|
224
|
+
return {
|
|
225
|
+
ok: false,
|
|
226
|
+
reason: 'skill_eval_receipt_required',
|
|
227
|
+
offenders: missing.map((entry) => entry.path),
|
|
228
|
+
missing,
|
|
229
|
+
message: 'a changed skill has no fresh passing evaluation from a judge different from its worker model, so this work cannot land yet.',
|
|
230
|
+
next_action: commands.length === 1
|
|
231
|
+
? `run \`${commands[0]}\`, then re-certify`
|
|
232
|
+
: `run a fresh skill evaluation for each changed skill, then re-certify: ${commands.join(', ')}`,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function appendSkillEvalReceipt(workspaceRoot, receipt) {
|
|
237
|
+
const root = path.resolve(workspaceRoot || process.cwd());
|
|
238
|
+
const file = path.join(root, SCORECARDS_PATH);
|
|
239
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
240
|
+
fs.appendFileSync(file, `${JSON.stringify(receipt)}\n`, 'utf8');
|
|
241
|
+
return file;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
module.exports = {
|
|
245
|
+
SKILL_EVAL_SCHEMA,
|
|
246
|
+
appendSkillEvalReceipt,
|
|
247
|
+
changedSkillFiles,
|
|
248
|
+
skillEvalGate,
|
|
249
|
+
};
|
package/lib/task-db.js
CHANGED
|
@@ -28,6 +28,7 @@ const os = require('os');
|
|
|
28
28
|
const crypto = require('crypto');
|
|
29
29
|
const { DatabaseSync } = require('node:sqlite');
|
|
30
30
|
const reviewIntegrity = require('./review-integrity');
|
|
31
|
+
const { isGenericScratchRoot } = require('./scratch-root');
|
|
31
32
|
const { isDecisionTask } = require('./task-decision');
|
|
32
33
|
const { parseVerifyCommand } = require('./auto-accept-certified');
|
|
33
34
|
const { taskExplanation } = require('./task-explanation');
|
|
@@ -269,6 +270,9 @@ function findWorkspaceRoot(start) {
|
|
|
269
270
|
if (test(cur)) return cur;
|
|
270
271
|
const parent = path.dirname(cur);
|
|
271
272
|
if (parent === cur) break;
|
|
273
|
+
// /tmp (and the other first-minute scratch roots) may themselves be a
|
|
274
|
+
// workspace. A child of that root is a new room and must not inherit it.
|
|
275
|
+
if (isGenericScratchRoot(parent)) break;
|
|
272
276
|
cur = parent;
|
|
273
277
|
}
|
|
274
278
|
return null;
|
package/lib/task-proof.js
CHANGED
|
@@ -8,24 +8,69 @@ const PATH_ONLY_PROOF_RE = /(?:^|[\s'"`])(?:\.{0,2}\/|~\/|\/Users\/|\/private\/|
|
|
|
8
8
|
const RECEIPT_OR_ARTIFACT_RE = /\b(?:receipt|artifact|screenshot|log|trace|path=|file=|bytes=|model=|opened=|https?:\/\/)\b/i;
|
|
9
9
|
const RESULT_PAIR_RE = /\b(?:typecheck|build|smoke|test|pytest|verifier|validation|validated|verified|render|diff|sync|lineage|projection)\b.{0,80}\b(?:pass|passed|failed|green|ok|exit\s*0|reviewed)\b|\b(?:pass|passed|failed|green|ok|exit\s*0|reviewed)\b.{0,80}\b(?:typecheck|build|smoke|test|pytest|verifier|validation|validated|verified|render|diff|sync|lineage|projection)\b/i;
|
|
10
10
|
const SUITE_GREEN_CLAIM_RE = /\b(?:tests|test\s+suite|suite)\s+(?:is|are|was|were)?\s*(?:all\s+)?(?:green|pass(?:es|ed)?|ok)\b|\b(?:all|full|whole|entire)\s+(?:tests?|test\s+suite|suite)\s+(?:is|are|was|were)?\s*(?:green|pass(?:es|ed)?|ok)\b|\ball\s+green\b|\b(?:npm|pnpm|yarn)\s+(?:run\s+)?test\b\x60?(?:\s+(?:\d+\s*\/\s*\d+|all|suite))?\s+(?:is\s+)?(?:green|pass(?:es|ed)?|ok)\b/i;
|
|
11
|
-
const
|
|
11
|
+
const CI_RUN_URL_RE = /https?:\/\/github[.]com\/[^/\s]+\/[^/\s]+\/actions\/runs\/\d+\b/i;
|
|
12
|
+
const CI_RUN_ID_RE = /\brun_id\s*=\s*\d+\b|\brun(?:\s+id)?\s*(?:=|:)?\s*\d+\b/i;
|
|
13
|
+
const CI_RUN_CITATION_RE = new RegExp(`${CI_RUN_URL_RE.source}|${CI_RUN_ID_RE.source}`, 'i');
|
|
12
14
|
const COMMIT_PINNED_LOCAL_VERIFY_RE = /\bcommit\s+[0-9a-f]{7,40}\b/i;
|
|
13
15
|
const EXIT_ZERO_RE = /\bexit(?:ed)?(?:\s+code)?\s*0\b/i;
|
|
14
|
-
const SUITE_GREEN_CITATION_REASON = 'cite
|
|
16
|
+
const SUITE_GREEN_CITATION_REASON = 'cite a fetched CI run URL (--proof-url with --i-fetched), a run id, or a commit-pinned verify command with exit 0 before claiming the suite is green';
|
|
17
|
+
const LOCAL_SUCCESS_PROOF_EXAMPLE = 'local success example: atris task ready <id> --verify "<cmd>" --result "<plain sentence>" (exit 0 plus the written atris/runs/ receipt path is enough; do not paste a CI URL unless atris fetched it or you pass --proof-url with --i-fetched)';
|
|
18
|
+
const FETCHED_PROOF_URL_RE = /\[(?:fetched|i-fetched)\]\s*https?:\/\/github[.]com\/[^/\s]+\/[^/\s]+\/actions\/runs\/\d+\b/i;
|
|
15
19
|
const HUMAN_PROOF_RE = /\b(?:team human approved|human approved|human approval|approved by|accepted by|reviewed by|customer replied|customer approved|customer accepted|replied)\b/i;
|
|
16
20
|
const FILE_ACTION_RE = /\b(?:changed|updated|edited|created|deleted|saved|wrote|patched|reviewed|verified|validated|opened|read)\b/i;
|
|
17
|
-
const VERIFIED_PROOF_RE = /^\[verified\]\s+`[^`]+`\s+passed\s+\(exit 0\)(?:\s|$)/
|
|
21
|
+
const VERIFIED_PROOF_RE = /^\[verified\]\s+`[^`]+`\s+passed\s+\(exit 0\)(?:\s|$)/im;
|
|
22
|
+
const LOCAL_RECEIPT_PATH_RE = /(?:^|[\s])(?:Receipt:\s*)?(?:atris\/runs\/|\.atris\/state\/)[^\s'"`,;)]+\.json\b/i;
|
|
18
23
|
const SECOND_ACTOR_EXECUTED_PROOF_RE = /^Second-actor check:\s+`[^`]+`\s+re-run by [^,]+,\s+exited 0(?:[.\s]|$)/i;
|
|
24
|
+
// Ready/receipt prose may name these. If this process did not run the named
|
|
25
|
+
// command, that sentence is a lie. Keep the list tight: these are the claims
|
|
26
|
+
// that already pass the meaningful-proof floor without executing anything.
|
|
27
|
+
const NAMED_READY_COMMAND_RE = /\b(?:npm test|node --test|git diff --check)\b/i;
|
|
28
|
+
const PROOF_COMMAND_NOT_RUN = 'proof_command_not_run';
|
|
19
29
|
|
|
20
30
|
function compactWhitespace(text) {
|
|
21
31
|
return String(text || '').replace(/\s+/g, ' ').trim();
|
|
22
32
|
}
|
|
23
33
|
|
|
34
|
+
function escapeRegExp(text) {
|
|
35
|
+
return String(text || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function namedUnrunProofCommand(proof, ranCommand = '') {
|
|
39
|
+
const text = String(proof || '');
|
|
40
|
+
const match = text.match(NAMED_READY_COMMAND_RE);
|
|
41
|
+
if (!match) return '';
|
|
42
|
+
const named = compactWhitespace(match[0]);
|
|
43
|
+
const ran = compactWhitespace(ranCommand);
|
|
44
|
+
if (ran && new RegExp(`\\b${escapeRegExp(named)}\\b`, 'i').test(ran)) return '';
|
|
45
|
+
return named;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function unrunNamedProofCommandIssue(proof, ranCommand = '') {
|
|
49
|
+
const named = namedUnrunProofCommand(proof, ranCommand);
|
|
50
|
+
if (!named) return null;
|
|
51
|
+
return {
|
|
52
|
+
reason: PROOF_COMMAND_NOT_RUN,
|
|
53
|
+
detail: `proof names \`${named}\`, but this process did not run it\nuse --verify "${named}" so the named command actually executes`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function hasFetchedCiCitation(text) {
|
|
58
|
+
return FETCHED_PROOF_URL_RE.test(text);
|
|
59
|
+
}
|
|
60
|
+
|
|
24
61
|
function hasSuiteGreenCitation(text) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
62
|
+
// A pasted actions URL is not proof unless atris fetched it (marked
|
|
63
|
+
// [fetched]/[i-fetched]) or the caller attested with --i-fetched.
|
|
64
|
+
if (hasFetchedCiCitation(text)) return true;
|
|
65
|
+
if (CI_RUN_URL_RE.test(text)) return false;
|
|
66
|
+
if (CI_RUN_ID_RE.test(text)) return true;
|
|
67
|
+
return COMMIT_PINNED_LOCAL_VERIFY_RE.test(text)
|
|
68
|
+
&& COMMAND_PROOF_RE.test(text)
|
|
69
|
+
&& EXIT_ZERO_RE.test(text);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function hasLocalVerifiedReceipt(text) {
|
|
73
|
+
return VERIFIED_PROOF_RE.test(text) && LOCAL_RECEIPT_PATH_RE.test(text);
|
|
29
74
|
}
|
|
30
75
|
|
|
31
76
|
function taskProofState(proof) {
|
|
@@ -34,6 +79,18 @@ function taskProofState(proof) {
|
|
|
34
79
|
if (GENERIC_COMPLETION_PROOF_RE.test(text)) {
|
|
35
80
|
return { ok: false, reason: 'proof must name what was verified, changed, approved, or produced' };
|
|
36
81
|
}
|
|
82
|
+
// An executed --verify that exited 0 and wrote an atris/runs receipt is
|
|
83
|
+
// meaningful proof on its own. Suite-green URL rules apply only to claims.
|
|
84
|
+
if (hasLocalVerifiedReceipt(text)) {
|
|
85
|
+
return { ok: true, reason: 'proof is a local verified command with receipt path' };
|
|
86
|
+
}
|
|
87
|
+
if (CI_RUN_URL_RE.test(text) && !hasFetchedCiCitation(text) && !SUITE_GREEN_CLAIM_RE.test(text)) {
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
code: 'unfetched_proof_url',
|
|
91
|
+
reason: 'CI run URLs only count after atris fetches them, or with --proof-url and --i-fetched',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
37
94
|
if (SUITE_GREEN_CLAIM_RE.test(text)) {
|
|
38
95
|
if (!hasSuiteGreenCitation(text)) {
|
|
39
96
|
return {
|
|
@@ -42,7 +99,7 @@ function taskProofState(proof) {
|
|
|
42
99
|
reason: SUITE_GREEN_CITATION_REASON,
|
|
43
100
|
};
|
|
44
101
|
}
|
|
45
|
-
return { ok: true, reason: 'proof cites a CI run or commit-pinned local verify' };
|
|
102
|
+
return { ok: true, reason: 'proof cites a fetched CI run or commit-pinned local verify' };
|
|
46
103
|
}
|
|
47
104
|
if (COMMAND_PROOF_RE.test(text)) return { ok: true, reason: 'proof names a command' };
|
|
48
105
|
if (HUMAN_PROOF_RE.test(text)) return { ok: true, reason: 'proof names human/customer approval' };
|
|
@@ -121,5 +178,9 @@ module.exports = {
|
|
|
121
178
|
taskProofLooksExecuted,
|
|
122
179
|
taskProofState,
|
|
123
180
|
taskProofExecutionState,
|
|
181
|
+
namedUnrunProofCommand,
|
|
182
|
+
unrunNamedProofCommandIssue,
|
|
124
183
|
buildVerifiedProof,
|
|
184
|
+
LOCAL_SUCCESS_PROOF_EXAMPLE,
|
|
185
|
+
PROOF_COMMAND_NOT_RUN,
|
|
125
186
|
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Ready/recap need a stronger bar than "command can fail in an empty dir".
|
|
4
|
+
// A bare `test -f` clears the empty-dir probe and still never exercises the
|
|
5
|
+
// change. Allowlist checks that actually run tests, read a symbol, or do a
|
|
6
|
+
// real diff/syntax check.
|
|
7
|
+
|
|
8
|
+
function classifyVerifier(command) {
|
|
9
|
+
const cmd = String(command || '').trim();
|
|
10
|
+
if (!cmd) return { kind: 'empty', ok: false, reason: 'no verifier command' };
|
|
11
|
+
|
|
12
|
+
if (/\b(node\s+--test|npm\s+(?:test|run\s+test(?:\S*)?)|pnpm\s+test|yarn\s+test|pytest\b|go\s+test|cargo\s+test|vitest\b|jest\b)\b/i.test(cmd)) {
|
|
13
|
+
return { kind: 'test_runner', ok: true, reason: 'test runner' };
|
|
14
|
+
}
|
|
15
|
+
if (/\b(git\s+diff\s+--(?:check|exit-code|quiet)|node\s+--check)\b/i.test(cmd)) {
|
|
16
|
+
return { kind: 'diff_check', ok: true, reason: 'diff or syntax check' };
|
|
17
|
+
}
|
|
18
|
+
if (/\b(?:rg|grep)\b/i.test(cmd) && /\S{2,}/.test(cmd.replace(/^(?:rg|grep)\b/i, ''))) {
|
|
19
|
+
return { kind: 'symbol_read', ok: true, reason: 'symbol or content read' };
|
|
20
|
+
}
|
|
21
|
+
if (/\batris\s+(?:verify|drill|slop)\b/i.test(cmd)) {
|
|
22
|
+
return { kind: 'atris_check', ok: true, reason: 'atris check command' };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Bare file/directory existence: passes when the file is there, says nothing
|
|
26
|
+
// about whether the change works.
|
|
27
|
+
if (/^(?:test\s+-[efsdL]\s+\S+|\[\s+-[efsdL]\s+\S+\s*\])(?:\s*(?:&&|\|\|)\s*(?:test\s+-[efsdL]\s+\S+|\[\s+-[efsdL]\s+\S+\s*\]))*$/i.test(cmd)
|
|
28
|
+
|| /^test\s+-[efsdL]\s+\S+$/i.test(cmd)
|
|
29
|
+
|| /^\[\s+-[efsdL]\s+\S+\s*\]$/i.test(cmd)) {
|
|
30
|
+
return {
|
|
31
|
+
kind: 'file_exists',
|
|
32
|
+
ok: false,
|
|
33
|
+
reason: 'file-exists check does not exercise the change; use a test runner, git diff --check, node --check, or rg/grep for a new symbol',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Unknown commands still need the empty-dir falsifier; allow them through
|
|
38
|
+
// that probe. Only bare file-exists is rejected here as not exercising work.
|
|
39
|
+
return {
|
|
40
|
+
kind: 'unknown',
|
|
41
|
+
ok: true,
|
|
42
|
+
reason: 'custom verifier; empty-dir falsifier still applies',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isRealTestRunnerProof(proof) {
|
|
47
|
+
const flat = String(proof || '').replace(/\s+/g, ' ').trim();
|
|
48
|
+
if (!flat) return false;
|
|
49
|
+
return /\b(node\s+--test|npm\s+(?:test|run\s+test(?:\S*)?)|pnpm\s+test|yarn\s+test|pytest\b|go\s+test|cargo\s+test|vitest\b|jest\b)\b/i.test(flat)
|
|
50
|
+
&& /\b(pass|passed|green|ok|0 failures?|\d+\/\d+)\b/i.test(flat);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function quoteVerifierCommand(proof) {
|
|
54
|
+
const flat = String(proof || '').replace(/\s+/g, ' ').trim();
|
|
55
|
+
if (!flat) return null;
|
|
56
|
+
const verified = flat.match(/\[verified\]\s*`([^`]+)`\s*passed/i);
|
|
57
|
+
if (verified) return verified[1].trim();
|
|
58
|
+
const backticked = flat.match(/`([^`]+)`/);
|
|
59
|
+
if (backticked && /\b(test|npm|node|git|rg|grep|pytest|vitest|jest)\b/i.test(backticked[1])) {
|
|
60
|
+
return backticked[1].trim();
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = {
|
|
66
|
+
classifyVerifier,
|
|
67
|
+
isRealTestRunnerProof,
|
|
68
|
+
quoteVerifierCommand,
|
|
69
|
+
};
|
package/lib/wish-audit.js
CHANGED
|
@@ -1047,13 +1047,13 @@ function auditWish(text, root = process.cwd(), options = {}) {
|
|
|
1047
1047
|
} catch {
|
|
1048
1048
|
validator = null;
|
|
1049
1049
|
}
|
|
1050
|
-
if (!executor) {
|
|
1050
|
+
if (!executor && !options.cloudRun) {
|
|
1051
1051
|
const question = 'Which working builder should handle this?';
|
|
1052
1052
|
const answered = answeredRepeatedQuestionValue(text, root, question, 'builder');
|
|
1053
1053
|
if (answered) executor = { id: slugify(answered), name: answered };
|
|
1054
1054
|
else questions.push(question);
|
|
1055
1055
|
}
|
|
1056
|
-
if (!validator) {
|
|
1056
|
+
if (!validator && !options.cloudRun) {
|
|
1057
1057
|
const question = 'Which working reviewer should validate it?';
|
|
1058
1058
|
const answered = answeredRepeatedQuestionValue(text, root, question, 'reviewer');
|
|
1059
1059
|
if (answered) validator = { id: slugify(answered), name: answered };
|
package/lib/wish-delegate.js
CHANGED
|
@@ -1639,9 +1639,18 @@ function applyAnswer(wish, answer, root, options = {}) {
|
|
|
1639
1639
|
answers: [...(wish.answers || []), answer],
|
|
1640
1640
|
};
|
|
1641
1641
|
const auditText = [wish.text, ...(answeredWish.answers || [])].join(' ');
|
|
1642
|
-
|
|
1642
|
+
// A cloud grant runs on the backend, so it never needs a local builder or
|
|
1643
|
+
// reviewer; only clarity questions still apply.
|
|
1644
|
+
const cloudRun = typeof options.cloudDispatch === 'function';
|
|
1645
|
+
const audited = auditWish(auditText, root, { engineOverride, originalText: wish.text, cloudRun });
|
|
1643
1646
|
const audit = bestGuessAfterRepeatedQuestion(answeredWish, audited, root);
|
|
1644
1647
|
if (!audit.ok) return askForInput(answeredWish, audit, root, asJson);
|
|
1648
|
+
// --cloud grants hand the answered wish to the backend instead of a local
|
|
1649
|
+
// engine. The caller supplies the dispatcher so this file stays free of the
|
|
1650
|
+
// cloud client.
|
|
1651
|
+
if (typeof options.cloudDispatch === 'function') {
|
|
1652
|
+
return options.cloudDispatch(answeredWish, auditText, root);
|
|
1653
|
+
}
|
|
1645
1654
|
if (noMission) return captureOnlyWish(answeredWish, audit, root, asJson);
|
|
1646
1655
|
return delegateWish(answeredWish, audit, root, asJson, { oneLap: wish.one_lap === true });
|
|
1647
1656
|
}
|