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,50 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Decision rows are human judgment calls, not agent work. Detect them from
|
|
4
|
+
// tags already in the data (primary tag, tags[], metadata.tags). The live
|
|
5
|
+
// marker is needs-human (CLI-879); decision is the same hold under a clearer
|
|
6
|
+
// name. Autonomous pickers must refuse these; list renderers must show them.
|
|
7
|
+
|
|
8
|
+
const DECISION_HOLD_TAGS = new Set([
|
|
9
|
+
'needs-human',
|
|
10
|
+
'needshuman',
|
|
11
|
+
'decision',
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
const DECISION_MARKER = '[decision]';
|
|
15
|
+
const DECISION_REFUSE_REASON = 'decision row: human judgment required';
|
|
16
|
+
|
|
17
|
+
function normalizeDecisionTag(value) {
|
|
18
|
+
return String(value == null ? '' : value).trim().toLowerCase().replace(/_/g, '-');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function taskTagTokens(task) {
|
|
22
|
+
if (!task || typeof task !== 'object') return [];
|
|
23
|
+
const fromTags = Array.isArray(task.tags) ? task.tags : [];
|
|
24
|
+
const fromTag = task.tag ? [task.tag] : [];
|
|
25
|
+
const fromMeta = task.metadata && Array.isArray(task.metadata.tags) ? task.metadata.tags : [];
|
|
26
|
+
const fromTitle = (String(task.title || '').match(/#([a-z0-9-]+)/gi) || []).map((t) => t.slice(1));
|
|
27
|
+
return [...fromTag, ...fromTags, ...fromMeta, ...fromTitle]
|
|
28
|
+
.map(normalizeDecisionTag)
|
|
29
|
+
.filter(Boolean);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isDecisionHoldTag(tag) {
|
|
33
|
+
return DECISION_HOLD_TAGS.has(normalizeDecisionTag(tag));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isDecisionTask(task) {
|
|
37
|
+
return taskTagTokens(task).some(isDecisionHoldTag);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function decisionMarkerFor(task) {
|
|
41
|
+
return isDecisionTask(task) ? DECISION_MARKER : '';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = {
|
|
45
|
+
DECISION_REFUSE_REASON,
|
|
46
|
+
taskTagTokens,
|
|
47
|
+
isDecisionHoldTag,
|
|
48
|
+
isDecisionTask,
|
|
49
|
+
decisionMarkerFor,
|
|
50
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const VERDICTS = new Set(['keep', 'kill', 'more']);
|
|
7
|
+
const SCOPES = new Set(['writing', 'design', 'code', 'any']);
|
|
8
|
+
const STOP_WORDS = new Set([
|
|
9
|
+
'and', 'are', 'because', 'for', 'from', 'has', 'have', 'into', 'its', 'not', 'of', 'on', 'or',
|
|
10
|
+
'that', 'the', 'their', 'this', 'to', 'was', 'were', 'with', 'without', 'you', 'your',
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
function tastePath(root) {
|
|
14
|
+
return path.join(root, 'atris', 'taste.json');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readTaste(root) {
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(fs.readFileSync(tastePath(root), 'utf8'));
|
|
20
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
21
|
+
} catch {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function slugify(subject) {
|
|
27
|
+
return String(subject || '')
|
|
28
|
+
.normalize('NFKD')
|
|
29
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
30
|
+
.toLowerCase()
|
|
31
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
32
|
+
.replace(/^-+|-+$/g, '')
|
|
33
|
+
.slice(0, 80)
|
|
34
|
+
.replace(/-+$/g, '');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function validEntry(entry) {
|
|
38
|
+
return entry
|
|
39
|
+
&& typeof entry === 'object'
|
|
40
|
+
&& VERDICTS.has(entry.verdict)
|
|
41
|
+
&& typeof entry.subject === 'string'
|
|
42
|
+
&& entry.subject.trim()
|
|
43
|
+
&& typeof entry.why === 'string'
|
|
44
|
+
&& entry.why.trim()
|
|
45
|
+
&& SCOPES.has(entry.scope)
|
|
46
|
+
&& typeof entry.added === 'string';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function entriesFrom(store) {
|
|
50
|
+
return Object.entries(store)
|
|
51
|
+
.filter(([, entry]) => validEntry(entry))
|
|
52
|
+
.map(([slug, entry]) => ({ slug, ...entry }));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function assertChoice(value, allowed, field) {
|
|
56
|
+
if (!allowed.has(value)) throw new Error(`${field} must be one of: ${[...allowed].join(', ')}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function addTaste({
|
|
60
|
+
verdict,
|
|
61
|
+
subject,
|
|
62
|
+
why,
|
|
63
|
+
scope = 'any',
|
|
64
|
+
example,
|
|
65
|
+
added,
|
|
66
|
+
root = process.cwd(),
|
|
67
|
+
} = {}) {
|
|
68
|
+
const cleanVerdict = String(verdict || '').trim().toLowerCase();
|
|
69
|
+
const cleanSubject = String(subject || '').trim();
|
|
70
|
+
const cleanWhy = String(why || '').trim();
|
|
71
|
+
const cleanScope = String(scope || 'any').trim().toLowerCase();
|
|
72
|
+
const cleanExample = example === undefined ? '' : String(example).trim();
|
|
73
|
+
const cleanAdded = String(added || '').trim();
|
|
74
|
+
|
|
75
|
+
assertChoice(cleanVerdict, VERDICTS, 'verdict');
|
|
76
|
+
assertChoice(cleanScope, SCOPES, 'scope');
|
|
77
|
+
if (!cleanSubject) throw new Error('subject is required');
|
|
78
|
+
if (!cleanWhy) throw new Error('why is required');
|
|
79
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(cleanAdded)) throw new Error('added must be an ISO date');
|
|
80
|
+
|
|
81
|
+
const baseSlug = slugify(cleanSubject);
|
|
82
|
+
if (!baseSlug) throw new Error('subject must contain letters or numbers');
|
|
83
|
+
|
|
84
|
+
const store = readTaste(root);
|
|
85
|
+
let slug = baseSlug;
|
|
86
|
+
let suffix = 2;
|
|
87
|
+
while (store[slug] && store[slug].subject !== cleanSubject) {
|
|
88
|
+
slug = `${baseSlug}-${suffix}`;
|
|
89
|
+
suffix += 1;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const entry = {
|
|
93
|
+
verdict: cleanVerdict,
|
|
94
|
+
subject: cleanSubject,
|
|
95
|
+
why: cleanWhy,
|
|
96
|
+
scope: cleanScope,
|
|
97
|
+
...(cleanExample ? { example: cleanExample } : {}),
|
|
98
|
+
added: cleanAdded,
|
|
99
|
+
};
|
|
100
|
+
store[slug] = entry;
|
|
101
|
+
|
|
102
|
+
const file = tastePath(root);
|
|
103
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
104
|
+
fs.writeFileSync(file, `${JSON.stringify(store, null, 2)}\n`, 'utf8');
|
|
105
|
+
return { slug, ...entry };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function listTaste({ root = process.cwd(), scope } = {}) {
|
|
109
|
+
const cleanScope = scope === undefined ? null : String(scope).trim().toLowerCase();
|
|
110
|
+
if (cleanScope) assertChoice(cleanScope, SCOPES, 'scope');
|
|
111
|
+
return entriesFrom(readTaste(root))
|
|
112
|
+
.filter((entry) => !cleanScope || cleanScope === 'any' || entry.scope === 'any' || entry.scope === cleanScope)
|
|
113
|
+
.sort((left, right) => right.added.localeCompare(left.added) || left.slug.localeCompare(right.slug));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function keywords(text) {
|
|
117
|
+
return [...new Set(String(text || '').toLowerCase().split(/[^a-z0-9]+/)
|
|
118
|
+
.filter((word) => word.length >= 3 && !STOP_WORDS.has(word)))];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function matchTaste({ briefText = '', scope = 'any', root = process.cwd() } = {}) {
|
|
122
|
+
const cleanScope = SCOPES.has(String(scope || '').toLowerCase())
|
|
123
|
+
? String(scope).toLowerCase()
|
|
124
|
+
: 'any';
|
|
125
|
+
const briefWords = new Set(keywords(briefText));
|
|
126
|
+
if (!briefWords.size) return [];
|
|
127
|
+
|
|
128
|
+
const matches = [];
|
|
129
|
+
for (const entry of entriesFrom(readTaste(root))) {
|
|
130
|
+
if (cleanScope !== 'any' && entry.scope !== 'any' && entry.scope !== cleanScope) continue;
|
|
131
|
+
|
|
132
|
+
const subjectMatches = keywords(`${entry.slug} ${entry.subject}`).filter((word) => briefWords.has(word));
|
|
133
|
+
const whyMatches = keywords(entry.why).filter((word) => briefWords.has(word));
|
|
134
|
+
const exampleMatches = keywords(entry.example || '').filter((word) => briefWords.has(word));
|
|
135
|
+
const overlap = new Set([...subjectMatches, ...whyMatches, ...exampleMatches]);
|
|
136
|
+
if (!overlap.size) continue;
|
|
137
|
+
|
|
138
|
+
const scopeScore = entry.scope === cleanScope ? 10_000 : entry.scope === 'any' ? 5_000 : 0;
|
|
139
|
+
const score = scopeScore
|
|
140
|
+
+ (subjectMatches.length * 300)
|
|
141
|
+
+ (exampleMatches.length * 200)
|
|
142
|
+
+ (whyMatches.length * 100)
|
|
143
|
+
+ [...overlap].reduce((total, word) => total + word.length, 0);
|
|
144
|
+
matches.push({ ...entry, why_matched: `brief mentions ${[...overlap].join(', ')}`, score });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return matches
|
|
148
|
+
.sort((left, right) => right.score - left.score || left.slug.localeCompare(right.slug))
|
|
149
|
+
.slice(0, 5)
|
|
150
|
+
.map(({ score, ...entry }) => entry);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = { addTaste, listTaste, matchTaste };
|
package/lib/team-presence.js
CHANGED
|
@@ -4,4 +4,20 @@ function encodeToolResult(result) {
|
|
|
4
4
|
return Buffer.from(JSON.stringify(result), 'utf8').toString('base64');
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
function toolResultEncodingEnabled(env = process.env) {
|
|
8
|
+
return env.ATRIS_TOOL_RESULT_B64 !== '0';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function buildToolResultBody(callId, result, env = process.env) {
|
|
12
|
+
if (!toolResultEncodingEnabled(env)) {
|
|
13
|
+
return { call_id: callId, result };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
call_id: callId,
|
|
18
|
+
result: encodeToolResult(result),
|
|
19
|
+
output_encoding: 'base64',
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = { buildToolResultBody, encodeToolResult, toolResultEncodingEnabled };
|
package/lib/voice-gate.js
CHANGED
|
@@ -8,6 +8,17 @@ const {
|
|
|
8
8
|
} = require('./autoland');
|
|
9
9
|
|
|
10
10
|
const EM_DASH = /\u2014/;
|
|
11
|
+
const JARGON = /\b(?:idempotent|deterministic|substrate|orchestration|invariant|canonical|materialize|heuristic)\b/gi;
|
|
12
|
+
const JARGON_SUGGESTIONS = {
|
|
13
|
+
idempotent: 'safe to repeat',
|
|
14
|
+
deterministic: 'predictable',
|
|
15
|
+
substrate: 'base',
|
|
16
|
+
orchestration: 'coordination',
|
|
17
|
+
invariant: 'rule that must stay true',
|
|
18
|
+
canonical: 'standard',
|
|
19
|
+
materialize: 'create',
|
|
20
|
+
heuristic: 'rule of thumb',
|
|
21
|
+
};
|
|
11
22
|
const NUMBER_WORDS = [
|
|
12
23
|
'no', 'one', 'two', 'three', 'four', 'five', 'six',
|
|
13
24
|
'seven', 'eight', 'nine', 'ten', 'eleven', 'twelve',
|
|
@@ -30,7 +41,7 @@ function findingsFor(text, pattern, rule, why) {
|
|
|
30
41
|
return findings;
|
|
31
42
|
}
|
|
32
43
|
|
|
33
|
-
function
|
|
44
|
+
function scanWords(text) {
|
|
34
45
|
const value = String(text || '');
|
|
35
46
|
const findings = findingsFor(value, EM_DASH, 'em-dash', 'use plain punctuation');
|
|
36
47
|
if (hasAgentJargon(value)) {
|
|
@@ -62,6 +73,55 @@ function scanText(text) {
|
|
|
62
73
|
return findings;
|
|
63
74
|
}
|
|
64
75
|
|
|
76
|
+
function paragraphsFor(text) {
|
|
77
|
+
return String(text || '')
|
|
78
|
+
.trim()
|
|
79
|
+
.split(/\r?\n(?:[ \t]*\r?\n)+/)
|
|
80
|
+
.map(paragraph => paragraph.trim())
|
|
81
|
+
.filter(Boolean);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function sentenceCount(text) {
|
|
85
|
+
const value = String(text || '').replace(/\s+/g, ' ').trim();
|
|
86
|
+
if (!value) return 0;
|
|
87
|
+
const endings = value.match(/[.!?]+(?=\s|$)/g) || [];
|
|
88
|
+
return endings.length + (/[.!?]+$/.test(value) ? 0 : 1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function scanShape(text) {
|
|
92
|
+
const paragraphs = paragraphsFor(text);
|
|
93
|
+
const findings = [];
|
|
94
|
+
for (const paragraph of paragraphs) {
|
|
95
|
+
if (sentenceCount(paragraph) > 2) {
|
|
96
|
+
findings.push({
|
|
97
|
+
rule: 'dense-block',
|
|
98
|
+
why: 'add a blank line after every two sentences',
|
|
99
|
+
snippet: paragraph,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (paragraphs.length > 3) {
|
|
104
|
+
findings.push({
|
|
105
|
+
rule: 'too-many-ideas',
|
|
106
|
+
why: 'split this into messages with no more than three paragraphs each',
|
|
107
|
+
snippet: paragraphs.slice(3).join('\n\n'),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
for (const match of String(text || '').matchAll(JARGON)) {
|
|
111
|
+
const suggestion = JARGON_SUGGESTIONS[match[0].toLowerCase()];
|
|
112
|
+
findings.push({
|
|
113
|
+
rule: 'jargon',
|
|
114
|
+
why: `replace "${match[0]}" with "${suggestion}"`,
|
|
115
|
+
snippet: match[0],
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return findings;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function scanText(text) {
|
|
122
|
+
return [...scanWords(text), ...scanShape(text)];
|
|
123
|
+
}
|
|
124
|
+
|
|
65
125
|
function cleanEmDashes(text) {
|
|
66
126
|
return String(text || '')
|
|
67
127
|
.replace(/\s*\u2014\s*/g, ', ')
|
|
@@ -86,13 +146,14 @@ function titleCanReplaceId(title) {
|
|
|
86
146
|
}
|
|
87
147
|
|
|
88
148
|
function gateForHuman(text, opts = {}) {
|
|
149
|
+
const shapeIssues = scanShape(text);
|
|
89
150
|
let cleaned = cleanEmDashes(text);
|
|
90
151
|
cleaned = dejargon(cleaned, { preserveTickets: true });
|
|
91
152
|
if (titleCanReplaceId(opts.title)) {
|
|
92
153
|
cleaned = cleaned.replace(globalPattern(voicePatterns.rawUlid), '');
|
|
93
154
|
}
|
|
94
155
|
cleaned = cleanAfterRemoval(cleaned);
|
|
95
|
-
const issues =
|
|
156
|
+
const issues = [...scanWords(cleaned), ...shapeIssues];
|
|
96
157
|
return { ok: issues.length === 0, text: cleaned, issues };
|
|
97
158
|
}
|
|
98
159
|
|
|
@@ -154,10 +215,76 @@ function isRetiredFillerReason(text) {
|
|
|
154
215
|
return RETIRED_FILLER_REASON.test(String(text || '').replace(/\s+/g, ' ').trim());
|
|
155
216
|
}
|
|
156
217
|
|
|
218
|
+
// One table turns every landing/refusal reason code into a plain sentence a
|
|
219
|
+
// fried reader can act on: what happened, and what unblocks it. JSON output
|
|
220
|
+
// keeps the raw codes; only human text goes through here. Fragments carry no
|
|
221
|
+
// trailing period because callers stitch them into their own sentences.
|
|
222
|
+
const LANDING_REASON_SENTENCES = {
|
|
223
|
+
accept_all_but_protected: 'the accept-everything policy covers it, so it lands unless a protected lane applies',
|
|
224
|
+
already_auto_accepted: 'it already landed on its own earlier, nothing left to do',
|
|
225
|
+
autoland_policy_missing_owner: 'the self-landing policy has no owner recorded, turn it on again to set one',
|
|
226
|
+
autoland_policy_off: 'self-landing is off, so everything waits for you',
|
|
227
|
+
certified_independent_review: 'an independent reviewer certified it, so it can land',
|
|
228
|
+
certified_strict_verify: 'it is certified and its recorded check passed, so it can land',
|
|
229
|
+
dead_exports: 'the change leaves behind code nothing calls, delete what the hygiene check names and re-certify',
|
|
230
|
+
declared_protected_lane: 'it touches a protected lane, so it waits for your decision',
|
|
231
|
+
forced_completion_needs_human: 'it was pushed to done without proof, so only a human can accept it',
|
|
232
|
+
insufficient_review_passes: 'it has not been reviewed enough times yet, one more pass unblocks it',
|
|
233
|
+
judge_equals_worker: 'built and judged by the same actor, hand the review to someone else',
|
|
234
|
+
mission_xp_requires_end_to_end_receipt: 'the proof does not show the whole flow working start to finish, attach that receipt or move it back to do',
|
|
235
|
+
needs_independent_reviewer: 'built and judged by the same actor, a review from someone else unblocks it',
|
|
236
|
+
needs_second_actor_review: 'the same actor built and reviewed it, someone else has to look before it lands',
|
|
237
|
+
needs_second_reviewer_or_third_pass: 'it needs one more independent check first',
|
|
238
|
+
no_verify_command: 'no check command was given, add one so the work can prove itself',
|
|
239
|
+
not_agent_certified: 'no reviewer has certified it yet, a second review unblocks it',
|
|
240
|
+
not_in_review: 'it is not up for review yet, finish the work and move it to review first',
|
|
241
|
+
probation_needs_review: 'the builder is still earning trust, so a human look is required before it lands',
|
|
242
|
+
proof_not_executed: 'the proof was written but never actually run, execute the check and record what it showed',
|
|
243
|
+
proof_required: 'no proof was given, say what was run and what it showed',
|
|
244
|
+
proof_unmerged_or_draft_pr_boundary: 'its proof points at an unmerged draft, merge it or prove the work another way',
|
|
245
|
+
receipt_verifier_failed: 'a saved receipt failed its re-check, fix what it names and re-certify',
|
|
246
|
+
strict_verify_missing: 'no recorded check command to re-run, add one and re-certify',
|
|
247
|
+
untagged_protected_lane_text: 'the description reads like protected-lane work without the tag, tag it or decide it yourself',
|
|
248
|
+
verification_pending: 'the recorded check has not been re-run yet, the next hourly pass runs it',
|
|
249
|
+
verifier_is_builder: 'the re-check actor built this row, another actor must re-check',
|
|
250
|
+
verify_command_not_allowed: 'its recorded check is not on the list this machine may run alone, run it yourself to unblock',
|
|
251
|
+
verify_failed: 'its check command failed on re-run, fix the failure and re-certify',
|
|
252
|
+
verify_passed: 'its check command passed on re-run',
|
|
253
|
+
verify_unrunnable: 'its check command could not even start, fix the command and re-certify',
|
|
254
|
+
verify_workdir_missing: 'the folder its check was recorded in is gone, re-run the check from a real folder',
|
|
255
|
+
verify_worktree_missing: 'the folder its check needs is gone, rebuild it or run the check by hand',
|
|
256
|
+
weak_proof: 'the proof is too thin to trust, cite a command, receipt, or approval',
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const DENIED_TAG_SENTENCES = {
|
|
260
|
+
billing: 'it touches money, so it is your decision',
|
|
261
|
+
deploy: 'it is a deploy, so it is your decision',
|
|
262
|
+
security: 'it touches security, so it is your decision',
|
|
263
|
+
customer: 'it is customer-facing, so it is your decision',
|
|
264
|
+
external: 'it is outward-facing, so it is your decision',
|
|
265
|
+
feedback: 'it answers customer feedback, so it is your decision',
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
function plainLandingReason(reason) {
|
|
269
|
+
const code = String(reason || '').trim();
|
|
270
|
+
if (!code) return 'no reason was recorded, re-run the review to get one';
|
|
271
|
+
if (LANDING_REASON_SENTENCES[code]) return LANDING_REASON_SENTENCES[code];
|
|
272
|
+
const denied = code.match(/^denied_tag_(.+)$/);
|
|
273
|
+
if (denied) {
|
|
274
|
+
return DENIED_TAG_SENTENCES[denied[1]]
|
|
275
|
+
|| `it is tagged ${denied[1].replace(/_/g, ' ')}, a protected lane, so it is your decision`;
|
|
276
|
+
}
|
|
277
|
+
const approval = code.match(/^approval_(.+)$/);
|
|
278
|
+
if (approval) return `a human already marked it ${approval[1].replace(/_/g, ' ')}, so it will not land on its own`;
|
|
279
|
+
// Unknown codes still never leak snake_case to a human.
|
|
280
|
+
return code.replace(/_/g, ' ');
|
|
281
|
+
}
|
|
282
|
+
|
|
157
283
|
module.exports = {
|
|
158
284
|
gateForHuman,
|
|
159
285
|
isRetiredFillerReason,
|
|
160
286
|
landingWhyClause,
|
|
161
287
|
numberWord,
|
|
288
|
+
plainLandingReason,
|
|
162
289
|
scanText,
|
|
163
290
|
};
|
package/lib/wiki.js
CHANGED
|
@@ -11,6 +11,13 @@ const LEGACY_WIKI_BRIEFS_SUBDIR = 'syntheses';
|
|
|
11
11
|
const WIKI_SUBDIRS = ['people', 'systems', 'concepts', WIKI_BRIEFS_SUBDIR];
|
|
12
12
|
const WIKI_STATUS_FILE = 'STATUS.md';
|
|
13
13
|
const WIKI_CONTENT_SUBDIRS = WIKI_SUBDIRS.map((subdir) => path.join(WIKI_ROOT, subdir));
|
|
14
|
+
const WIKI_METABOLISM_MARKER = path.join('.atris', 'state', 'wiki.metabolism.json');
|
|
15
|
+
const DEFAULT_WIKI_METABOLISM_BUDGETS = Object.freeze({
|
|
16
|
+
index: 40,
|
|
17
|
+
log: 60,
|
|
18
|
+
folder: 30,
|
|
19
|
+
});
|
|
20
|
+
const CONSOLIDATION_GROWTH_THRESHOLD = 10;
|
|
14
21
|
|
|
15
22
|
function getWikiRoot(mode = 'public') {
|
|
16
23
|
return mode === 'private' ? PRIVATE_WIKI_ROOT : WIKI_ROOT;
|
|
@@ -577,7 +584,122 @@ function isConfidence(value) {
|
|
|
577
584
|
return Number.isFinite(n) && n >= 0 && n <= 1;
|
|
578
585
|
}
|
|
579
586
|
|
|
580
|
-
function
|
|
587
|
+
function countBullets(filePath, topLevelOnly = false) {
|
|
588
|
+
if (!fs.existsSync(filePath)) return 0;
|
|
589
|
+
const pattern = topLevelOnly ? /^-\s+\S/ : /^\s*-\s+\S/;
|
|
590
|
+
return fs.readFileSync(filePath, 'utf8')
|
|
591
|
+
.split('\n')
|
|
592
|
+
.filter((line) => pattern.test(line))
|
|
593
|
+
.length;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function countWikiMetabolismEntries(projectRoot = process.cwd(), mode = 'public') {
|
|
597
|
+
const wikiDir = path.join(projectRoot, getWikiRoot(mode));
|
|
598
|
+
const counts = {
|
|
599
|
+
'index.md': countBullets(path.join(wikiDir, 'index.md')),
|
|
600
|
+
'log.md': countBullets(path.join(wikiDir, 'log.md'), true),
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
for (const subdir of WIKI_SUBDIRS) {
|
|
604
|
+
counts[`${subdir}/`] = walkMarkdownFiles(path.join(wikiDir, subdir)).length;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return counts;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function readWikiMetabolismMarker(projectRoot = process.cwd()) {
|
|
611
|
+
const markerPath = path.join(projectRoot, WIKI_METABOLISM_MARKER);
|
|
612
|
+
if (!fs.existsSync(markerPath)) return null;
|
|
613
|
+
|
|
614
|
+
try {
|
|
615
|
+
const marker = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
|
|
616
|
+
if (!marker || typeof marker !== 'object' || !marker.entryCountAtConsolidation || typeof marker.entryCountAtConsolidation !== 'object') {
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
return marker;
|
|
620
|
+
} catch {
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function writeWikiMetabolismMarker(projectRoot = process.cwd(), entryCounts = {}, lastConsolidatedAt = new Date().toISOString()) {
|
|
626
|
+
const markerPath = path.join(projectRoot, WIKI_METABOLISM_MARKER);
|
|
627
|
+
const marker = {
|
|
628
|
+
lastConsolidatedAt,
|
|
629
|
+
entryCountAtConsolidation: { ...entryCounts },
|
|
630
|
+
};
|
|
631
|
+
fs.mkdirSync(path.dirname(markerPath), { recursive: true });
|
|
632
|
+
fs.writeFileSync(markerPath, `${JSON.stringify(marker, null, 2)}\n`, 'utf8');
|
|
633
|
+
return marker;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function wikiBudgetForSurface(surface, budgets) {
|
|
637
|
+
if (surface === 'index.md') return Number.isFinite(budgets.index) ? budgets.index : DEFAULT_WIKI_METABOLISM_BUDGETS.index;
|
|
638
|
+
if (surface === 'log.md') return Number.isFinite(budgets.log) ? budgets.log : DEFAULT_WIKI_METABOLISM_BUDGETS.log;
|
|
639
|
+
const folder = surface.replace(/\/$/, '');
|
|
640
|
+
if (budgets.folders && Number.isFinite(budgets.folders[folder])) return budgets.folders[folder];
|
|
641
|
+
if (Number.isFinite(budgets[folder])) return budgets[folder];
|
|
642
|
+
return Number.isFinite(budgets.folder) ? budgets.folder : DEFAULT_WIKI_METABOLISM_BUDGETS.folder;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function wikiMetabolismSurfacePath(surface, mode = 'public') {
|
|
646
|
+
return `${getWikiRoot(mode)}/${surface}`;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function findWikiMetabolismFindings(projectRoot = process.cwd(), mode = 'public', budgets = DEFAULT_WIKI_METABOLISM_BUDGETS) {
|
|
650
|
+
const counts = countWikiMetabolismEntries(projectRoot, mode);
|
|
651
|
+
const marker = readWikiMetabolismMarker(projectRoot);
|
|
652
|
+
const findings = [];
|
|
653
|
+
|
|
654
|
+
for (const [surface, count] of Object.entries(counts)) {
|
|
655
|
+
const budget = wikiBudgetForSurface(surface, budgets || {});
|
|
656
|
+
const page = wikiMetabolismSurfacePath(surface, mode);
|
|
657
|
+
if (count > budget) {
|
|
658
|
+
findings.push({
|
|
659
|
+
page,
|
|
660
|
+
surface,
|
|
661
|
+
code: 'over-budget',
|
|
662
|
+
count,
|
|
663
|
+
budget,
|
|
664
|
+
message: `${surface} has ${count} entries, over budget ${budget}`,
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const baseline = Number(marker?.entryCountAtConsolidation?.[surface]);
|
|
669
|
+
const growth = Number.isFinite(baseline) ? count - baseline : 0;
|
|
670
|
+
if (growth >= CONSOLIDATION_GROWTH_THRESHOLD) {
|
|
671
|
+
findings.push({
|
|
672
|
+
page,
|
|
673
|
+
surface,
|
|
674
|
+
code: 'consolidation-due',
|
|
675
|
+
count,
|
|
676
|
+
baselineCount: baseline,
|
|
677
|
+
growth,
|
|
678
|
+
message: `${surface} grew by ${growth} entries since the last consolidation`,
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
return findings;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function wikiMetabolismNudge(projectRoot = process.cwd(), mode = 'public') {
|
|
687
|
+
try {
|
|
688
|
+
const findings = findWikiMetabolismFindings(projectRoot, mode);
|
|
689
|
+
return findings.length > 0
|
|
690
|
+
? 'the wiki has grown a lot since its last pruning; run: atris wiki consolidate'
|
|
691
|
+
: '';
|
|
692
|
+
} catch {
|
|
693
|
+
return '';
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function recordWikiConsolidation(projectRoot = process.cwd(), mode = 'public') {
|
|
698
|
+
const counts = countWikiMetabolismEntries(projectRoot, mode);
|
|
699
|
+
return writeWikiMetabolismMarker(projectRoot, counts);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function validateAgentReadableWikiPages(projectRoot = process.cwd(), mode = 'public', options = {}) {
|
|
581
703
|
const findings = [];
|
|
582
704
|
const pages = readWikiPages(projectRoot, mode);
|
|
583
705
|
const stalePages = new Set(findStaleWikiPages(projectRoot, mode).map((item) => item.page));
|
|
@@ -609,9 +731,12 @@ function validateAgentReadableWikiPages(projectRoot = process.cwd(), mode = 'pub
|
|
|
609
731
|
}
|
|
610
732
|
}
|
|
611
733
|
|
|
734
|
+
findings.push(...findWikiMetabolismFindings(projectRoot, mode, options.budgets));
|
|
735
|
+
|
|
612
736
|
return {
|
|
613
737
|
ok: findings.length === 0,
|
|
614
738
|
pageCount: pages.length,
|
|
739
|
+
surfaceCounts: countWikiMetabolismEntries(projectRoot, mode),
|
|
615
740
|
findingCount: findings.length,
|
|
616
741
|
findings,
|
|
617
742
|
};
|
|
@@ -802,18 +927,29 @@ Output:
|
|
|
802
927
|
- No hedging if the wiki is stale or messy`;
|
|
803
928
|
}
|
|
804
929
|
|
|
930
|
+
function buildConsolidatePrompt(mode = 'public') {
|
|
931
|
+
const wikiRoot = getWikiRoot(mode);
|
|
932
|
+
return `Atris wiki consolidation pass
|
|
933
|
+
|
|
934
|
+
Read ${wikiRoot}/index.md, ${wikiRoot}/log.md, and every page under the wiki subfolders.
|
|
935
|
+
|
|
936
|
+
Pruning rules:
|
|
937
|
+
- UPDATE facts that changed.
|
|
938
|
+
- DELETE stale facts, duplicates, and mechanics.
|
|
939
|
+
- ADD nothing new.
|
|
940
|
+
|
|
941
|
+
Keep rules:
|
|
942
|
+
- Entries marked as user-stated conventions or explicit remember-this survive.
|
|
943
|
+
- Every kept entry keeps its provenance as a source link or date.
|
|
944
|
+
|
|
945
|
+
Consolidate overlapping entries, preserve the strongest source-backed version, and leave index.md and page links consistent.`;
|
|
946
|
+
}
|
|
947
|
+
|
|
805
948
|
module.exports = {
|
|
806
949
|
WIKI_ROOT,
|
|
807
|
-
CONTEXT_ROOT,
|
|
808
950
|
PRIVATE_WIKI_ROOT,
|
|
809
|
-
|
|
810
|
-
LEGACY_WIKI_ROOT,
|
|
811
|
-
WIKI_SUBDIRS,
|
|
812
|
-
WIKI_CONTENT_SUBDIRS,
|
|
813
|
-
WIKI_SCHEMA: buildWikiSchema(),
|
|
814
|
-
WIKI_STATUS_FILE,
|
|
951
|
+
WIKI_METABOLISM_MARKER,
|
|
815
952
|
getWikiRoot,
|
|
816
|
-
getContextRoot,
|
|
817
953
|
ensureWikiScaffold,
|
|
818
954
|
ensureContextScaffold,
|
|
819
955
|
findLocalWikiDir,
|
|
@@ -822,6 +958,12 @@ module.exports = {
|
|
|
822
958
|
readWikiPages,
|
|
823
959
|
findStaleWikiPages,
|
|
824
960
|
findWikiOrphans,
|
|
961
|
+
countWikiMetabolismEntries,
|
|
962
|
+
readWikiMetabolismMarker,
|
|
963
|
+
writeWikiMetabolismMarker,
|
|
964
|
+
findWikiMetabolismFindings,
|
|
965
|
+
wikiMetabolismNudge,
|
|
966
|
+
recordWikiConsolidation,
|
|
825
967
|
validateAgentReadableWikiPages,
|
|
826
968
|
findSuggestedSources,
|
|
827
969
|
stageWikiIngest,
|
|
@@ -830,4 +972,5 @@ module.exports = {
|
|
|
830
972
|
buildIngestPrompt,
|
|
831
973
|
buildQueryPrompt,
|
|
832
974
|
buildLintPrompt,
|
|
975
|
+
buildConsolidatePrompt,
|
|
833
976
|
};
|
package/lib/wish-audit.js
CHANGED
|
@@ -167,7 +167,7 @@ const BUDGET_LABELS = {
|
|
|
167
167
|
const WISH_SWEEP_LIMIT = 3;
|
|
168
168
|
const WISH_MISSION_RUNNER = 'claude';
|
|
169
169
|
const TEST_VERIFY_COMMAND = 'node --test';
|
|
170
|
-
const JOURNAL_RESULT_TEXT = '
|
|
170
|
+
const JOURNAL_RESULT_TEXT = 'the wish is recorded and the result comes back for review';
|
|
171
171
|
const WAITING_INPUT_STATUSES = new Set(['needs_input', 'waiting_input']);
|
|
172
172
|
const PATH_SLOT_QUESTION = 'Where should I find the file or folder you named?';
|
|
173
173
|
const AUDIENCE_WORDS = /\b(?:for|user|users|customer|customers|operator|operators|agent|agents|developer|developers|admin|admins|member|members|team|teams|visitor|visitors|client|clients|student|students|founder|founders|newcomer|newcomers)\b/i;
|
|
@@ -490,6 +490,24 @@ function resolveRole(role, root) {
|
|
|
490
490
|
return resolver(role, root);
|
|
491
491
|
}
|
|
492
492
|
|
|
493
|
+
// Route-time validator policy: the routing decision must not depend on which
|
|
494
|
+
// validator binaries happen to exist on this machine. Prefer a ready distinct
|
|
495
|
+
// validator, else fall back to the registry's policy order; the dispatch
|
|
496
|
+
// flight reports an honest validator failure if the engine is still missing
|
|
497
|
+
// when validation actually runs.
|
|
498
|
+
function policyValidator(root, excludeId = '') {
|
|
499
|
+
try {
|
|
500
|
+
const ranked = engineRegistry.engineRegistryView(root)
|
|
501
|
+
.filter((engine) => engine.roles.includes('validator'))
|
|
502
|
+
.filter((engine) => engine.id !== String(excludeId || ''))
|
|
503
|
+
.sort((a, b) => Number(a.fallback_order) - Number(b.fallback_order)
|
|
504
|
+
|| String(a.id).localeCompare(String(b.id)));
|
|
505
|
+
return ranked.find((engine) => engine.health && engine.health.status === 'ready') || ranked[0] || null;
|
|
506
|
+
} catch {
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
493
511
|
function resolveExecutor(root, engineOverride = '') {
|
|
494
512
|
if (engineOverride) {
|
|
495
513
|
return engineRegistry.resolveEngineForRoleWithPreference('executor', root, engineOverride);
|
|
@@ -1239,7 +1257,8 @@ function auditWish(text, root = process.cwd(), options = {}) {
|
|
|
1239
1257
|
const question = 'Which working reviewer should validate it?';
|
|
1240
1258
|
const answered = answeredRepeatedQuestionValue(text, root, question, 'reviewer');
|
|
1241
1259
|
if (answered) validator = { id: slugify(answered), name: answered };
|
|
1242
|
-
else
|
|
1260
|
+
else if (options.assumeValidator) validator = policyValidator(root, executor && executor.id);
|
|
1261
|
+
if (!validator) questions.push(question);
|
|
1243
1262
|
}
|
|
1244
1263
|
|
|
1245
1264
|
let missing = missingNamedInputs(originalText, root);
|
|
@@ -1346,9 +1365,6 @@ function verifyOutcomeText(planOrCommand) {
|
|
|
1346
1365
|
}
|
|
1347
1366
|
|
|
1348
1367
|
module.exports = {
|
|
1349
|
-
BUDGET_LABELS,
|
|
1350
|
-
JOURNAL_RESULT_TEXT,
|
|
1351
|
-
TEST_VERIFY_COMMAND,
|
|
1352
1368
|
WAITING_INPUT_STATUSES,
|
|
1353
1369
|
WISH_MISSION_RUNNER,
|
|
1354
1370
|
WISH_SWEEP_LIMIT,
|
|
@@ -1357,8 +1373,6 @@ module.exports = {
|
|
|
1357
1373
|
deriveVerifyPlan,
|
|
1358
1374
|
inferBudgetTier,
|
|
1359
1375
|
missingNamedInputs,
|
|
1360
|
-
namedTestFiles,
|
|
1361
|
-
PATH_SLOT_QUESTION,
|
|
1362
1376
|
quoteText,
|
|
1363
1377
|
sharesMeaningfulWords,
|
|
1364
1378
|
validateEngineOverride,
|