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
package/lib/ax-auto-lane.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
3
7
|
const SHORT_LOOKUP_MAX_CHARS = 140;
|
|
4
8
|
const LONG_INPUT_MIN_CHARS = 1200;
|
|
5
9
|
const TINY_MESSAGE_MAX_CHARS = 20;
|
|
@@ -9,14 +13,14 @@ const TRADEOFF_DEPTH_MIN_CHARS = 100;
|
|
|
9
13
|
|
|
10
14
|
// Anchored to the start: a lookup word mid-sentence is usually a subordinate
|
|
11
15
|
// clause (my fan spins when i run tests), not a lookup.
|
|
12
|
-
const LOOKUP_START_RE = /^(?:(?:please|pls|hey|hi|yo)[,: ]+)?(?:whats|whos|wheres|what|who|when|where|list|show|define)\b/i;
|
|
16
|
+
const LOOKUP_START_RE = /^(?:(?:please|pls|hey|hi|yo)[,: ]+)?(?:whats|whos|wheres|whens|what|who|when|where|list|show|define)\b/i;
|
|
13
17
|
// Words that turn a lookup-shaped message into a judgment call: never fast.
|
|
14
|
-
const JUDGMENT_WORD_RE = /\b(?:should|tradeoffs?|better|best|recommend|worth|good way)\b/i;
|
|
18
|
+
const JUDGMENT_WORD_RE = /\b(?:should|tradeoffs?|better|best|recommend|worth|good way)\b|\b(?:legal|compliance|gdpr|lawsuit|liability|subpoena)\b/i;
|
|
15
19
|
const HEAVY_REASONING_RE = /\b(?:design(?:s|ed|ing)?|architect(?:s|ed|ing)?|prov(?:e|es|ed|ing)|analy(?:ze|zes|zed|zing|se|ses|sed|sing)|compar(?:e|es|ed|ing)|plan(?:s|ned|ning)?|sketch(?:es|ed|ing)?)\b|\bwalk(?:s|ed|ing)? (?:me |us )?through\b/i;
|
|
16
20
|
const DECISION_PHRASE_RE = /\b(?:should|whether|decide|deciding)\b/i;
|
|
17
|
-
const ARGUE_BOTH_RE = /\bargue\b|\bboth sides\b|\bsteelman\b/i;
|
|
21
|
+
const ARGUE_BOTH_RE = /\bargue\b|\bboth sides\b|\bsteelman\b|\bmath it out\b|\bdo the math\b|\bshow your math\b/i;
|
|
18
22
|
const CODE_EDIT_RE = /\b(?:fix(?:es|ed|ing)?|refactor(?:s|ed|ing)?|renam(?:e|es|ed|ing)|debug(?:s|ged|ging)?)\b/i;
|
|
19
|
-
const CODE_DIAGNOSIS_RE = /\bwhy\b|\bwrong\b|\bfail(?:s|ed|ing)?\b|\bresolv(?:e|es|ed|ing)?\b|\bbroken\b|\bnot work/i;
|
|
23
|
+
const CODE_DIAGNOSIS_RE = /\bwhy\b|\bwrong\b|\bbug\b|\bfail(?:s|ed|ing)?\b|\bresolv(?:e|es|ed|ing)?\b|\bbroken\b|\bnot work/i;
|
|
20
24
|
const CODE_CONTEXT_RE = /```|\bregexp?\b|(?:^|\n)\s*(?:traceback\b|(?:[a-z]+)?(?:error|exception):|at\s+\S+\s+\()/im;
|
|
21
25
|
const ERROR_EXPLAIN_RE = /\bexplain (?:this|the) error\b|\bwhat does (?:this|the) error mean\b/i;
|
|
22
26
|
// Yes/no capability checks are lookups even without a lookup keyword.
|
|
@@ -25,6 +29,12 @@ const YESNO_LOOKUP_RE = /^(?:does|is|are|can|do)\b/i;
|
|
|
25
29
|
const HOWTO_LOOKUP_RE = /^how (?:do|does|did) (?:i|we|you)\b/i;
|
|
26
30
|
const HOWTO_LOOKUP_MAX_CHARS = 40;
|
|
27
31
|
const YESNO_LOOKUP_MAX_CHARS = 80;
|
|
32
|
+
// Incidents and legal surfaces are stakes, not size: never route them cheap.
|
|
33
|
+
const INCIDENT_RE = /\b(?:prod(?:uction)? is down|outage|everything (?:500|about:blank|fail)|users can'?t (?:login|log in)|data loss|site is down for everyone)\b/i;
|
|
34
|
+
const LEGAL_DRAFT_RE = /\b(?:terms of service|privacy policy|\bdpa\b|contract)\b/i;
|
|
35
|
+
const STAKES_WORD_RE = /\b(?:legal|compliance|gdpr|lawsuit|liability|subpoena)\b/i;
|
|
36
|
+
// Abstract topics have no cheap correct answer even when the ask is short.
|
|
37
|
+
const DEEP_TOPIC_RE = /\b(?:consciousness|free will|sentien\w*|computab\w*|meaning of life|the universe|morality|ethics of)\b/i;
|
|
28
38
|
// Troubleshooting language means advice, not recall: keep it out of fast.
|
|
29
39
|
const TROUBLE_RE = /\bfail(?:s|ed|ing)?\b|\bcrash(?:es|ed|ing)?\b|\bbroken\b|\bnot working\b/i;
|
|
30
40
|
const WEIGHING_WORD_RE = /\b(?:consider|weigh|rank|estimate|think (?:about|through))\b/i;
|
|
@@ -42,6 +52,38 @@ function threshold(value, fallback) {
|
|
|
42
52
|
return Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
43
53
|
}
|
|
44
54
|
|
|
55
|
+
function words(text) {
|
|
56
|
+
return String(text || '').toLowerCase().match(/[a-z0-9]+/g) || [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function overrideMatches(text, override) {
|
|
60
|
+
if (!override || typeof override !== 'object' || !override.id || !override.test
|
|
61
|
+
|| !override.lane || !override.reason) return false;
|
|
62
|
+
const test = override.test;
|
|
63
|
+
const normalized = text.toLowerCase();
|
|
64
|
+
if (test.startsWith !== undefined
|
|
65
|
+
&& !normalized.startsWith(String(test.startsWith).toLowerCase())) return false;
|
|
66
|
+
if (Array.isArray(test.includesAll)) {
|
|
67
|
+
const messageWords = new Set(words(text));
|
|
68
|
+
if (!test.includesAll.every((word) => messageWords.has(String(word).toLowerCase()))) return false;
|
|
69
|
+
}
|
|
70
|
+
if (Number.isFinite(test.maxChars) && text.length > test.maxChars) return false;
|
|
71
|
+
if (Number.isFinite(test.minChars) && text.length < test.minChars) return false;
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function loadOverrides(filePath) {
|
|
76
|
+
const resolved = filePath
|
|
77
|
+
|| process.env.ATRIS_ROUTER_OVERRIDES_PATH
|
|
78
|
+
|| path.join(os.homedir(), '.atris', 'router-overrides.json');
|
|
79
|
+
try {
|
|
80
|
+
const parsed = JSON.parse(fs.readFileSync(resolved, 'utf8'));
|
|
81
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
82
|
+
} catch (_) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
45
87
|
function pickLane(message, opts = {}) {
|
|
46
88
|
const text = String(message || '').trim();
|
|
47
89
|
const shortLookupMaxChars = threshold(opts.shortLookupMaxChars, SHORT_LOOKUP_MAX_CHARS);
|
|
@@ -50,6 +92,17 @@ function pickLane(message, opts = {}) {
|
|
|
50
92
|
const commaCount = (text.match(/,/g) || []).length;
|
|
51
93
|
const hasCodeContext = CODE_CONTEXT_RE.test(text);
|
|
52
94
|
|
|
95
|
+
const overrides = Array.isArray(opts.overrides) ? opts.overrides : [];
|
|
96
|
+
for (const override of overrides) {
|
|
97
|
+
if (overrideMatches(text, override)) {
|
|
98
|
+
return {
|
|
99
|
+
lane: override.lane,
|
|
100
|
+
reason: override.reason,
|
|
101
|
+
override_id: override.id,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
53
106
|
if (text.length > longInputMinChars) {
|
|
54
107
|
return { lane: 'max', reason: 'max fits this long input.' };
|
|
55
108
|
}
|
|
@@ -64,6 +117,12 @@ function pickLane(message, opts = {}) {
|
|
|
64
117
|
|| (WEIGHING_WORD_RE.test(text) && commaCount >= WEIGHING_FACTOR_MIN_COMMAS)) {
|
|
65
118
|
return { lane: 'max', reason: 'max fits a decision weighing several factors.' };
|
|
66
119
|
}
|
|
120
|
+
if (INCIDENT_RE.test(text)) {
|
|
121
|
+
return { lane: 'max', reason: 'max fits a live incident, stakes beat size.' };
|
|
122
|
+
}
|
|
123
|
+
if (LEGAL_DRAFT_RE.test(text) && /\b(?:draft|write|cover|review)\b/i.test(text)) {
|
|
124
|
+
return { lane: 'max', reason: 'max fits drafting legal terms.' };
|
|
125
|
+
}
|
|
67
126
|
if (HEAVY_REASONING_RE.test(text)) {
|
|
68
127
|
return { lane: 'max', reason: 'max fits this reasoning-heavy request.' };
|
|
69
128
|
}
|
|
@@ -99,13 +158,15 @@ function pickLane(message, opts = {}) {
|
|
|
99
158
|
if (text.length <= YESNO_LOOKUP_MAX_CHARS
|
|
100
159
|
&& YESNO_LOOKUP_RE.test(text)
|
|
101
160
|
&& !JUDGMENT_WORD_RE.test(text)
|
|
102
|
-
&& !TROUBLE_RE.test(text)
|
|
161
|
+
&& !TROUBLE_RE.test(text)
|
|
162
|
+
&& !DEEP_TOPIC_RE.test(text)) {
|
|
103
163
|
return { lane: 'fast', reason: 'fast fits this yes or no lookup.' };
|
|
104
164
|
}
|
|
105
165
|
if (text.length <= shortLookupMaxChars
|
|
106
166
|
&& LOOKUP_START_RE.test(text)
|
|
107
167
|
&& !JUDGMENT_WORD_RE.test(text)
|
|
108
|
-
&& !TROUBLE_RE.test(text)
|
|
168
|
+
&& !TROUBLE_RE.test(text)
|
|
169
|
+
&& !DEEP_TOPIC_RE.test(text)) {
|
|
109
170
|
return { lane: 'fast', reason: 'fast fits this short factual lookup.' };
|
|
110
171
|
}
|
|
111
172
|
return { lane: 'pro', reason: 'pro fits this general request.' };
|
|
@@ -114,6 +175,6 @@ function pickLane(message, opts = {}) {
|
|
|
114
175
|
module.exports = {
|
|
115
176
|
LONG_INPUT_MIN_CHARS,
|
|
116
177
|
SHORT_LOOKUP_MAX_CHARS,
|
|
117
|
-
|
|
178
|
+
loadOverrides,
|
|
118
179
|
pickLane,
|
|
119
180
|
};
|
package/lib/bench/context.js
CHANGED
package/lib/bench/report.js
CHANGED
package/lib/bench/runner.js
CHANGED
|
@@ -219,6 +219,22 @@ async function runSetup(spec, ctx) {
|
|
|
219
219
|
await setup(ctx);
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
// The one live prompt gate (lesson: benchmark-prompt-paths): receipt tests run
|
|
223
|
+
// the solution/null engines, which never read the prompt, so a malformed
|
|
224
|
+
// prompt.md sails through green dry-runs and only a paid real-engine run finds
|
|
225
|
+
// it. Every prompt an engine receives flows through here, and the tests assert
|
|
226
|
+
// real task prompts against the same function.
|
|
227
|
+
function readTaskPrompt(spec) {
|
|
228
|
+
const text = fs.readFileSync(spec.promptPath, 'utf8');
|
|
229
|
+
if (!text.trim() || text.trim().length < 20) {
|
|
230
|
+
throw new BenchInfraError(`${spec.id}: prompt.md is empty or too short to brief an engine`);
|
|
231
|
+
}
|
|
232
|
+
if (/\{\{[^}]*\}\}|<%|\uFFFD|\u0000/.test(text)) {
|
|
233
|
+
throw new BenchInfraError(`${spec.id}: prompt.md carries unresolved template or encoding artifacts`);
|
|
234
|
+
}
|
|
235
|
+
return text;
|
|
236
|
+
}
|
|
237
|
+
|
|
222
238
|
function formatEngineFailure(engineName, result) {
|
|
223
239
|
const stdout = String(result && result.stdout ? result.stdout : '').trim();
|
|
224
240
|
const stderr = String(result && result.stderr ? result.stderr : '').trim();
|
|
@@ -256,7 +272,7 @@ async function runAgentTaskSpec(spec, options = {}) {
|
|
|
256
272
|
}
|
|
257
273
|
|
|
258
274
|
await runSetup(spec, ctx);
|
|
259
|
-
const promptText =
|
|
275
|
+
const promptText = readTaskPrompt(spec);
|
|
260
276
|
const engineResult = await withTimeout(Promise.resolve(engine.run(promptText, ctx.workspace, timeoutMs)), spec, timeoutMs);
|
|
261
277
|
if (!engineResult || engineResult.status !== 0) {
|
|
262
278
|
throw new Error(formatEngineFailure(options.engine, engineResult));
|
|
@@ -492,21 +508,12 @@ function packMetadata(options = {}) {
|
|
|
492
508
|
module.exports = {
|
|
493
509
|
BenchInfraError,
|
|
494
510
|
DEFAULT_PACK,
|
|
495
|
-
appendResultRecord,
|
|
496
|
-
benchmarksRoot,
|
|
497
|
-
defaultTasksDir,
|
|
498
|
-
discoverPacks,
|
|
499
|
-
exitCodeForRecord,
|
|
500
511
|
findPython,
|
|
501
|
-
isInfraFailure,
|
|
502
512
|
loadTaskSpecs,
|
|
503
513
|
packMetadata,
|
|
504
514
|
readResultRecords,
|
|
515
|
+
readTaskPrompt,
|
|
505
516
|
runBench,
|
|
506
|
-
runTaskSpec,
|
|
507
|
-
runTaskSpecs,
|
|
508
|
-
selectTaskSpecs,
|
|
509
517
|
summarizeTasks,
|
|
510
518
|
taskMetadata,
|
|
511
|
-
writeBaselineRecord,
|
|
512
519
|
};
|
package/lib/brief-ledger.js
CHANGED
|
@@ -331,17 +331,11 @@ function renderBriefReview(review = {}, { lessons = false } = {}) {
|
|
|
331
331
|
}
|
|
332
332
|
|
|
333
333
|
module.exports = {
|
|
334
|
-
LEDGER_FILE,
|
|
335
|
-
OUTCOMES,
|
|
336
334
|
appendBriefRecord,
|
|
337
|
-
authorFromWorktree,
|
|
338
|
-
briefSignals,
|
|
339
335
|
buildBriefReview,
|
|
340
|
-
buildLessons,
|
|
341
336
|
ledgerPath,
|
|
342
337
|
latestBriefs,
|
|
343
338
|
mirrorBriefRecord,
|
|
344
|
-
normalizeRecord,
|
|
345
339
|
readBriefLedger,
|
|
346
340
|
renderBriefReview,
|
|
347
341
|
stampBriefOutcome,
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const os = require('node:os');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const SIMULATE_USAGE = 'Usage: atris business simulate "<idea>" [--roles a,b,c,d] [--name <business name>] [--dry-run]';
|
|
7
|
+
const DEFAULT_ROLE_NAMES = Object.freeze(['maker', 'voice', 'builder', 'ops']);
|
|
8
|
+
|
|
9
|
+
const NAME_STOP_WORDS = new Set([
|
|
10
|
+
'a', 'an', 'and', 'at', 'build', 'by', 'create', 'for', 'from', 'in',
|
|
11
|
+
'launch', 'make', 'my', 'of', 'on', 'our', 'sell', 'start', 'the', 'to',
|
|
12
|
+
'with',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const ROLE_TEMPLATES = Object.freeze([
|
|
16
|
+
{
|
|
17
|
+
key: 'maker',
|
|
18
|
+
role: 'Product and design',
|
|
19
|
+
description: 'Owns product direction, design, and concrete product artifacts',
|
|
20
|
+
mission: (idea) => `Turn "${idea}" into a concrete product direction with usable design artifacts.`,
|
|
21
|
+
goals: (idea, roleNames) => [
|
|
22
|
+
`Define the smallest product version of "${idea}" that a customer could understand and use`,
|
|
23
|
+
'Create the core product or design artifacts needed by the rest of the team',
|
|
24
|
+
`Hand the ${roleNames[1]} and ${roleNames[2]} roles clear inputs, constraints, and final source files`,
|
|
25
|
+
],
|
|
26
|
+
doneWhen: '`product/` holds a clear product brief, final source artifacts, and handoff notes the other roles can use.',
|
|
27
|
+
rules: [
|
|
28
|
+
'Make concrete artifacts, not moodboards or vague strategy.',
|
|
29
|
+
'Do not claim something is ready until the source files and proof are in the workspace.',
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
key: 'voice',
|
|
34
|
+
role: 'Brand and copy',
|
|
35
|
+
description: 'Owns positioning, brand voice, product copy, and launch drafts',
|
|
36
|
+
mission: (idea) => `Make "${idea}" understandable in five seconds and memorable the next day.`,
|
|
37
|
+
goals: (_idea, roleNames) => [
|
|
38
|
+
'Write a one-page brand brief with audience, promise, voice, and a short story',
|
|
39
|
+
`Write the customer-facing product and surface copy the ${roleNames[2]} role needs`,
|
|
40
|
+
'Draft a small launch package without posting or contacting anyone',
|
|
41
|
+
],
|
|
42
|
+
doneWhen: '`brand/` holds the brand brief, final product copy, and clearly marked launch drafts.',
|
|
43
|
+
rules: [
|
|
44
|
+
'Use plain, specific language and remove generic hype.',
|
|
45
|
+
'Draft only. Never publish, message customers, or make external claims without approval.',
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
key: 'builder',
|
|
50
|
+
role: 'Surface and storefront',
|
|
51
|
+
description: 'Owns the customer-facing surface and the path through it',
|
|
52
|
+
mission: (idea) => `Build a working customer-facing surface for "${idea}" that makes the next action obvious.`,
|
|
53
|
+
goals: () => [
|
|
54
|
+
'Build the smallest complete surface that presents the real product and copy',
|
|
55
|
+
'Connect every primary action to a safe simulated or test-mode path',
|
|
56
|
+
'Verify the full path at phone and desktop sizes and record the proof',
|
|
57
|
+
],
|
|
58
|
+
doneWhen: 'A customer can open the surface, understand the offer, and complete the simulated path without a broken step.',
|
|
59
|
+
rules: [
|
|
60
|
+
'Use only existing approved accounts and deployment rails.',
|
|
61
|
+
'Keep payments, publishing, and customer contact simulated until a human approves them.',
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
key: 'ops',
|
|
66
|
+
role: 'Numbers and fulfillment',
|
|
67
|
+
description: 'Owns economics, operating choices, fulfillment, and the human handoff',
|
|
68
|
+
mission: (idea) => `Make the numbers and operating path for "${idea}" honest enough to act on tomorrow morning.`,
|
|
69
|
+
goals: () => [
|
|
70
|
+
'Build simple unit economics with sourced assumptions and the key margin or viability test',
|
|
71
|
+
'Compare practical fulfillment or delivery options and recommend one with reasons',
|
|
72
|
+
'Write the exact checklist of account, payment, legal, or external steps left for a human',
|
|
73
|
+
],
|
|
74
|
+
doneWhen: '`ops/` holds the economics, the operating recommendation, and a human-steps checklist.',
|
|
75
|
+
rules: [
|
|
76
|
+
'Use sourced numbers and label every assumption.',
|
|
77
|
+
'Never create accounts, spend money, enter payment details, or hide a human-only step.',
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
function compactText(value) {
|
|
83
|
+
return String(value || '').replace(/\s+/g, ' ').trim();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function slugify(value) {
|
|
87
|
+
return compactText(value)
|
|
88
|
+
.toLowerCase()
|
|
89
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
90
|
+
.replace(/\s+/g, '-')
|
|
91
|
+
.replace(/-+/g, '-')
|
|
92
|
+
.replace(/^-+|-+$/g, '');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function titleCaseWord(word) {
|
|
96
|
+
return String(word || '')
|
|
97
|
+
.toLowerCase()
|
|
98
|
+
.split('-')
|
|
99
|
+
.map((part) => part ? part.charAt(0).toUpperCase() + part.slice(1) : '')
|
|
100
|
+
.join('-');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function titleCase(value) {
|
|
104
|
+
return compactText(value)
|
|
105
|
+
.split(/[\s_-]+/)
|
|
106
|
+
.filter(Boolean)
|
|
107
|
+
.map(titleCaseWord)
|
|
108
|
+
.join(' ');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function deriveBusinessName(idea, fallback = 'Business Simulation') {
|
|
112
|
+
const words = compactText(idea).match(/[A-Za-z0-9]+(?:['-][A-Za-z0-9]+)*/g) || [];
|
|
113
|
+
const meaningful = words.filter((word) => !NAME_STOP_WORDS.has(word.toLowerCase()));
|
|
114
|
+
if (meaningful.length === 0) return fallback;
|
|
115
|
+
return meaningful.slice(0, 3).map(titleCaseWord).join(' ');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function normalizeRoleNames(value) {
|
|
119
|
+
const rawNames = Array.isArray(value)
|
|
120
|
+
? value
|
|
121
|
+
: String(value || '').split(',');
|
|
122
|
+
const names = rawNames.map((name) => slugify(name)).filter(Boolean);
|
|
123
|
+
|
|
124
|
+
if (names.length !== ROLE_TEMPLATES.length) {
|
|
125
|
+
throw new Error(`--roles requires exactly ${ROLE_TEMPLATES.length} comma-separated role names.`);
|
|
126
|
+
}
|
|
127
|
+
if (new Set(names).size !== names.length) {
|
|
128
|
+
throw new Error('--roles must contain four distinct role names.');
|
|
129
|
+
}
|
|
130
|
+
return names;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function parseRoleNames(value) {
|
|
134
|
+
if (value === undefined || value === null || compactText(value) === '') {
|
|
135
|
+
return [...DEFAULT_ROLE_NAMES];
|
|
136
|
+
}
|
|
137
|
+
return normalizeRoleNames(value);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function buildRoleSet(idea, roleNames = DEFAULT_ROLE_NAMES) {
|
|
141
|
+
const cleanIdea = compactText(idea);
|
|
142
|
+
const names = normalizeRoleNames(roleNames);
|
|
143
|
+
|
|
144
|
+
return ROLE_TEMPLATES.map((template, index) => ({
|
|
145
|
+
key: template.key,
|
|
146
|
+
name: names[index],
|
|
147
|
+
title: titleCase(names[index]),
|
|
148
|
+
role: template.role,
|
|
149
|
+
description: template.description,
|
|
150
|
+
mission: template.mission(cleanIdea),
|
|
151
|
+
goals: template.goals(cleanIdea, names),
|
|
152
|
+
doneWhen: template.doneWhen,
|
|
153
|
+
rules: [...template.rules],
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function renderMemberMarkdown(role) {
|
|
158
|
+
return [
|
|
159
|
+
'---',
|
|
160
|
+
`name: ${role.name}`,
|
|
161
|
+
`role: ${role.title}`,
|
|
162
|
+
`description: ${role.description}`,
|
|
163
|
+
'version: 1.0.0',
|
|
164
|
+
'',
|
|
165
|
+
'skills: []',
|
|
166
|
+
'',
|
|
167
|
+
'permissions:',
|
|
168
|
+
' can-read: true',
|
|
169
|
+
' can-execute: true',
|
|
170
|
+
' can-approve: false',
|
|
171
|
+
' can-accept-task: false',
|
|
172
|
+
' approval-required: []',
|
|
173
|
+
'',
|
|
174
|
+
'tools: []',
|
|
175
|
+
'---',
|
|
176
|
+
'',
|
|
177
|
+
`# ${role.title}`,
|
|
178
|
+
'',
|
|
179
|
+
'## Mission',
|
|
180
|
+
role.mission,
|
|
181
|
+
'',
|
|
182
|
+
'## Goals',
|
|
183
|
+
...role.goals.map((goal) => `- ${goal}`),
|
|
184
|
+
'',
|
|
185
|
+
'## Done when',
|
|
186
|
+
role.doneWhen,
|
|
187
|
+
'',
|
|
188
|
+
'## Rules',
|
|
189
|
+
...role.rules.map((rule) => `- ${rule}`),
|
|
190
|
+
'',
|
|
191
|
+
].join('\n');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function renderMissionMarkdown(role) {
|
|
195
|
+
return [
|
|
196
|
+
`# ${role.title} Mission`,
|
|
197
|
+
'',
|
|
198
|
+
'## North Star',
|
|
199
|
+
role.mission,
|
|
200
|
+
'',
|
|
201
|
+
'## Loop',
|
|
202
|
+
'',
|
|
203
|
+
'1. Read `atris/ENDGAME.md` and the latest tick log.',
|
|
204
|
+
'2. Take the smallest unfinished item owned by this role.',
|
|
205
|
+
'3. Write the artifact and proof into the workspace.',
|
|
206
|
+
'4. Append the result, blocker, and next handoff to the tick log.',
|
|
207
|
+
'',
|
|
208
|
+
'## Stop condition',
|
|
209
|
+
'',
|
|
210
|
+
`Stop when ${role.doneWhen.charAt(0).toLowerCase()}${role.doneWhen.slice(1)}`,
|
|
211
|
+
'',
|
|
212
|
+
].join('\n');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function formatCreationTime(createdAt) {
|
|
216
|
+
const date = createdAt instanceof Date ? createdAt : new Date(createdAt);
|
|
217
|
+
if (Number.isNaN(date.getTime())) throw new Error(`Invalid simulation creation time: ${createdAt}`);
|
|
218
|
+
return date.toISOString().replace('T', ' ').slice(0, 16) + ' UTC';
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function renderEndgame({ businessName, idea, roles, createdAt }) {
|
|
222
|
+
const definition = roles.map((role, index) =>
|
|
223
|
+
`${index + 1}. [ ] ${role.title} (${role.name}): ${role.doneWhen}`
|
|
224
|
+
);
|
|
225
|
+
definition.push(`${roles.length + 1}. [ ] Integration: all four role outputs work together as one coherent, verified customer path.`);
|
|
226
|
+
|
|
227
|
+
return [
|
|
228
|
+
`# ${businessName} - Endgame`,
|
|
229
|
+
'',
|
|
230
|
+
`One night, one goal: make "${compactText(idea)}" launch-ready as a coherent business simulation. Everything done except the steps only a human can do.`,
|
|
231
|
+
'',
|
|
232
|
+
'## Definition of done',
|
|
233
|
+
...definition,
|
|
234
|
+
'',
|
|
235
|
+
'## Blocked on human (by design)',
|
|
236
|
+
'Real accounts, money, legal acceptance, publishing, customer contact, and irreversible external actions stay on the operator checklist.',
|
|
237
|
+
'',
|
|
238
|
+
'## Tick log',
|
|
239
|
+
`- ${formatCreationTime(createdAt)} - business created, team hired, missions written. Ready for the first loop.`,
|
|
240
|
+
'',
|
|
241
|
+
].join('\n');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function parseSimulationArgs(args = []) {
|
|
245
|
+
const positional = [];
|
|
246
|
+
let explicitName = '';
|
|
247
|
+
let rolesValue;
|
|
248
|
+
let dryRun = false;
|
|
249
|
+
|
|
250
|
+
for (let index = 0; index < args.length; index++) {
|
|
251
|
+
const arg = args[index];
|
|
252
|
+
const next = args[index + 1];
|
|
253
|
+
|
|
254
|
+
if (arg === '--dry-run' || arg === '-n') {
|
|
255
|
+
dryRun = true;
|
|
256
|
+
} else if (arg === '--name') {
|
|
257
|
+
if (!next || next.startsWith('-')) throw new Error(`Missing value for --name.\n${SIMULATE_USAGE}`);
|
|
258
|
+
explicitName = next;
|
|
259
|
+
index++;
|
|
260
|
+
} else if (arg.startsWith('--name=')) {
|
|
261
|
+
explicitName = arg.slice('--name='.length);
|
|
262
|
+
} else if (arg === '--roles') {
|
|
263
|
+
if (!next || next.startsWith('-')) throw new Error(`Missing value for --roles.\n${SIMULATE_USAGE}`);
|
|
264
|
+
rolesValue = next;
|
|
265
|
+
index++;
|
|
266
|
+
} else if (arg.startsWith('--roles=')) {
|
|
267
|
+
rolesValue = arg.slice('--roles='.length);
|
|
268
|
+
} else if (arg.startsWith('-')) {
|
|
269
|
+
throw new Error(`Unknown option: ${arg}\n${SIMULATE_USAGE}`);
|
|
270
|
+
} else {
|
|
271
|
+
positional.push(arg);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const idea = compactText(positional.join(' '));
|
|
276
|
+
if (!idea) throw new Error(SIMULATE_USAGE);
|
|
277
|
+
|
|
278
|
+
const businessName = compactText(explicitName) || deriveBusinessName(idea);
|
|
279
|
+
const businessSlug = slugify(businessName);
|
|
280
|
+
if (!businessSlug) throw new Error(`Business name must contain a letter or number.\n${SIMULATE_USAGE}`);
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
idea,
|
|
284
|
+
businessName,
|
|
285
|
+
businessSlug,
|
|
286
|
+
roleNames: parseRoleNames(rolesValue),
|
|
287
|
+
dryRun,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function shellQuote(value) {
|
|
292
|
+
return JSON.stringify(String(value));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function buildSimulationPlan({
|
|
296
|
+
idea,
|
|
297
|
+
businessName,
|
|
298
|
+
businessSlug,
|
|
299
|
+
roleNames = DEFAULT_ROLE_NAMES,
|
|
300
|
+
createdAt,
|
|
301
|
+
homeDir = os.homedir(),
|
|
302
|
+
workspacePath,
|
|
303
|
+
}) {
|
|
304
|
+
const cleanIdea = compactText(idea);
|
|
305
|
+
const name = compactText(businessName) || deriveBusinessName(cleanIdea);
|
|
306
|
+
const slug = slugify(businessSlug || name) || 'business-simulation';
|
|
307
|
+
const roles = buildRoleSet(cleanIdea, roleNames);
|
|
308
|
+
const created = createdAt || new Date().toISOString();
|
|
309
|
+
const root = workspacePath || path.join(homeDir, 'arena', 'atris-business', slug);
|
|
310
|
+
|
|
311
|
+
const files = [];
|
|
312
|
+
for (const role of roles) {
|
|
313
|
+
files.push({
|
|
314
|
+
path: `atris/team/${role.name}/MEMBER.md`,
|
|
315
|
+
content: renderMemberMarkdown(role),
|
|
316
|
+
kind: 'member',
|
|
317
|
+
role,
|
|
318
|
+
});
|
|
319
|
+
files.push({
|
|
320
|
+
path: `atris/team/${role.name}/MISSION.md`,
|
|
321
|
+
content: renderMissionMarkdown(role),
|
|
322
|
+
kind: 'mission',
|
|
323
|
+
role,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
files.push({
|
|
327
|
+
path: 'atris/ENDGAME.md',
|
|
328
|
+
content: renderEndgame({ businessName: name, idea: cleanIdea, roles, createdAt: created }),
|
|
329
|
+
kind: 'endgame',
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
idea: cleanIdea,
|
|
334
|
+
businessName: name,
|
|
335
|
+
businessSlug: slug,
|
|
336
|
+
workspacePath: root,
|
|
337
|
+
createdAt: created,
|
|
338
|
+
roles,
|
|
339
|
+
files,
|
|
340
|
+
commands: [
|
|
341
|
+
`atris business init ${shellQuote(name)}`,
|
|
342
|
+
...roles.flatMap((role) => [
|
|
343
|
+
`atris member create ${role.name} --role=${shellQuote(role.title)} --description=${shellQuote(role.description)}`,
|
|
344
|
+
`atris member push ${role.name}`,
|
|
345
|
+
]),
|
|
346
|
+
],
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function renderDryRun(plan) {
|
|
351
|
+
const lines = [
|
|
352
|
+
'dry run: no cloud calls made and no files written.',
|
|
353
|
+
'',
|
|
354
|
+
`business: ${plan.businessName} (${plan.businessSlug})`,
|
|
355
|
+
`idea: ${plan.idea}`,
|
|
356
|
+
`workspace: ${plan.workspacePath}`,
|
|
357
|
+
'',
|
|
358
|
+
'would run:',
|
|
359
|
+
...plan.commands.map((command) => ` ${command}`),
|
|
360
|
+
'',
|
|
361
|
+
`roster: ${plan.roles.map((role) => `${role.name} (${role.role})`).join(', ')}`,
|
|
362
|
+
];
|
|
363
|
+
|
|
364
|
+
for (const file of plan.files) {
|
|
365
|
+
lines.push('', `--- ${file.path} ---`, file.content.trimEnd());
|
|
366
|
+
}
|
|
367
|
+
lines.push('');
|
|
368
|
+
return lines.join('\n');
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
module.exports = {
|
|
372
|
+
SIMULATE_USAGE,
|
|
373
|
+
buildRoleSet,
|
|
374
|
+
buildSimulationPlan,
|
|
375
|
+
deriveBusinessName,
|
|
376
|
+
parseRoleNames,
|
|
377
|
+
parseSimulationArgs,
|
|
378
|
+
renderDryRun,
|
|
379
|
+
renderEndgame,
|
|
380
|
+
renderMemberMarkdown,
|
|
381
|
+
slugify,
|
|
382
|
+
};
|
package/lib/chat-log-scan.js
CHANGED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawnSync } = require('node:child_process');
|
|
4
|
+
|
|
5
|
+
function runGit(args, cwd = process.cwd()) {
|
|
6
|
+
return spawnSync('git', args, {
|
|
7
|
+
cwd,
|
|
8
|
+
encoding: 'utf8',
|
|
9
|
+
timeout: 30000,
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function checkoutBehind(cwd = process.cwd()) {
|
|
14
|
+
const countResult = runGit(['rev-list', '--count', 'HEAD..@{upstream}'], cwd);
|
|
15
|
+
if (countResult.status !== 0) return null;
|
|
16
|
+
|
|
17
|
+
const count = Number.parseInt(countResult.stdout.trim(), 10);
|
|
18
|
+
if (!Number.isInteger(count) || count < 1) return null;
|
|
19
|
+
|
|
20
|
+
const upstreamResult = runGit(
|
|
21
|
+
['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}'],
|
|
22
|
+
cwd
|
|
23
|
+
);
|
|
24
|
+
const upstream = upstreamResult.status === 0
|
|
25
|
+
? upstreamResult.stdout.trim()
|
|
26
|
+
: 'origin';
|
|
27
|
+
const remote = upstream.includes('/') ? upstream.split('/')[0] : upstream;
|
|
28
|
+
|
|
29
|
+
return { count, upstream, remote };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function checkoutBehindMessage(cwd = process.cwd()) {
|
|
33
|
+
const behind = checkoutBehind(cwd);
|
|
34
|
+
if (!behind) return null;
|
|
35
|
+
const unit = behind.count === 1 ? 'commit' : 'commits';
|
|
36
|
+
return `this checkout is ${behind.count} ${unit} behind ${behind.remote}; run atris sync-checkout`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function trackedTreeIsClean(cwd = process.cwd()) {
|
|
40
|
+
const unstaged = runGit(['diff', '--quiet', '--'], cwd);
|
|
41
|
+
if (unstaged.status !== 0) return false;
|
|
42
|
+
|
|
43
|
+
const staged = runGit(['diff', '--cached', '--quiet', '--'], cwd);
|
|
44
|
+
return staged.status === 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = {
|
|
48
|
+
checkoutBehind,
|
|
49
|
+
checkoutBehindMessage,
|
|
50
|
+
runGit,
|
|
51
|
+
trackedTreeIsClean,
|
|
52
|
+
};
|