atris 3.30.1 → 3.30.3
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 +6 -0
- package/atris/AGENTS.md +11 -0
- package/atris/CLAUDE.md +5 -0
- package/atris/atris.md +19 -4
- package/atris/policies/atris-design.md +71 -0
- package/atris/policies/design-seed.md +187 -0
- package/atris/skills/atris/SKILL.md +26 -3
- package/atris/skills/design/SKILL.md +3 -2
- package/atris/skills/loop/SKILL.md +5 -3
- package/atris/team/_template/MEMBER.md +19 -0
- package/atris.md +63 -23
- package/ax +1434 -1698
- package/bin/atris.js +5 -1
- package/commands/agent-spawn.js +2 -2
- package/commands/brain.js +92 -7
- package/commands/brainstorm.js +62 -22
- package/commands/business-sync.js +92 -8
- package/commands/business.js +13 -7
- package/commands/chat-scan.js +102 -0
- package/commands/computer.js +758 -15
- package/commands/deck.js +505 -105
- package/commands/init.js +6 -1
- package/commands/launchpad.js +638 -0
- package/commands/log-sync.js +44 -13
- package/commands/loop-front.js +290 -0
- package/commands/member.js +124 -3
- package/commands/mission.js +717 -32
- package/commands/pull.js +37 -28
- package/commands/pulse.js +11 -8
- package/commands/run.js +79 -39
- package/commands/sync.js +36 -17
- package/commands/task.js +342 -66
- package/commands/xp.js +3 -0
- package/commands/youtube.js +221 -7
- package/decks/README.md +89 -0
- package/decks/archetype-catalog.json +180 -0
- package/decks/atris-antislop-pitch.json +48 -0
- package/decks/atris-archetypes-v2.json +83 -0
- package/decks/atris-one-loop-pitch.json +80 -0
- package/decks/atris-seed-pitch-v3.json +118 -0
- package/decks/atris-seed-pitch-v4-skeleton.json +106 -0
- package/decks/atris-seed-pitch-v5.json +109 -0
- package/decks/atris-seed-pitch-v6.json +137 -0
- package/decks/atris-seed-pitch-v7.json +133 -0
- package/decks/atris-single-shot-proof.json +74 -0
- package/decks/mark-pincus-narrative.json +102 -0
- package/decks/mark-pincus-sourcery.json +94 -0
- package/decks/yash-applied-compute-detailed.json +150 -0
- package/decks/yash-applied-compute-generalist.json +82 -0
- package/decks/yash-applied-compute-narrative.json +54 -0
- package/lib/ax-prefs.js +5 -12
- package/lib/chat-log-scan.js +377 -0
- package/lib/context-gatherer.js +35 -1
- package/lib/deck-compose.js +145 -0
- package/lib/deck-history.js +64 -0
- package/lib/deck-layout.js +169 -0
- package/lib/deck-review.js +431 -0
- package/lib/deck-schema.js +154 -0
- package/lib/file-ops.js +2 -2
- package/lib/functional-owner.js +189 -0
- package/lib/slides-deck.js +512 -58
- package/lib/task-db.js +109 -2
- package/package.json +2 -1
- package/templates/business-starter/team/START_HERE.md +12 -8
- package/utils/auth.js +4 -0
- package/utils/config.js +4 -0
- package/atris/atrisDev.md +0 -717
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Deck spec schema validation — catch a bad spec before it hits the Slides API.
|
|
2
|
+
//
|
|
3
|
+
// lintSpec (lib/deck-review.js) judges *taste* (clip risk, template fatigue,
|
|
4
|
+
// AI-tell copy). validateSpec judges *shape*: unknown slide types, missing
|
|
5
|
+
// required fields, bad theme, malformed arrays. It runs on `deck lint` and
|
|
6
|
+
// gates `deck build`, so a typo fails locally with a slide index instead of a
|
|
7
|
+
// cryptic batch-update error after the network round trip.
|
|
8
|
+
//
|
|
9
|
+
// Findings share lintSpec's shape: { severity, slide, rule, message } where
|
|
10
|
+
// slide 0 is a spec-level finding and slide N (1-based) points at a slide.
|
|
11
|
+
|
|
12
|
+
const { THEMES, ARCHE } = require('./slides-deck');
|
|
13
|
+
|
|
14
|
+
const KNOWN_THEMES = new Set(Object.keys(THEMES));
|
|
15
|
+
|
|
16
|
+
// Per-type contract.
|
|
17
|
+
// need: list of alternative-groups; each group needs >=1 field with content.
|
|
18
|
+
// arr: fields that, when present, must be non-empty arrays.
|
|
19
|
+
// Keep this in lockstep with ARCHE in slides-deck.js (a drift test enforces it).
|
|
20
|
+
const SLIDE_SCHEMA = {
|
|
21
|
+
title: { need: [['headline', 'title']], arr: {} },
|
|
22
|
+
statement: { need: [['text', 'headline']], arr: {} },
|
|
23
|
+
columns: { need: [['columns']], arr: { columns: { min: 1 } } },
|
|
24
|
+
panel: { need: [['panel']], arr: {} },
|
|
25
|
+
chips: { need: [['chips']], arr: { chips: { min: 1 } } },
|
|
26
|
+
bignumber: { need: [['number', 'value']], arr: {} },
|
|
27
|
+
close: { need: [], arr: { buttons: { min: 0 } } },
|
|
28
|
+
timeline: { need: [['steps']], arr: { steps: { min: 1 } } },
|
|
29
|
+
versus: { need: [['left'], ['right']], arr: {} },
|
|
30
|
+
metricgrid: { need: [['metrics']], arr: { metrics: { min: 1 } } },
|
|
31
|
+
receipt: { need: [['fields']], arr: { fields: { min: 1 } } },
|
|
32
|
+
stack: { need: [['layers']], arr: { layers: { min: 1 } } },
|
|
33
|
+
quote: { need: [['text']], arr: {} },
|
|
34
|
+
hero: { need: [['headline', 'text']], arr: {} },
|
|
35
|
+
interstitial: { need: [['text', 'headline']], arr: {} },
|
|
36
|
+
lede: { need: [['headline', 'title']], arr: {} },
|
|
37
|
+
prose: { need: [['paragraphs', 'body']], arr: { paragraphs: { min: 0 } } },
|
|
38
|
+
split: { need: [['body', 'text']], arr: {} },
|
|
39
|
+
bullets: { need: [['items']], arr: { items: { min: 1 } } },
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function hasContent(v) {
|
|
43
|
+
if (v == null) return false;
|
|
44
|
+
if (typeof v === 'string') return v.trim().length > 0;
|
|
45
|
+
if (Array.isArray(v)) return v.length > 0;
|
|
46
|
+
if (typeof v === 'object') return Object.keys(v).length > 0;
|
|
47
|
+
return Boolean(v) || v === 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function validateSpec(spec) {
|
|
51
|
+
const findings = [];
|
|
52
|
+
if (!spec || typeof spec !== 'object' || Array.isArray(spec)) {
|
|
53
|
+
findings.push({ severity: 'error', slide: 0, rule: 'spec-shape', message: 'Spec must be a JSON object' });
|
|
54
|
+
return findings;
|
|
55
|
+
}
|
|
56
|
+
if (spec.theme != null && !KNOWN_THEMES.has(spec.theme)) {
|
|
57
|
+
findings.push({
|
|
58
|
+
severity: 'error',
|
|
59
|
+
slide: 0,
|
|
60
|
+
rule: 'unknown-theme',
|
|
61
|
+
message: `Unknown theme "${spec.theme}". Use: ${[...KNOWN_THEMES].join(', ')}`,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const slides = spec.slides;
|
|
65
|
+
if (!Array.isArray(slides) || slides.length === 0) {
|
|
66
|
+
findings.push({ severity: 'error', slide: 0, rule: 'no-slides', message: 'Spec needs a non-empty "slides" array' });
|
|
67
|
+
return findings;
|
|
68
|
+
}
|
|
69
|
+
slides.forEach((slide, i) => {
|
|
70
|
+
const n = i + 1;
|
|
71
|
+
if (!slide || typeof slide !== 'object' || Array.isArray(slide)) {
|
|
72
|
+
findings.push({ severity: 'error', slide: n, rule: 'slide-shape', message: 'Slide must be an object' });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const type = slide.type;
|
|
76
|
+
if (!hasContent(type)) {
|
|
77
|
+
findings.push({ severity: 'error', slide: n, rule: 'missing-type', message: 'Slide is missing a "type"' });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const def = SLIDE_SCHEMA[type];
|
|
81
|
+
if (!def) {
|
|
82
|
+
findings.push({
|
|
83
|
+
severity: 'error',
|
|
84
|
+
slide: n,
|
|
85
|
+
rule: 'unknown-slide-type',
|
|
86
|
+
message: `Unknown slide type "${type}". Known: ${Object.keys(SLIDE_SCHEMA).join(', ')}`,
|
|
87
|
+
});
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
(def.need || []).forEach((alts) => {
|
|
91
|
+
if (!alts.some((k) => hasContent(slide[k]))) {
|
|
92
|
+
const label = alts.map((a) => `"${a}"`).join(' or ');
|
|
93
|
+
findings.push({ severity: 'error', slide: n, rule: 'missing-field', message: `"${type}" slide needs ${label}` });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
Object.entries(def.arr || {}).forEach(([field, ruleDef]) => {
|
|
97
|
+
const v = slide[field];
|
|
98
|
+
if (v == null) return;
|
|
99
|
+
if (!Array.isArray(v)) {
|
|
100
|
+
findings.push({ severity: 'error', slide: n, rule: 'bad-array', message: `Slide field "${field}" must be an array` });
|
|
101
|
+
} else if (ruleDef.min && v.length < ruleDef.min) {
|
|
102
|
+
findings.push({ severity: 'warn', slide: n, rule: 'empty-array', message: `Slide field "${field}" is empty` });
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
// Nested arrays the flat `arr` contract can't reach: a non-array here would
|
|
106
|
+
// pass the gate and then crash the renderer, so check shape explicitly.
|
|
107
|
+
if (type === 'panel' && slide.panel && slide.panel.rows != null && !Array.isArray(slide.panel.rows)) {
|
|
108
|
+
findings.push({ severity: 'error', slide: n, rule: 'bad-array', message: 'panel.rows must be an array' });
|
|
109
|
+
}
|
|
110
|
+
if (type === 'versus') {
|
|
111
|
+
for (const side of ['left', 'right']) {
|
|
112
|
+
const items = slide[side] && slide[side].items;
|
|
113
|
+
if (items != null && !Array.isArray(items)) {
|
|
114
|
+
findings.push({ severity: 'error', slide: n, rule: 'bad-array', message: `versus ${side}.items must be an array` });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
return findings;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Order findings for display: spec-level first, then by slide, errors above warns.
|
|
123
|
+
const SEV_RANK = { error: 0, warn: 1, info: 2 };
|
|
124
|
+
function sortFindings(findings) {
|
|
125
|
+
return [...findings].sort((a, b) => {
|
|
126
|
+
if (a.slide !== b.slide) return a.slide - b.slide;
|
|
127
|
+
return (SEV_RANK[a.severity] ?? 3) - (SEV_RANK[b.severity] ?? 3);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function hasErrors(findings) {
|
|
132
|
+
return findings.some((f) => f.severity === 'error');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Schema validation (shape) + lint (taste), merged and ordered.
|
|
136
|
+
function checkSpec(spec, lintSpec) {
|
|
137
|
+
const schema = validateSpec(spec);
|
|
138
|
+
// Only run the taste lint when the shape is sane enough to walk slides.
|
|
139
|
+
const taste = !hasErrors(schema.filter((f) => f.rule === 'spec-shape' || f.rule === 'no-slides')) && typeof lintSpec === 'function'
|
|
140
|
+
? lintSpec(spec)
|
|
141
|
+
: [];
|
|
142
|
+
return sortFindings([...schema, ...taste]);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = {
|
|
146
|
+
KNOWN_THEMES,
|
|
147
|
+
SLIDE_SCHEMA,
|
|
148
|
+
validateSpec,
|
|
149
|
+
sortFindings,
|
|
150
|
+
hasErrors,
|
|
151
|
+
hasContent,
|
|
152
|
+
checkSpec,
|
|
153
|
+
ARCHE_TYPES: Object.keys(ARCHE),
|
|
154
|
+
};
|
package/lib/file-ops.js
CHANGED
|
@@ -105,8 +105,8 @@ function parseInboxItems(content) {
|
|
|
105
105
|
if (trimmed.startsWith('(Empty')) return;
|
|
106
106
|
const parsed = trimmed.match(/^- \*\*I(\d+):\*\*\s*(.+)$|^- \*\*I(\d+):\s+(.+)$/);
|
|
107
107
|
if (parsed) {
|
|
108
|
-
const id = parseInt(parsed[1]
|
|
109
|
-
const text = parsed[2]
|
|
108
|
+
const id = parseInt(parsed[1] !== undefined ? parsed[1] : parsed[3], 10);
|
|
109
|
+
const text = parsed[2] !== undefined ? parsed[2] : parsed[4];
|
|
110
110
|
items.push({ id, text, line: trimmed });
|
|
111
111
|
}
|
|
112
112
|
});
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const escapeRegExp = require('./escape-regexp');
|
|
6
|
+
|
|
7
|
+
const ENGINE_OWNER_IDS = new Set([
|
|
8
|
+
'atris-2-fast',
|
|
9
|
+
'atris2',
|
|
10
|
+
'atris2-fast',
|
|
11
|
+
'claude',
|
|
12
|
+
'codex',
|
|
13
|
+
'codex-executor',
|
|
14
|
+
'cursor',
|
|
15
|
+
'devin',
|
|
16
|
+
'executor',
|
|
17
|
+
'openclaw',
|
|
18
|
+
'windsurf',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const FUNCTIONAL_MEMBER_TOPICS = [
|
|
22
|
+
{
|
|
23
|
+
owner: 'task-planner',
|
|
24
|
+
terms: ['task', 'plan', 'planner', 'planning', 'preview', 'landing', 'queue', 'backlog', 'owner', 'assignment', 'routing'],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
owner: 'architect',
|
|
28
|
+
terms: ['member', 'team', 'factory', 'architecture', 'workflow', 'role', 'permission', 'policy', 'contract'],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
owner: 'mission-lead',
|
|
32
|
+
terms: ['mission', 'overnight', '24/7', 'autonomy', 'always-on', 'loop', 'heartbeat', 'cadence', 'lease', 'goal'],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
owner: 'validator',
|
|
36
|
+
terms: ['proof', 'review', 'verify', 'verifier', 'validation', 'test', 'check', 'inspect'],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
owner: 'launcher',
|
|
40
|
+
terms: ['release', 'launch', 'publish', 'ship', 'dmg', 'zip', 'post'],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
owner: 'researcher',
|
|
44
|
+
terms: ['research', 'paper', 'source', 'market', 'investigate'],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
owner: 'wiki-miner',
|
|
48
|
+
terms: ['wiki', 'knowledge', 'map', 'docs', 'documentation', 'journal'],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
owner: 'navigator',
|
|
52
|
+
terms: ['navigation', 'where', 'find', 'locate', 'map'],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
owner: 'improver',
|
|
56
|
+
terms: ['improve', 'cleanup', 'clean', 'maintenance', 'tidy', 'stale'],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
owner: 'signal-scout',
|
|
60
|
+
terms: ['signal', 'signals', 'error', 'failure', 'pattern', 'alert', 'watch', 'log', 'logs', 'infer'],
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
function normalizeOwnerSlug(value) {
|
|
65
|
+
return String(value || '')
|
|
66
|
+
.trim()
|
|
67
|
+
.toLowerCase()
|
|
68
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
69
|
+
.replace(/^-+|-+$/g, '');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isEngineOwner(value) {
|
|
73
|
+
return ENGINE_OWNER_IDS.has(normalizeOwnerSlug(value));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function listWorkspaceMemberSlugs(root) {
|
|
77
|
+
const teamDir = path.join(root || process.cwd(), 'atris', 'team');
|
|
78
|
+
try {
|
|
79
|
+
return new Set(fs.readdirSync(teamDir, { withFileTypes: true })
|
|
80
|
+
.filter(entry => entry.isDirectory())
|
|
81
|
+
.map(entry => entry.name)
|
|
82
|
+
.filter(name => name !== '_template')
|
|
83
|
+
.filter(name => fs.existsSync(path.join(teamDir, name, 'MEMBER.md')))
|
|
84
|
+
.map(normalizeOwnerSlug));
|
|
85
|
+
} catch (_) {
|
|
86
|
+
return new Set();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function firstAvailableFunctionalOwner(candidates, existingMembers) {
|
|
91
|
+
for (const candidate of candidates) {
|
|
92
|
+
const owner = normalizeOwnerSlug(candidate);
|
|
93
|
+
if (!owner) continue;
|
|
94
|
+
if (existingMembers.size === 0 || existingMembers.has(owner)) return owner;
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function ownerTermScore(text, term, weight = 1) {
|
|
100
|
+
const haystack = String(text || '').toLowerCase();
|
|
101
|
+
const needle = String(term || '').toLowerCase();
|
|
102
|
+
if (!haystack || !needle) return 0;
|
|
103
|
+
if (/^[a-z0-9-]+$/.test(needle)) {
|
|
104
|
+
const re = new RegExp(`(^|[^a-z0-9])${escapeRegExp(needle)}([^a-z0-9]|$)`, 'i');
|
|
105
|
+
return re.test(haystack) ? weight : 0;
|
|
106
|
+
}
|
|
107
|
+
return haystack.includes(needle) ? weight : 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function proposedMemberSlugFromTask({ title, tag }) {
|
|
111
|
+
const source = normalizeOwnerSlug(tag && tag !== true ? tag : title);
|
|
112
|
+
const core = source
|
|
113
|
+
.split('-')
|
|
114
|
+
.filter(part => part && !['add', 'build', 'create', 'design', 'fix', 'make', 'replace', 'ship', 'the', 'with'].includes(part))
|
|
115
|
+
.slice(0, 3)
|
|
116
|
+
.join('-');
|
|
117
|
+
return `${core || 'feature'}-owner`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function resolveFunctionalOwner({ requestedOwner, title, tag, note, goal, root, fallbackOwners } = {}) {
|
|
121
|
+
const requested = normalizeOwnerSlug(requestedOwner);
|
|
122
|
+
const existingMembers = listWorkspaceMemberSlugs(root);
|
|
123
|
+
const highSignalText = [title, note, goal].filter(Boolean).join(' ').toLowerCase();
|
|
124
|
+
const tagText = tag && tag !== true ? String(tag).toLowerCase() : '';
|
|
125
|
+
const text = [highSignalText, tagText].filter(Boolean).join(' ');
|
|
126
|
+
|
|
127
|
+
if (requested && !isEngineOwner(requested)) {
|
|
128
|
+
return {
|
|
129
|
+
owner: requested,
|
|
130
|
+
reason: 'explicit_functional_owner',
|
|
131
|
+
requested_owner: requested,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const exact = firstAvailableFunctionalOwner(
|
|
136
|
+
FUNCTIONAL_MEMBER_TOPICS.filter(topic => text.includes(topic.owner)).map(topic => topic.owner),
|
|
137
|
+
existingMembers
|
|
138
|
+
);
|
|
139
|
+
if (exact) {
|
|
140
|
+
return {
|
|
141
|
+
owner: exact,
|
|
142
|
+
reason: requested ? 'engine_owner_resolved_by_exact_member' : 'inferred_exact_member',
|
|
143
|
+
requested_owner: requested || null,
|
|
144
|
+
executed_by: requested && isEngineOwner(requested) ? requested : null,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const scored = FUNCTIONAL_MEMBER_TOPICS
|
|
149
|
+
.map(topic => ({
|
|
150
|
+
owner: topic.owner,
|
|
151
|
+
score: topic.terms.reduce((sum, term) => (
|
|
152
|
+
sum
|
|
153
|
+
+ ownerTermScore(highSignalText, term, 3)
|
|
154
|
+
+ ownerTermScore(tagText, term, 1)
|
|
155
|
+
), 0),
|
|
156
|
+
}))
|
|
157
|
+
.filter(row => row.score > 0)
|
|
158
|
+
.sort((a, b) => b.score - a.score);
|
|
159
|
+
const inferred = firstAvailableFunctionalOwner(scored.map(row => row.owner), existingMembers);
|
|
160
|
+
if (inferred) {
|
|
161
|
+
return {
|
|
162
|
+
owner: inferred,
|
|
163
|
+
reason: requested ? 'engine_owner_resolved_by_task_signal' : 'inferred_by_task_signal',
|
|
164
|
+
requested_owner: requested || null,
|
|
165
|
+
executed_by: requested && isEngineOwner(requested) ? requested : null,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const fallback = firstAvailableFunctionalOwner(fallbackOwners || ['architect', 'task-planner', 'mission-lead', 'validator'], existingMembers)
|
|
170
|
+
|| Array.from(existingMembers).find(owner => !isEngineOwner(owner))
|
|
171
|
+
|| 'architect';
|
|
172
|
+
return {
|
|
173
|
+
owner: fallback,
|
|
174
|
+
reason: requested ? 'engine_owner_resolved_to_factory_owner' : 'missing_functional_owner_fit',
|
|
175
|
+
requested_owner: requested || null,
|
|
176
|
+
executed_by: requested && isEngineOwner(requested) ? requested : null,
|
|
177
|
+
proposed_member: proposedMemberSlugFromTask({ title, tag }),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = {
|
|
182
|
+
ENGINE_OWNER_IDS,
|
|
183
|
+
FUNCTIONAL_MEMBER_TOPICS,
|
|
184
|
+
normalizeOwnerSlug,
|
|
185
|
+
isEngineOwner,
|
|
186
|
+
listWorkspaceMemberSlugs,
|
|
187
|
+
proposedMemberSlugFromTask,
|
|
188
|
+
resolveFunctionalOwner,
|
|
189
|
+
};
|