triad-plus 1.7.0 → 1.9.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +23 -12
  3. package/adapters/antigravity/.agents/agents/triad-developer/agent.md +7 -3
  4. package/adapters/antigravity/.agents/agents/triad-evaluator/agent.md +5 -0
  5. package/adapters/antigravity/.agents/agents/triad-orchestrator/agent.md +5 -0
  6. package/adapters/antigravity/.agents/agents/triad-reviewer/agent.md +7 -2
  7. package/adapters/antigravity/.agents/skills/triad/SKILL.md +22 -0
  8. package/adapters/claude-code/.claude/agents/triad-developer.md +9 -4
  9. package/adapters/claude-code/.claude/agents/triad-evaluator.md +5 -0
  10. package/adapters/claude-code/.claude/agents/triad-reviewer.md +10 -5
  11. package/adapters/claude-code/.claude/commands/triad.md +22 -0
  12. package/adapters/claude-code/README.md +11 -3
  13. package/adapters/codex/prompts/triad.md +28 -0
  14. package/adapters/copilot/.github/agents/triad-developer.agent.md +12 -6
  15. package/adapters/copilot/.github/agents/triad-evaluator.agent.md +7 -1
  16. package/adapters/copilot/.github/agents/triad-orchestrator.agent.md +15 -1
  17. package/adapters/copilot/.github/agents/triad-reviewer.agent.md +11 -5
  18. package/adapters/copilot/.github/skills/triad/SKILL.md +23 -0
  19. package/adapters/hermes/skills/triad/SKILL.md +23 -0
  20. package/adapters/opencode/.opencode/agents/triad-developer.md +9 -4
  21. package/adapters/opencode/.opencode/agents/triad-evaluator.md +6 -0
  22. package/adapters/opencode/.opencode/agents/triad-orchestrator.md +15 -0
  23. package/adapters/opencode/.opencode/agents/triad-reviewer.md +10 -3
  24. package/adapters/opencode/.opencode/commands/triad.md +21 -0
  25. package/adapters/opencode/README.md +19 -4
  26. package/adapters/registry.mjs +2 -0
  27. package/bin/triad-plus.js +8 -2
  28. package/docs/architecture.md +15 -0
  29. package/docs/assignment-packets.md +35 -0
  30. package/docs/bmad-integration.md +187 -82
  31. package/docs/configuration.md +28 -0
  32. package/docs/evaluator-plus.md +9 -0
  33. package/docs/npx-installation.md +4 -0
  34. package/docs/operating-guide.it.md +18 -0
  35. package/docs/operating-guide.md +17 -0
  36. package/docs/quality-contract.md +119 -0
  37. package/docs/verification.md +37 -0
  38. package/integrations/bmad/README.md +17 -9
  39. package/integrations/bmad/epics-parser.mjs +697 -0
  40. package/integrations/bmad/story-importer.mjs +143 -5
  41. package/package.json +2 -2
  42. package/runtime/lib/assignment-packet.mjs +516 -0
  43. package/runtime/lib/quality-baseline.mjs +205 -0
  44. package/runtime/triad-assignment-packet.mjs +62 -0
  45. package/runtime/triad-bmad-intake.mjs +150 -0
  46. package/runtime/triad-evaluator-validate.mjs +261 -0
  47. package/runtime/triad-verify.mjs +33 -7
  48. package/schemas/evaluator-plus-result.schema.json +16 -0
  49. package/schemas/quality-baseline.schema.json +42 -0
  50. package/schemas/verification-evidence.schema.json +9 -1
  51. package/skills/triad-loop-bootstrap/SKILL.md +47 -4
  52. package/skills/triad-loop-bootstrap/assets/loop-template/handoff-report.template.md +13 -0
  53. package/skills/triad-loop-bootstrap/assets/loop-template/quality-baseline.json +15 -0
  54. package/skills/triad-loop-bootstrap/assets/loop-template/run-state.yaml +7 -0
  55. package/skills/triad-loop-bootstrap/assets/loop-template/runtime/assignments/assignment.template.json +16 -1
  56. package/skills/triad-loop-bootstrap/assets/project.yaml +5 -0
  57. package/skills/triad-loop-developer/SKILL.md +19 -8
  58. package/skills/triad-loop-evaluator/SKILL.md +12 -2
  59. package/skills/triad-loop-orchestrator/SKILL.md +134 -11
  60. package/skills/triad-loop-reviewer/SKILL.md +15 -5
@@ -0,0 +1,205 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile, realpath } from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ const SHA256 = /^[a-f0-9]{64}$/i;
6
+ const QUALITY_SCOPES = new Set(["product_quality", "delivery_closure"]);
7
+
8
+ function qualityError(code, message) {
9
+ const error = new Error(message);
10
+ error.code = code;
11
+ return error;
12
+ }
13
+
14
+ function objectLike(value) {
15
+ return value !== null && typeof value === "object" && !Array.isArray(value);
16
+ }
17
+
18
+ /**
19
+ * Return a JSON-compatible value with object keys sorted recursively. Arrays
20
+ * intentionally retain their declared order: source and criterion order is
21
+ * part of the authored contract, while object formatting is not.
22
+ */
23
+ export function canonicalizeQualityValue(value) {
24
+ if (Array.isArray(value)) return value.map(canonicalizeQualityValue);
25
+ if (!objectLike(value)) return value;
26
+ return Object.fromEntries(
27
+ Object.keys(value)
28
+ .sort()
29
+ .map((key) => [key, canonicalizeQualityValue(value[key])])
30
+ );
31
+ }
32
+
33
+ export function qualityBaselinePayload(manifest) {
34
+ if (!objectLike(manifest)) return manifest;
35
+ const { fingerprint: _fingerprint, ...payload } = manifest;
36
+ return canonicalizeQualityValue(payload);
37
+ }
38
+
39
+ export function qualityBaselineFingerprint(manifest) {
40
+ const canonical = JSON.stringify(qualityBaselinePayload(manifest));
41
+ return createHash("sha256").update(canonical).digest("hex");
42
+ }
43
+
44
+ function projectRelativePath(root, value, label) {
45
+ if (typeof value !== "string" || !value.trim() || path.isAbsolute(value)) {
46
+ throw qualityError("quality_baseline_invalid", `${label} must be a non-empty project-relative path`);
47
+ }
48
+ const normalized = value.replaceAll("\\", "/");
49
+ const posix = path.posix.normalize(normalized);
50
+ if (posix === "." || posix === ".." || posix.startsWith("../") || normalized.split("/").includes("..")) {
51
+ throw qualityError("quality_baseline_invalid", `${label} escapes the project root`);
52
+ }
53
+ const resolved = path.resolve(root, normalized);
54
+ if (!resolved.startsWith(`${root}${path.sep}`)) {
55
+ throw qualityError("quality_baseline_invalid", `${label} escapes the project root`);
56
+ }
57
+ return resolved;
58
+ }
59
+
60
+ function validateSha(value, label) {
61
+ if (typeof value !== "string" || !SHA256.test(value)) {
62
+ throw qualityError("quality_baseline_invalid", `${label} must be a SHA-256 hex digest`);
63
+ }
64
+ }
65
+
66
+ function rejectUnknownProperties(value, allowed, label) {
67
+ for (const key of Object.keys(value)) {
68
+ if (!allowed.has(key)) throw qualityError("quality_baseline_invalid", `${label} contains unknown property: ${key}`);
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Validate the manifest shape and its self-declared fingerprint without
74
+ * touching source files. This is useful to callers that only need to inspect
75
+ * the immutable contract before resolving it against a project.
76
+ */
77
+ export function validateQualityBaselineManifest(manifest) {
78
+ if (!objectLike(manifest)) throw qualityError("quality_baseline_invalid", "quality baseline must be a JSON object");
79
+ rejectUnknownProperties(manifest, new Set(["schema_version", "id", "revision", "sources", "criteria", "fingerprint"]), "quality baseline manifest");
80
+ if (manifest.schema_version !== 1) throw qualityError("quality_baseline_invalid", "quality baseline schema_version must be 1");
81
+ if (typeof manifest.id !== "string" || !manifest.id.trim()) throw qualityError("quality_baseline_invalid", "quality baseline id must be non-empty");
82
+ if (!Number.isInteger(manifest.revision) || manifest.revision < 1) throw qualityError("quality_baseline_invalid", "quality baseline revision must be a positive integer");
83
+ if (!Array.isArray(manifest.sources) || manifest.sources.length === 0) throw qualityError("quality_baseline_invalid", "quality baseline must declare at least one source");
84
+ if (!Array.isArray(manifest.criteria)) throw qualityError("quality_baseline_invalid", "quality baseline criteria must be an array");
85
+
86
+ const sourceIds = new Set();
87
+ for (const source of manifest.sources) {
88
+ if (!objectLike(source)) throw qualityError("quality_baseline_invalid", "quality baseline sources must contain objects");
89
+ rejectUnknownProperties(source, new Set(["id", "role", "path", "sha256"]), "quality baseline source");
90
+ if (typeof source.id !== "string" || !source.id.trim()) throw qualityError("quality_baseline_invalid", "quality baseline source id must be non-empty");
91
+ if (sourceIds.has(source.id)) throw qualityError("quality_baseline_invalid", `duplicate quality baseline source id: ${source.id}`);
92
+ sourceIds.add(source.id);
93
+ if (typeof source.role !== "string" || !source.role.trim()) throw qualityError("quality_baseline_invalid", `quality baseline source role is missing: ${source.id}`);
94
+ if (typeof source.path !== "string" || !source.path.trim() || path.isAbsolute(source.path)) throw qualityError("quality_baseline_invalid", `quality baseline source path must be project-relative: ${source.id}`);
95
+ const normalizedSourcePath = path.posix.normalize(source.path.replaceAll("\\", "/"));
96
+ if (normalizedSourcePath === "." || normalizedSourcePath === ".." || normalizedSourcePath.startsWith("../") || source.path.replaceAll("\\", "/").split("/").includes("..")) throw qualityError("quality_baseline_invalid", `quality baseline source path escapes the project root: ${source.id}`);
97
+ validateSha(source.sha256, `quality baseline source ${source.id}`);
98
+ }
99
+
100
+ const criterionIds = new Set();
101
+ for (const criterion of manifest.criteria) {
102
+ if (!objectLike(criterion)) throw qualityError("quality_baseline_invalid", "quality baseline criteria must contain objects");
103
+ rejectUnknownProperties(criterion, new Set(["id", "scope", "requirement"]), "quality baseline criterion");
104
+ if (typeof criterion.id !== "string" || !criterion.id.trim()) throw qualityError("quality_baseline_invalid", "quality baseline criterion id must be non-empty");
105
+ if (criterionIds.has(criterion.id)) throw qualityError("quality_baseline_invalid", `duplicate quality baseline criterion id: ${criterion.id}`);
106
+ criterionIds.add(criterion.id);
107
+ if (!QUALITY_SCOPES.has(criterion.scope)) throw qualityError("quality_baseline_invalid", `invalid quality baseline criterion scope: ${criterion.id}`);
108
+ if (typeof criterion.requirement !== "string" || !criterion.requirement.trim()) throw qualityError("quality_baseline_invalid", `quality baseline criterion requirement must be non-empty: ${criterion.id}`);
109
+ }
110
+
111
+ validateSha(manifest.fingerprint, "quality baseline fingerprint");
112
+ const calculatedFingerprint = qualityBaselineFingerprint(manifest);
113
+ if (manifest.fingerprint.toLowerCase() !== calculatedFingerprint) {
114
+ throw qualityError("quality_baseline_invalid", "quality baseline declared fingerprint does not match its canonical content");
115
+ }
116
+ return { manifest, fingerprint: calculatedFingerprint };
117
+ }
118
+
119
+ async function verifySources(manifest, root) {
120
+ const verifiedSources = [];
121
+ for (const source of manifest.sources) {
122
+ const sourcePath = projectRelativePath(root, source.path, `quality baseline source ${source.id}`);
123
+ let resolvedSource;
124
+ try {
125
+ resolvedSource = await realpath(sourcePath);
126
+ } catch {
127
+ throw qualityError("quality_baseline_invalid", `quality baseline source is missing: ${source.path}`);
128
+ }
129
+ if (!resolvedSource.startsWith(`${root}${path.sep}`)) {
130
+ throw qualityError("quality_baseline_invalid", `quality baseline source escapes the project root: ${source.path}`);
131
+ }
132
+ let content;
133
+ try {
134
+ content = await readFile(resolvedSource);
135
+ } catch {
136
+ throw qualityError("quality_baseline_invalid", `quality baseline source cannot be read: ${source.path}`);
137
+ }
138
+ const actual = createHash("sha256").update(content).digest("hex");
139
+ if (actual !== source.sha256.toLowerCase()) {
140
+ throw qualityError("quality_baseline_drift", `quality baseline source hash mismatch: ${source.path}`);
141
+ }
142
+ verifiedSources.push({ id: source.id, path: source.path, sha256: actual });
143
+ }
144
+ return verifiedSources;
145
+ }
146
+
147
+ /**
148
+ * Load, validate, fingerprint, and resolve the immutable quality contract
149
+ * against source files under the project root.
150
+ */
151
+ export async function loadQualityBaseline(manifestPath, { projectRoot = process.cwd(), expectedFingerprint = null } = {}) {
152
+ const root = await realpath(projectRoot);
153
+ const resolvedManifestPath = projectRelativePath(root, manifestPath, "quality baseline path");
154
+ let source;
155
+ try {
156
+ source = await readFile(resolvedManifestPath, "utf8");
157
+ } catch {
158
+ throw qualityError("quality_baseline_invalid", `quality baseline manifest is missing: ${manifestPath}`);
159
+ }
160
+ let manifest;
161
+ try {
162
+ manifest = JSON.parse(source);
163
+ } catch (error) {
164
+ throw qualityError("quality_baseline_invalid", `quality baseline manifest is not valid JSON: ${error.message}`);
165
+ }
166
+ const validated = validateQualityBaselineManifest(manifest);
167
+ if (expectedFingerprint !== null && expectedFingerprint !== undefined) {
168
+ validateSha(expectedFingerprint, "expected quality baseline fingerprint");
169
+ if (expectedFingerprint.toLowerCase() !== validated.fingerprint) {
170
+ throw qualityError("quality_baseline_drift", "quality baseline fingerprint does not match the assignment binding");
171
+ }
172
+ }
173
+ const sources = await verifySources(manifest, root);
174
+ return {
175
+ manifest,
176
+ path: path.relative(root, resolvedManifestPath),
177
+ fingerprint: validated.fingerprint,
178
+ sources,
179
+ criteria: manifest.criteria,
180
+ };
181
+ }
182
+
183
+ /** Resolve the optional assignment binding while preserving legacy mode. */
184
+ export async function resolveQualityContract(assignment, projectRoot) {
185
+ const manifestPath = assignment?.quality_baseline_path;
186
+ const expectedFingerprint = assignment?.expected_quality_baseline_fingerprint;
187
+ const pathPresent = typeof manifestPath === "string" && manifestPath.trim();
188
+ const fingerprintPresent = typeof expectedFingerprint === "string" && expectedFingerprint.trim();
189
+ if (!pathPresent && !fingerprintPresent) return null;
190
+ if (!pathPresent || !fingerprintPresent) {
191
+ throw qualityError("quality_baseline_invalid", "quality contract requires quality_baseline_path and expected_quality_baseline_fingerprint together");
192
+ }
193
+ return loadQualityBaseline(manifestPath, { projectRoot, expectedFingerprint });
194
+ }
195
+
196
+ export function qualityCriteriaByScope(manifestOrContract) {
197
+ const manifest = manifestOrContract?.manifest ?? manifestOrContract;
198
+ if (!objectLike(manifest) || !Array.isArray(manifest.criteria)) throw qualityError("quality_baseline_invalid", "quality baseline criteria are unavailable");
199
+ return {
200
+ product_quality: manifest.criteria.filter((criterion) => criterion.scope === "product_quality"),
201
+ delivery_closure: manifest.criteria.filter((criterion) => criterion.scope === "delivery_closure"),
202
+ };
203
+ }
204
+
205
+ export { QUALITY_SCOPES };
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { writeAssignmentPacket } from "./lib/assignment-packet.mjs";
7
+
8
+ function option(argv, name) {
9
+ const index = argv.indexOf(name);
10
+ return index >= 0 ? argv[index + 1] : null;
11
+ }
12
+
13
+ function usage() {
14
+ process.stderr.write(`Usage: node .triad-runtime/triad-assignment-packet.mjs --project <control-workspace> --assignment <assignment.json> [--output <packet.md>]\n`);
15
+ }
16
+
17
+ async function main() {
18
+ const argv = process.argv.slice(2);
19
+ if (argv.includes("--help") || argv.includes("-h")) {
20
+ usage();
21
+ return;
22
+ }
23
+ const project = option(argv, "--project") ?? process.cwd();
24
+ const assignmentOption = option(argv, "--assignment");
25
+ if (!assignmentOption) throw Object.assign(new Error("--assignment is required"), { code: "assignment_packet_invalid" });
26
+ const projectRoot = path.resolve(project);
27
+ const assignmentPath = path.resolve(projectRoot, assignmentOption);
28
+ const assignment = JSON.parse(await readFile(assignmentPath, "utf8"));
29
+ const output = option(argv, "--output");
30
+ const result = await writeAssignmentPacket(assignment, {
31
+ projectRoot,
32
+ assignmentPath,
33
+ packetPath: output ? path.resolve(projectRoot, output) : null,
34
+ });
35
+ process.stdout.write(`${JSON.stringify({
36
+ status: "ready",
37
+ assignment: {
38
+ id: result.assignment.assignment_id,
39
+ feature_id: result.assignment.feature_id,
40
+ attempt: result.assignment.attempt,
41
+ },
42
+ packet_path: result.assignment.assignment_packet_path,
43
+ packet_absolute_path: result.packetPath,
44
+ packet_sha256: result.sha256,
45
+ dispatch: {
46
+ cwd: result.context.cwd,
47
+ control_workspace: result.context.controlWorkspace,
48
+ repository: result.context.repository,
49
+ branch: result.context.branch,
50
+ card_path: result.assignment.card_path,
51
+ packet_path: result.assignment.assignment_packet_path,
52
+ mandatory_skill_paths: result.context.requiredSkills.map((skill) => skill.path),
53
+ },
54
+ })}\n`);
55
+ }
56
+
57
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
58
+ main().catch((error) => {
59
+ process.stderr.write(`${error.code ? `${error.code}: ` : ""}${error.message}\n`);
60
+ process.exitCode = 2;
61
+ });
62
+ }
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { access } from 'node:fs/promises';
4
+ import { realpathSync } from 'node:fs';
5
+ import path from 'node:path';
6
+ import { fileURLToPath, pathToFileURL } from 'node:url';
7
+
8
+ const runtimeDirectory = path.dirname(fileURLToPath(import.meta.url));
9
+
10
+ async function loadIntegration() {
11
+ const candidates = [
12
+ path.join(runtimeDirectory, 'integrations', 'bmad', 'epics-parser.mjs'),
13
+ path.resolve(runtimeDirectory, '..', 'integrations', 'bmad', 'epics-parser.mjs')
14
+ ];
15
+ for (const candidate of candidates) {
16
+ try {
17
+ await access(candidate);
18
+ return import(pathToFileURL(candidate).href);
19
+ } catch {}
20
+ }
21
+ throw new Error('BMAD native intake assets are not installed');
22
+ }
23
+
24
+ function option(argv, name) {
25
+ const index = argv.indexOf(name);
26
+ return index >= 0 ? argv[index + 1] : null;
27
+ }
28
+
29
+ function repeated(argv, name) {
30
+ const values = [];
31
+ for (let index = 0; index < argv.length; index += 1) {
32
+ if (argv[index] !== name) continue;
33
+ const value = argv[index + 1];
34
+ if (!value || value.startsWith('--')) throw new Error(`${name} requires a value`);
35
+ values.push(value);
36
+ index += 1;
37
+ }
38
+ return values;
39
+ }
40
+
41
+ function usage() {
42
+ process.stderr.write(`Native BMAD epics.md intake (integration/automation API)
43
+
44
+ Usage:
45
+ node .triad-runtime/triad-bmad-intake.mjs --source <epics.md>
46
+ node .triad-runtime/triad-bmad-intake.mjs --source <epics.md> --project <control> --output <features-dir> [--repository <id>] [--required-gate <story-id>=<gate-id>] [--depends-on <story-id>=<card-id>]
47
+
48
+ Without --output the command performs read-only ingestion and prints canonical
49
+ Stories. With --output it resolves Triad execution readiness and writes normal
50
+ Cards plus integration-side provenance. This command is an internal
51
+ deterministic primitive; the normal user entry point is the host's /triad
52
+ workflow with the epics.md path.
53
+ `);
54
+ }
55
+
56
+ function keyed(values, label) {
57
+ const result = {};
58
+ for (const value of values) {
59
+ const separator = value.indexOf('=');
60
+ if (separator <= 0 || separator === value.length - 1) throw new Error(`${label} must use STORY-ID=VALUE`);
61
+ const storyId = value.slice(0, separator).trim();
62
+ const item = value.slice(separator + 1).trim();
63
+ if (!storyId || !item) throw new Error(`${label} must use STORY-ID=VALUE`);
64
+ (result[storyId] ??= []).push(item);
65
+ }
66
+ return result;
67
+ }
68
+
69
+ async function main() {
70
+ const argv = process.argv.slice(2);
71
+ if (argv.includes('--help') || argv.includes('-h')) {
72
+ usage();
73
+ return;
74
+ }
75
+ const source = option(argv, '--source');
76
+ if (!source) throw new Error('--source is required');
77
+ const output = option(argv, '--output');
78
+ const project = option(argv, '--project');
79
+ const repository = option(argv, '--repository');
80
+ const requiredGatesByStory = keyed(repeated(argv, '--required-gate'), '--required-gate');
81
+ const dependsOnByStory = keyed(repeated(argv, '--depends-on'), '--depends-on');
82
+ const { ingestBmadEpics, writeImportedEpics } = await loadIntegration();
83
+ if (output && !project) throw new Error('--project is required when --output is supplied');
84
+ const options = {
85
+ sourcePath: source,
86
+ projectRoot: project,
87
+ targetRepository: repository,
88
+ requiredGatesByStory,
89
+ dependsOnByStory
90
+ };
91
+ if (output) {
92
+ const result = await writeImportedEpics({ ...options, outputDirectory: output });
93
+ process.stdout.write(`${JSON.stringify({
94
+ status: 'ready',
95
+ source_kind: result.source_kind,
96
+ source_path: result.source_path,
97
+ source_sha256: result.source_sha256,
98
+ epic_count: result.epics.length,
99
+ story_count: result.stories.length,
100
+ card_count: result.outputs.length,
101
+ cards: result.outputs.map(({ story, cardPath, provenancePath, provenance }) => ({
102
+ story_id: story.id,
103
+ epic_id: story.epicId,
104
+ card_path: cardPath,
105
+ provenance_path: provenancePath,
106
+ card_sha256: provenance.card_sha256
107
+ }))
108
+ })}\n`);
109
+ return;
110
+ }
111
+ const result = await ingestBmadEpics(options);
112
+ process.stdout.write(`${JSON.stringify({
113
+ status: 'ingested',
114
+ source_kind: result.source_kind,
115
+ source_path: result.source_path,
116
+ source_sha256: result.source_sha256,
117
+ epic_count: result.epics.length,
118
+ story_count: result.stories.length,
119
+ stories: result.stories.map((story) => ({
120
+ epic_id: story.epicId,
121
+ epic_title: story.epicTitle,
122
+ story_id: story.id,
123
+ title: story.title,
124
+ status: story.status,
125
+ ingestion_ready: story.ingestionReady,
126
+ execution_ready: story.executionReady,
127
+ source_heading: story.sourceHeading,
128
+ source_range: story.sourceRange
129
+ }))
130
+ })}\n`);
131
+ }
132
+
133
+ // `mktemp`/system temporary roots on macOS are commonly reached through the
134
+ // `/var` -> `/private/var` symlink. Compare canonical paths so the installed
135
+ // runtime CLI still runs when invoked from such a control workspace.
136
+ function isMainModule() {
137
+ if (!process.argv[1]) return false;
138
+ try {
139
+ return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
140
+ } catch {
141
+ return false;
142
+ }
143
+ }
144
+
145
+ if (isMainModule()) {
146
+ main().catch((error) => {
147
+ process.stderr.write(`${error.code ? `${error.code}: ` : ''}${error.message}\n`);
148
+ process.exitCode = 2;
149
+ });
150
+ }