gh-inari 0.9.0 → 0.10.2

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.
@@ -0,0 +1,1463 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { open } from "node:fs/promises";
3
+ import { readFileSync } from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { ArtifactInputError, ArtifactPreparationError, loadCanonicalArtifact, parseArtifactInputDocument, prepareIssueArtifact, preparePullRequestArtifact, projectExistingArtifact, renderIssueArtifact, renderPullRequestArtifact, } from "./artifact.js";
7
+ import { effectiveFieldConstraints, projectContract, SemanticValidationError, } from "./contract/index.js";
8
+ import { GitHubAdapter, isGitHubAdapterError } from "./github/index.js";
9
+ import { assertPullRequestSyncInputComplete, parsePullRequestSyncInput, projectPullRequestSyncInput, renderPullRequestSyncInputHelp, } from "./pr-sync-input.js";
10
+ import { compileLocalGovernedContract, compileRepositoryGovernedContract, createGovernedIssue, createGovernedPullRequest, discoverRepositoryTemplates, rejectGovernedPolicyOverride, } from "./governance.js";
11
+ import { discoverTemplates } from "./template-discovery.js";
12
+ import { applySemanticPatch, assessExistingArtifact, currentArtifactInput, diffArtifact, prepareRemediationArtifact, prepareSyncInput, remediationDiagnosticReport, remediationFailureDetails, readGovernedExistingArtifact, RemediationError, translateRemediationFailure, updateGovernedExistingArtifact, } from "./reconciliation.js";
13
+ import { discoverSemanticTemplates, importNativeTemplate, renderSemanticCompactSchema, syncSemanticTemplates, SEMANTIC_ISSUE_DIRECTORY, SEMANTIC_PULL_REQUEST_FILE, SEMANTIC_TEMPLATE_DIRECTORY, } from "./semantic-template.js";
14
+ import { findSkillScenario, MAX_SKILL_OUTPUT_BYTES, projectSkillIndexToJson, projectSkillIndexToText, projectSkillScenarioToJson, projectSkillScenarioToText, SKILL_SCENARIOS, } from "./skill.js";
15
+ import { AGENT_INVOCATION_CONTRACT, COMMAND_CONTRACT_VERSION, COMMAND_OPTIONS, INARI_COMMANDS, RUNTIME_CAPABILITIES, commandExample, commandInvocation, commandRecoveryInvocation, commandTemplateSchemaInvocation, commandUsage, getCommandForPositionals, getDomainCommands, getOption, optionSyntax, projectCommandHelp, tokenizeCommandArgv, } from "./command-contract.js";
16
+ const EXIT_USAGE = 1;
17
+ const EXIT_VALIDATION = 2;
18
+ const EXIT_REMOTE = 3;
19
+ const EXIT_INTERNAL = 4;
20
+ const DIAGNOSTIC_PROTOCOL_VERSION = 1;
21
+ const { extensionInstall: INSTALL_COMMAND, extensionUpdate: UPDATE_COMMAND, fallback: FALLBACK_COMMAND, } = AGENT_INVOCATION_CONTRACT;
22
+ const CANONICAL_INVOCATION = AGENT_INVOCATION_CONTRACT.canonical;
23
+ const BOOLEAN_OPTIONS = new Set([
24
+ "help",
25
+ "json",
26
+ "version",
27
+ "diagnose",
28
+ "doctor",
29
+ "draft",
30
+ "maintainerCanModify",
31
+ "compact",
32
+ "check",
33
+ "dryRun",
34
+ ]);
35
+ const VALUE_OPTIONS = new Set([
36
+ "from",
37
+ "template",
38
+ "policy",
39
+ "repository",
40
+ "title",
41
+ "head",
42
+ "base",
43
+ "to",
44
+ "requireCapability",
45
+ "minimumVersion",
46
+ ]);
47
+ const METADATA_OPTION_KEYS = ["title", "head", "base", "draft", "maintainerCanModify"];
48
+ /** The installed gh-inari executable entrypoint. */
49
+ export async function runCli(argv, dependencies = {}) {
50
+ const metadata = dependencies.packageMetadata ?? readPackageMetadata();
51
+ if (!isOwnedInvocation(argv))
52
+ return runGhFallback(argv, dependencies);
53
+ let parsed;
54
+ try {
55
+ parsed = parseArguments(argv);
56
+ }
57
+ catch (error) {
58
+ const reportedError = intentAwareCreateOptionError(argv, error) ?? error;
59
+ const shape = toErrorShape(reportedError);
60
+ const json = argv.some((token) => token === "--json" || token === "--json=true");
61
+ if (json)
62
+ console.log(JSON.stringify({ ok: false, error: shape }));
63
+ else if (reportedError instanceof CliError && reportedError.code === "GOVERNED_CREATE_OPTION")
64
+ console.error(`${shape.code}: ${shape.message}`);
65
+ else if (isMachineCommandTokens(argv))
66
+ console.log(JSON.stringify({ ok: false, error: shape }));
67
+ else
68
+ console.error(`${shape.code}: ${shape.message}`);
69
+ return classifyExitCode(reportedError);
70
+ }
71
+ const diagnosticRequested = parsed.options.diagnose === true ||
72
+ parsed.options.doctor === true ||
73
+ parsed.positionals[0] === "diagnose" ||
74
+ parsed.positionals[0] === "doctor";
75
+ const versionRequested = parsed.options.version === true || parsed.positionals[0] === "version";
76
+ const helpRequested = parsed.options.help !== undefined && parsed.options.help !== false;
77
+ if (helpRequested || (parsed.positionals.length === 0 && !versionRequested && !diagnosticRequested)) {
78
+ printHelpFor(parsed.positionals, parsed.options.help);
79
+ return parsed.positionals.length === 0 && !helpRequested ? EXIT_USAGE : 0;
80
+ }
81
+ const json = parsed.options.json === true;
82
+ try {
83
+ if (versionRequested)
84
+ return runVersion(metadata, parsed.options, json);
85
+ if (diagnosticRequested)
86
+ return runDiagnostic(metadata, parsed.options, json, dependencies);
87
+ const root = path.resolve(dependencies.repositoryRoot ?? process.cwd());
88
+ const [domain, command, ...rest] = parsed.positionals;
89
+ if (parsed.fields.length > 0 && !isFieldCapableCommand(domain, command)) {
90
+ throw fieldUnsupportedCommandError(parsed.positionals);
91
+ }
92
+ if (domain === "template" && command === "list") {
93
+ return await runTemplateList(root, parsed.options.repository, dependencies);
94
+ }
95
+ if (domain === "template" && command === "sync") {
96
+ return await runTemplateSync(root, parsed.options.check === true);
97
+ }
98
+ if (domain === "template" && command === "import") {
99
+ return await runTemplateImport(root, rest, parsed, json);
100
+ }
101
+ if (domain === "issue" || domain === "pr") {
102
+ return await runArtifactCommand(domain, command, rest, parsed, root, dependencies, json);
103
+ }
104
+ if (domain === "skill") {
105
+ return runSkillCommand(command, json);
106
+ }
107
+ throw new CliError("UNKNOWN_COMMAND", `Unknown command "${parsed.positionals.join(" ")}".`);
108
+ }
109
+ catch (error) {
110
+ const shape = toErrorShape(error);
111
+ if (json || isMachineCommand(parsed.positionals))
112
+ console.log(JSON.stringify({ ok: false, error: shape }));
113
+ else
114
+ console.error(`${shape.code}: ${shape.message}`);
115
+ return classifyExitCode(error);
116
+ }
117
+ }
118
+ function runVersion(metadata, options, json) {
119
+ const info = runtimeInfo(metadata);
120
+ const requirements = runtimeRequirements(options, false);
121
+ const missingCapabilities = requirements.capabilities.filter((capability) => !info.capabilities.includes(capability));
122
+ const versionSupported = requirements.minimumVersion === undefined || versionAtLeast(info.version, requirements.minimumVersion);
123
+ const ok = missingCapabilities.length === 0 && versionSupported;
124
+ if (json) {
125
+ console.log(JSON.stringify({
126
+ ok,
127
+ ...info,
128
+ ...(ok
129
+ ? {}
130
+ : {
131
+ error: {
132
+ code: "RUNTIME_REQUIREMENT_UNMET",
133
+ message: runtimeRequirementMessage(info, missingCapabilities, requirements.minimumVersion),
134
+ ...(missingCapabilities.length === 0 ? {} : { missingCapabilities }),
135
+ ...(requirements.minimumVersion === undefined ? {} : { minimumVersion: requirements.minimumVersion }),
136
+ recovery: FALLBACK_COMMAND,
137
+ },
138
+ }),
139
+ }));
140
+ }
141
+ else {
142
+ console.log(`${metadata.name} ${metadata.version}`);
143
+ if (!ok)
144
+ console.error(`gh-inari: ${runtimeRequirementMessage(info, missingCapabilities, requirements.minimumVersion)}`);
145
+ }
146
+ return ok ? 0 : EXIT_VALIDATION;
147
+ }
148
+ function runDiagnostic(metadata, options, json, dependencies) {
149
+ const info = runtimeInfo(metadata);
150
+ const requirements = runtimeRequirements(options, true);
151
+ const canonical = diagnoseCanonicalRuntime(info, requirements);
152
+ const compatibility = probeCompatibilityExtension(requirements, dependencies.runDiagnosticCommand);
153
+ const ok = canonical.status === "ready";
154
+ const output = {
155
+ ok,
156
+ ...info,
157
+ requiredCapabilities: requirements.capabilities,
158
+ ...(requirements.minimumVersion === undefined ? {} : { minimumVersion: requirements.minimumVersion }),
159
+ canonical: projectRuntimeDiagnostic(CANONICAL_INVOCATION, canonical),
160
+ compatibility: projectRuntimeDiagnostic(AGENT_INVOCATION_CONTRACT.compatibility, compatibility, "extension"),
161
+ };
162
+ if (json)
163
+ console.log(JSON.stringify(output));
164
+ else {
165
+ console.log(`${metadata.name} ${metadata.version}`);
166
+ if (ok)
167
+ console.log(`${CANONICAL_INVOCATION}: ready (${canonical.version ?? "unknown version"})`);
168
+ else {
169
+ console.error(`${CANONICAL_INVOCATION}: ${runtimeDiagnosticMessage(canonical, "canonical runtime")}`);
170
+ console.error(`Action: ${canonical.recovery}`);
171
+ }
172
+ if (compatibility.status !== "ready") {
173
+ console.error(`${AGENT_INVOCATION_CONTRACT.compatibility} (compatibility): ${runtimeDiagnosticMessage(compatibility, "extension")}`);
174
+ console.error(`Action: ${compatibility.recovery}`);
175
+ }
176
+ }
177
+ return ok ? 0 : EXIT_VALIDATION;
178
+ }
179
+ function runtimeInfo(metadata) {
180
+ return {
181
+ name: metadata.name,
182
+ version: metadata.version,
183
+ protocol: DIAGNOSTIC_PROTOCOL_VERSION,
184
+ commandContractVersion: COMMAND_CONTRACT_VERSION,
185
+ capabilities: [...RUNTIME_CAPABILITIES],
186
+ invocation: {
187
+ canonical: CANONICAL_INVOCATION,
188
+ compatibility: AGENT_INVOCATION_CONTRACT.compatibility,
189
+ direct: AGENT_INVOCATION_CONTRACT.direct,
190
+ fallback: FALLBACK_COMMAND,
191
+ },
192
+ };
193
+ }
194
+ function runtimeRequirements(options, defaultCapabilities) {
195
+ const requestedCapability = options.requireCapability;
196
+ const capabilities = typeof requestedCapability === "string"
197
+ ? [requestedCapability]
198
+ : defaultCapabilities
199
+ ? [...RUNTIME_CAPABILITIES]
200
+ : [];
201
+ const requestedMinimum = options.minimumVersion;
202
+ if (requestedMinimum !== undefined && typeof requestedMinimum !== "string")
203
+ throw new CliError("INVALID_OPTION", "Option --minimum-version requires a version value.", "--minimum-version");
204
+ if (typeof requestedMinimum === "string" && parseVersion(requestedMinimum) === undefined)
205
+ throw new CliError("INVALID_OPTION", `Option --minimum-version must be a semantic version (received "${requestedMinimum}").`, "--minimum-version");
206
+ return {
207
+ capabilities,
208
+ ...(typeof requestedMinimum === "string" ? { minimumVersion: requestedMinimum } : {}),
209
+ };
210
+ }
211
+ function diagnoseCanonicalRuntime(info, requirements) {
212
+ const missingCapabilities = requirements.capabilities.filter((capability) => !info.capabilities.includes(capability));
213
+ if (missingCapabilities.length > 0 ||
214
+ (requirements.minimumVersion !== undefined && !versionAtLeast(info.version, requirements.minimumVersion))) {
215
+ return {
216
+ status: "stale",
217
+ version: info.version,
218
+ capabilities: info.capabilities,
219
+ ...(missingCapabilities.length === 0 ? {} : { missingCapabilities }),
220
+ detail: runtimeRequirementMessage(info, missingCapabilities, requirements.minimumVersion),
221
+ recovery: FALLBACK_COMMAND,
222
+ };
223
+ }
224
+ return { status: "ready", version: info.version, capabilities: info.capabilities, recovery: FALLBACK_COMMAND };
225
+ }
226
+ function probeCompatibilityExtension(requirements, runCommand) {
227
+ const execute = runCommand ?? runGhDiagnosticCommand;
228
+ const list = execute(["extension", "list"]);
229
+ if (list.status !== 0) {
230
+ return {
231
+ status: "unavailable",
232
+ detail: diagnosticProcessDetail(list),
233
+ recovery: FALLBACK_COMMAND,
234
+ };
235
+ }
236
+ if (!hasInariExtension(list.stdout))
237
+ return { status: "missing", recovery: INSTALL_COMMAND };
238
+ const version = execute(["inari", "--version", "--json"]);
239
+ if (version.status !== 0) {
240
+ return {
241
+ status: "stale",
242
+ detail: diagnosticProcessDetail(version),
243
+ recovery: UPDATE_COMMAND,
244
+ };
245
+ }
246
+ let parsed;
247
+ try {
248
+ parsed = JSON.parse(version.stdout.trim());
249
+ }
250
+ catch {
251
+ return {
252
+ status: "stale",
253
+ detail: "the installed extension does not support machine-readable version output",
254
+ recovery: UPDATE_COMMAND,
255
+ };
256
+ }
257
+ if (!isRuntimeInfo(parsed)) {
258
+ return {
259
+ status: "stale",
260
+ detail: "the installed extension returned an incompatible version contract",
261
+ recovery: UPDATE_COMMAND,
262
+ };
263
+ }
264
+ if (parsed.protocol !== DIAGNOSTIC_PROTOCOL_VERSION) {
265
+ return {
266
+ status: "stale",
267
+ version: parsed.version,
268
+ capabilities: parsed.capabilities,
269
+ detail: `the installed extension uses diagnostic protocol ${parsed.protocol}; expected ${DIAGNOSTIC_PROTOCOL_VERSION}`,
270
+ recovery: UPDATE_COMMAND,
271
+ };
272
+ }
273
+ if (parsed.invocation.canonical !== CANONICAL_INVOCATION) {
274
+ return {
275
+ status: "stale",
276
+ version: parsed.version,
277
+ capabilities: parsed.capabilities,
278
+ detail: `the installed extension reports "${parsed.invocation.canonical}" as canonical; expected "${CANONICAL_INVOCATION}"`,
279
+ recovery: UPDATE_COMMAND,
280
+ };
281
+ }
282
+ if (parsed.commandContractVersion !== COMMAND_CONTRACT_VERSION) {
283
+ return {
284
+ status: "stale",
285
+ version: parsed.version,
286
+ capabilities: parsed.capabilities,
287
+ detail: `the installed extension uses command contract ${parsed.commandContractVersion ?? "unknown"}; expected ${COMMAND_CONTRACT_VERSION}`,
288
+ recovery: UPDATE_COMMAND,
289
+ };
290
+ }
291
+ const missingCapabilities = requirements.capabilities.filter((capability) => !parsed.capabilities.includes(capability));
292
+ if (missingCapabilities.length > 0 ||
293
+ (requirements.minimumVersion !== undefined && !versionAtLeast(parsed.version, requirements.minimumVersion))) {
294
+ return {
295
+ status: "stale",
296
+ version: parsed.version,
297
+ capabilities: parsed.capabilities,
298
+ ...(missingCapabilities.length === 0 ? {} : { missingCapabilities }),
299
+ detail: runtimeRequirementMessage(parsed, missingCapabilities, requirements.minimumVersion),
300
+ recovery: UPDATE_COMMAND,
301
+ };
302
+ }
303
+ return { status: "ready", version: parsed.version, capabilities: parsed.capabilities, recovery: UPDATE_COMMAND };
304
+ }
305
+ function runGhDiagnosticCommand(args) {
306
+ try {
307
+ const result = spawnSync("gh", [...args], {
308
+ encoding: "utf8",
309
+ maxBuffer: 64 * 1024,
310
+ timeout: 3_000,
311
+ });
312
+ return {
313
+ status: result.status,
314
+ stdout: result.stdout ?? "",
315
+ stderr: result.stderr ?? "",
316
+ ...(result.error === undefined ? {} : { error: result.error.message }),
317
+ };
318
+ }
319
+ catch (error) {
320
+ return {
321
+ status: null,
322
+ stdout: "",
323
+ stderr: "",
324
+ error: error instanceof Error ? error.message : "unable to execute gh",
325
+ };
326
+ }
327
+ }
328
+ /** Delegates argv gh-inari does not own to the real `gh` binary, so `gh inari` is a strict superset of `gh`. */
329
+ function runGhFallback(argv, dependencies) {
330
+ const execute = dependencies.runGhFallback ?? runGhPassthroughCommand;
331
+ return execute(argv);
332
+ }
333
+ function runGhPassthroughCommand(argv) {
334
+ const result = spawnSync("gh", [...argv], { stdio: "inherit" });
335
+ if (result.error)
336
+ throw new CliError("GH_FALLBACK_FAILED", `Cannot execute gh: ${result.error.message}.`);
337
+ return result.status ?? EXIT_INTERNAL;
338
+ }
339
+ function hasInariExtension(output) {
340
+ return output.split(/\r?\n/u).some((line) => /^\s*gh\s+inari(?:\s|$)/u.test(line));
341
+ }
342
+ function isRuntimeInfo(value) {
343
+ if (typeof value !== "object" || value === null)
344
+ return false;
345
+ const candidate = value;
346
+ const invocation = candidate.invocation;
347
+ return (candidate.ok !== false &&
348
+ typeof candidate.name === "string" &&
349
+ candidate.name === "gh-inari" &&
350
+ typeof candidate.version === "string" &&
351
+ typeof candidate.protocol === "number" &&
352
+ (candidate.commandContractVersion === undefined || typeof candidate.commandContractVersion === "string") &&
353
+ Array.isArray(candidate.capabilities) &&
354
+ candidate.capabilities.every((capability) => typeof capability === "string") &&
355
+ typeof invocation === "object" &&
356
+ invocation !== null &&
357
+ typeof invocation.canonical === "string" &&
358
+ typeof invocation.direct === "string" &&
359
+ typeof invocation.fallback === "string");
360
+ }
361
+ function runtimeRequirementMessage(info, missingCapabilities, minimumVersion) {
362
+ const requirements = [];
363
+ if (missingCapabilities.length > 0)
364
+ requirements.push(`missing capability ${missingCapabilities.map((value) => `"${value}"`).join(", ")}`);
365
+ if (minimumVersion !== undefined && !versionAtLeast(info.version, minimumVersion))
366
+ requirements.push(`version ${info.version} is older than required ${minimumVersion}`);
367
+ return requirements.length === 0 ? "runtime requirements are not satisfied" : requirements.join("; ");
368
+ }
369
+ function projectRuntimeDiagnostic(invocation, diagnostic, kind) {
370
+ return {
371
+ invocation,
372
+ ...(kind === undefined ? {} : { kind }),
373
+ status: diagnostic.status,
374
+ ...(diagnostic.version === undefined ? {} : { version: diagnostic.version }),
375
+ ...(diagnostic.capabilities === undefined ? {} : { capabilities: diagnostic.capabilities }),
376
+ ...(diagnostic.missingCapabilities === undefined ? {} : { missingCapabilities: diagnostic.missingCapabilities }),
377
+ ...(diagnostic.detail === undefined ? {} : { detail: diagnostic.detail }),
378
+ recovery: diagnostic.recovery,
379
+ };
380
+ }
381
+ function runtimeDiagnosticMessage(diagnostic, subject) {
382
+ if (diagnostic.status === "missing")
383
+ return `the ${subject} is not installed`;
384
+ if (diagnostic.status === "unavailable")
385
+ return diagnostic.detail ?? "the GitHub CLI could not be executed";
386
+ if (diagnostic.status === "stale")
387
+ return diagnostic.detail ?? `the ${subject} is stale`;
388
+ return `the ${subject} is ready`;
389
+ }
390
+ function diagnosticProcessDetail(result) {
391
+ const detail = (result.error ?? result.stderr ?? "").trim().split(/\r?\n/u)[0];
392
+ return detail === "" ? "the GitHub CLI command failed" : detail.slice(0, 240);
393
+ }
394
+ function parseVersion(value) {
395
+ const match = /^(?:v)?(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.exec(value);
396
+ if (match === null)
397
+ return undefined;
398
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
399
+ }
400
+ function versionAtLeast(actual, minimum) {
401
+ const actualParts = parseVersion(actual);
402
+ const minimumParts = parseVersion(minimum);
403
+ if (actualParts === undefined || minimumParts === undefined)
404
+ return false;
405
+ for (let index = 0; index < actualParts.length; index += 1) {
406
+ if (actualParts[index] !== minimumParts[index])
407
+ return actualParts[index] > minimumParts[index];
408
+ }
409
+ return true;
410
+ }
411
+ class CliError extends Error {
412
+ code;
413
+ path;
414
+ details;
415
+ constructor(code, message, path, details) {
416
+ super(message);
417
+ this.name = "CliError";
418
+ this.code = code;
419
+ this.path = path;
420
+ this.details = details;
421
+ }
422
+ }
423
+ const CREATE_RECOVERY_ACTIONS = 3;
424
+ /**
425
+ * Recognized gh-compatible create guidance is intentionally narrow. In
426
+ * particular, the body value is never parsed, echoed, or accepted as an
427
+ * alternate governed input path.
428
+ */
429
+ function intentAwareCreateOptionError(argv, error) {
430
+ if (!(error instanceof CliError) ||
431
+ error.code !== "INVALID_OPTION" ||
432
+ !getOption("rawBody").aliases.some((option) => error.message === `Unknown option ${option}.`))
433
+ return undefined;
434
+ const domain = governedCreateDomain(argv);
435
+ const option = findGovernedCreateOption(argv);
436
+ if (domain === undefined || option === undefined || error.message !== `Unknown option ${option}.`)
437
+ return undefined;
438
+ const recovery = createRecoveryActions(domain);
439
+ return new CliError("GOVERNED_CREATE_OPTION", `Option ${option} is a gh-compatible raw Markdown input, but governed ${domain} creation requires Inari's canonical structured input. ` +
440
+ `Use ${recovery[0]?.command}, then ${recovery[1]?.command}, and create with ${recovery[2]?.command}.`, "$argv", {
441
+ option,
442
+ domain,
443
+ operation: "create",
444
+ recovery,
445
+ });
446
+ }
447
+ function createRecoveryActions(domain) {
448
+ const actions = [
449
+ { action: "discover-template", command: commandInvocation("template.list") },
450
+ { action: "inspect-schema", command: commandTemplateSchemaInvocation(domain) },
451
+ {
452
+ action: "create",
453
+ command: commandRecoveryInvocation(`${domain}.create`),
454
+ },
455
+ ];
456
+ return actions.slice(0, CREATE_RECOVERY_ACTIONS);
457
+ }
458
+ function findGovernedCreateOption(argv) {
459
+ return tokenizeCommandArgv(argv).options.find((occurrence) => occurrence.definition?.id === "rawBody")?.rawName;
460
+ }
461
+ /** Locate only the governed domain/create positionals; option values are never treated as commands. */
462
+ function governedCreateDomain(argv) {
463
+ const { positionals } = tokenizeCommandArgv(argv);
464
+ const domain = positionals[0];
465
+ return (domain === "issue" || domain === "pr") && positionals[1] === "create" ? domain : undefined;
466
+ }
467
+ /** Bound for local --from <file> and stdin artifact input, independent of semantic field constraints. */
468
+ const MAX_INPUT_BYTES = 1_048_576;
469
+ function inputTooLargeError(observedBytes) {
470
+ return new CliError("INPUT_TOO_LARGE", `Input exceeds the maximum allowed size of ${MAX_INPUT_BYTES} bytes.`, "--from", { limitBytes: MAX_INPUT_BYTES, observedBytes });
471
+ }
472
+ function skillOutputExceedsBudgetError(scenarioId, observedBytes) {
473
+ return new CliError("SKILL_OUTPUT_EXCEEDS_BUDGET", `Skill output exceeds the maximum allowed size of ${MAX_SKILL_OUTPUT_BYTES} bytes.`, "skill", { limitBytes: MAX_SKILL_OUTPUT_BYTES, observedBytes, scenarioId });
474
+ }
475
+ function unknownSkillScenarioError(scenarioId) {
476
+ return new CliError("UNKNOWN_SKILL_SCENARIO", `Unknown skill scenario "${scenarioId}".`, "$argv[1]", {
477
+ scenarioId,
478
+ knownScenarios: SKILL_SCENARIOS.map((scenario) => scenario.id),
479
+ });
480
+ }
481
+ function runSkillCommand(scenarioId, json) {
482
+ const output = scenarioId === undefined
483
+ ? json
484
+ ? JSON.stringify(projectSkillIndexToJson())
485
+ : projectSkillIndexToText()
486
+ : (() => {
487
+ const scenario = findSkillScenario(scenarioId);
488
+ if (scenario === undefined)
489
+ throw unknownSkillScenarioError(scenarioId);
490
+ return json ? JSON.stringify(projectSkillScenarioToJson(scenario)) : projectSkillScenarioToText(scenario);
491
+ })();
492
+ const observedBytes = Buffer.byteLength(output, "utf8");
493
+ if (observedBytes > MAX_SKILL_OUTPUT_BYTES)
494
+ throw skillOutputExceedsBudgetError(scenarioId, observedBytes);
495
+ console.log(output);
496
+ return 0;
497
+ }
498
+ function invalidArtifactNumberError(domain, value) {
499
+ const message = value === undefined
500
+ ? `A ${domain} number is required.`
501
+ : `"${value}" is not a valid ${domain} number. Use a positive integer.`;
502
+ return new CliError("INVALID_ARTIFACT_NUMBER", message, "$argv[0]", { domain, value });
503
+ }
504
+ async function runTemplateList(root, repository, dependencies) {
505
+ let discovery;
506
+ if (typeof repository === "string") {
507
+ const adapter = createAdapter(dependencies, root, repository);
508
+ await adapter.resolveRepositoryContext();
509
+ discovery = await discoverRepositoryTemplates(adapter);
510
+ }
511
+ else {
512
+ discovery = await discoverTemplates(root);
513
+ }
514
+ const semanticTemplates = typeof repository === "string" ? [] : await discoverSemanticTemplates(root);
515
+ const hint = semanticTemplates.length === 0 && typeof repository !== "string"
516
+ ? `no semantic templates found under ${SEMANTIC_TEMPLATE_DIRECTORY}/; ` +
517
+ `expected ${SEMANTIC_ISSUE_DIRECTORY}/<id>.json, ${SEMANTIC_PULL_REQUEST_FILE}, ` +
518
+ `or ${SEMANTIC_TEMPLATE_DIRECTORY}/pull-requests/<id>.json`
519
+ : undefined;
520
+ console.log(JSON.stringify({
521
+ templates: discovery.templates,
522
+ semanticTemplates,
523
+ ...(hint === undefined ? {} : { semanticTemplatesHint: hint }),
524
+ }));
525
+ return 0;
526
+ }
527
+ async function runTemplateSync(root, check) {
528
+ const result = await syncSemanticTemplates(root, check);
529
+ console.log(JSON.stringify(result));
530
+ return check && result.changed ? EXIT_VALIDATION : 0;
531
+ }
532
+ async function runTemplateImport(root, rest, parsed, json) {
533
+ const nativePath = typeof parsed.options.from === "string" ? parsed.options.from : rest[0];
534
+ if (nativePath === undefined)
535
+ throw new CliError("INPUT_REQUIRED", "Use template import --from <native-template>.", "--from");
536
+ const imported = await importNativeTemplate(root, nativePath, typeof parsed.options.to === "string" ? parsed.options.to : undefined);
537
+ if (json)
538
+ console.log(JSON.stringify({ ok: true, ...imported }));
539
+ else {
540
+ console.log(imported.path);
541
+ if (imported.warning !== undefined)
542
+ console.error(`warning: ${imported.warning}`);
543
+ }
544
+ return 0;
545
+ }
546
+ async function runArtifactCommand(domain, command, rest, parsed, root, dependencies, json) {
547
+ if (command === "schema") {
548
+ let contract;
549
+ if (typeof parsed.options.repository === "string") {
550
+ rejectGovernedPolicyOverride(parsed.options.policy);
551
+ const adapter = createAdapter(dependencies, root, parsed.options.repository);
552
+ await adapter.resolveRepositoryContext();
553
+ contract = await compileRepositoryGovernedContract(adapter, domain, templateSelector(parsed, rest[0]), {
554
+ templateResolver: dependencies.templateResolver,
555
+ });
556
+ }
557
+ else {
558
+ contract = await compileLocalGovernedContract(domain, root, templateSelector(parsed, rest[0]), parsed.options.policy, { templateResolver: dependencies.templateResolver });
559
+ }
560
+ const projection = projectContract(contract);
561
+ const syncInput = domain === "pr" ? projectPullRequestSyncInput(contract) : undefined;
562
+ if (parsed.options.compact === true)
563
+ console.log(JSON.stringify({
564
+ schema: renderSemanticCompactSchema(contract),
565
+ metadata: projection.metadata,
566
+ ...(syncInput === undefined ? {} : { syncInput }),
567
+ }));
568
+ else
569
+ console.log(JSON.stringify({
570
+ contract,
571
+ template: contract.templateIdentity,
572
+ ...projection,
573
+ directFields: projectDirectFieldUsage(contract),
574
+ ...(syncInput === undefined ? {} : { syncInput }),
575
+ }));
576
+ return 0;
577
+ }
578
+ if (command === "validate" || command === "render" || command === "create") {
579
+ if (command === "validate" &&
580
+ rest[0] !== undefined &&
581
+ isPositiveInteger(rest[0]) &&
582
+ parsed.options.from === undefined &&
583
+ parsed.fields.length === 0) {
584
+ return runExistingValidation(domain, Number(rest[0]), parsed, root, dependencies, json);
585
+ }
586
+ if (command === "validate" || command === "render") {
587
+ let contract;
588
+ if (typeof parsed.options.repository === "string") {
589
+ rejectGovernedPolicyOverride(parsed.options.policy);
590
+ const adapter = createAdapter(dependencies, root, parsed.options.repository);
591
+ await adapter.resolveRepositoryContext();
592
+ contract = await compileRepositoryGovernedContract(adapter, domain, templateSelector(parsed, rest[0]), {
593
+ templateResolver: dependencies.templateResolver,
594
+ });
595
+ }
596
+ else {
597
+ contract = await compileLocalGovernedContract(domain, root, templateSelector(parsed, rest[0]), parsed.options.policy, { templateResolver: dependencies.templateResolver });
598
+ }
599
+ const document = await resolveArtifactInputDocument(parsed, contract);
600
+ const preparedDocument = mergeOptionMetadata(document, parsed.options);
601
+ if (command === "validate") {
602
+ const validation = loadCanonicalArtifact(contract, preparedDocument);
603
+ console.log(JSON.stringify({
604
+ valid: validation.valid,
605
+ violations: validation.violations,
606
+ values: validation.canonical,
607
+ ...(domain === "issue" && validation.dependencies === undefined
608
+ ? {}
609
+ : { dependencies: validation.dependencies }),
610
+ // Progressive --field discovery: each unresolved field's type/required/constraints,
611
+ // reusing the existing #120/#121 partial-classification projection rather than a
612
+ // second field table -- so retrying with more --field values is guided by the same
613
+ // contract metadata resolveDirectFields itself accepts.
614
+ missingFields: validation.missingFields,
615
+ invalidFields: validation.invalidFields,
616
+ }));
617
+ return validation.valid ? 0 : EXIT_VALIDATION;
618
+ }
619
+ const body = domain === "issue"
620
+ ? renderIssueArtifact(contract, preparedDocument)
621
+ : renderPullRequestArtifact(contract, preparedDocument.fields);
622
+ if (json)
623
+ console.log(JSON.stringify({ valid: true, body }));
624
+ else
625
+ process.stdout.write(body);
626
+ return 0;
627
+ }
628
+ rejectGovernedPolicyOverride(parsed.options.policy);
629
+ const adapter = createAdapter(dependencies, root, parsed.options.repository);
630
+ await adapter.resolveRepositoryContext();
631
+ const contract = await compileRepositoryGovernedContract(adapter, domain, templateSelector(parsed, rest[0]), {
632
+ templateResolver: dependencies.templateResolver,
633
+ });
634
+ const document = await resolveArtifactInputDocument(parsed, contract);
635
+ const preparedDocument = mergeOptionMetadata(document, parsed.options);
636
+ if (domain === "issue") {
637
+ const prepared = prepareIssueArtifact(contract, preparedDocument);
638
+ const created = await createGovernedIssue(adapter, prepared.artifact);
639
+ console.log(JSON.stringify({ ok: true, artifact: created.artifact, governance: created.governance }));
640
+ return 0;
641
+ }
642
+ const prepared = preparePullRequestArtifact(contract, preparedDocument);
643
+ const created = await createGovernedPullRequest(adapter, prepared.artifact);
644
+ console.log(JSON.stringify({ ok: true, artifact: created.artifact, governance: created.governance }));
645
+ return 0;
646
+ }
647
+ if (command === "check" || command === "edit" || command === "normalize" || command === "sync") {
648
+ if (rest[0] === undefined || !isPositiveInteger(rest[0])) {
649
+ throw invalidArtifactNumberError(domain, rest[0]);
650
+ }
651
+ return runExistingRemediation(domain, command, Number(rest[0]), parsed, root, dependencies, json);
652
+ }
653
+ if ((command === "validate" || command === "explain") &&
654
+ rest[0] !== undefined &&
655
+ isPositiveInteger(rest[0]) &&
656
+ parsed.options.from === undefined) {
657
+ return runExistingValidation(domain, Number(rest[0]), parsed, root, dependencies, true);
658
+ }
659
+ if (command === "explain" && (rest[0] === undefined || !isPositiveInteger(rest[0]))) {
660
+ throw invalidArtifactNumberError(domain, rest[0]);
661
+ }
662
+ if (command === "get") {
663
+ if (rest[0] !== undefined && isPositiveInteger(rest[0])) {
664
+ return runExistingGet(domain, Number(rest[0]), parsed, root, dependencies);
665
+ }
666
+ throw invalidArtifactNumberError(domain, rest[0]);
667
+ }
668
+ throw new CliError("UNKNOWN_COMMAND", `Unknown ${domain} command "${command ?? ""}".`);
669
+ }
670
+ async function runExistingValidation(domain, number, parsed, root, dependencies, json) {
671
+ rejectGovernedPolicyOverride(parsed.options.policy);
672
+ const adapter = createAdapter(dependencies, root, parsed.options.repository);
673
+ await adapter.resolveRepositoryContext();
674
+ const read = await readGovernedExistingArtifact(adapter, domain, number, templateSelector(parsed, undefined));
675
+ const { remote, result } = read;
676
+ const projection = projectExistingArtifact(result);
677
+ const output = {
678
+ valid: projection.valid,
679
+ classification: projection.classification,
680
+ number,
681
+ url: remote.url,
682
+ diagnostics: projection.diagnostics,
683
+ ...(projection.violations === undefined ? {} : { violations: projection.violations }),
684
+ ...(projection.dependencies === undefined ? {} : { dependencies: projection.dependencies }),
685
+ ...(projection.attemptedTemplates === undefined ? {} : { attemptedTemplates: projection.attemptedTemplates }),
686
+ };
687
+ console.log(JSON.stringify(output));
688
+ return result.valid ? 0 : EXIT_VALIDATION;
689
+ }
690
+ async function runExistingGet(domain, number, parsed, root, dependencies) {
691
+ rejectGovernedPolicyOverride(parsed.options.policy);
692
+ const adapter = createAdapter(dependencies, root, parsed.options.repository);
693
+ await adapter.resolveRepositoryContext();
694
+ const { remote, contract, result } = await readGovernedExistingArtifact(adapter, domain, number, templateSelector(parsed, undefined));
695
+ const projection = projectExistingArtifact(result);
696
+ const output = {
697
+ valid: projection.valid,
698
+ projection: projection.projection,
699
+ classification: projection.classification,
700
+ kind: domain === "issue" ? "issue" : "pull_request",
701
+ number: remote.number,
702
+ url: remote.url,
703
+ ...(contract === undefined ? {} : { template: contract.templateIdentity }),
704
+ metadata: existingArtifactMetadata(domain, remote),
705
+ ...(projection.fields === undefined ? {} : { fields: projection.fields }),
706
+ ...(projection.dependencies === undefined ? {} : { dependencies: projection.dependencies }),
707
+ diagnostics: projection.diagnostics,
708
+ ...(projection.violations === undefined ? {} : { violations: projection.violations }),
709
+ ...(projection.attemptedTemplates === undefined ? {} : { attemptedTemplates: projection.attemptedTemplates }),
710
+ };
711
+ console.log(JSON.stringify(output));
712
+ return result.valid ? 0 : EXIT_VALIDATION;
713
+ }
714
+ async function runExistingRemediation(domain, operation, number, parsed, root, dependencies, json) {
715
+ void json;
716
+ rejectUnsupportedRemediationMetadata(domain, operation, parsed.options);
717
+ rejectGovernedPolicyOverride(parsed.options.policy);
718
+ const adapter = createAdapter(dependencies, root, parsed.options.repository);
719
+ await adapter.resolveRepositoryContext();
720
+ const read = await readGovernedExistingArtifact(adapter, domain, number, templateSelector(parsed, undefined));
721
+ const assessment = assessExistingArtifact(domain, read);
722
+ const base = {
723
+ operation,
724
+ kind: domain === "issue" ? "issue" : "pull_request",
725
+ number: read.remote.number,
726
+ url: read.remote.url,
727
+ ...(read.contract === undefined ? {} : { template: read.contract.templateIdentity }),
728
+ };
729
+ if (operation === "check") {
730
+ console.log(JSON.stringify({
731
+ ok: assessment.status === "valid-current",
732
+ ...base,
733
+ status: assessment.status,
734
+ classification: read.result.classification,
735
+ valid: assessment.status === "valid-current",
736
+ normalizable: assessment.normalizable,
737
+ diagnostics: assessment.diagnostics,
738
+ ...(read.result.classification === "semantic" ? { violations: read.result.violations } : {}),
739
+ ...(read.result.attemptedTemplates === undefined ? {} : { attemptedTemplates: read.result.attemptedTemplates }),
740
+ }));
741
+ return assessment.status === "valid-current" ? 0 : EXIT_VALIDATION;
742
+ }
743
+ if (read.contract === undefined) {
744
+ throw new RemediationError(operation === "normalize"
745
+ ? "NORMALIZATION_UNSAFE"
746
+ : operation === "edit"
747
+ ? "SEMANTIC_PATCH_UNSUPPORTED"
748
+ : "SYNC_CURRENT_UNSUPPORTED", "No authoritative template could be selected for the existing artifact.", "$.template", operation === "edit" || operation === "normalize" ? remediationFailureDetails(read) : undefined, operation === "edit" || operation === "normalize"
749
+ ? remediationDiagnosticReport(domain, operation, read)
750
+ : undefined);
751
+ }
752
+ let desiredInput;
753
+ try {
754
+ if (operation === "normalize") {
755
+ if (!read.result.valid || !read.result.parse.parsed) {
756
+ throw new RemediationError("NORMALIZATION_UNSAFE", "Normalization requires a semantically valid artifact whose values can be round-tripped canonically.", "$.artifact");
757
+ }
758
+ desiredInput = currentArtifactInput(domain, read);
759
+ }
760
+ else {
761
+ const input = await resolveArtifactInputDocument(parsed, read.contract, domain === "pr" && operation === "sync", operation === "edit" && hasEditMetadataOption(parsed.options));
762
+ desiredInput =
763
+ operation === "edit"
764
+ ? applySemanticPatch(domain, read, mergeOptionMetadata(input, parsed.options))
765
+ : prepareSyncInput(domain, read, input);
766
+ }
767
+ }
768
+ catch (error) {
769
+ if (operation === "edit" || operation === "normalize" || operation === "sync") {
770
+ throw translateRemediationFailure(domain, operation, read, error);
771
+ }
772
+ throw error;
773
+ }
774
+ let desired;
775
+ try {
776
+ desired = prepareRemediationArtifact(domain, read.contract, desiredInput);
777
+ }
778
+ catch (error) {
779
+ if (operation === "edit" || operation === "normalize" || operation === "sync") {
780
+ throw translateRemediationFailure(domain, operation, read, error, desiredInput);
781
+ }
782
+ throw error;
783
+ }
784
+ const diff = diffArtifact(domain, read, desired, operation === "sync");
785
+ const resultBase = {
786
+ ...base,
787
+ changed: diff.changed,
788
+ noOp: !diff.changed,
789
+ diff,
790
+ };
791
+ if (!diff.changed || parsed.options.dryRun === true) {
792
+ console.log(JSON.stringify({
793
+ ok: true,
794
+ ...resultBase,
795
+ ...(parsed.options.dryRun === true
796
+ ? {
797
+ dryRun: true,
798
+ mutation: "not-performed",
799
+ ...(operation === "edit"
800
+ ? { resulting: projectRemediationResult(domain, read.contract, desiredInput, desired) }
801
+ : {}),
802
+ }
803
+ : {}),
804
+ }));
805
+ return 0;
806
+ }
807
+ const mutated = await updateGovernedExistingArtifact(adapter, domain, number, desired);
808
+ console.log(JSON.stringify({
809
+ ok: true,
810
+ ...resultBase,
811
+ mutation: "applied",
812
+ artifact: { number: mutated.artifact.number, url: mutated.artifact.url },
813
+ governance: mutated.governance,
814
+ }));
815
+ return 0;
816
+ }
817
+ function existingArtifactMetadata(domain, remote) {
818
+ if (domain === "issue") {
819
+ if (!("labels" in remote) || !("assignees" in remote))
820
+ throw new Error("Issue metadata response is invalid.");
821
+ return {
822
+ title: remote.title,
823
+ state: remote.state,
824
+ labels: remote.labels,
825
+ assignees: remote.assignees,
826
+ };
827
+ }
828
+ if (!("draft" in remote) || !("head" in remote) || !("base" in remote))
829
+ throw new Error("Pull request metadata response is invalid.");
830
+ return {
831
+ title: remote.title,
832
+ state: remote.state,
833
+ draft: remote.draft,
834
+ head: remote.head,
835
+ base: remote.base,
836
+ ...(remote.maintainerCanModify === undefined ? {} : { maintainerCanModify: remote.maintainerCanModify }),
837
+ };
838
+ }
839
+ function projectRemediationResult(domain, contract, input, artifact) {
840
+ if (contract === undefined)
841
+ throw new Error("A remediation result requires a selected contract.");
842
+ const loaded = loadCanonicalArtifact(contract, input);
843
+ const fields = loaded.canonical;
844
+ if (domain === "issue") {
845
+ const issue = artifact;
846
+ return {
847
+ fields,
848
+ metadata: {
849
+ title: issue.title,
850
+ ...(issue.labels === undefined ? {} : { labels: issue.labels }),
851
+ ...(issue.assignees === undefined ? {} : { assignees: issue.assignees }),
852
+ },
853
+ ...(loaded.dependencies === undefined ? {} : { dependencies: loaded.dependencies }),
854
+ body: issue.body,
855
+ };
856
+ }
857
+ const pullRequest = artifact;
858
+ return {
859
+ fields,
860
+ metadata: {
861
+ title: pullRequest.title,
862
+ head: pullRequest.head,
863
+ base: pullRequest.base,
864
+ ...(pullRequest.draft === undefined ? {} : { draft: pullRequest.draft }),
865
+ ...(pullRequest.maintainerCanModify === undefined
866
+ ? {}
867
+ : { maintainerCanModify: pullRequest.maintainerCanModify }),
868
+ },
869
+ body: pullRequest.body,
870
+ };
871
+ }
872
+ function createAdapter(dependencies, root, repository) {
873
+ const factory = dependencies.createAdapter ?? ((options) => new GitHubAdapter(options));
874
+ return factory({ cwd: root, ...(typeof repository === "string" ? { repository } : {}) });
875
+ }
876
+ async function readInputDocument(value, parser = parseArtifactInputDocument) {
877
+ if (typeof value !== "string" || value.length === 0)
878
+ throw new CliError("INPUT_REQUIRED", "Use --from <file.json>.", "--from");
879
+ let source;
880
+ if (value === "-")
881
+ source = await readStdin();
882
+ else {
883
+ try {
884
+ const handle = await open(value, "r");
885
+ try {
886
+ const stats = await handle.stat();
887
+ if (stats.size > MAX_INPUT_BYTES)
888
+ throw inputTooLargeError(stats.size);
889
+ source = await handle.readFile("utf8");
890
+ if (Buffer.byteLength(source, "utf8") > MAX_INPUT_BYTES)
891
+ throw inputTooLargeError(Buffer.byteLength(source, "utf8"));
892
+ }
893
+ finally {
894
+ await handle.close();
895
+ }
896
+ }
897
+ catch (cause) {
898
+ if (cause instanceof CliError)
899
+ throw cause;
900
+ const error = new CliError("INPUT_READ_FAILED", `Cannot read input file "${value}".`, "--from");
901
+ if (cause instanceof Error)
902
+ error.cause = cause;
903
+ throw error;
904
+ }
905
+ }
906
+ let parsed;
907
+ try {
908
+ parsed = JSON.parse(source);
909
+ }
910
+ catch (cause) {
911
+ const error = new CliError("INPUT_INVALID_JSON", "Input file must contain valid JSON.", "--from");
912
+ if (cause instanceof Error)
913
+ error.cause = cause;
914
+ throw error;
915
+ }
916
+ return parser(parsed);
917
+ }
918
+ function mergeOptionMetadata(document, options) {
919
+ const metadata = {
920
+ ...document.metadata,
921
+ ...(typeof options.title === "string" ? { title: options.title } : {}),
922
+ ...(typeof options.head === "string" ? { head: options.head } : {}),
923
+ ...(typeof options.base === "string" ? { base: options.base } : {}),
924
+ ...(typeof options.draft === "boolean" ? { draft: options.draft } : {}),
925
+ ...(typeof options.maintainerCanModify === "boolean" ? { maintainerCanModify: options.maintainerCanModify } : {}),
926
+ };
927
+ return {
928
+ fields: document.fields,
929
+ metadata,
930
+ ...(document.dependencies === undefined ? {} : { dependencies: document.dependencies }),
931
+ };
932
+ }
933
+ function hasEditMetadataOption(options) {
934
+ return METADATA_OPTION_KEYS.some((key) => Object.prototype.hasOwnProperty.call(options, key));
935
+ }
936
+ function rejectUnsupportedRemediationMetadata(domain, operation, options) {
937
+ if (operation === "edit")
938
+ return;
939
+ const supplied = METADATA_OPTION_KEYS.filter((key) => Object.prototype.hasOwnProperty.call(options, key));
940
+ if (supplied.length === 0)
941
+ return;
942
+ const flags = supplied.map((key) => (key === "maintainerCanModify" ? "--maintainer-can-modify" : `--${key}`));
943
+ const label = flags.length === 1 ? "flag" : "flags";
944
+ const verb = flags.length === 1 ? "is" : "are";
945
+ const guidance = operation === "sync"
946
+ ? "use the documented --from input contract for metadata changes"
947
+ : "this remediation command does not accept metadata mutation flags";
948
+ throw new CliError("METADATA_UNSUPPORTED_COMMAND", `${flags.join(", ")} ${label} ${verb} not accepted by ${domain} ${operation}; ${guidance}.`, `$.metadata.${supplied[0]}`, {
949
+ command: `${domain} ${operation}`,
950
+ metadata: supplied,
951
+ flags,
952
+ });
953
+ }
954
+ /** Bound on how many accepted field names an unknown-field diagnostic lists before truncating. */
955
+ const MAX_LISTED_FIELDS = 12;
956
+ /** Bound on how many close-name suggestions an unknown-field diagnostic offers. */
957
+ const MAX_FIELD_SUGGESTIONS = 3;
958
+ /** Suggestions only surface within this edit distance; beyond it a name is not "close". */
959
+ const MAX_SUGGESTION_DISTANCE = 3;
960
+ function compareStrings(left, right) {
961
+ return left < right ? -1 : left > right ? 1 : 0;
962
+ }
963
+ function levenshteinDistance(left, right) {
964
+ const rows = left.length + 1;
965
+ const cols = right.length + 1;
966
+ const previous = new Array(cols);
967
+ const current = new Array(cols);
968
+ for (let column = 0; column < cols; column += 1)
969
+ previous[column] = column;
970
+ for (let row = 1; row < rows; row += 1) {
971
+ current[0] = row;
972
+ for (let column = 1; column < cols; column += 1) {
973
+ const cost = left[row - 1] === right[column - 1] ? 0 : 1;
974
+ current[column] = Math.min((previous[column] ?? 0) + 1, (current[column - 1] ?? 0) + 1, (previous[column - 1] ?? 0) + cost);
975
+ }
976
+ for (let column = 0; column < cols; column += 1)
977
+ previous[column] = current[column] ?? 0;
978
+ }
979
+ return previous[cols - 1] ?? 0;
980
+ }
981
+ function unknownFieldError(name, allowedFields) {
982
+ const suggestions = allowedFields
983
+ .map((candidate) => ({ candidate, distance: levenshteinDistance(candidate, name) }))
984
+ .filter((entry) => entry.distance <= MAX_SUGGESTION_DISTANCE)
985
+ .sort((left, right) => left.distance - right.distance || compareStrings(left.candidate, right.candidate))
986
+ .slice(0, MAX_FIELD_SUGGESTIONS)
987
+ .map((entry) => entry.candidate);
988
+ return new CliError("FIELD_UNKNOWN", `Unknown field "${name}" for this template.`, "--field", {
989
+ field: name,
990
+ allowedFields: allowedFields.slice(0, MAX_LISTED_FIELDS),
991
+ allowedFieldCount: allowedFields.length,
992
+ ...(suggestions.length === 0 ? {} : { suggestions }),
993
+ });
994
+ }
995
+ function duplicateFieldError(name, occurrences) {
996
+ return new CliError("FIELD_DUPLICATE", `Field "${name}" was provided ${occurrences} times as a scalar --field option; a scalar field accepts exactly one value.`, "--field", { field: name, occurrences });
997
+ }
998
+ function fieldConflictError(names) {
999
+ return new CliError("FIELD_CONFLICT", `Field(s) ${names.join(", ")} were supplied by both --from and --field; remove one source.`, "--field", { fields: names });
1000
+ }
1001
+ /** True only for the issue/pr commands that actually resolve an ArtifactInputDocument from --field. */
1002
+ function isFieldCapableCommand(domain, command) {
1003
+ if (domain !== "issue" && domain !== "pr")
1004
+ return false;
1005
+ const definition = getCommandForPositionals([domain, command ?? ""]);
1006
+ return definition?.optionIds.includes("field") === true;
1007
+ }
1008
+ function fieldUnsupportedCommandError(positionals) {
1009
+ const label = positionals.length === 0 ? "this command" : `"${positionals.join(" ")}"`;
1010
+ const supported = getDomainCommands("issue")
1011
+ .filter((entry) => entry.optionIds.includes("field"))
1012
+ .map((entry) => entry.operation)
1013
+ .sort(compareStrings);
1014
+ return new CliError("FIELD_UNSUPPORTED_COMMAND", `--field is only supported by issue/pr ${supported.join(", ")}; ${label} does not accept direct field input.`, "--field", { command: positionals.join(" "), supportedCommands: supported });
1015
+ }
1016
+ /**
1017
+ * The one field-usage projection shared by direct --field acceptance
1018
+ * (resolveDirectFields, below) and discovery/help (the `schema` command's
1019
+ * `directFields`, and progressive help via missing/invalid field
1020
+ * diagnostics). Both read this same contract-derived list, so the CLI's
1021
+ * documented `--field` syntax and its runtime acceptance cannot drift from
1022
+ * each other or from the selected canonical contract.
1023
+ */
1024
+ function projectDirectFieldUsage(contract) {
1025
+ return contract.sections
1026
+ .flatMap((section) => section.fields)
1027
+ .sort((left, right) => compareStrings(left.id, right.id))
1028
+ .map((field) => {
1029
+ const repeatable = field.type === "array" || field.type === "checklist";
1030
+ const required = effectiveFieldConstraints(contract, field).required;
1031
+ return {
1032
+ name: field.id,
1033
+ type: repeatable ? "array" : "string",
1034
+ required,
1035
+ repeatable,
1036
+ cliSyntax: field.type === "checklist"
1037
+ ? `--field ${field.id}=<option-id> (repeatable)`
1038
+ : repeatable
1039
+ ? `--field ${field.id}=<value> (repeatable)`
1040
+ : `--field ${field.id}=<value>`,
1041
+ };
1042
+ });
1043
+ }
1044
+ /**
1045
+ * Resolve raw `--field` occurrences against the selected canonical contract:
1046
+ * `projectDirectFieldUsage` is the only authority for accepted field names,
1047
+ * scalar-vs-list shape, and requiredness -- there is no second, handwritten
1048
+ * field table here. A repeatable field accumulates every occurrence in argv
1049
+ * order (deterministic repeated-value ordering); any other field accepts at
1050
+ * most one occurrence.
1051
+ */
1052
+ function resolveDirectFields(contract, entries) {
1053
+ const usage = projectDirectFieldUsage(contract);
1054
+ const usageByName = new Map(usage.map((entry) => [entry.name, entry]));
1055
+ const allowedFields = usage.map((entry) => entry.name);
1056
+ const grouped = new Map();
1057
+ for (const entry of entries) {
1058
+ if (!usageByName.has(entry.name))
1059
+ throw unknownFieldError(entry.name, allowedFields);
1060
+ const values = grouped.get(entry.name);
1061
+ if (values === undefined)
1062
+ grouped.set(entry.name, [entry.value]);
1063
+ else
1064
+ values.push(entry.value);
1065
+ }
1066
+ const fields = {};
1067
+ for (const [name, values] of grouped) {
1068
+ if (usageByName.get(name)?.repeatable === true) {
1069
+ fields[name] = values;
1070
+ continue;
1071
+ }
1072
+ if (values.length > 1)
1073
+ throw duplicateFieldError(name, values.length);
1074
+ fields[name] = values[0];
1075
+ }
1076
+ return fields;
1077
+ }
1078
+ /** Merge direct-field values into a document under a deterministic, order-independent conflict rule. */
1079
+ function mergeDirectFields(document, directFields) {
1080
+ const directNames = Object.keys(directFields);
1081
+ if (directNames.length === 0)
1082
+ return document;
1083
+ const conflicts = directNames
1084
+ .filter((name) => Object.prototype.hasOwnProperty.call(document.fields, name))
1085
+ .sort(compareStrings);
1086
+ if (conflicts.length > 0)
1087
+ throw fieldConflictError(conflicts);
1088
+ return {
1089
+ fields: { ...document.fields, ...directFields },
1090
+ metadata: document.metadata,
1091
+ ...(document.dependencies === undefined ? {} : { dependencies: document.dependencies }),
1092
+ };
1093
+ }
1094
+ /**
1095
+ * Resolve one artifact input document from `--from` and/or `--field`, sharing
1096
+ * the same candidate/normalization/validation path regardless of source. At
1097
+ * least one of the two is required; when both are present, `--from` supplies
1098
+ * the base document and direct fields are merged in under a conflict rule
1099
+ * that never depends on which flag appeared first in argv.
1100
+ */
1101
+ async function resolveArtifactInputDocument(parsed, contract, requirePullRequestSyncInput = false, allowEmpty = false) {
1102
+ const hasFrom = typeof parsed.options.from === "string";
1103
+ if (!hasFrom && parsed.fields.length === 0 && !allowEmpty) {
1104
+ throw new CliError("INPUT_REQUIRED", "Use --from <file.json> or --field <name>=<value>.", "--from");
1105
+ }
1106
+ const document = hasFrom
1107
+ ? await readInputDocument(parsed.options.from, requirePullRequestSyncInput ? parsePullRequestSyncInput : undefined)
1108
+ : { fields: {}, metadata: {} };
1109
+ const directFields = resolveDirectFields(contract, parsed.fields);
1110
+ const merged = mergeDirectFields(document, directFields);
1111
+ return requirePullRequestSyncInput ? assertPullRequestSyncInputComplete(merged) : merged;
1112
+ }
1113
+ function templateSelector(parsed, positional) {
1114
+ return typeof parsed.options.template === "string" ? parsed.options.template : positional;
1115
+ }
1116
+ function parseArguments(argv) {
1117
+ const options = {};
1118
+ const fields = [];
1119
+ const tokenized = tokenizeCommandArgv(argv);
1120
+ for (const occurrence of tokenized.options) {
1121
+ const option = occurrence.definition;
1122
+ if (option === undefined) {
1123
+ if (occurrence.rawName.startsWith("--")) {
1124
+ throw new CliError("INVALID_OPTION", `Unknown option ${occurrence.rawName}.`);
1125
+ }
1126
+ throw new CliError("INVALID_OPTION", `Unknown option ${occurrence.rawName}.`);
1127
+ }
1128
+ if (option.id === "rawBody")
1129
+ throw new CliError("INVALID_OPTION", `Unknown option ${occurrence.rawName}.`);
1130
+ if (option.arity === "required" && occurrence.value === undefined) {
1131
+ throw new CliError("INVALID_OPTION", `Option ${occurrence.rawName} requires a value.`);
1132
+ }
1133
+ if (option.id === "help") {
1134
+ if (occurrence.value === undefined) {
1135
+ options.help = true;
1136
+ continue;
1137
+ }
1138
+ if (occurrence.value !== "full" && occurrence.value !== "json")
1139
+ throw new CliError("INVALID_OPTION", `Option ${occurrence.rawName} accepts only full or json.`);
1140
+ options.help = occurrence.value;
1141
+ continue;
1142
+ }
1143
+ if (option.id === "field") {
1144
+ const raw = occurrence.value;
1145
+ if (raw === undefined)
1146
+ throw new CliError("INVALID_OPTION", "Option --field requires a value.");
1147
+ const separatorIndex = raw.indexOf("=");
1148
+ if (separatorIndex <= 0)
1149
+ throw new CliError("INVALID_OPTION", 'Option --field requires "<name>=<value>" syntax.', "--field");
1150
+ fields.push({ name: raw.slice(0, separatorIndex), value: raw.slice(separatorIndex + 1) });
1151
+ continue;
1152
+ }
1153
+ const key = option.id;
1154
+ if (option.valueType === "boolean") {
1155
+ if (occurrence.value === undefined) {
1156
+ options[key] = true;
1157
+ continue;
1158
+ }
1159
+ if (occurrence.value !== "true" && occurrence.value !== "false")
1160
+ throw new CliError("INVALID_OPTION", `Option ${occurrence.rawName} must be true or false.`);
1161
+ options[key] = occurrence.value === "true";
1162
+ continue;
1163
+ }
1164
+ if (occurrence.value === undefined || occurrence.value.length === 0)
1165
+ throw new CliError("INVALID_OPTION", `Option ${occurrence.rawName} requires a value.`);
1166
+ options[key] = occurrence.value;
1167
+ }
1168
+ return { positionals: tokenized.positionals, options, fields };
1169
+ }
1170
+ function toErrorShape(error) {
1171
+ if (error instanceof CliError)
1172
+ return {
1173
+ code: error.code,
1174
+ message: error.message,
1175
+ ...(error.path === undefined ? {} : { path: error.path }),
1176
+ ...(error.details === undefined ? {} : { details: error.details }),
1177
+ };
1178
+ if (error instanceof SemanticValidationError)
1179
+ return {
1180
+ code: "SEMANTIC_VALIDATION_FAILED",
1181
+ message: error.message,
1182
+ violations: error.violations,
1183
+ ...(error.details === undefined ? {} : { details: error.details }),
1184
+ ...(error.diagnostics === undefined ? {} : { diagnostics: error.diagnostics }),
1185
+ };
1186
+ if (error instanceof RemediationError)
1187
+ return {
1188
+ code: error.code,
1189
+ message: error.message,
1190
+ ...(error.path === undefined ? {} : { path: error.path }),
1191
+ ...(error.details === undefined ? {} : { details: error.details }),
1192
+ ...(error.diagnostics === undefined ? {} : { diagnostics: error.diagnostics }),
1193
+ };
1194
+ if (error instanceof ArtifactInputError)
1195
+ return {
1196
+ code: error.code,
1197
+ message: error.message,
1198
+ path: error.path,
1199
+ ...(error.details === undefined ? {} : { details: error.details }),
1200
+ };
1201
+ if (error instanceof ArtifactPreparationError) {
1202
+ return { code: error.code, message: error.message, diagnostics: error.diagnostics };
1203
+ }
1204
+ if (isGitHubAdapterError(error))
1205
+ return { code: error.code, message: error.message, details: error.details };
1206
+ if (isObjectWithCode(error))
1207
+ return {
1208
+ code: error.code,
1209
+ message: typeof error.message === "string" ? error.message : "Operation failed.",
1210
+ ...(typeof error.path === "string" ? { path: error.path } : {}),
1211
+ ...(typeof error.details === "object" ? { details: error.details } : {}),
1212
+ ...(Array.isArray(error.violations) ? { violations: error.violations } : {}),
1213
+ };
1214
+ return { code: "INTERNAL_ERROR", message: error instanceof Error ? error.message : "Operation failed." };
1215
+ }
1216
+ function classifyExitCode(error) {
1217
+ if (error instanceof SemanticValidationError ||
1218
+ error instanceof ArtifactInputError ||
1219
+ error instanceof RemediationError ||
1220
+ error instanceof ArtifactPreparationError)
1221
+ return EXIT_VALIDATION;
1222
+ if (isGitHubAdapterError(error))
1223
+ return EXIT_REMOTE;
1224
+ if (isObjectWithCode(error) &&
1225
+ typeof error.code === "string" &&
1226
+ (error.code.includes("TEMPLATE") || error.code.includes("POLICY")))
1227
+ return EXIT_VALIDATION;
1228
+ if (error instanceof CliError &&
1229
+ (error.code === "UNKNOWN_COMMAND" ||
1230
+ error.code === "INVALID_OPTION" ||
1231
+ error.code === "INPUT_REQUIRED" ||
1232
+ error.code === "INPUT_READ_FAILED" ||
1233
+ error.code === "FIELD_UNSUPPORTED_COMMAND" ||
1234
+ error.code === "METADATA_UNSUPPORTED_COMMAND" ||
1235
+ error.code === "GOVERNED_CREATE_OPTION"))
1236
+ return EXIT_USAGE;
1237
+ if (error instanceof CliError &&
1238
+ (error.code === "INPUT_INVALID_JSON" ||
1239
+ error.code === "INPUT_TOO_LARGE" ||
1240
+ error.code === "INVALID_ARTIFACT_NUMBER" ||
1241
+ error.code === "UNKNOWN_SKILL_SCENARIO" ||
1242
+ error.code === "SKILL_OUTPUT_EXCEEDS_BUDGET" ||
1243
+ error.code === "FIELD_UNKNOWN" ||
1244
+ error.code === "FIELD_DUPLICATE" ||
1245
+ error.code === "FIELD_CONFLICT"))
1246
+ return EXIT_VALIDATION;
1247
+ if (isObjectWithCode(error) && error.code === "GOVERNANCE_POLICY_OVERRIDE_FORBIDDEN")
1248
+ return EXIT_VALIDATION;
1249
+ if (isObjectWithCode(error) && error.code.startsWith("GOVERNANCE_"))
1250
+ return EXIT_REMOTE;
1251
+ if (isObjectWithCode(error) && /^(?:ISSUE_FORM|PR_TEMPLATE|IR_|CONTRACT_)/u.test(error.code))
1252
+ return EXIT_VALIDATION;
1253
+ return EXIT_INTERNAL;
1254
+ }
1255
+ /**
1256
+ * True when argv targets a command Inari implements; false means it must fall
1257
+ * back to the real `gh` binary. The same tokenizer is used by parseArguments,
1258
+ * so supported option values cannot become routing positionals.
1259
+ */
1260
+ function isOwnedInvocation(argv) {
1261
+ const { positionals } = tokenizeCommandArgv(argv);
1262
+ const first = positionals[0];
1263
+ if (first === undefined)
1264
+ return true;
1265
+ if (first === "diagnose" || first === "doctor" || first === "version" || first === "help")
1266
+ return true;
1267
+ if (first === "skill")
1268
+ return true;
1269
+ if (argv.includes("--version") || argv.includes("--diagnose") || argv.includes("--doctor"))
1270
+ return true;
1271
+ const helpRequested = argv.some((token) => token === "--help" || token.startsWith("--help="));
1272
+ if (helpRequested && (first === "issue" || first === "pr" || first === "template") && positionals.length === 1)
1273
+ return true;
1274
+ return getCommandForPositionals(positionals) !== undefined;
1275
+ }
1276
+ function isMachineCommand(positionals) {
1277
+ return getCommandForPositionals(positionals) !== undefined;
1278
+ }
1279
+ function isMachineCommandTokens(argv) {
1280
+ const { positionals } = tokenizeCommandArgv(argv);
1281
+ if (argv.includes("--diagnose") ||
1282
+ argv.includes("--doctor") ||
1283
+ positionals[0] === "diagnose" ||
1284
+ positionals[0] === "doctor")
1285
+ return true;
1286
+ if (positionals[0] === "version")
1287
+ return argv.includes("--json");
1288
+ return positionals[0] === "skill" || getCommandForPositionals(positionals) !== undefined;
1289
+ }
1290
+ function isPositiveInteger(value) {
1291
+ return /^[1-9]\d*$/u.test(value);
1292
+ }
1293
+ function isObjectWithCode(value) {
1294
+ return typeof value === "object" && value !== null && "code" in value && typeof value.code === "string";
1295
+ }
1296
+ function readPackageMetadata() {
1297
+ const packagePath = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
1298
+ const value = JSON.parse(requireFile(packagePath));
1299
+ if (typeof value.name !== "string" || typeof value.version !== "string") {
1300
+ throw new Error("package.json must define the package name and version.");
1301
+ }
1302
+ return {
1303
+ name: value.name,
1304
+ version: value.version,
1305
+ description: typeof value.description === "string" ? value.description : "",
1306
+ };
1307
+ }
1308
+ function requireFile(filePath) {
1309
+ return readFileSync(filePath, "utf8");
1310
+ }
1311
+ async function readStdin() {
1312
+ const chunks = [];
1313
+ let totalBytes = 0;
1314
+ for await (const chunk of process.stdin) {
1315
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1316
+ totalBytes += buffer.byteLength;
1317
+ if (totalBytes > MAX_INPUT_BYTES)
1318
+ throw inputTooLargeError(totalBytes);
1319
+ chunks.push(buffer);
1320
+ }
1321
+ return Buffer.concat(chunks).toString("utf8");
1322
+ }
1323
+ const DOMAIN_PASSTHROUGH_EXAMPLE = {
1324
+ issue: "issue list",
1325
+ pr: "pr checks",
1326
+ template: "template view",
1327
+ };
1328
+ /** Dispatches to root, domain, or leaf help from the canonical command model. */
1329
+ function printHelpFor(positionals, helpValue) {
1330
+ if (helpValue === "json")
1331
+ return console.log(JSON.stringify(projectCommandHelp(positionals)));
1332
+ if (helpValue === "full")
1333
+ return printFullHelp();
1334
+ const [domain, command] = positionals;
1335
+ if (domain === "issue" || domain === "pr") {
1336
+ const definition = command === undefined ? undefined : getCommandForPositionals([domain, command]);
1337
+ if (definition !== undefined && definition.domain === domain)
1338
+ return printLeafHelp(definition);
1339
+ return printDomainHelp(domain);
1340
+ }
1341
+ if (domain === "template") {
1342
+ const definition = command === undefined ? undefined : getCommandForPositionals([domain, command]);
1343
+ if (definition !== undefined && definition.domain === domain)
1344
+ return printLeafHelp(definition);
1345
+ return printDomainHelp("template");
1346
+ }
1347
+ if (domain === "skill")
1348
+ return printSkillHelp(command);
1349
+ printRootHelp();
1350
+ }
1351
+ function printRootHelp() {
1352
+ console.log(`Usage: inari <command> [...]
1353
+
1354
+ A governed GitHub CLI. Issue and PR commands under governed templates run
1355
+ through Inari; every other command passes through to the real gh binary
1356
+ with the original argv and exit status.
1357
+
1358
+ Domains:
1359
+ issue Governed Issue schema, validation, rendering, and lifecycle
1360
+ pr Governed pull request schema, validation, rendering, and lifecycle
1361
+ template Semantic template authoring and native template sync
1362
+ skill Bounded operational playbooks for common governed workflows
1363
+
1364
+ All other commands (e.g. repo, auth, pr list, issue view) are passed through to gh.
1365
+
1366
+ Run \`inari <domain> --help\` for that domain's operations.
1367
+ Run \`inari --help=full\` for the complete command and option reference.
1368
+ Run \`inari --version\` or \`inari --diagnose\` for machine-readable runtime checks.`);
1369
+ }
1370
+ function printDomainHelp(domain) {
1371
+ const lines = getDomainCommands(domain).map((entry) => ` ${commandUsage(entry)}`);
1372
+ console.log(`Usage: inari ${domain} <command> [...]
1373
+
1374
+ Operations:
1375
+ ${lines.join("\n")}
1376
+
1377
+ Commands outside this list under "${domain}" (e.g. \`${DOMAIN_PASSTHROUGH_EXAMPLE[domain]}\`) pass through to gh.
1378
+
1379
+ Run \`inari ${domain} <command> --help\` for that command's inputs and an example.`);
1380
+ }
1381
+ function printSkillHelp(scenarioId) {
1382
+ if (scenarioId !== undefined) {
1383
+ const scenario = findSkillScenario(scenarioId);
1384
+ if (scenario === undefined)
1385
+ return printSkillHelp(undefined);
1386
+ console.log(`Usage: ${commandInvocation("skill.scenario")} ${scenario.id} --help
1387
+
1388
+ ${scenario.title}
1389
+
1390
+ Run \`${scenario.canonicalEntrypoint}\` for the playbook.`);
1391
+ return;
1392
+ }
1393
+ const lines = SKILL_SCENARIOS.map((scenario) => ` skill ${scenario.id} [--json] - ${scenario.title}`);
1394
+ console.log(`Usage: inari skill [scenario] [--json]
1395
+
1396
+ Bounded operational playbooks for common governed workflows. \`inari skill\`
1397
+ lists scenarios; \`inari skill <scenario>\` prints that scenario's playbook.
1398
+
1399
+ Scenarios:
1400
+ ${lines.join("\n")}
1401
+
1402
+ Run \`inari skill <scenario> --help\` for that scenario's summary.
1403
+ Run \`inari <domain> --help\` for exact command syntax used by a playbook.`);
1404
+ }
1405
+ function printLeafHelp(command) {
1406
+ const options = command.optionIds
1407
+ .filter((id) => id !== "help" && id !== "json")
1408
+ .map((id) => ` ${optionSyntax(getOption(id))} ${getOption(id).description}`)
1409
+ .join("\n");
1410
+ console.log(`Usage: inari ${commandUsage(command)}
1411
+
1412
+ ${leafSummary(command)}
1413
+
1414
+ Example:
1415
+ ${commandExample(command.id)}
1416
+
1417
+ Options:
1418
+ ${options}
1419
+
1420
+ Run \`inari --help=full\` for the complete option reference.`);
1421
+ }
1422
+ function printFullHelp() {
1423
+ const commands = ["template", "issue", "pr"]
1424
+ .flatMap((domain) => INARI_COMMANDS.filter((entry) => entry.domain === domain))
1425
+ .map((entry) => ` ${commandUsage(entry)}`)
1426
+ .join("\n");
1427
+ const options = Object.values(COMMAND_OPTIONS)
1428
+ .map((option) => ` ${optionSyntax(option)} ${option.description}`)
1429
+ .join("\n");
1430
+ console.log(`Usage: inari <command> [options]
1431
+
1432
+ Commands:
1433
+ ${commands}
1434
+ skill [scenario] [--json]
1435
+
1436
+ Options:
1437
+ ${options}
1438
+
1439
+ Create always validates and renders before invoking gh. Schema, validate, render, check, and --dry-run remediation never mutate GitHub.
1440
+ Edit is the primary patch path: it preserves omitted fields and metadata, validates the complete result, and renders canonical Markdown before mutation. Normalize preserves existing semantic values; issue sync preserves omitted current values; pr sync reconciles a complete desired semantic state.
1441
+
1442
+ All other commands pass through to the real gh binary unchanged.
1443
+
1444
+ Canonical invocation: inari
1445
+ Compatibility invocation: gh inari
1446
+ Canonical install: npm install --global gh-inari
1447
+ PATH-independent fallback: npx --yes gh-inari
1448
+ Extension compatibility path: gh extension install yohn-jp/gh-inari`);
1449
+ }
1450
+ function leafSummary(command) {
1451
+ if (command.id === "pr.sync")
1452
+ return `${command.summary} ${renderPullRequestSyncInputHelp()}`;
1453
+ if (command.id === "issue.validate" || command.id === "pr.validate") {
1454
+ const noun = command.domain === "issue" ? "issue" : "pull request";
1455
+ return `${command.summary} Run \`${command.domain} schema\` for its contract-derived directFields projection; existing ${noun} validation uses a positive number.`;
1456
+ }
1457
+ if (command.id === "issue.schema" || command.id === "pr.schema") {
1458
+ const noun = command.domain === "issue" ? "issue" : "pull request";
1459
+ return `${command.summary} Dynamic --field names, types, requiredness, and checklist option IDs come from the selected ${noun} artifact contract.`;
1460
+ }
1461
+ return command.summary;
1462
+ }
1463
+ //# sourceMappingURL=cli-core.js.map