bmad-plus 0.13.0 → 0.16.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/CHANGELOG.md +74 -0
- package/README.md +113 -487
- package/SECURITY.md +71 -0
- package/THIRD-PARTY-LICENSES.md +349 -0
- package/osint-agent-package/README.md +1 -1
- package/package.json +14 -3
- package/readme-international/README.de.md +18 -8
- package/readme-international/README.es.md +19 -9
- package/readme-international/README.fr.md +18 -8
- package/src/bmad-plus/agents/agent-architect-dev/SKILL.md +11 -13
- package/src/bmad-plus/agents/agent-orchestrator/SKILL.md +148 -9
- package/src/bmad-plus/agents/agent-quality/SKILL.md +41 -11
- package/src/bmad-plus/data/role-triggers.yaml +19 -0
- package/src/bmad-plus/module-help.csv +1 -0
- package/src/bmad-plus/module.yaml +1 -0
- package/src/bmad-plus/packs/pack-dev-studio/README.md +133 -141
- package/src/bmad-plus/packs/pack-dev-studio/SKILL.md +49 -0
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/analyst-agent.md +35 -60
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/document-project.md +59 -59
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/domain-research.md +55 -93
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/market-research.md +58 -93
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/prfaq.md +55 -132
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/product-brief.md +63 -78
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/tech-writer-agent.md +54 -69
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/technical-research.md +54 -93
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/architect-agent.md +32 -60
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/create-architecture.md +67 -71
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/create-epics-stories.md +61 -90
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/generate-project-context.md +56 -78
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/implementation-readiness.md +55 -88
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-01-init.md +20 -153
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-01b-continue.md +20 -173
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-02-context.md +14 -220
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-03-starter.md +20 -329
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-04-decisions.md +15 -314
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-05-patterns.md +15 -355
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-06-structure.md +15 -375
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-07-validation.md +14 -357
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-08-complete.md +13 -78
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/checkpoint-preview.md +52 -65
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-01-gather-context.md +14 -81
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-02-review.md +14 -31
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-03-triage.md +14 -45
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-04-present.md +13 -128
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review.md +61 -87
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/correct-course.md +55 -298
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/create-story.md +54 -426
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-agent.md +48 -69
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story-checklist.md +24 -80
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story.md +64 -482
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/investigate.md +50 -184
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/qa-e2e-tests.md +57 -173
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/quick-dev.md +56 -108
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/retrospective.md +54 -1509
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/sprint-planning.md +54 -296
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/sprint-status.md +41 -283
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/create-prd.md +58 -18
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/create-ux-design.md +103 -72
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/edit-prd.md +55 -27
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/pm-agent.md +34 -60
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/prd.md +46 -87
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/steps/step-01-init.md +10 -0
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/ux-designer-agent.md +30 -60
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/validate-prd.md +57 -27
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/advanced-elicitation.md +47 -138
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/adversarial-review.md +48 -34
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/bmad-help.md +51 -68
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/brainstorming.md +46 -3
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/customize.md +68 -109
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/distillator.md +53 -174
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/edge-case-hunter.md +39 -53
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/editorial-review-prose.md +45 -83
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/editorial-review-structure.md +45 -176
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/index-docs.md +45 -63
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/party-mode.md +53 -124
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/shard-doc.md +44 -100
- package/src/bmad-plus/packs/pack-dev-studio/dev-studio-orchestrator.md +56 -116
- package/src/bmad-plus/packs/pack-dev-studio/shared/architecture-decision-template.md +49 -12
- package/src/bmad-plus/packs/pack-dev-studio/shared/bwml-spec.md +51 -328
- package/src/bmad-plus/packs/pack-dev-studio/shared/catalog.json +489 -0
- package/src/bmad-plus/packs/pack-dev-studio/shared/execution.md +69 -0
- package/src/bmad-plus/packs/pack-dev-studio/shared/module-help.csv +39 -32
- package/src/bmad-plus/packs/pack-dev-studio/upstream-sync.yaml +85 -14
- package/src/bmad-plus/packs/pack-memory/README.md +35 -4
- package/src/bmad-plus/packs/pack-memory/memory-orchestrator.md +33 -6
- package/src/bmad-plus/packs/pack-memory/shared/karpathy-guardrails.md +3 -3
- package/src/bmad-plus/packs/pack-memory/shared/memory-protocol.md +27 -3
- package/src/bmad-plus/packs/pack-memory/zecher-agent.md +18 -2
- package/src/bmad-plus/packs/pack-seo/SKILL.md +27 -1
- package/src/bmad-plus/packs/pack-seo/seo-chief.md +16 -1
- package/src/bmad-plus/packs/pack-seo/seo-judge.md +12 -0
- package/src/bmad-plus/packs/pack-seo/seo-scout.md +12 -0
- package/src/bmad-plus/skills/bmad-plus-autopilot/SKILL.md +49 -12
- package/src/bmad-plus/skills/bmad-plus-parallel/SKILL.md +17 -3
- package/src/bmad-plus/skills/bmad-plus-sync/SKILL.md +76 -65
- package/src/bmad-plus/skills/bmad-plus-uat/SKILL.md +144 -0
- package/src/bmad-plus/skills/bmad-plus-uat/ref/uat-results.schema.json +60 -0
- package/src/bmad-plus/skills/bmad-plus-uat/ref/uat-spec.schema.json +121 -0
- package/src/bmad-plus/skills/bmad-plus-uat/ref/uat-triage.schema.json +60 -0
- package/src/bmad-plus/skills/bmad-plus-uat/template/page.html +552 -0
- package/src/bmad-plus/skills/bmad-plus-uat/template/strings.json +362 -0
- package/src/bmad-plus/skills/dev-studio/SKILL.md +19 -0
- package/tools/build/check-install-contract.js +367 -17
- package/tools/build/generate.js +229 -40
- package/tools/build/generated-adapters/.codex/AGENTS.md +1 -1
- package/tools/build/generated-adapters/.cursor/rules/bmad-plus.mdc +1 -1
- package/tools/build/generated-adapters/.opencode/AGENTS.md +1 -1
- package/tools/build/generated-adapters/AGENTS.md +1 -1
- package/tools/build/generated-adapters/CLAUDE.md +1 -1
- package/tools/build/generated-adapters/CONVENTIONS.md +1 -1
- package/tools/build/generated-adapters/GEMINI.md +1 -1
- package/tools/build/pack-delivery.js +78 -0
- package/tools/cli/bmad-plus-cli.js +15 -12
- package/tools/cli/commands/doctor.js +50 -189
- package/tools/cli/commands/install.js +22 -3
- package/tools/cli/commands/memory-journal-cmd.js +119 -19
- package/tools/cli/commands/nexus.js +111 -0
- package/tools/cli/commands/studio.js +68 -0
- package/tools/cli/commands/uat.js +389 -0
- package/tools/cli/lib/README-memory-journal.md +19 -8
- package/tools/cli/lib/installation-health.js +366 -0
- package/tools/cli/lib/memory-journal.js +0 -0
- package/tools/cli/lib/memory-outcomes.js +293 -0
- package/tools/cli/lib/memory-store.js +139 -0
- package/tools/cli/lib/nexus-process.js +377 -0
- package/tools/cli/lib/nexus.js +1532 -0
- package/tools/cli/lib/pack-copy.js +39 -11
- package/tools/cli/lib/packs.js +134 -11
- package/tools/cli/lib/python-health.js +233 -0
- package/tools/cli/lib/python-provision.js +2 -2
- package/tools/cli/lib/studio.js +310 -0
- package/tools/cli/lib/uat.js +869 -0
- package/tools/maintain/upstream-candidate.js +456 -0
- package/tools/release/publication-content.js +903 -0
- package/tools/release/supply-chain.js +282 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const crypto = require('node:crypto');
|
|
6
|
+
const os = require('node:os');
|
|
7
|
+
|
|
8
|
+
const DIRECTORY = '.bmad/memory';
|
|
9
|
+
const LOCK = DIRECTORY + '/writer.lock';
|
|
10
|
+
const MAX_BYTES = 2 * 1024 * 1024;
|
|
11
|
+
const held = new Set();
|
|
12
|
+
|
|
13
|
+
function safeFile(baseDir, relative) {
|
|
14
|
+
const root = path.resolve(baseDir);
|
|
15
|
+
if (
|
|
16
|
+
typeof relative !== 'string' ||
|
|
17
|
+
!relative ||
|
|
18
|
+
relative.includes(':') ||
|
|
19
|
+
path.isAbsolute(relative) ||
|
|
20
|
+
relative.split(/[\\/]/).some((p) => !p || p === '..' || p === '.')
|
|
21
|
+
) {
|
|
22
|
+
throw new Error('Memory path must be a normalized project-relative path.');
|
|
23
|
+
}
|
|
24
|
+
const target = path.resolve(root, relative);
|
|
25
|
+
if (!target.startsWith(root + path.sep)) throw new Error('Memory path escapes project.');
|
|
26
|
+
let current = path.parse(target).root;
|
|
27
|
+
for (const part of target.slice(current.length).split(path.sep)) {
|
|
28
|
+
current = path.join(current, part);
|
|
29
|
+
let stat;
|
|
30
|
+
try {
|
|
31
|
+
stat = fs.lstatSync(current);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error.code === 'ENOENT') continue;
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
if (stat.isSymbolicLink())
|
|
37
|
+
throw new Error('Memory paths cannot follow symbolic links or junctions.');
|
|
38
|
+
if (current !== target && !stat.isDirectory())
|
|
39
|
+
throw new Error('Memory parent is not a directory.');
|
|
40
|
+
}
|
|
41
|
+
return target;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readText(baseDir, relative, { missing = false } = {}) {
|
|
45
|
+
const file = safeFile(baseDir, relative);
|
|
46
|
+
let stat;
|
|
47
|
+
try {
|
|
48
|
+
stat = fs.statSync(file);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (missing && error.code === 'ENOENT') return null;
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
if (!stat.isFile() || stat.size > MAX_BYTES)
|
|
54
|
+
throw new Error('Memory source must be a regular file of at most 2 MiB.');
|
|
55
|
+
const buffer = fs.readFileSync(file);
|
|
56
|
+
if (buffer.length > MAX_BYTES) throw new Error('Memory source exceeded 2 MiB while reading.');
|
|
57
|
+
const text = buffer.toString('utf8');
|
|
58
|
+
if (!Buffer.from(text).equals(buffer)) throw new Error('Memory source must contain valid UTF-8.');
|
|
59
|
+
return text;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Synchronous, process-shared mutex; stale owners require explicit operator inspection. */
|
|
63
|
+
function withMemoryLock(baseDir, operation) {
|
|
64
|
+
const root = path.resolve(baseDir);
|
|
65
|
+
if (held.has(root)) return operation(); // Nested synchronous legacy score/promotion writes.
|
|
66
|
+
const file = safeFile(root, LOCK);
|
|
67
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
68
|
+
const owner = JSON.stringify({
|
|
69
|
+
pid: process.pid,
|
|
70
|
+
hostname: os.hostname(),
|
|
71
|
+
token: crypto.randomUUID(),
|
|
72
|
+
});
|
|
73
|
+
let descriptor;
|
|
74
|
+
for (let attempt = 0; attempt < 200; attempt++) {
|
|
75
|
+
try {
|
|
76
|
+
descriptor = fs.openSync(safeFile(root, LOCK), 'wx');
|
|
77
|
+
break;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (error.code !== 'EEXIST') throw error;
|
|
80
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (descriptor === undefined)
|
|
84
|
+
throw new Error(
|
|
85
|
+
'Memory writer lock is occupied. Inspect writer.lock and its owner; do not remove a live lock.'
|
|
86
|
+
);
|
|
87
|
+
try {
|
|
88
|
+
fs.writeFileSync(descriptor, owner);
|
|
89
|
+
fs.fsyncSync(descriptor);
|
|
90
|
+
held.add(root);
|
|
91
|
+
const result = operation();
|
|
92
|
+
if (result && typeof result.then === 'function')
|
|
93
|
+
throw new Error('Memory lock operations must be synchronous.');
|
|
94
|
+
return result;
|
|
95
|
+
} finally {
|
|
96
|
+
held.delete(root);
|
|
97
|
+
fs.closeSync(descriptor);
|
|
98
|
+
if (readText(root, LOCK, { missing: true }) === owner) fs.unlinkSync(safeFile(root, LOCK));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function appendRecord(baseDir, relative, record) {
|
|
103
|
+
return withMemoryLock(baseDir, () => {
|
|
104
|
+
const file = safeFile(baseDir, relative);
|
|
105
|
+
const prior = readText(baseDir, relative, { missing: true }) || '';
|
|
106
|
+
const line = JSON.stringify(record) + '\n';
|
|
107
|
+
if (Buffer.byteLength(prior + line) > MAX_BYTES)
|
|
108
|
+
throw new Error('Memory store is full; archive it explicitly before adding records.');
|
|
109
|
+
if (prior && !prior.endsWith('\n'))
|
|
110
|
+
throw new Error('Memory store has an incomplete final line; inspect it before writing.');
|
|
111
|
+
const descriptor = fs.openSync(file, 'a');
|
|
112
|
+
try {
|
|
113
|
+
fs.writeFileSync(descriptor, line);
|
|
114
|
+
fs.fsyncSync(descriptor);
|
|
115
|
+
} finally {
|
|
116
|
+
fs.closeSync(descriptor);
|
|
117
|
+
}
|
|
118
|
+
return record;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function writeJson(baseDir, relative, value) {
|
|
123
|
+
return withMemoryLock(baseDir, () => {
|
|
124
|
+
const file = safeFile(baseDir, relative);
|
|
125
|
+
const text = JSON.stringify(value, null, 2) + '\n';
|
|
126
|
+
if (Buffer.byteLength(text) > MAX_BYTES) throw new Error('Memory store exceeds 2 MiB.');
|
|
127
|
+
const temporary = safeFile(baseDir, relative + '.' + crypto.randomUUID() + '.tmp');
|
|
128
|
+
const descriptor = fs.openSync(temporary, 'wx');
|
|
129
|
+
try {
|
|
130
|
+
fs.writeFileSync(descriptor, text);
|
|
131
|
+
fs.fsyncSync(descriptor);
|
|
132
|
+
} finally {
|
|
133
|
+
fs.closeSync(descriptor);
|
|
134
|
+
}
|
|
135
|
+
fs.renameSync(temporary, file);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = { withMemoryLock, appendRecord, writeJson, readText, safeFile, LOCK, MAX_BYTES };
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
/** One foreground owner, one direct child. Never take ownership from a recorded PID. */
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const crypto = require('node:crypto');
|
|
5
|
+
const { spawn } = require('node:child_process');
|
|
6
|
+
|
|
7
|
+
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
|
|
8
|
+
const AUTH_PATH_KEYS = ['HOME', 'USERPROFILE', 'HOMEDRIVE', 'HOMEPATH', 'CODEX_HOME'];
|
|
9
|
+
|
|
10
|
+
function normalizeExecution(root, value, helpers) {
|
|
11
|
+
if (value === undefined) return undefined;
|
|
12
|
+
const { fail, relative, directoryAt, executable, stringList } = helpers;
|
|
13
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
14
|
+
fail('execution must be a command or codex-exec object.');
|
|
15
|
+
if (!['command', 'codex-exec'].includes(value.adapter)) fail('Unsupported execution adapter.');
|
|
16
|
+
const allowed = [
|
|
17
|
+
'adapter',
|
|
18
|
+
'command',
|
|
19
|
+
'input',
|
|
20
|
+
'instructions',
|
|
21
|
+
'resources',
|
|
22
|
+
'cwd',
|
|
23
|
+
'outputLimitBytes',
|
|
24
|
+
...(value.adapter === 'command' ? ['args', 'blockedExitCodes'] : ['sandbox', 'model']),
|
|
25
|
+
];
|
|
26
|
+
if (Object.keys(value).some((key) => !allowed.includes(key)))
|
|
27
|
+
fail('Unknown execution field; shell, environment and arbitrary Codex flags are unsupported.');
|
|
28
|
+
const input = relative(value.input);
|
|
29
|
+
const instructions = stringList(value.instructions ?? [], 'execution instructions', {
|
|
30
|
+
paths: true,
|
|
31
|
+
});
|
|
32
|
+
const resources = [
|
|
33
|
+
...new Set([
|
|
34
|
+
input,
|
|
35
|
+
...instructions,
|
|
36
|
+
...stringList(value.resources ?? [], 'execution resources', { paths: true }),
|
|
37
|
+
]),
|
|
38
|
+
];
|
|
39
|
+
const cwd = value.cwd ?? '.';
|
|
40
|
+
directoryAt(root, cwd);
|
|
41
|
+
const outputLimitBytes = value.outputLimitBytes ?? 256 * 1024;
|
|
42
|
+
if (
|
|
43
|
+
!Number.isInteger(outputLimitBytes) ||
|
|
44
|
+
outputLimitBytes < 1024 ||
|
|
45
|
+
outputLimitBytes > 2 * 1024 * 1024
|
|
46
|
+
)
|
|
47
|
+
fail('execution outputLimitBytes must be between 1024 and 2097152.');
|
|
48
|
+
const resolved = executable(value.command);
|
|
49
|
+
let args;
|
|
50
|
+
let blockedExitCodes = [];
|
|
51
|
+
let sandbox;
|
|
52
|
+
let model;
|
|
53
|
+
if (value.adapter === 'command') {
|
|
54
|
+
args = value.args;
|
|
55
|
+
if (
|
|
56
|
+
!Array.isArray(args) ||
|
|
57
|
+
args.length > 100 ||
|
|
58
|
+
args.some((arg) => typeof arg !== 'string' || arg.length > 8000 || arg.includes('\0'))
|
|
59
|
+
)
|
|
60
|
+
fail('execution args must be a bounded literal argument array.');
|
|
61
|
+
blockedExitCodes = value.blockedExitCodes ?? [];
|
|
62
|
+
if (
|
|
63
|
+
!Array.isArray(blockedExitCodes) ||
|
|
64
|
+
blockedExitCodes.length > 10 ||
|
|
65
|
+
blockedExitCodes.some((code) => !Number.isInteger(code) || code < 1 || code > 255)
|
|
66
|
+
)
|
|
67
|
+
fail('blockedExitCodes must contain explicit nonzero exit codes between 1 and 255.');
|
|
68
|
+
} else {
|
|
69
|
+
sandbox = value.sandbox ?? 'read-only';
|
|
70
|
+
if (!['read-only', 'workspace-write'].includes(sandbox))
|
|
71
|
+
fail('codex-exec supports read-only or workspace-write only.');
|
|
72
|
+
model = value.model ?? null;
|
|
73
|
+
if (
|
|
74
|
+
model !== null &&
|
|
75
|
+
(typeof model !== 'string' || !/^[a-zA-Z0-9][a-zA-Z0-9._/-]{0,119}$/.test(model))
|
|
76
|
+
)
|
|
77
|
+
fail('Codex model must be an explicit bounded model identifier.');
|
|
78
|
+
// stdin carries task text; it is never parsed as flags or a shell command.
|
|
79
|
+
// Auth stays with the installed CLI. No user-config or bypass flag is imported.
|
|
80
|
+
// Project and host rules still apply; this is not a Nexus sandbox.
|
|
81
|
+
args = [
|
|
82
|
+
'-a',
|
|
83
|
+
'never',
|
|
84
|
+
'exec',
|
|
85
|
+
'--json',
|
|
86
|
+
'--color',
|
|
87
|
+
'never',
|
|
88
|
+
'--sandbox',
|
|
89
|
+
sandbox,
|
|
90
|
+
'--skip-git-repo-check',
|
|
91
|
+
'--ignore-user-config',
|
|
92
|
+
];
|
|
93
|
+
if (model) args.push('--model', model);
|
|
94
|
+
args.push('-');
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
adapter: value.adapter,
|
|
98
|
+
...resolved,
|
|
99
|
+
args: [...args],
|
|
100
|
+
input,
|
|
101
|
+
instructions,
|
|
102
|
+
resources,
|
|
103
|
+
cwd,
|
|
104
|
+
outputLimitBytes,
|
|
105
|
+
blockedExitCodes: [...new Set(blockedExitCodes)],
|
|
106
|
+
...(value.adapter === 'codex-exec' ? { sandbox, model } : {}),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function inputBytes(root, execution, safeTarget) {
|
|
111
|
+
const parts = [];
|
|
112
|
+
let size = 0;
|
|
113
|
+
for (const file of [...execution.instructions, execution.input]) {
|
|
114
|
+
const target = safeTarget(root, file);
|
|
115
|
+
const stat = fs.statSync(target);
|
|
116
|
+
if (!stat.isFile() || stat.size > 256 * 1024)
|
|
117
|
+
throw new Error('Nexus: execution input must be a regular file no larger than 256 KiB.');
|
|
118
|
+
const bytes = fs.readFileSync(target);
|
|
119
|
+
// Round-trip catches invalid UTF-8; NUL has no place in an agent instruction packet.
|
|
120
|
+
if (!Buffer.from(bytes.toString('utf8')).equals(bytes) || bytes.includes(0))
|
|
121
|
+
throw new Error('Nexus: execution input must contain UTF-8 text without NUL bytes.');
|
|
122
|
+
const prefix = execution.instructions.length
|
|
123
|
+
? Buffer.from('\n--- ' + file + ' ---\n')
|
|
124
|
+
: Buffer.alloc(0);
|
|
125
|
+
size += prefix.length + bytes.length;
|
|
126
|
+
if (size > 1024 * 1024) throw new Error('Nexus: combined execution input exceeds 1 MiB.');
|
|
127
|
+
parts.push(prefix, bytes);
|
|
128
|
+
}
|
|
129
|
+
return Buffer.concat(parts);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function executionEnvironment(execution, minimal) {
|
|
133
|
+
const values = { ...minimal.values };
|
|
134
|
+
if (execution.adapter === 'codex-exec') {
|
|
135
|
+
for (const key of AUTH_PATH_KEYS) {
|
|
136
|
+
const source = Object.keys(process.env).find((name) => name.toUpperCase() === key);
|
|
137
|
+
if (source !== undefined) values[key] = process.env[source];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// No values or credential hashes enter execution receipts. CLI-owned auth files
|
|
141
|
+
// are not read by Nexus. Verifier environments remain the separate stricter policy.
|
|
142
|
+
return {
|
|
143
|
+
values,
|
|
144
|
+
receipt: {
|
|
145
|
+
policy: execution.adapter === 'codex-exec' ? 'local-codex-auth-paths-v1' : minimal.policy,
|
|
146
|
+
inheritedKeys: Object.keys(values).sort(),
|
|
147
|
+
credentialValuesRecorded: false,
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function interpretOutput(execution, stdout, code) {
|
|
153
|
+
if (execution.blockedExitCodes.includes(code)) return { outcome: 'blocked', provider: null };
|
|
154
|
+
if (execution.adapter !== 'codex-exec')
|
|
155
|
+
return { outcome: code === 0 ? 'completed' : 'failed', provider: null };
|
|
156
|
+
let threadId = null;
|
|
157
|
+
let completed = false;
|
|
158
|
+
let failed = false;
|
|
159
|
+
let approval = false;
|
|
160
|
+
let usage = null;
|
|
161
|
+
for (const line of stdout.split(/\r?\n/).filter(Boolean)) {
|
|
162
|
+
let event;
|
|
163
|
+
try {
|
|
164
|
+
event = JSON.parse(line);
|
|
165
|
+
} catch {
|
|
166
|
+
return { outcome: 'failed', provider: { problem: 'Invalid Codex JSONL.' } };
|
|
167
|
+
}
|
|
168
|
+
if (!event || typeof event !== 'object' || Array.isArray(event))
|
|
169
|
+
return { outcome: 'failed', provider: { problem: 'Invalid Codex event object.' } };
|
|
170
|
+
if (event.type === 'thread.started' && typeof event.thread_id === 'string')
|
|
171
|
+
threadId = event.thread_id;
|
|
172
|
+
if (event.type === 'turn.completed') {
|
|
173
|
+
completed = true;
|
|
174
|
+
if (event.usage && typeof event.usage === 'object') {
|
|
175
|
+
usage = Object.fromEntries(
|
|
176
|
+
Object.entries(event.usage).filter(
|
|
177
|
+
([key, value]) =>
|
|
178
|
+
['input_tokens', 'cached_input_tokens', 'output_tokens'].includes(key) &&
|
|
179
|
+
Number.isSafeInteger(value) &&
|
|
180
|
+
value >= 0
|
|
181
|
+
)
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (event.type === 'turn.failed' || event.type === 'error') {
|
|
186
|
+
failed = true;
|
|
187
|
+
if (event.error?.code === 'approval_required' || event.code === 'approval_required')
|
|
188
|
+
approval = true;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
outcome: approval
|
|
193
|
+
? 'blocked'
|
|
194
|
+
: code === 0 && completed && threadId && !failed
|
|
195
|
+
? 'completed'
|
|
196
|
+
: 'failed',
|
|
197
|
+
provider: { threadId, completed, failed, usage, concreteModel: null, monetaryCost: null },
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function runOwnedProcess({ root, execution, input, environment, deadline, callbacks }) {
|
|
202
|
+
return new Promise((resolve) => {
|
|
203
|
+
const startedAt = new Date().toISOString();
|
|
204
|
+
const buffers = { stdout: [], stderr: [] };
|
|
205
|
+
const observed = { stdout: 0, stderr: 0 };
|
|
206
|
+
let captured = 0;
|
|
207
|
+
let child;
|
|
208
|
+
let exitCode = null;
|
|
209
|
+
let signal = null;
|
|
210
|
+
let error = null;
|
|
211
|
+
let reason = null;
|
|
212
|
+
let childExited = false;
|
|
213
|
+
let closed = false;
|
|
214
|
+
let done = false;
|
|
215
|
+
let stopTimer;
|
|
216
|
+
let closeTimer;
|
|
217
|
+
let pollTimer;
|
|
218
|
+
let heartbeatAt = 0;
|
|
219
|
+
let signalled = false;
|
|
220
|
+
|
|
221
|
+
function finish() {
|
|
222
|
+
if (done) return;
|
|
223
|
+
done = true;
|
|
224
|
+
clearTimeout(stopTimer);
|
|
225
|
+
clearTimeout(closeTimer);
|
|
226
|
+
clearInterval(pollTimer);
|
|
227
|
+
process.removeListener('SIGINT', interrupt);
|
|
228
|
+
process.removeListener('SIGTERM', interrupt);
|
|
229
|
+
child?.stdin?.destroy();
|
|
230
|
+
child?.stdout?.destroy();
|
|
231
|
+
child?.stderr?.destroy();
|
|
232
|
+
if (!childExited) child?.unref();
|
|
233
|
+
const stdout = Buffer.concat(buffers.stdout);
|
|
234
|
+
const stderr = Buffer.concat(buffers.stderr);
|
|
235
|
+
const interpreted = interpretOutput(execution, stdout.toString('utf8'), exitCode);
|
|
236
|
+
resolve({
|
|
237
|
+
startedAt,
|
|
238
|
+
endedAt: new Date().toISOString(),
|
|
239
|
+
exitCode,
|
|
240
|
+
signal,
|
|
241
|
+
error,
|
|
242
|
+
reason,
|
|
243
|
+
spawned: Boolean(child?.pid),
|
|
244
|
+
directChildExited: Boolean(child?.pid) && childExited,
|
|
245
|
+
streamsClosed: closed,
|
|
246
|
+
directChildSignalled: signalled,
|
|
247
|
+
descendantState: 'unobserved',
|
|
248
|
+
outcome:
|
|
249
|
+
reason || !childExited || !closed
|
|
250
|
+
? 'ambiguous'
|
|
251
|
+
: error || signal
|
|
252
|
+
? 'failed'
|
|
253
|
+
: interpreted.outcome,
|
|
254
|
+
provider: interpreted.provider,
|
|
255
|
+
output: Object.fromEntries(
|
|
256
|
+
[
|
|
257
|
+
['stdout', stdout],
|
|
258
|
+
['stderr', stderr],
|
|
259
|
+
].map(([name, bytes]) => [
|
|
260
|
+
name,
|
|
261
|
+
{
|
|
262
|
+
text: bytes.toString('utf8'),
|
|
263
|
+
base64: bytes.toString('base64'),
|
|
264
|
+
sha256: sha256(bytes),
|
|
265
|
+
capturedBytes: bytes.length,
|
|
266
|
+
observedBytes: observed[name],
|
|
267
|
+
complete: closed && !reason && observed[name] === bytes.length,
|
|
268
|
+
hashScope: 'captured-bytes',
|
|
269
|
+
},
|
|
270
|
+
])
|
|
271
|
+
),
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
function stop(why) {
|
|
275
|
+
reason ||= why;
|
|
276
|
+
if (child && !childExited && !signalled) {
|
|
277
|
+
// This object was returned by this spawn call. Never signal a saved PID.
|
|
278
|
+
try {
|
|
279
|
+
signalled = child.kill('SIGTERM');
|
|
280
|
+
} catch (caught) {
|
|
281
|
+
error ||= caught.message;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (!stopTimer) stopTimer = setTimeout(finish, 1000);
|
|
285
|
+
}
|
|
286
|
+
function interrupt() {
|
|
287
|
+
stop('supervisor-interrupted');
|
|
288
|
+
}
|
|
289
|
+
function capture(name, bytes) {
|
|
290
|
+
observed[name] += bytes.length;
|
|
291
|
+
const keep = Math.min(bytes.length, execution.outputLimitBytes - captured);
|
|
292
|
+
if (keep > 0) {
|
|
293
|
+
buffers[name].push(bytes.subarray(0, keep));
|
|
294
|
+
captured += keep;
|
|
295
|
+
}
|
|
296
|
+
if (keep < bytes.length) stop('output-limit');
|
|
297
|
+
}
|
|
298
|
+
process.on('SIGINT', interrupt);
|
|
299
|
+
process.on('SIGTERM', interrupt);
|
|
300
|
+
try {
|
|
301
|
+
const pending = callbacks.poll();
|
|
302
|
+
if (pending || Date.now() >= deadline) {
|
|
303
|
+
reason = pending || 'deadline';
|
|
304
|
+
childExited = true;
|
|
305
|
+
closed = true;
|
|
306
|
+
finish();
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
child = spawn(execution.command, execution.args, {
|
|
310
|
+
cwd: path.join(root, execution.cwd),
|
|
311
|
+
env: environment.values,
|
|
312
|
+
shell: false,
|
|
313
|
+
windowsHide: true,
|
|
314
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
315
|
+
});
|
|
316
|
+
child.stdout.on('data', (bytes) => capture('stdout', bytes));
|
|
317
|
+
child.stderr.on('data', (bytes) => capture('stderr', bytes));
|
|
318
|
+
child.stdin.on('error', (caught) => {
|
|
319
|
+
if (caught.code !== 'EPIPE') error ||= caught.message;
|
|
320
|
+
});
|
|
321
|
+
child.on('error', (caught) => {
|
|
322
|
+
error = caught.message;
|
|
323
|
+
// Spawn failure creates no child; close follows on supported Node versions.
|
|
324
|
+
if (!child.pid) {
|
|
325
|
+
childExited = true;
|
|
326
|
+
exitCode = null;
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
child.on('exit', (code, receivedSignal) => {
|
|
330
|
+
childExited = true;
|
|
331
|
+
exitCode = code;
|
|
332
|
+
signal = receivedSignal;
|
|
333
|
+
closeTimer = setTimeout(() => {
|
|
334
|
+
reason ||= 'retained-pipes';
|
|
335
|
+
finish();
|
|
336
|
+
}, 250);
|
|
337
|
+
});
|
|
338
|
+
child.on('close', (code, receivedSignal) => {
|
|
339
|
+
closed = true;
|
|
340
|
+
exitCode = code;
|
|
341
|
+
signal = receivedSignal;
|
|
342
|
+
finish();
|
|
343
|
+
});
|
|
344
|
+
callbacks.spawned(child.pid ?? null);
|
|
345
|
+
child.stdin.end(input);
|
|
346
|
+
pollTimer = setInterval(() => {
|
|
347
|
+
try {
|
|
348
|
+
const requested = callbacks.poll();
|
|
349
|
+
if (requested) stop(requested);
|
|
350
|
+
if (Date.now() >= deadline) stop('deadline');
|
|
351
|
+
if (Date.now() - heartbeatAt >= 1000) {
|
|
352
|
+
callbacks.heartbeat();
|
|
353
|
+
heartbeatAt = Date.now();
|
|
354
|
+
}
|
|
355
|
+
} catch (caught) {
|
|
356
|
+
error ||= caught.message;
|
|
357
|
+
stop('owner-state-unavailable');
|
|
358
|
+
}
|
|
359
|
+
}, 100);
|
|
360
|
+
} catch (caught) {
|
|
361
|
+
error = caught.message;
|
|
362
|
+
if (!child) {
|
|
363
|
+
childExited = true;
|
|
364
|
+
closed = true;
|
|
365
|
+
finish();
|
|
366
|
+
} else stop('launch-observation-failed');
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
module.exports = {
|
|
372
|
+
normalizeExecution,
|
|
373
|
+
inputBytes,
|
|
374
|
+
executionEnvironment,
|
|
375
|
+
interpretOutput,
|
|
376
|
+
runOwnedProcess,
|
|
377
|
+
};
|