atris 3.37.1 → 3.40.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 +25 -6
- package/README.md +2 -0
- package/atris/PERSONA.md +11 -11
- package/atris.md +17 -2
- package/ax +160 -34
- package/bin/atris.js +78 -6
- package/commands/activate.js +16 -3
- package/commands/agent-spawn.js +13 -18
- package/commands/autoland.js +76 -47
- package/commands/autopilot-front.js +0 -4
- package/commands/autopilot.js +0 -12
- package/commands/bench.js +10 -12
- package/commands/brain.js +0 -7
- package/commands/brief.js +0 -2
- package/commands/business-sync.js +0 -1
- package/commands/business.js +452 -12
- package/commands/chat-scan.js +5 -7
- package/commands/clean.js +31 -2
- package/commands/close.js +46 -11
- package/commands/cloud.js +0 -1
- package/commands/codex-goal.js +8 -12
- package/commands/computer.js +6 -5
- package/commands/console.js +64 -11
- package/commands/decide.js +166 -0
- package/commands/deck.js +30 -12
- package/commands/dream.js +0 -7
- package/commands/drill.js +14 -25
- package/commands/engine.js +206 -26
- package/commands/experiments.js +0 -4
- package/commands/fleet-report.js +12 -4
- package/commands/game.js +261 -0
- package/commands/gm.js +17 -7
- package/commands/goal.js +0 -6
- package/commands/harvest.js +1 -5
- package/commands/improve.js +29 -8
- package/commands/init.js +41 -37
- package/commands/integrations.js +0 -7
- package/commands/land.js +77 -20
- package/commands/launchpad.js +1 -16
- package/commands/lesson.js +17 -7
- package/commands/lifecycle.js +5 -5
- package/commands/log.js +55 -5
- package/commands/loop-front.js +0 -2
- package/commands/loops.js +6 -5
- package/commands/meet.js +0 -2
- package/commands/member.js +622 -573
- package/commands/mission.js +790 -314
- package/commands/next.js +0 -1
- package/commands/now.js +0 -12
- package/commands/one-lap.js +8 -8
- package/commands/orb.js +0 -4
- package/commands/pack-craft.js +0 -1
- package/commands/pack.js +3599 -424
- package/commands/play.js +6 -6
- package/commands/probe.js +12 -6
- package/commands/pulse.js +88 -64
- package/commands/push.js +0 -4
- package/commands/radar.js +14 -11
- package/commands/recover.js +340 -0
- package/commands/release.js +10 -9
- package/commands/report.js +0 -1
- package/commands/router.js +320 -0
- package/commands/run-front.js +0 -2
- package/commands/search.js +0 -3
- package/commands/site-deploy.js +870 -0
- package/commands/site.js +11 -2
- package/commands/skill.js +7 -2
- package/commands/slop.js +237 -7
- package/commands/status.js +6 -0
- package/commands/stream.js +4 -20
- package/commands/study.js +0 -3
- package/commands/sync-checkout.js +51 -0
- package/commands/sync.js +11 -40
- package/commands/task.js +999 -595
- package/commands/taste.js +101 -0
- package/commands/teach.js +86 -0
- package/commands/team.js +83 -3
- package/commands/unknowns.js +0 -10
- package/commands/update.js +0 -1
- package/commands/vercel.js +4 -2
- package/commands/visualize.js +0 -5
- package/commands/voice.js +195 -0
- package/commands/watch.js +1 -28
- package/commands/wiki.js +42 -15
- package/commands/wish.js +0 -1
- package/commands/workflow.js +2 -2
- package/commands/worktree.js +88 -28
- package/commands/xp.js +28 -30
- package/commands/youtube.js +0 -2
- package/lib/accept-verify-gate.js +126 -0
- package/lib/activity-stream.js +0 -2
- package/lib/arg-parser.js +41 -0
- package/lib/auto-accept-certified.js +261 -3
- package/lib/autoland.js +66 -3
- package/lib/ax-auto-lane.js +68 -7
- package/lib/bench/context.js +0 -2
- package/lib/bench/report.js +0 -1
- package/lib/bench/runner.js +18 -11
- package/lib/brief-ledger.js +0 -6
- package/lib/business-simulate.js +382 -0
- package/lib/chat-log-scan.js +0 -5
- package/lib/checkout-sync.js +52 -0
- package/lib/claude-boot-block.js +48 -0
- package/lib/cloud-mission.js +0 -6
- package/lib/codex-flight.js +0 -1
- package/lib/conductor-artifacts.js +25 -0
- package/lib/context-gatherer.js +7 -2
- package/lib/deck-compose.js +0 -1
- package/lib/deck-history.js +0 -1
- package/lib/deck-layout.js +0 -5
- package/lib/deck-review.js +0 -7
- package/lib/deck-schema.js +0 -1
- package/lib/default-verifier.js +0 -2
- package/lib/engine-registry.js +157 -28
- package/lib/experiments/daily.js +0 -6
- package/lib/falsifier-probe.js +84 -0
- package/lib/fleet.js +275 -42
- package/lib/functional-owner.js +0 -3
- package/lib/git-spawn.js +15 -0
- package/lib/inspect-fields.js +0 -1
- package/lib/json-file.js +37 -0
- package/lib/known-commands.js +2 -2
- package/lib/learnings.js +0 -2
- package/lib/lesson-preflight.js +146 -0
- package/lib/loop-doctor.js +11 -19
- package/lib/manifest.js +0 -2
- package/lib/member-alive.js +0 -1
- package/lib/member-switches.js +0 -5
- package/lib/mission-artifact.js +0 -1
- package/lib/mission-human-asks.js +28 -0
- package/lib/mission-protected-lane.js +296 -0
- package/lib/mission-room.js +0 -2
- package/lib/mission-runtime-loop.js +0 -1
- package/lib/next-moves.js +0 -18
- package/lib/official-cli-integration.js +47 -6
- package/lib/orb-context.js +14 -4
- package/lib/orb-scorecard.js +0 -1
- package/lib/outbound-send-gate.js +0 -6
- package/lib/pack-capabilities.js +685 -0
- package/lib/permission-grants.js +0 -6
- package/lib/policy-lessons.js +0 -2
- package/lib/pulse.js +68 -15
- package/lib/review-integrity.js +0 -2
- package/lib/router-brain.js +51 -3
- package/lib/runner-command.js +0 -10
- package/lib/runtime-bootstrap.js +0 -1
- package/lib/scorecard.js +0 -1
- package/lib/self-drive.js +45 -14
- package/lib/state-detection.js +0 -6
- package/lib/task-db.js +137 -7
- package/lib/task-decision.js +50 -0
- package/lib/taste-lessons.js +153 -0
- package/lib/team-presence.js +0 -1
- package/lib/tool-result-encode.js +17 -1
- package/lib/voice-gate.js +129 -2
- package/lib/wiki.js +152 -9
- package/lib/wish-audit.js +21 -7
- package/lib/wish-delegate.js +27 -21
- package/lib/wish-stats.js +0 -5
- package/lib/wish-store.js +0 -1
- package/lib/workspace-safety.js +0 -2
- package/lib/zip.js +95 -8
- package/package.json +2 -1
- package/templates/business-starter/persona.md +9 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const ACTIVE_STATUSES = new Set(['observed', 'open', 'attempted']);
|
|
7
|
+
const KEYWORD_STOP_WORDS = new Set([
|
|
8
|
+
'and', 'are', 'for', 'from', 'into', 'not', 'of', 'on', 'or', 'the', 'this', 'to', 'with', 'without',
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
function readJson(filePath) {
|
|
12
|
+
try {
|
|
13
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
14
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
15
|
+
} catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readLessonText(filePath) {
|
|
21
|
+
let content = '';
|
|
22
|
+
try {
|
|
23
|
+
content = fs.readFileSync(filePath, 'utf8');
|
|
24
|
+
} catch {
|
|
25
|
+
return new Map();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const bySlug = new Map();
|
|
29
|
+
for (const line of content.split(/\r?\n/)) {
|
|
30
|
+
const match = line.match(/^\s*-\s*\*\*\[([^\]]+)\]\s*([a-z0-9][a-z0-9-]*)\*\*\s*[—:-]+\s*(?:pass|fail)?\s*[—:-]*\s*(.*)$/i);
|
|
31
|
+
if (!match || bySlug.has(match[2])) continue;
|
|
32
|
+
const text = match[3].replace(/^\[resolved\]\s*/i, '').trim();
|
|
33
|
+
if (text) bySlug.set(match[2], text);
|
|
34
|
+
}
|
|
35
|
+
return bySlug;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isPromptRule(mechanism) {
|
|
39
|
+
if (mechanism && typeof mechanism === 'object') {
|
|
40
|
+
const kind = String(mechanism.type || mechanism.kind || '').toLowerCase();
|
|
41
|
+
return kind === 'prompt-rule' || kind === 'prompt_rule' || mechanism.prompt_rule === true;
|
|
42
|
+
}
|
|
43
|
+
return /^\s*prompt[-_\s]?rule\b/i.test(String(mechanism || ''));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function lessonIsEligible(lesson) {
|
|
47
|
+
const status = String(lesson && lesson.status || '').toLowerCase();
|
|
48
|
+
if (ACTIVE_STATUSES.has(status)) return true;
|
|
49
|
+
return status === 'resolved' && isPromptRule(lesson.mechanism);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeFile(file) {
|
|
53
|
+
return String(file || '')
|
|
54
|
+
.trim()
|
|
55
|
+
.replace(/\\/g, '/')
|
|
56
|
+
.replace(/^\.\//, '')
|
|
57
|
+
.replace(/\/{2,}/g, '/')
|
|
58
|
+
.replace(/\/$/, '');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function pathSpecificity(file) {
|
|
62
|
+
const normalized = normalizeFile(file);
|
|
63
|
+
const segments = normalized.split('/').filter(Boolean).length;
|
|
64
|
+
return (segments * 100) + normalized.length;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function overlapStrength(appliesTo, file) {
|
|
68
|
+
const lessonPath = normalizeFile(appliesTo).toLowerCase();
|
|
69
|
+
const taskPath = normalizeFile(file).toLowerCase();
|
|
70
|
+
if (!lessonPath || !taskPath) return 0;
|
|
71
|
+
if (lessonPath === taskPath) return 3;
|
|
72
|
+
if (taskPath.endsWith(`/${lessonPath}`) || lessonPath.endsWith(`/${taskPath}`)) return 3;
|
|
73
|
+
if (taskPath.startsWith(`${lessonPath}/`) || lessonPath.startsWith(`${taskPath}/`)) return 2;
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function slugKeywords(slug) {
|
|
78
|
+
return [...new Set(String(slug || '').toLowerCase().split(/[^a-z0-9]+/)
|
|
79
|
+
.filter((word) => word.length >= 3 && !KEYWORD_STOP_WORDS.has(word)))];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function keywordAppears(text, keyword) {
|
|
83
|
+
const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
84
|
+
return new RegExp(`(^|[^a-z0-9])${escaped}([^a-z0-9]|$)`, 'i').test(text);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function bestMatch({ slug, lesson, briefText, files }) {
|
|
88
|
+
const brief = String(briefText || '');
|
|
89
|
+
const briefLower = brief.toLowerCase().replace(/\\/g, '/');
|
|
90
|
+
const appliesTo = Array.isArray(lesson.applies_to) ? lesson.applies_to : [];
|
|
91
|
+
let best = null;
|
|
92
|
+
|
|
93
|
+
for (const lessonFile of appliesTo) {
|
|
94
|
+
const normalizedLessonFile = normalizeFile(lessonFile);
|
|
95
|
+
if (!normalizedLessonFile) continue;
|
|
96
|
+
for (const taskFile of files) {
|
|
97
|
+
const strength = overlapStrength(normalizedLessonFile, taskFile);
|
|
98
|
+
if (!strength) continue;
|
|
99
|
+
const score = 300000 + (strength * 10000) + pathSpecificity(normalizedLessonFile) - appliesTo.length;
|
|
100
|
+
if (!best || score > best.score) {
|
|
101
|
+
best = { score, why: `file overlaps ${normalizedLessonFile}` };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (briefLower.includes(normalizedLessonFile.toLowerCase())) {
|
|
105
|
+
const score = 200000 + pathSpecificity(normalizedLessonFile) - appliesTo.length;
|
|
106
|
+
if (!best || score > best.score) {
|
|
107
|
+
best = { score, why: `brief mentions ${normalizedLessonFile}` };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const matchedKeywords = slugKeywords(slug).filter((keyword) => keywordAppears(brief, keyword));
|
|
113
|
+
if (matchedKeywords.length) {
|
|
114
|
+
const keywordLength = matchedKeywords.reduce((total, keyword) => total + keyword.length, 0);
|
|
115
|
+
const score = 100000 + (matchedKeywords.length * 1000) + keywordLength;
|
|
116
|
+
if (!best || score > best.score) {
|
|
117
|
+
best = { score, why: `brief mentions ${matchedKeywords.join(', ')}` };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return best;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function matchLessons({ briefText = '', files = [], root = process.cwd() } = {}) {
|
|
125
|
+
const metadata = readJson(path.join(root, 'atris', 'lessons.json'));
|
|
126
|
+
const lessonText = readLessonText(path.join(root, 'atris', 'lessons.md'));
|
|
127
|
+
const normalizedFiles = (Array.isArray(files) ? files : [files]).map(normalizeFile).filter(Boolean);
|
|
128
|
+
const matches = [];
|
|
129
|
+
|
|
130
|
+
for (const [slug, lesson] of Object.entries(metadata)) {
|
|
131
|
+
if (slug === '_schema' || !lesson || typeof lesson !== 'object') continue;
|
|
132
|
+
if (!lessonIsEligible(lesson)) continue;
|
|
133
|
+
const text = lessonText.get(slug);
|
|
134
|
+
if (!text) continue;
|
|
135
|
+
const matched = bestMatch({ slug, lesson, briefText, files: normalizedFiles });
|
|
136
|
+
if (!matched) continue;
|
|
137
|
+
matches.push({ slug, text, why_matched: matched.why, score: matched.score });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return matches
|
|
141
|
+
.sort((left, right) => right.score - left.score || left.slug.localeCompare(right.slug))
|
|
142
|
+
.slice(0, 5)
|
|
143
|
+
.map(({ slug, text, why_matched }) => ({ slug, text, why_matched }));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = { matchLessons };
|
package/lib/loop-doctor.js
CHANGED
|
@@ -20,8 +20,6 @@ const HEADLESS_CAPABLE_RUNNERS = new Set([
|
|
|
20
20
|
'codex',
|
|
21
21
|
'cursor',
|
|
22
22
|
'devin',
|
|
23
|
-
'hermes',
|
|
24
|
-
'hermes-agent',
|
|
25
23
|
]);
|
|
26
24
|
const FINDING_PRIORITY = {
|
|
27
25
|
repeated_auth_failure: 500,
|
|
@@ -311,7 +309,7 @@ function missionLockPidAlive(pid) {
|
|
|
311
309
|
}
|
|
312
310
|
}
|
|
313
311
|
|
|
314
|
-
function
|
|
312
|
+
function missionLockRecords(root, pidAlive = missionLockPidAlive) {
|
|
315
313
|
const stateDir = path.join(root, '.atris', 'state');
|
|
316
314
|
let names = [];
|
|
317
315
|
try {
|
|
@@ -319,18 +317,21 @@ function staleMissionLockFinding(root, nowIso, pidAlive = missionLockPidAlive) {
|
|
|
319
317
|
} catch {
|
|
320
318
|
return [];
|
|
321
319
|
}
|
|
322
|
-
|
|
323
|
-
for (const name of names) {
|
|
320
|
+
return names.map((name) => {
|
|
324
321
|
const file = path.join(stateDir, name);
|
|
325
322
|
let lock = null;
|
|
326
323
|
try { lock = JSON.parse(fs.readFileSync(file, 'utf8')); } catch {}
|
|
327
324
|
const pid = Number(lock && lock.pid);
|
|
328
325
|
if (!Number.isInteger(pid) || pid <= 0) {
|
|
329
|
-
|
|
330
|
-
} else if (!pidAlive(pid)) {
|
|
331
|
-
stale.push({ lock: name, reason: 'dead-pid', pid });
|
|
326
|
+
return { lock: name, reason: 'unparseable', pid: null, stale: true };
|
|
332
327
|
}
|
|
333
|
-
|
|
328
|
+
const alive = pidAlive(pid);
|
|
329
|
+
return { lock: name, reason: alive ? 'live-pid' : 'dead-pid', pid, stale: !alive };
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function staleMissionLockFinding(root, nowIso, pidAlive = missionLockPidAlive) {
|
|
334
|
+
const stale = missionLockRecords(root, pidAlive).filter((lock) => lock.stale);
|
|
334
335
|
if (!stale.length) return [];
|
|
335
336
|
return [{
|
|
336
337
|
kind: 'stale_mission_lock',
|
|
@@ -400,17 +401,8 @@ function scanLoopReceipts({ root = process.cwd(), now = new Date(), freshnessMs
|
|
|
400
401
|
|
|
401
402
|
module.exports = {
|
|
402
403
|
scanLoopReceipts,
|
|
403
|
-
|
|
404
|
-
authFailureFindings,
|
|
405
|
-
rewardFlatlineFinding,
|
|
406
|
-
zeroDueCapableFinding,
|
|
407
|
-
staleMissionLockFinding,
|
|
408
|
-
isDueCapableMission,
|
|
404
|
+
missionLockRecords,
|
|
409
405
|
latestMissionRecords,
|
|
410
|
-
orderFindings,
|
|
411
406
|
nonPositiveRewardPressure,
|
|
412
|
-
applyRewardPressure,
|
|
413
407
|
FINDING_PRIORITY,
|
|
414
|
-
DEFAULT_RECEIPT_LIMIT,
|
|
415
|
-
DEFAULT_FINDING_FRESHNESS_MS,
|
|
416
408
|
};
|
package/lib/manifest.js
CHANGED
package/lib/member-alive.js
CHANGED
package/lib/member-switches.js
CHANGED
|
@@ -129,14 +129,9 @@ function memberArgFromArgv(argv = process.argv) {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
module.exports = {
|
|
132
|
-
SWITCHES_REL,
|
|
133
|
-
switchesPath,
|
|
134
|
-
loadMemberSwitches,
|
|
135
|
-
saveMemberSwitches,
|
|
136
132
|
isLocalMember,
|
|
137
133
|
getMemberSwitch,
|
|
138
134
|
isMemberAwake,
|
|
139
|
-
isMemberLoopAwake,
|
|
140
135
|
setMemberAwake,
|
|
141
136
|
setMemberLoopAwake,
|
|
142
137
|
readLoopFlag,
|
package/lib/mission-artifact.js
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function normalizeHumanAsk(ask) {
|
|
4
|
+
if (typeof ask === 'string') {
|
|
5
|
+
return { text: ask, answered_at: null };
|
|
6
|
+
}
|
|
7
|
+
if (!ask || typeof ask !== 'object' || Array.isArray(ask)) {
|
|
8
|
+
return { text: String(ask || ''), answered_at: null };
|
|
9
|
+
}
|
|
10
|
+
return {
|
|
11
|
+
...ask,
|
|
12
|
+
text: String(ask.text || ''),
|
|
13
|
+
answered_at: ask.answered_at || null,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function normalizeHumanAsks(asks) {
|
|
18
|
+
return Array.isArray(asks) ? asks.map(normalizeHumanAsk) : [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function openHumanAsks(asks) {
|
|
22
|
+
return normalizeHumanAsks(asks).filter((ask) => ask.text.trim() && !ask.answered_at);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = {
|
|
26
|
+
normalizeHumanAsks,
|
|
27
|
+
openHumanAsks,
|
|
28
|
+
};
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { spawnSync } = require('child_process');
|
|
7
|
+
const { DENIED_TAGS } = require('./fleet');
|
|
8
|
+
|
|
9
|
+
const PATH_SURFACES = [
|
|
10
|
+
['middleware/proxy', /(?:^|[/_.-])(?:middleware|proxy)(?:[/_.-]|$)/i],
|
|
11
|
+
['csp', /(?:^|[/_.-])csp(?:[/_.-]|$)/i],
|
|
12
|
+
['auth', /(?:^|[/_.-])auth(?:entication|orization)?(?:[/_.-]|$)/i],
|
|
13
|
+
['session', /(?:^|[/_.-])sessions?(?:[/_.-]|$)/i],
|
|
14
|
+
['cookie', /(?:^|[/_.-])cookies?(?:[/_.-]|$)/i],
|
|
15
|
+
['cors', /(?:^|[/_.-])cors(?:[/_.-]|$)/i],
|
|
16
|
+
['sandbox', /(?:^|[/_.-])sandbox(?:[/_.-]|$)/i],
|
|
17
|
+
['permission', /(?:^|[/_.-])permissions?(?:[/_.-]|$)/i],
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const CONTENT_SURFACES = [
|
|
21
|
+
['csp', /\bContent-Security-Policy\b/i],
|
|
22
|
+
['csp nonce', /\bnonce\b/i],
|
|
23
|
+
['iframe sandbox', /\bsandbox\s*=/i],
|
|
24
|
+
['cookie', /\bSet-Cookie\b/i],
|
|
25
|
+
['cors', /\bAccess-Control-Allow-[A-Za-z-]+\b/i],
|
|
26
|
+
['public dynamic rendering', /\bforce-dynamic\b/i],
|
|
27
|
+
['auth header', /\b(?:Authorization\s*[:=].*\bBearer\b|headers?\b[^\n]*\bauthorization\b|\bauthorization\b[^\n]*headers?\b)/i],
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
function normalizedTags(tags) {
|
|
31
|
+
const list = Array.isArray(tags) ? tags : [tags];
|
|
32
|
+
return list
|
|
33
|
+
.flatMap((tag) => String(tag || '').split(','))
|
|
34
|
+
.map((tag) => tag.trim().toLowerCase().replace(/_/g, '-'))
|
|
35
|
+
.filter(Boolean);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function changedPathsFromDiff(diff) {
|
|
39
|
+
const paths = [];
|
|
40
|
+
for (const line of String(diff || '').split(/\r?\n/)) {
|
|
41
|
+
const header = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
|
|
42
|
+
if (header) {
|
|
43
|
+
paths.push(header[1], header[2]);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const file = line.match(/^(?:---|\+\+\+) (?:[ab]\/)?(.+)$/);
|
|
47
|
+
if (file && file[1] !== '/dev/null') paths.push(file[1]);
|
|
48
|
+
}
|
|
49
|
+
return Array.from(new Set(paths));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Prose is not a protected surface. Docs, judge cards, wikis, journals and run
|
|
53
|
+
// receipts routinely *describe* CSP or force-dynamic without changing any
|
|
54
|
+
// behaviour — replaying this guard over 60 real commits, 5 of 6 hits were
|
|
55
|
+
// markdown that merely mentioned the words. A guard that pauses on every judge
|
|
56
|
+
// card gets switched off, which is worse than no guard. Content signals
|
|
57
|
+
// therefore apply to code only; PATH signals still apply to every file.
|
|
58
|
+
const CODE_FILE = /\.(?:[cm]?[jt]sx?|py|rb|go|rs|java|kt|swift|php|sh|bash|zsh|sql|ya?ml|toml|env)$/i;
|
|
59
|
+
|
|
60
|
+
function isCodeFile(file) {
|
|
61
|
+
const f = String(file || '').trim();
|
|
62
|
+
if (!f || f === '/dev/null') return false;
|
|
63
|
+
if (/\.(?:md|mdx|markdown|txt|rst|adoc)$/i.test(f)) return false;
|
|
64
|
+
return CODE_FILE.test(f);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Walk the diff file-by-file so a changed line is attributed to the file it
|
|
68
|
+
// came from, then keep only lines from code files.
|
|
69
|
+
function changedCodeLinesFromDiff(diff) {
|
|
70
|
+
const lines = [];
|
|
71
|
+
let inCode = false;
|
|
72
|
+
for (const line of String(diff || '').split(/\r?\n/)) {
|
|
73
|
+
const header = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
|
|
74
|
+
if (header) {
|
|
75
|
+
inCode = isCodeFile(header[1]) || isCodeFile(header[2]);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (/^(?:---|\+\+\+) /.test(line)) continue;
|
|
79
|
+
if (!inCode) continue;
|
|
80
|
+
if (/^[+-]/.test(line)) lines.push(line.slice(1));
|
|
81
|
+
}
|
|
82
|
+
return lines;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function matchProtectedMissionDiff(diff, { tags = [] } = {}) {
|
|
86
|
+
const matches = [];
|
|
87
|
+
const add = (surface, signal, detail) => {
|
|
88
|
+
if (matches.some((match) => match.surface === surface && match.signal === signal && match.detail === detail)) return;
|
|
89
|
+
matches.push({ surface, signal, detail });
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
for (const tag of normalizedTags(tags)) {
|
|
93
|
+
if (DENIED_TAGS.includes(tag)) add(tag, 'tag', tag);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (const file of changedPathsFromDiff(diff)) {
|
|
97
|
+
for (const [surface, pattern] of PATH_SURFACES) {
|
|
98
|
+
if (pattern.test(file)) add(surface, 'path', file);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const line of changedCodeLinesFromDiff(diff)) {
|
|
103
|
+
for (const [surface, pattern] of CONTENT_SURFACES) {
|
|
104
|
+
if (pattern.test(line)) add(surface, 'content', 'changed line');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
protected: matches.length > 0,
|
|
110
|
+
surfaces: Array.from(new Set(matches.map((match) => match.surface))),
|
|
111
|
+
matches,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function runGitDiff(git, args, root) {
|
|
116
|
+
const result = git(args, root);
|
|
117
|
+
if (!result || result.error || result.status !== 0) {
|
|
118
|
+
const detail = result?.error?.message
|
|
119
|
+
|| String(result?.stderr || result?.stdout || `git ${args.join(' ')} failed`).trim();
|
|
120
|
+
return { ok: false, detail: detail || 'git diff could not be read' };
|
|
121
|
+
}
|
|
122
|
+
return { ok: true, diff: String(result.stdout || '') };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function defaultGit(gitBin = 'git') {
|
|
126
|
+
return (args, root) => spawnSync(gitBin, args, {
|
|
127
|
+
cwd: root,
|
|
128
|
+
encoding: 'utf8',
|
|
129
|
+
timeout: 10000,
|
|
130
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function readMissionDiff({
|
|
135
|
+
root = process.cwd(),
|
|
136
|
+
baseRef = '',
|
|
137
|
+
git = defaultGit(),
|
|
138
|
+
includeUnstaged = true,
|
|
139
|
+
} = {}) {
|
|
140
|
+
const parts = [];
|
|
141
|
+
const staged = runGitDiff(git, ['diff', '--cached', '--no-ext-diff', '--unified=0', '--no-color', '--'], root);
|
|
142
|
+
if (!staged.ok) return staged;
|
|
143
|
+
parts.push(staged.diff);
|
|
144
|
+
|
|
145
|
+
if (includeUnstaged) {
|
|
146
|
+
const unstaged = runGitDiff(git, ['diff', '--no-ext-diff', '--unified=0', '--no-color', '--'], root);
|
|
147
|
+
if (!unstaged.ok) return unstaged;
|
|
148
|
+
parts.push(unstaged.diff);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (baseRef) {
|
|
152
|
+
const committed = runGitDiff(git, ['diff', '--no-ext-diff', '--unified=0', '--no-color', baseRef, 'HEAD', '--'], root);
|
|
153
|
+
if (!committed.ok) return committed;
|
|
154
|
+
parts.push(committed.diff);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { ok: true, diff: parts.filter(Boolean).join('\n') };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function protectedPause(reason, detail = {}) {
|
|
161
|
+
return {
|
|
162
|
+
ok: false,
|
|
163
|
+
allowed: false,
|
|
164
|
+
status: 'paused-for-review',
|
|
165
|
+
reason,
|
|
166
|
+
...detail,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function unreadableMissionGuard(error) {
|
|
171
|
+
return protectedPause('mission diff could not be read; repair the tooling failure and rerun', {
|
|
172
|
+
status: 'paused-for-retry',
|
|
173
|
+
unreadable: true,
|
|
174
|
+
detail: error?.message || String(error || 'unknown diff read failure'),
|
|
175
|
+
surfaces: ['unreadable diff'],
|
|
176
|
+
matches: [],
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function inspectMissionProtectedDiff({
|
|
181
|
+
root = process.cwd(),
|
|
182
|
+
baseRef = '',
|
|
183
|
+
tags = [],
|
|
184
|
+
readDiff = readMissionDiff,
|
|
185
|
+
git,
|
|
186
|
+
includeUnstaged = true,
|
|
187
|
+
} = {}) {
|
|
188
|
+
let read;
|
|
189
|
+
try {
|
|
190
|
+
read = readDiff({ root, baseRef, tags, git, includeUnstaged });
|
|
191
|
+
} catch (error) {
|
|
192
|
+
return unreadableMissionGuard(error);
|
|
193
|
+
}
|
|
194
|
+
if (!read || read.ok !== true) {
|
|
195
|
+
return unreadableMissionGuard(read?.detail);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const matched = matchProtectedMissionDiff(read.diff, { tags });
|
|
199
|
+
if (matched.protected) {
|
|
200
|
+
return protectedPause(`protected mission surface requires human review: ${matched.surfaces.join(', ')}`, {
|
|
201
|
+
unreadable: false,
|
|
202
|
+
surfaces: matched.surfaces,
|
|
203
|
+
matches: matched.matches,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
ok: true,
|
|
208
|
+
allowed: true,
|
|
209
|
+
status: 'clear',
|
|
210
|
+
reason: 'mission diff contains no protected surface',
|
|
211
|
+
unreadable: false,
|
|
212
|
+
surfaces: [],
|
|
213
|
+
matches: [],
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function guardMissionLanding(options = {}) {
|
|
218
|
+
const inspection = inspectMissionProtectedDiff(options);
|
|
219
|
+
if (!inspection.allowed) return inspection;
|
|
220
|
+
const land = typeof options.land === 'function' ? options.land : () => ({ ok: true, landed: true });
|
|
221
|
+
return { ...land(), protected_lane_guard: inspection };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function gitSubcommand(args) {
|
|
225
|
+
const consumesValue = new Set(['-C', '-c', '--git-dir', '--work-tree', '--namespace', '--config-env']);
|
|
226
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
227
|
+
const arg = String(args[index] || '');
|
|
228
|
+
if (consumesValue.has(arg)) {
|
|
229
|
+
index += 1;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (arg.startsWith('-')) continue;
|
|
233
|
+
return arg;
|
|
234
|
+
}
|
|
235
|
+
return '';
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function prepareMissionGitGuard({
|
|
239
|
+
root = process.cwd(),
|
|
240
|
+
tags = [],
|
|
241
|
+
pathValue = process.env.PATH || '',
|
|
242
|
+
} = {}) {
|
|
243
|
+
const resolved = spawnSync('sh', ['-c', 'command -v git'], {
|
|
244
|
+
cwd: root,
|
|
245
|
+
encoding: 'utf8',
|
|
246
|
+
timeout: 5000,
|
|
247
|
+
});
|
|
248
|
+
const gitBin = String(resolved.stdout || '').trim();
|
|
249
|
+
if (resolved.status !== 0 || !gitBin) {
|
|
250
|
+
throw new Error('mission git guard could not resolve git');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'atris-mission-git-'));
|
|
254
|
+
const wrapper = path.join(dir, 'git');
|
|
255
|
+
const modulePath = __filename;
|
|
256
|
+
const config = { root: path.resolve(root), tags: normalizedTags(tags), gitBin };
|
|
257
|
+
const source = [
|
|
258
|
+
'#!/usr/bin/env node',
|
|
259
|
+
"'use strict';",
|
|
260
|
+
`const { spawnSync } = require('child_process');`,
|
|
261
|
+
`const guard = require(${JSON.stringify(modulePath)});`,
|
|
262
|
+
`const config = ${JSON.stringify(config)};`,
|
|
263
|
+
gitSubcommand.toString(),
|
|
264
|
+
defaultGit.toString(),
|
|
265
|
+
`const args = process.argv.slice(2);`,
|
|
266
|
+
`if (gitSubcommand(args) === 'commit') {`,
|
|
267
|
+
` const result = guard.inspectMissionProtectedDiff({ root: config.root, tags: config.tags, includeUnstaged: false, git: defaultGit(config.gitBin) });`,
|
|
268
|
+
` if (!result.allowed) {`,
|
|
269
|
+
` process.stderr.write(result.reason + '\\n');`,
|
|
270
|
+
` process.exit(78);`,
|
|
271
|
+
` }`,
|
|
272
|
+
`}`,
|
|
273
|
+
`const child = spawnSync(config.gitBin, args, { cwd: process.cwd(), stdio: 'inherit' });`,
|
|
274
|
+
`if (child.error) { process.stderr.write(child.error.message + '\\n'); process.exit(1); }`,
|
|
275
|
+
`process.exit(child.status == null ? 1 : child.status);`,
|
|
276
|
+
'',
|
|
277
|
+
].join('\n');
|
|
278
|
+
fs.writeFileSync(wrapper, source, { encoding: 'utf8', mode: 0o755 });
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
env: { ...process.env, PATH: `${dir}${path.delimiter}${pathValue}` },
|
|
282
|
+
pathPrefix: dir,
|
|
283
|
+
cleanup() {
|
|
284
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch {}
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
module.exports = {
|
|
290
|
+
defaultGit,
|
|
291
|
+
matchProtectedMissionDiff,
|
|
292
|
+
unreadableMissionGuard,
|
|
293
|
+
inspectMissionProtectedDiff,
|
|
294
|
+
guardMissionLanding,
|
|
295
|
+
prepareMissionGitGuard,
|
|
296
|
+
};
|
package/lib/mission-room.js
CHANGED
package/lib/next-moves.js
CHANGED
|
@@ -844,37 +844,19 @@ function nextMoves(root = process.cwd(), limit = 3) {
|
|
|
844
844
|
module.exports = {
|
|
845
845
|
moveId,
|
|
846
846
|
norm,
|
|
847
|
-
titleKey,
|
|
848
847
|
isGenericInboxPlaceholder,
|
|
849
|
-
WEIGHT,
|
|
850
848
|
SELF_IMPROVEMENT_SEED_TITLE,
|
|
851
|
-
cleanSuggestedTargetTitle,
|
|
852
|
-
suggestedTargetFromReportText,
|
|
853
849
|
latestSuggestedTarget,
|
|
854
|
-
parseInboxTitles,
|
|
855
|
-
cleanInboxMoveTitle,
|
|
856
850
|
readRoadmapOpenItems,
|
|
857
|
-
readWaitingWishCards,
|
|
858
|
-
readActionableMissionCards,
|
|
859
|
-
readActiveMissions,
|
|
860
|
-
readUnreviewedShippedWishCards,
|
|
861
|
-
readDreamCards,
|
|
862
851
|
markDreamCardConsumed,
|
|
863
|
-
dreamCardId,
|
|
864
852
|
readEndgameMove,
|
|
865
|
-
readActiveTasks,
|
|
866
|
-
isInternalNextMoveTask,
|
|
867
853
|
readHandledTaskTitles,
|
|
868
854
|
latestInboxItems,
|
|
869
855
|
todayInboxItems,
|
|
870
|
-
gatherCandidates,
|
|
871
856
|
pickNextMoves,
|
|
872
|
-
readNextParked,
|
|
873
857
|
parkNextCard,
|
|
874
858
|
nextCards,
|
|
875
|
-
addMissionOnlyFallback,
|
|
876
859
|
nextMoves,
|
|
877
|
-
readDecisions,
|
|
878
860
|
recordDecision,
|
|
879
861
|
seedInboxFromMove,
|
|
880
862
|
pickRoadmapSeed,
|