atris 3.52.0 → 3.55.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/engines/SKILL.md +8 -7
- package/atris/skills/x-search/SKILL.md +24 -22
- package/atris/skills/youtube/SKILL.md +89 -40
- package/atris/team/_template/SOUL.md +40 -0
- package/atris/team/validator/MEMBER.md +1 -2
- package/ax +592 -91
- package/bin/atris +21 -0
- package/bin/atris.js +580 -378
- 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/computer.js +2 -2
- 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 +399 -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 +339 -74
- package/commands/now.js +25 -6
- 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 +96 -40
- 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 +418 -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 -33
- package/lib/account-bound.js +47 -0
- package/lib/auto-accept-certified.js +10 -2
- 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/engine-ask.js +11 -3
- package/lib/engine-registry.js +32 -7
- 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-ledger-compact.js +127 -0
- package/lib/mission-root.js +4 -2
- package/lib/mission-runtime-loop.js +30 -2
- package/lib/next-moves.js +34 -34
- package/lib/noninteractive.js +88 -0
- package/lib/permission-grants.js +10 -1
- package/lib/policy-lessons.js +4 -1
- package/lib/revision-metric.js +279 -0
- package/lib/runner-command.js +5 -4
- package/lib/scratch-root.js +48 -0
- package/lib/skill-eval-gate.js +249 -0
- package/lib/task-db.js +22 -2
- package/lib/task-proof.js +75 -9
- package/lib/task-receipt.js +5 -1
- 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 +4 -2
- package/scripts/det/checklist-score.js +191 -0
- package/scripts/det/ytsearch +31 -0
- package/scripts/outbound-artifact-gate.js +227 -0
- package/utils/update-check.js +15 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// Pareto-3 checklist quality score.
|
|
5
|
+
//
|
|
6
|
+
// Measures the three front doors a brand-new person actually hits, the way
|
|
7
|
+
// they would hit them: from an empty folder, with no context, reading only
|
|
8
|
+
// what the tool prints. Every point is earned by a live probe, never opinion.
|
|
9
|
+
//
|
|
10
|
+
// 1. onboard - `atris init --yes` in a fresh folder
|
|
11
|
+
// 2. chat - one real answered message through `ax`
|
|
12
|
+
// 3. mission - the end-to-end loop drill in its own sandbox
|
|
13
|
+
//
|
|
14
|
+
// Rubric per feature (100 points):
|
|
15
|
+
// cold start works, zero setup ......... 40
|
|
16
|
+
// speed to first value ................. 20 (full marks at target, 0 at 4x)
|
|
17
|
+
// plain language on the first screen ... 20 (fraction of clean lines)
|
|
18
|
+
// graceful failure on wrong input ...... 20 (honest exit + plain reason,
|
|
19
|
+
// no stack trace)
|
|
20
|
+
//
|
|
21
|
+
// Usage: node scripts/det/checklist-score.js [--json]
|
|
22
|
+
|
|
23
|
+
const { spawnSync } = require('node:child_process');
|
|
24
|
+
const fs = require('node:fs');
|
|
25
|
+
const os = require('node:os');
|
|
26
|
+
const path = require('node:path');
|
|
27
|
+
|
|
28
|
+
const repoRoot = path.resolve(__dirname, '..', '..');
|
|
29
|
+
const cliPath = path.join(repoRoot, 'bin', 'atris.js');
|
|
30
|
+
const axPath = path.join(repoRoot, 'ax');
|
|
31
|
+
const { scanText } = require(path.join(repoRoot, 'lib', 'voice-gate'));
|
|
32
|
+
|
|
33
|
+
// System nouns a person with no idea should never have to decode on a first
|
|
34
|
+
// screen. Backticked command examples are exempt (they are copy targets).
|
|
35
|
+
const SYSTEM_NOUNS = /\b(?:verifier|worktree|subagent|jsonl|projection|runner[- ]profile|heartbeat|orchestrat\w+|frontmatter|monorepo)\b/i;
|
|
36
|
+
const STACK_FRAME = /^\s+at .+:\d+:\d+\)?$/;
|
|
37
|
+
|
|
38
|
+
function run(cmd, args, options = {}) {
|
|
39
|
+
const started = Date.now();
|
|
40
|
+
const result = spawnSync(cmd, args, {
|
|
41
|
+
cwd: options.cwd,
|
|
42
|
+
encoding: 'utf8',
|
|
43
|
+
timeout: options.timeoutMs || 180000,
|
|
44
|
+
env: { ...process.env, ATRIS_SKIP_UPDATE_CHECK: '1', ...(options.env || {}) },
|
|
45
|
+
});
|
|
46
|
+
return {
|
|
47
|
+
status: result.status,
|
|
48
|
+
error: result.error || null,
|
|
49
|
+
stdout: result.stdout || '',
|
|
50
|
+
stderr: result.stderr || '',
|
|
51
|
+
ms: Date.now() - started,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function speedPoints(ms, targetMs) {
|
|
56
|
+
if (ms <= targetMs) return 20;
|
|
57
|
+
const over = (ms - targetMs) / (targetMs * 3);
|
|
58
|
+
return Math.max(0, Math.round(20 * (1 - over)));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function plainPoints(text) {
|
|
62
|
+
const lines = String(text || '')
|
|
63
|
+
.split(/\r?\n/)
|
|
64
|
+
.map((line) => line.trim())
|
|
65
|
+
.filter(Boolean)
|
|
66
|
+
.slice(0, 30);
|
|
67
|
+
if (!lines.length) return { points: 0, dirty: ['(no output at all)'] };
|
|
68
|
+
const dirty = [];
|
|
69
|
+
for (const line of lines) {
|
|
70
|
+
// Command examples are copy targets, not prose; the reader with no idea
|
|
71
|
+
// reads the description column, so scan only the human words.
|
|
72
|
+
if (/^`.*`$/.test(line) || /^\$\s/.test(line) || /^(?:ax|atris|npx)\s/.test(line)) continue;
|
|
73
|
+
const bare = line
|
|
74
|
+
.replace(/`[^`]*`/g, '')
|
|
75
|
+
.replace(/--[\w-]+/g, '')
|
|
76
|
+
.replace(/\[[^\]]*\]/g, '')
|
|
77
|
+
.replace(/<[^>]*>/g, '');
|
|
78
|
+
if (STACK_FRAME.test(line) || /\u2014/.test(bare) || SYSTEM_NOUNS.test(bare) || scanText(bare).length) {
|
|
79
|
+
dirty.push(line.slice(0, 80));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const clean = lines.length - dirty.length;
|
|
83
|
+
return { points: Math.round((20 * clean) / lines.length), dirty };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function failurePoints(result) {
|
|
87
|
+
const text = `${result.stdout}\n${result.stderr}`;
|
|
88
|
+
const honest = result.status !== 0 && !result.error;
|
|
89
|
+
const noStack = !text.split(/\r?\n/).some((line) => STACK_FRAME.test(line))
|
|
90
|
+
&& !/\b(?:TypeError|ReferenceError|SyntaxError)\b/.test(text);
|
|
91
|
+
const saysWhy = /\b(?:unknown|not found|no such|missing|usage|help|try|error)\b/i.test(text);
|
|
92
|
+
let points = 0;
|
|
93
|
+
if (honest) points += 8;
|
|
94
|
+
if (noStack) points += 6;
|
|
95
|
+
if (saysWhy) points += 6;
|
|
96
|
+
return points;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function scoreOnboard(base) {
|
|
100
|
+
const dir = fs.mkdtempSync(path.join(base, 'onboard-'));
|
|
101
|
+
const cold = run(process.execPath, [cliPath, 'init', '--yes'], { cwd: dir });
|
|
102
|
+
const artifact = fs.existsSync(path.join(dir, 'atris'));
|
|
103
|
+
const coldPoints = cold.status === 0 && artifact ? 40 : 0;
|
|
104
|
+
const plain = plainPoints(cold.stdout);
|
|
105
|
+
const bad = run(process.execPath, [cliPath, 'definitely-not-a-command'], { cwd: dir });
|
|
106
|
+
return {
|
|
107
|
+
feature: 'onboard: atris init in an empty folder',
|
|
108
|
+
dir,
|
|
109
|
+
cold: coldPoints,
|
|
110
|
+
speed: speedPoints(cold.ms, 10000),
|
|
111
|
+
plain: plain.points,
|
|
112
|
+
failure: failurePoints(bad),
|
|
113
|
+
ms: cold.ms,
|
|
114
|
+
dirty_lines: plain.dirty,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function scoreChat(base, workspaceDir) {
|
|
119
|
+
const cold = run(process.execPath, [axPath, '--fast', '--print', 'Reply with exactly: checklist-ok'], {
|
|
120
|
+
cwd: workspaceDir,
|
|
121
|
+
timeoutMs: 120000,
|
|
122
|
+
});
|
|
123
|
+
let answered = false;
|
|
124
|
+
try {
|
|
125
|
+
const payload = JSON.parse(cold.stdout.slice(cold.stdout.indexOf('{')));
|
|
126
|
+
answered = payload.ok === true && /checklist-ok/.test(String(payload.output || ''));
|
|
127
|
+
} catch {}
|
|
128
|
+
const helpScreen = run(process.execPath, [axPath, '--help'], { cwd: workspaceDir, timeoutMs: 30000 });
|
|
129
|
+
const plain = plainPoints(helpScreen.stdout);
|
|
130
|
+
const bad = run(process.execPath, [axPath, '--fast', '--print'], { cwd: workspaceDir, timeoutMs: 30000 });
|
|
131
|
+
return {
|
|
132
|
+
feature: 'chat: one real answered message through ax',
|
|
133
|
+
cold: cold.status !== null && answered ? 40 : 0,
|
|
134
|
+
speed: speedPoints(cold.ms, 15000),
|
|
135
|
+
plain: plain.points,
|
|
136
|
+
failure: failurePoints(bad),
|
|
137
|
+
ms: cold.ms,
|
|
138
|
+
dirty_lines: plain.dirty,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function scoreMission(base, workspaceDir) {
|
|
143
|
+
const cold = run(process.execPath, [cliPath, 'drill', '--json'], { cwd: workspaceDir, timeoutMs: 300000 });
|
|
144
|
+
let passed = false;
|
|
145
|
+
let stages = 0;
|
|
146
|
+
try {
|
|
147
|
+
const payload = JSON.parse(cold.stdout.slice(cold.stdout.indexOf('{')));
|
|
148
|
+
passed = payload.pass === true;
|
|
149
|
+
stages = Array.isArray(payload.stages) ? payload.stages.length : 0;
|
|
150
|
+
} catch {}
|
|
151
|
+
const status = run(process.execPath, [cliPath, 'mission'], { cwd: workspaceDir, timeoutMs: 60000 });
|
|
152
|
+
const plain = plainPoints(status.stdout);
|
|
153
|
+
const bad = run(process.execPath, [cliPath, 'mission', 'run', '9999'], { cwd: workspaceDir, timeoutMs: 60000 });
|
|
154
|
+
return {
|
|
155
|
+
feature: `mission: full loop drill (${stages} stages) plus status screen`,
|
|
156
|
+
cold: passed ? 40 : 0,
|
|
157
|
+
speed: speedPoints(cold.ms, 90000),
|
|
158
|
+
plain: plain.points,
|
|
159
|
+
failure: failurePoints(bad),
|
|
160
|
+
ms: cold.ms,
|
|
161
|
+
dirty_lines: plain.dirty,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function main() {
|
|
166
|
+
const asJson = process.argv.includes('--json');
|
|
167
|
+
const base = fs.mkdtempSync(path.join(os.tmpdir(), 'atris-checklist-'));
|
|
168
|
+
const onboard = scoreOnboard(base);
|
|
169
|
+
const chat = scoreChat(base, onboard.dir);
|
|
170
|
+
const mission = scoreMission(base, onboard.dir);
|
|
171
|
+
const features = [onboard, chat, mission].map((f) => ({
|
|
172
|
+
...f,
|
|
173
|
+
total: f.cold + f.speed + f.plain + f.failure,
|
|
174
|
+
}));
|
|
175
|
+
const overall = Math.round(features.reduce((sum, f) => sum + f.total, 0) / features.length);
|
|
176
|
+
const report = { schema: 'atris.checklist_score.v1', overall, features };
|
|
177
|
+
fs.rmSync(base, { recursive: true, force: true });
|
|
178
|
+
if (asJson) {
|
|
179
|
+
console.log(JSON.stringify(report, null, 2));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
console.log('Pareto-3 checklist quality score\n');
|
|
183
|
+
for (const f of features) {
|
|
184
|
+
console.log(` ${f.feature}`);
|
|
185
|
+
console.log(` cold start ${f.cold}/40 · speed ${f.speed}/20 (${(f.ms / 1000).toFixed(1)}s) · plain words ${f.plain}/20 · graceful failure ${f.failure}/20 => ${f.total}/100`);
|
|
186
|
+
if (f.dirty_lines.length) console.log(` lines that need plain words: ${f.dirty_lines.length}`);
|
|
187
|
+
}
|
|
188
|
+
console.log(`\n overall: ${overall}/100`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
main();
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ytsearch <query> [limit]: free YouTube discovery via yt-dlp ytsearchN.
|
|
3
|
+
# Zero Atris credits. Prints one line per video:
|
|
4
|
+
# title | channel | duration | views | upload_date | https://youtu.be/ID
|
|
5
|
+
# upload_date is YYYYMMDD (or NA) so callers can apply a freshness gate.
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
USAGE='usage: ytsearch "<query>" [limit]'
|
|
9
|
+
|
|
10
|
+
QUERY="${1:-}"
|
|
11
|
+
LIMIT="${2:-5}"
|
|
12
|
+
|
|
13
|
+
if [ -z "$QUERY" ] || [ "$QUERY" = '-h' ] || [ "$QUERY" = '--help' ] || [ "$QUERY" = 'help' ]; then
|
|
14
|
+
echo "$USAGE" >&2
|
|
15
|
+
exit 2
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
if ! [[ "$LIMIT" =~ ^[1-9][0-9]*$ ]]; then
|
|
19
|
+
echo "limit must be a positive integer" >&2
|
|
20
|
+
exit 2
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
if ! command -v yt-dlp >/dev/null 2>&1; then
|
|
24
|
+
echo "yt-dlp not found on PATH" >&2
|
|
25
|
+
exit 2
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# flat-playlist keeps this a metadata-only discovery call (no download).
|
|
29
|
+
yt-dlp --no-update --flat-playlist --no-warnings \
|
|
30
|
+
--print '%(title)s | %(channel)s | %(duration_string)s | %(view_count)s | %(upload_date)s | https://youtu.be/%(id)s' \
|
|
31
|
+
"ytsearch${LIMIT}:${QUERY}"
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
|
|
5
|
+
const HTML_TAG_RE = /<!doctype\s+html|<\/?(?:html|head|body|div|section|article|main|table|thead|tbody|tr|td|th|p|br|span|h[1-6]|ul|ol|li|style|script|a|img|strong|em)\b[^>]*>/i;
|
|
6
|
+
const ENCODED_HTML_RE = /<\/?(?:html|head|body|div|table|tr|td|p|span|h[1-6]|ul|ol|li|style|script|svg)\b/i;
|
|
7
|
+
const RENDERED_SOURCE_FENCE_RE = /```\s*(?:html|xml|jsx|tsx|css|svg|mermaid)\b/i;
|
|
8
|
+
const COACH_INTERNAL_RE = /(?:\b(?:CLI|WEB|BE)-\d+\b|\b[0-9A-HJKMNP-TV-Z]{26}\b|\bmission-[a-z0-9-]+|(?:^|\s)--[a-z][a-z-]*\b|(?:^|\s)(?:\.atris|atris\/runs)\/|(?:^|\s)\/(?:Users|home|workspace|tmp)\/\S+)/im;
|
|
9
|
+
const COACH_PRESSURE_RE = /\b(?:asap|urgent|time-sensitive|immediately|friendly reminder|just checking in|you still haven'?t|don'?t forget|why haven'?t you|you need to|productive day|great work|nice work|good job|keep the streak)\b/i;
|
|
10
|
+
|
|
11
|
+
const SLOP_RULES = [
|
|
12
|
+
{
|
|
13
|
+
id: 'copy-slop-corporate-filler',
|
|
14
|
+
re: /\b(?:revolutionary|game[- ]changing|cutting[- ]edge|seamlessly|effortlessly|robust|powerful|comprehensive|leverage|utilize|facilitate|synergy|holistic|pivotal|crucial|unlock|supercharge)\b/i,
|
|
15
|
+
message: 'copy contains corporate filler or hype',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: 'copy-slop-ai-tell',
|
|
19
|
+
re: /\b(?:it'?s worth noting that|as you can see|in order to|at the end of the day|great question|absolutely)\b/i,
|
|
20
|
+
message: 'copy contains an AI-tell phrase',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: 'copy-slop-punctuation',
|
|
24
|
+
re: /[\u2014\u2728\u{1F680}\u{1F4A1}\u{1F3AF}]/u,
|
|
25
|
+
message: 'copy contains punctuation or decorative symbols blocked by the anti-slop gate',
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const VALID_FORMATS = new Set(['plain', 'html', 'markdown', 'visual', 'source']);
|
|
30
|
+
const VALID_COACH_SURFACES = new Set(['morning', 'evening', 'warm-ping']);
|
|
31
|
+
|
|
32
|
+
function usage() {
|
|
33
|
+
return [
|
|
34
|
+
'Usage:',
|
|
35
|
+
' node scripts/outbound-artifact-gate.js --channel email --format plain --body-file body.txt',
|
|
36
|
+
' node scripts/outbound-artifact-gate.js --channel email --format html --body-file body.html --proof-file render.txt',
|
|
37
|
+
'',
|
|
38
|
+
'Options:',
|
|
39
|
+
' --channel <email|slack|doc|deck|web|other>',
|
|
40
|
+
' --format <plain|html|markdown|visual|source>',
|
|
41
|
+
' --body <text>',
|
|
42
|
+
' --body-file <path>',
|
|
43
|
+
' --proof-file <path> Required for html and visual formats',
|
|
44
|
+
' --coach-surface <morning|evening|warm-ping>',
|
|
45
|
+
' --signal-proof <path> Required for a warm-ping coach surface',
|
|
46
|
+
' --visual Require visual proof even when format is not visual',
|
|
47
|
+
' --allow-slop Skip anti-slop copy checks',
|
|
48
|
+
].join('\n');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseArgs(argv) {
|
|
52
|
+
const options = {
|
|
53
|
+
channel: 'other',
|
|
54
|
+
format: 'plain',
|
|
55
|
+
body: '',
|
|
56
|
+
bodyFile: null,
|
|
57
|
+
proofFile: null,
|
|
58
|
+
coachSurface: null,
|
|
59
|
+
signalProof: null,
|
|
60
|
+
visual: false,
|
|
61
|
+
allowSlop: false,
|
|
62
|
+
help: false,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
66
|
+
const arg = argv[i];
|
|
67
|
+
if (!arg.startsWith('--')) {
|
|
68
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const eq = arg.indexOf('=');
|
|
72
|
+
const key = (eq === -1 ? arg.slice(2) : arg.slice(2, eq)).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
73
|
+
const inlineValue = eq === -1 ? null : arg.slice(eq + 1);
|
|
74
|
+
|
|
75
|
+
if (key === 'help') {
|
|
76
|
+
options.help = true;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (key === 'visual' || key === 'allowSlop') {
|
|
80
|
+
options[key] = true;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const value = inlineValue !== null ? inlineValue : argv[i + 1];
|
|
85
|
+
if (value === undefined || value.startsWith('--')) {
|
|
86
|
+
throw new Error(`Missing value for --${arg.slice(2)}`);
|
|
87
|
+
}
|
|
88
|
+
i += inlineValue === null ? 1 : 0;
|
|
89
|
+
|
|
90
|
+
if (!Object.prototype.hasOwnProperty.call(options, key)) {
|
|
91
|
+
throw new Error(`Unknown option: --${arg.slice(2)}`);
|
|
92
|
+
}
|
|
93
|
+
options[key] = value;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return options;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function readBody(options) {
|
|
100
|
+
if (options.bodyFile) {
|
|
101
|
+
return fs.readFileSync(options.bodyFile, 'utf8');
|
|
102
|
+
}
|
|
103
|
+
return String(options.body || '');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function proofExists(proofFile) {
|
|
107
|
+
if (!proofFile) return false;
|
|
108
|
+
try {
|
|
109
|
+
fs.statSync(proofFile);
|
|
110
|
+
return true;
|
|
111
|
+
} catch (_err) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function addError(errors, id, message) {
|
|
117
|
+
errors.push({ id, message });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function scanOutboundArtifact(options, body) {
|
|
121
|
+
const errors = [];
|
|
122
|
+
const format = String(options.format || 'plain').toLowerCase();
|
|
123
|
+
const channel = String(options.channel || 'other').toLowerCase();
|
|
124
|
+
const coachSurface = options.coachSurface ? String(options.coachSurface).toLowerCase() : null;
|
|
125
|
+
|
|
126
|
+
if (!VALID_FORMATS.has(format)) {
|
|
127
|
+
addError(errors, 'invalid-format', `format must be one of ${Array.from(VALID_FORMATS).join(', ')}`);
|
|
128
|
+
return errors;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (coachSurface && !VALID_COACH_SURFACES.has(coachSurface)) {
|
|
132
|
+
addError(errors, 'invalid-coach-surface', `coach surface must be one of ${Array.from(VALID_COACH_SURFACES).join(', ')}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const sendsSource = format === 'source';
|
|
136
|
+
const needsRenderProof = format === 'html' || format === 'visual' || options.visual;
|
|
137
|
+
|
|
138
|
+
if (format === 'plain' && (HTML_TAG_RE.test(body) || ENCODED_HTML_RE.test(body))) {
|
|
139
|
+
addError(errors, 'raw-html-in-plain-body', 'plain body contains HTML source; send rendered HTML or rewrite as plain text');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!sendsSource && RENDERED_SOURCE_FENCE_RE.test(body)) {
|
|
143
|
+
addError(errors, 'rendered-source-fence', 'body contains source that should be rendered before sending');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (channel === 'email' && format === 'markdown' && RENDERED_SOURCE_FENCE_RE.test(body)) {
|
|
147
|
+
addError(errors, 'markdown-email-source', 'email body contains fenced rendered source; attach source intentionally or render it first');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (needsRenderProof && !proofExists(options.proofFile)) {
|
|
151
|
+
addError(errors, 'render-proof-missing', 'HTML or visual sends need --proof-file with preview, screenshot, PDF, or rendered-email receipt');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (coachSurface) {
|
|
155
|
+
if (COACH_INTERNAL_RE.test(body)) {
|
|
156
|
+
addError(errors, 'coach-internal-language', 'coach copy contains an internal id, flag, or path');
|
|
157
|
+
}
|
|
158
|
+
if (COACH_PRESSURE_RE.test(body)) {
|
|
159
|
+
addError(errors, 'coach-pressure-language', 'coach copy contains urgency, guilt, nagging, or generic productivity praise');
|
|
160
|
+
}
|
|
161
|
+
if (coachSurface === 'warm-ping' && !proofExists(options.signalProof)) {
|
|
162
|
+
addError(errors, 'coach-signal-proof-missing', 'warm pings need --signal-proof for the fresh human event');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!options.allowSlop) {
|
|
167
|
+
for (const rule of SLOP_RULES) {
|
|
168
|
+
if (rule.re.test(body)) {
|
|
169
|
+
addError(errors, rule.id, rule.message);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return errors;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function main() {
|
|
178
|
+
let options;
|
|
179
|
+
try {
|
|
180
|
+
options = parseArgs(process.argv.slice(2));
|
|
181
|
+
} catch (err) {
|
|
182
|
+
console.error(err.message);
|
|
183
|
+
console.error(usage());
|
|
184
|
+
process.exitCode = 2;
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (options.help) {
|
|
189
|
+
console.log(usage());
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
let body;
|
|
194
|
+
try {
|
|
195
|
+
body = readBody(options);
|
|
196
|
+
} catch (err) {
|
|
197
|
+
console.error(`failed to read body: ${err.message}`);
|
|
198
|
+
process.exitCode = 2;
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const errors = scanOutboundArtifact(options, body);
|
|
203
|
+
if (errors.length) {
|
|
204
|
+
console.error('outbound artifact gate failed');
|
|
205
|
+
for (const error of errors) {
|
|
206
|
+
console.error(`- ${error.id}: ${error.message}`);
|
|
207
|
+
}
|
|
208
|
+
process.exitCode = 1;
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
console.log('outbound artifact gate passed');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (require.main === module) {
|
|
216
|
+
main();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
module.exports = {
|
|
220
|
+
HTML_TAG_RE,
|
|
221
|
+
RENDERED_SOURCE_FENCE_RE,
|
|
222
|
+
SLOP_RULES,
|
|
223
|
+
COACH_INTERNAL_RE,
|
|
224
|
+
COACH_PRESSURE_RE,
|
|
225
|
+
parseArgs,
|
|
226
|
+
scanOutboundArtifact,
|
|
227
|
+
};
|
package/utils/update-check.js
CHANGED
|
@@ -293,6 +293,20 @@ function formatInstallGitWarning(state) {
|
|
|
293
293
|
'npm install -g atris@latest may not change the code currently on PATH; resolve that checkout before trusting upgrade status.';
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
+
/**
|
|
297
|
+
* Human version line. Published/sealed installs stay plain.
|
|
298
|
+
* Git checkouts append `(git <sha> [dirty] <branch>)`.
|
|
299
|
+
*/
|
|
300
|
+
function formatCliVersionLine(version, packageRoot = path.join(__dirname, '..')) {
|
|
301
|
+
const base = String(version || 'unknown').replace(/^v/, '');
|
|
302
|
+
if (!isGitCheckout(packageRoot)) return `atris v${base}`;
|
|
303
|
+
const state = inspectInstallGitState(packageRoot);
|
|
304
|
+
const sha = state.head || 'unknown';
|
|
305
|
+
const branch = state.detached ? 'detached' : (state.branch || 'unknown');
|
|
306
|
+
if (state.dirty) return `atris v${base} (git ${sha} dirty ${branch})`;
|
|
307
|
+
return `atris v${base} (git ${sha} ${branch})`;
|
|
308
|
+
}
|
|
309
|
+
|
|
296
310
|
function normalizeAutoUpdateMode(env = process.env) {
|
|
297
311
|
const raw = String(env.ATRIS_AUTO_UPDATE || '').trim().toLowerCase();
|
|
298
312
|
if (['0', 'false', 'no', 'off', 'notify'].includes(raw)) return 'off';
|
|
@@ -349,6 +363,7 @@ module.exports = {
|
|
|
349
363
|
shouldAutoUpdate,
|
|
350
364
|
inspectInstallGitState,
|
|
351
365
|
formatInstallGitWarning,
|
|
366
|
+
formatCliVersionLine,
|
|
352
367
|
isGitCheckout,
|
|
353
368
|
getNpmSelfUpdateCommand,
|
|
354
369
|
getNpmSelfUpdateSpawnArgs,
|