sortie-dogs 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 zufall-upon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,200 @@
1
+ # Sortie-dogs
2
+
3
+ **Give OpenCode a task; get a bounded, validated implementation loop instead of an open-ended agent run.**
4
+
5
+ ![Sortie-dogs coordinating a bounded implementation workflow](https://raw.githubusercontent.com/zufall-upon/Sortie-dogs/main/docs/assets/sortie-workflow.gif)
6
+
7
+ Sortie-dogs is an opt-in OpenCode orchestration plugin. It turns a task into a
8
+ scoped plan, parallel investigation, dedicated implementation, canonical
9
+ validation, and evidence-backed completion—while preserving standard OpenCode
10
+ agents and settings.
11
+
12
+ Requirements: Node.js 22.6 or newer, npm, and OpenCode.
13
+
14
+ Guides: [日本語](docs/guide-ja.md) · [简体中文](docs/guide-zh-CN.md)
15
+
16
+ ## Why Sortie-dogs
17
+
18
+ - **Focused when invited, invisible otherwise.** Activate it with `/sortie` or
19
+ select `dog-coordinator`; ordinary OpenCode sessions remain unchanged.
20
+ - **Parallel context without uncontrolled fan-out.** Every worker handoff uses
21
+ exactly three bounded scouts before implementation begins.
22
+ - **Writes stay inside the assignment.** Exact source or operation manifests
23
+ gate edits and handoffs.
24
+ - **One accountable implementation path.** A dedicated Sol worker handles
25
+ implementation, remediation, and blocker resolution.
26
+ - **Evidence before completion.** Canonical validation, risk-based review, and
27
+ terminal evidence gate coordinator-owned completion and commits.
28
+ - **Long work can recover.** Restart recovery and bounded compaction continue
29
+ from retained handoff context rather than silently starting over.
30
+
31
+ ## The workflow
32
+
33
+ 1. **Brief and plan** — `dog-coordinator` turns the request into acceptance
34
+ criteria, a write manifest, and validation requirements.
35
+ 2. **Exactly three scouts** — bounded, read-only investigation collects
36
+ complementary evidence without expanding the write scope.
37
+ 3. **Dedicated worker** — the Sol worker implements only the approved manifest
38
+ and also owns scoped remediation or blocker resolution.
39
+ 4. **Canonical validation** — the declared test or build command must produce
40
+ acceptable evidence.
41
+ 5. **Risk-based review** — high-risk candidates receive independent review;
42
+ low-risk candidates can skip that extra pass after validation.
43
+ 6. **Coordinator completion** — only the coordinator closes the loop and owns
44
+ any commit after manifest, validation, review, and evidence gates pass.
45
+ 7. **Bounded continuation** — restart recovery and compaction handoffs preserve
46
+ progress; repeated batches remain bounded rather than becoming endless
47
+ delegation.
48
+
49
+ ## A visual walkthrough
50
+
51
+ ### Control complexity
52
+
53
+ ![Bounded roles and gates containing orchestration complexity](https://raw.githubusercontent.com/zufall-upon/Sortie-dogs/main/docs/assets/sortie-complexity.png)
54
+
55
+ The coordinator keeps investigation, implementation, validation, and review in
56
+ separate roles. Manifest gates keep their writes bounded even as the project
57
+ gets more complex.
58
+
59
+ ### Finish with evidence
60
+
61
+ ![Validated work reaching coordinator-owned completion](https://raw.githubusercontent.com/zufall-upon/Sortie-dogs/main/docs/assets/sortie-complete.png)
62
+
63
+ Validation and risk-based review happen before coordinator-owned completion, so
64
+ the result returns with a concise record of what changed and how it was checked.
65
+
66
+ ## Install from npm
67
+
68
+ Install the public package in the target project, then generate the
69
+ project-local runtime files:
70
+
71
+ ```sh
72
+ npm install --save-dev sortie-dogs
73
+ npx sortie-dogs init .
74
+ ```
75
+
76
+ Create `.opencode/plugins/sortie-dogs.ts` as the OpenCode plugin bridge:
77
+
78
+ ```ts
79
+ export { SortieDogsPlugin } from "sortie-dogs/plugin";
80
+ ```
81
+
82
+ OpenCode discovers the bridge automatically; no `plugin` entry in
83
+ `opencode.json` is required. Restart OpenCode, then start a task:
84
+
85
+ ```text
86
+ /sortie <task>
87
+ ```
88
+
89
+ Selecting `dog-coordinator` directly also activates the workflow.
90
+
91
+ ## Scope and session guarantees
92
+
93
+ The plugin is passive by default. It activates a session only when a message
94
+ uses `/sortie` or the selected agent is `dog-coordinator`. It validates exact
95
+ write scope through source or operation manifests and rejects invalid worker
96
+ handoffs. Standard OpenCode agents, roles, settings, and unrelated sessions are
97
+ preserved.
98
+
99
+ On `session.idle`, the final handoff is checked and the session is released. A
100
+ `session.deleted` event also releases it. A later request must activate the
101
+ workflow again.
102
+
103
+ ## Model routing
104
+
105
+ `dog-coordinator` and `dog-scout` default to `openai/gpt-5.6-luna` with the
106
+ `xhigh` variant. This is the recommended balance: bounded prompts, concise
107
+ scout evidence, and fewer unnecessary context or tool turns can reduce token
108
+ use while preserving quality. Project-local routing can override either
109
+ default.
110
+
111
+ The `implementation`, `remediation`, and `blocker-resolution` roles always use
112
+ the dedicated Sol worker; user configuration cannot replace those routes. For
113
+ other explicitly routed roles, resolution is deterministic: Sortie-dogs tries
114
+ the preferred target, then ordered fallbacks. Roles without either a built-in
115
+ default or an explicit route keep OpenCode's already selected model.
116
+
117
+ ```json
118
+ {
119
+ "modelRouting": {
120
+ "dog-coordinator": {
121
+ "preferred": { "model": "openai/gpt-5.6-luna", "variant": "xhigh" }
122
+ },
123
+ "dog-scout": {
124
+ "preferred": { "model": "openai/gpt-5.6-luna", "variant": "xhigh" }
125
+ },
126
+ "dog-advisor": {
127
+ "preferred": { "model": "fable/opus", "variant": "thinking" },
128
+ "fallback": [{ "model": "provider/general" }]
129
+ },
130
+ "dog-reviewer": {
131
+ "preferred": { "model": "fable/opus", "variant": "thinking" },
132
+ "fallback": [{ "model": "provider/general" }]
133
+ }
134
+ },
135
+ "modelCatalog": {
136
+ "project": [
137
+ { "model": "openai/gpt-5.6-luna", "variants": ["xhigh"] },
138
+ { "model": "fable/opus", "variants": ["thinking"] },
139
+ { "model": "provider/general" }
140
+ ]
141
+ }
142
+ }
143
+ ```
144
+
145
+ Save project configuration as `.opencode/sortie-dogs.json`. `modelCatalog`
146
+ declares provider models and named variants that are actually available;
147
+ Sortie-dogs does not invent, probe, or translate variants. Resolution tries the
148
+ preferred target and then its fallbacks, rejecting an explicitly routed role
149
+ when no candidate appears in the catalog. The advisor and reviewer routes above
150
+ are optional secondary examples; omit them when they are not needed.
151
+
152
+ `dog-advisor` accepts bounded Strategy or SourceReview consultation from the
153
+ coordinator. `dog-reviewer` independently checks high-risk candidates after
154
+ canonical validation. Neither role implements, stages, commits, or acts as a
155
+ user-facing worker.
156
+
157
+ ## Updates and migration
158
+
159
+ After replacing the dependency with a newer release asset, run:
160
+
161
+ ```sh
162
+ npx sortie-dogs init .
163
+ ```
164
+
165
+ `init` is idempotent. It updates files owned by Sortie-dogs, migrates recognized
166
+ older runtime files, and records the installed version in
167
+ `.opencode/sortie-dogs.version`. Conflicting or unrecognized files remain
168
+ untouched and initialization stops safely. User-owned configuration—including
169
+ `.opencode/sortie-dogs.json`—and standard OpenCode files are preserved.
170
+
171
+ ## Safe manual removal
172
+
173
+ There is no supported Sortie-dogs uninstall command. Removing the npm dependency
174
+ is a separate package-manager operation: run `npm uninstall sortie-dogs` only in
175
+ the directory whose `package.json` declares it. Never delete `package.json` or
176
+ `package-lock.json` to remove the package.
177
+
178
+ To remove generated runtime files manually, delete only these exact
179
+ Sortie-dogs-owned paths:
180
+
181
+ ```text
182
+ .opencode/agent/dog-coordinator.md
183
+ .opencode/agent/dog-worker.md
184
+ .opencode/agent/dog-scout.md
185
+ .opencode/agent/dog-reviewer.md
186
+ .opencode/agent/dog-advisor.md
187
+ .opencode/command/sortie.md
188
+ .opencode/sortie-dogs.version
189
+ ```
190
+
191
+ Never delete the `.opencode`, `.opencode/agent`, or `.opencode/command`
192
+ directories, and never use a wildcard such as `*.md`. Preserve the standard
193
+ `plan`, `build`, and `builder` agents and every other user-owned file. Do not
194
+ remove `.opencode/sortie-dogs.json`, the plugin bridge, other agents, or OpenCode
195
+ settings as part of runtime-file removal.
196
+
197
+ The legacy files `.opencode/agent/coordinator-mk2a2.md` and
198
+ `.opencode/agent/sol-worker-mk2a2.md` may be removed only after an old
199
+ Sortie-dogs marker or the content confirms Sortie-dogs ownership. If ownership
200
+ is unclear or a filename is unexpected, stop and inspect instead of deleting.
@@ -0,0 +1 @@
1
+ export declare function run(argv: readonly string[]): Promise<number>;
@@ -0,0 +1,317 @@
1
+ #!/usr/bin/env node
2
+ import { open } from "node:fs/promises";
3
+ import { resolve } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ const core = await import(`../core/validate-schema.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`);
6
+ const diagnostics = await import(`../core/diagnostics.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`);
7
+ const manifestValidator = await import(`../core/validate-manifest.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`);
8
+ const pathUtils = await import(`../core/path.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`);
9
+ const initializer = await import(`../core/initialize.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`);
10
+ const LIMITS = {
11
+ handoffBytes: 2 * 1024 * 1024,
12
+ manifestBytes: 512 * 1024,
13
+ changedPathsBytes: 1024 * 1024,
14
+ changedPaths: 10_000,
15
+ arrayItems: 10_000,
16
+ pathCharacters: 512,
17
+ jsonDepth: 32,
18
+ };
19
+ const USAGE = `Usage: sortie-dogs lint <handoff.json> [<handoff.json> ...]
20
+ [--manifest <operation-manifest.json>]
21
+ [--changed-paths-from <file|->]
22
+ [--changed-path <path> ...]
23
+ [--format text|json] [--quiet] [--strict]`;
24
+ const INIT_USAGE = "Usage: sortie-dogs init [project-root]";
25
+ class InputFailure extends Error {
26
+ safeMessage;
27
+ constructor(safeMessage) {
28
+ super(safeMessage);
29
+ this.name = "InputFailure";
30
+ this.safeMessage = safeMessage;
31
+ }
32
+ }
33
+ function parseArguments(argv) {
34
+ if (argv[0] === "--help") {
35
+ return argv.length === 1 ? { kind: "help" } : { kind: "usage" };
36
+ }
37
+ if (argv[0] !== "lint")
38
+ return { kind: "usage" };
39
+ const parsed = {
40
+ handoffs: [],
41
+ changedPaths: [],
42
+ changedPathsProvided: false,
43
+ format: "text",
44
+ quiet: false,
45
+ strict: false,
46
+ };
47
+ for (let index = 1; index < argv.length; index += 1) {
48
+ const argument = argv[index];
49
+ if (argument === "--help")
50
+ return { kind: "help" };
51
+ if (argument === "--quiet" || argument === "--strict") {
52
+ parsed[argument === "--quiet" ? "quiet" : "strict"] = true;
53
+ continue;
54
+ }
55
+ if (argument === "--manifest" || argument === "--changed-paths-from" ||
56
+ argument === "--changed-path" || argument === "--format") {
57
+ const value = argv[index + 1];
58
+ if (value === undefined || value.startsWith("--"))
59
+ return { kind: "usage" };
60
+ index += 1;
61
+ if (argument === "--manifest") {
62
+ if (parsed.manifest !== undefined)
63
+ return { kind: "usage" };
64
+ parsed.manifest = value;
65
+ }
66
+ else if (argument === "--changed-paths-from") {
67
+ if (parsed.changedPathsFrom !== undefined)
68
+ return { kind: "usage" };
69
+ parsed.changedPathsFrom = value;
70
+ parsed.changedPathsProvided = true;
71
+ }
72
+ else if (argument === "--changed-path") {
73
+ parsed.changedPaths.push(value);
74
+ parsed.changedPathsProvided = true;
75
+ }
76
+ else {
77
+ if (value !== "text" && value !== "json")
78
+ return { kind: "usage" };
79
+ parsed.format = value;
80
+ }
81
+ continue;
82
+ }
83
+ if (argument.startsWith("-"))
84
+ return { kind: "usage" };
85
+ parsed.handoffs.push(argument);
86
+ }
87
+ return parsed.handoffs.length === 0 ? { kind: "usage" } : { kind: "run", value: parsed };
88
+ }
89
+ async function readBoundedFile(file, limit, label) {
90
+ let handle;
91
+ try {
92
+ handle = await open(file, "r");
93
+ const buffer = Buffer.allocUnsafe(limit + 1);
94
+ let length = 0;
95
+ while (length <= limit) {
96
+ const result = await handle.read(buffer, length, buffer.length - length, null);
97
+ if (result.bytesRead === 0)
98
+ return buffer.subarray(0, length);
99
+ length += result.bytesRead;
100
+ }
101
+ throw new InputFailure(`${label} exceeds the size limit.`);
102
+ }
103
+ catch (error) {
104
+ if (error instanceof InputFailure)
105
+ throw error;
106
+ throw new InputFailure(`${label} could not be read.`);
107
+ }
108
+ finally {
109
+ await handle?.close().catch(() => undefined);
110
+ }
111
+ }
112
+ async function readBoundedStdin(limit) {
113
+ const chunks = [];
114
+ let length = 0;
115
+ try {
116
+ for await (const chunk of process.stdin) {
117
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
118
+ length += buffer.length;
119
+ if (length > limit)
120
+ throw new InputFailure("Changed paths input exceeds the size limit.");
121
+ chunks.push(buffer);
122
+ }
123
+ }
124
+ catch (error) {
125
+ if (error instanceof InputFailure)
126
+ throw error;
127
+ throw new InputFailure("Changed paths input could not be read.");
128
+ }
129
+ return Buffer.concat(chunks, length);
130
+ }
131
+ function checkJsonResources(value) {
132
+ const pending = [{ value, depth: 1 }];
133
+ while (pending.length > 0) {
134
+ const current = pending.pop();
135
+ const isContainer = Array.isArray(current.value) ||
136
+ (current.value !== null && typeof current.value === "object");
137
+ if (isContainer && current.depth > LIMITS.jsonDepth) {
138
+ throw new InputFailure("JSON input exceeds the nesting depth limit.");
139
+ }
140
+ if (Array.isArray(current.value)) {
141
+ if (current.value.length > LIMITS.arrayItems) {
142
+ throw new InputFailure("JSON input exceeds the array item limit.");
143
+ }
144
+ for (const item of current.value)
145
+ pending.push({ value: item, depth: current.depth + 1 });
146
+ }
147
+ else if (current.value !== null && typeof current.value === "object") {
148
+ for (const item of Object.values(current.value)) {
149
+ pending.push({ value: item, depth: current.depth + 1 });
150
+ }
151
+ }
152
+ }
153
+ }
154
+ function parseJson(buffer, label) {
155
+ let value;
156
+ try {
157
+ value = JSON.parse(buffer.toString("utf8"));
158
+ }
159
+ catch {
160
+ throw new InputFailure(`${label} is not valid JSON.`);
161
+ }
162
+ checkJsonResources(value);
163
+ return value;
164
+ }
165
+ function normalizeChangedPaths(paths) {
166
+ const normalized = new Set();
167
+ for (const path of paths) {
168
+ if (path.length > LIMITS.pathCharacters) {
169
+ throw new InputFailure("Changed paths input is invalid.");
170
+ }
171
+ try {
172
+ normalized.add(pathUtils.normalizeRelativePath(path));
173
+ }
174
+ catch (error) {
175
+ if (!(error instanceof pathUtils.RelativePathError))
176
+ throw error;
177
+ throw new InputFailure("Changed paths input is invalid.");
178
+ }
179
+ if (normalized.size > LIMITS.changedPaths) {
180
+ throw new InputFailure("Changed paths input exceeds the count limit.");
181
+ }
182
+ }
183
+ return [...normalized];
184
+ }
185
+ function changedPathLines(buffer) {
186
+ return buffer.toString("utf8").split(/\r?\n/u).filter((line) => line.length > 0);
187
+ }
188
+ function safeSchemaPointer(diagnostic) {
189
+ if (diagnostic.code !== "schema_additionalProperties")
190
+ return diagnostic.pointer;
191
+ const slash = diagnostic.pointer.lastIndexOf("/");
192
+ return `${diagnostic.pointer.slice(0, Math.max(0, slash))}/@unknown`;
193
+ }
194
+ function associate(file, input) {
195
+ return input.map((diagnostic) => ({
196
+ file,
197
+ code: diagnostic.code,
198
+ severity: diagnostic.severity,
199
+ pointer: diagnostic.code.startsWith("schema_")
200
+ ? safeSchemaPointer(diagnostic)
201
+ : diagnostic.pointer,
202
+ message: diagnostic.message,
203
+ }));
204
+ }
205
+ function neutralizeTextControlCharacters(value) {
206
+ return [...value].map((character) => {
207
+ const codePoint = character.codePointAt(0);
208
+ if (codePoint === 0x0a)
209
+ return "\\n";
210
+ if (codePoint === 0x0d)
211
+ return "\\r";
212
+ if (codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f)) {
213
+ return `\\u${codePoint.toString(16).padStart(4, "0")}`;
214
+ }
215
+ return character;
216
+ }).join("");
217
+ }
218
+ function render(output, format) {
219
+ if (format === "json")
220
+ return `${JSON.stringify(output)}\n`;
221
+ return output.map(({ file, pointer, code, severity, message }) => `${file} ${neutralizeTextControlCharacters(pointer || "/")} ${code} ${severity} ${neutralizeTextControlCharacters(message)}\n`).join("");
222
+ }
223
+ export async function run(argv) {
224
+ if (argv[0] === "init") {
225
+ if (argv[1] === "--help" && argv.length === 2) {
226
+ process.stdout.write(`${INIT_USAGE}\n`);
227
+ return 0;
228
+ }
229
+ if (argv.length > 2 || argv[1]?.startsWith("-") === true) {
230
+ process.stderr.write(`${INIT_USAGE}\n`);
231
+ return 2;
232
+ }
233
+ try {
234
+ const initialized = await initializer.initializeProject(argv[1]);
235
+ process.stdout.write(initialized.status === "installed"
236
+ ? `Initialized Sortie-dogs ${initialized.version}.\n`
237
+ : `Sortie-dogs ${initialized.version} is already initialized.\n`);
238
+ return 0;
239
+ }
240
+ catch (error) {
241
+ process.stderr.write(error instanceof initializer.ProjectInitializationError
242
+ ? `${error.message}\n`
243
+ : "Initialization failed.\n");
244
+ return 2;
245
+ }
246
+ }
247
+ const result = parseArguments(argv);
248
+ if (result.kind === "help") {
249
+ process.stdout.write(`${USAGE}\n`);
250
+ return 0;
251
+ }
252
+ if (result.kind === "usage") {
253
+ process.stderr.write(`${USAGE}\n`);
254
+ return 2;
255
+ }
256
+ const options = result.value;
257
+ const output = [];
258
+ const failures = [];
259
+ let manifest;
260
+ let changedPaths;
261
+ let changedPathsResolved = true;
262
+ if (options.manifest !== undefined) {
263
+ try {
264
+ const value = parseJson(await readBoundedFile(options.manifest, LIMITS.manifestBytes, "Manifest input"), "Manifest input");
265
+ const validation = core.validateOperationManifestSchema(value);
266
+ if (validation.ok)
267
+ manifest = validation.value;
268
+ else
269
+ output.push(...associate("manifest", validation.diagnostics));
270
+ }
271
+ catch (error) {
272
+ failures.push(error instanceof InputFailure ? error.safeMessage : "Manifest input could not be processed.");
273
+ }
274
+ }
275
+ try {
276
+ const fromFile = options.changedPathsFrom === undefined
277
+ ? []
278
+ : changedPathLines(options.changedPathsFrom === "-"
279
+ ? await readBoundedStdin(LIMITS.changedPathsBytes)
280
+ : await readBoundedFile(options.changedPathsFrom, LIMITS.changedPathsBytes, "Changed paths input"));
281
+ changedPaths = normalizeChangedPaths([...fromFile, ...options.changedPaths]);
282
+ }
283
+ catch (error) {
284
+ changedPathsResolved = false;
285
+ failures.push(error instanceof InputFailure ? error.safeMessage : "Changed paths input could not be processed.");
286
+ }
287
+ for (let index = 0; index < options.handoffs.length; index += 1) {
288
+ const file = `handoff[${index}]`;
289
+ try {
290
+ const value = parseJson(await readBoundedFile(options.handoffs[index], LIMITS.handoffBytes, "Handoff input"), "Handoff input");
291
+ const validation = core.validateHandoffSchema(value);
292
+ if (!validation.ok) {
293
+ output.push(...associate(file, validation.diagnostics));
294
+ continue;
295
+ }
296
+ output.push(...associate(file, diagnostics.lint(validation.value).diagnostics));
297
+ if (manifest !== undefined && changedPathsResolved) {
298
+ output.push(...associate(file, manifestValidator.validateManifest(validation.value, manifest, changedPaths, options.changedPathsProvided)));
299
+ }
300
+ }
301
+ catch (error) {
302
+ failures.push(error instanceof InputFailure ? error.safeMessage : "Handoff input could not be processed.");
303
+ }
304
+ }
305
+ if (!options.quiet)
306
+ process.stdout.write(render(output, options.format));
307
+ for (const failure of failures)
308
+ process.stderr.write(`${failure}\n`);
309
+ if (failures.length > 0)
310
+ return 2;
311
+ const hasFailure = output.some(({ severity }) => severity === "error" ||
312
+ (options.strict && severity === "warning"));
313
+ return hasFailure ? 1 : 0;
314
+ }
315
+ if (process.argv[1] !== undefined && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) {
316
+ process.exitCode = await run(process.argv.slice(2));
317
+ }
@@ -0,0 +1,2 @@
1
+ import type { Handoff, LintOptions, LintResult } from "./types.js";
2
+ export declare function lint(handoff: Handoff, options?: LintOptions): LintResult;
@@ -0,0 +1,65 @@
1
+ const validateSemantics = await import(`./validate-semantics.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`);
2
+ const DEFAULT_SEVERITIES = {
3
+ H001: "error",
4
+ H002: "error",
5
+ H003: "error",
6
+ H004: "error",
7
+ H005: "error",
8
+ H006: "error",
9
+ H007: "error",
10
+ H008: "error",
11
+ H009: "warning",
12
+ H010: "error",
13
+ };
14
+ function compareText(left, right) {
15
+ return left < right ? -1 : left > right ? 1 : 0;
16
+ }
17
+ function comparePointer(left, right) {
18
+ const leftSegments = left.split("/");
19
+ const rightSegments = right.split("/");
20
+ const length = Math.min(leftSegments.length, rightSegments.length);
21
+ for (let index = 0; index < length; index += 1) {
22
+ const leftSegment = leftSegments[index];
23
+ const rightSegment = rightSegments[index];
24
+ if (leftSegment === rightSegment)
25
+ continue;
26
+ if (/^\d+$/.test(leftSegment) && /^\d+$/.test(rightSegment)) {
27
+ const numericOrder = BigInt(leftSegment) < BigInt(rightSegment) ? -1 : BigInt(leftSegment) > BigInt(rightSegment) ? 1 : 0;
28
+ if (numericOrder !== 0)
29
+ return numericOrder;
30
+ }
31
+ return compareText(leftSegment, rightSegment);
32
+ }
33
+ return leftSegments.length - rightSegments.length;
34
+ }
35
+ function compareDiagnostics(left, right) {
36
+ return (comparePointer(left.pointer, right.pointer) ||
37
+ compareText(left.code, right.code));
38
+ }
39
+ function createLintResult(issues, profile, options = {}) {
40
+ const enabledCodes = options.codes === undefined ? undefined : new Set(options.codes);
41
+ const severityOverrides = options.severity;
42
+ const diagnostics = issues
43
+ .filter((issue) => enabledCodes === undefined || enabledCodes.has(issue.code))
44
+ .map((issue) => ({
45
+ code: issue.code,
46
+ severity: severityOverrides?.[issue.code] ??
47
+ (issue.code === "H006" && profile === "minimal"
48
+ ? "warning"
49
+ : DEFAULT_SEVERITIES[issue.code]),
50
+ pointer: issue.path,
51
+ message: issue.message,
52
+ }))
53
+ .sort(compareDiagnostics);
54
+ const counts = { error: 0, warning: 0, info: 0 };
55
+ for (const diagnostic of diagnostics)
56
+ counts[diagnostic.severity] += 1;
57
+ return {
58
+ diagnostics,
59
+ counts,
60
+ ok: counts.error === 0,
61
+ };
62
+ }
63
+ export function lint(handoff, options) {
64
+ return createLintResult(validateSemantics.lintHandoff(handoff), handoff.profile, options);
65
+ }
@@ -0,0 +1,14 @@
1
+ export type InitializationStatus = "installed" | "unchanged";
2
+ export interface InitializeProjectResult {
3
+ readonly status: InitializationStatus;
4
+ readonly version: string;
5
+ readonly installedPaths: readonly string[];
6
+ readonly preservedLegacyPaths: readonly string[];
7
+ }
8
+ export type ProjectInitializationErrorCode = "conflict" | "incompatible-version" | "invalid-project" | "unsafe-path" | "write-failed";
9
+ export declare class ProjectInitializationError extends Error {
10
+ readonly code: ProjectInitializationErrorCode;
11
+ constructor(code: ProjectInitializationErrorCode, message: string, options?: ErrorOptions);
12
+ }
13
+ /** Installs the packaged runtime into one existing project without changing user settings. */
14
+ export declare function initializeProject(projectRoot?: string): Promise<InitializeProjectResult>;