@open-agent-toolkit/cli 0.1.34 → 0.1.36
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/assets/agents/oat-reviewer.md +22 -2
- package/assets/docs/cli-utilities/config-and-local-state.md +11 -5
- package/assets/docs/cli-utilities/tool-packs.md +1 -1
- package/assets/docs/cli-utilities/workflow-gates.md +103 -19
- package/assets/docs/contributing/design-principles.md +1 -1
- package/assets/docs/reference/cli-reference.md +4 -3
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/oat-project-implement/SKILL.md +7 -3
- package/assets/skills/oat-project-import-plan/SKILL.md +53 -9
- package/assets/skills/oat-project-plan/SKILL.md +7 -3
- package/assets/skills/oat-project-quick-start/SKILL.md +37 -9
- package/assets/skills/oat-project-review-provide/SKILL.md +12 -4
- package/assets/skills/oat-project-review-receive/SKILL.md +3 -1
- package/dist/app/create-program.d.ts.map +1 -1
- package/dist/app/create-program.js +3 -6
- package/dist/app/help-config.d.ts +12 -0
- package/dist/app/help-config.d.ts.map +1 -0
- package/dist/app/help-config.js +15 -0
- package/dist/commands/doctor/index.d.ts.map +1 -1
- package/dist/commands/doctor/index.js +2 -1
- package/dist/commands/gate/index.d.ts.map +1 -1
- package/dist/commands/gate/index.js +352 -16
- package/dist/commands/gate/review-verdict.d.ts +15 -0
- package/dist/commands/gate/review-verdict.d.ts.map +1 -0
- package/dist/commands/gate/review-verdict.js +180 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +4 -0
- package/dist/commands/init/index.d.ts.map +1 -1
- package/dist/commands/init/index.js +2 -1
- package/dist/commands/init/tools/brainstorm/index.d.ts.map +1 -1
- package/dist/commands/init/tools/brainstorm/index.js +2 -1
- package/dist/commands/init/tools/core/index.d.ts.map +1 -1
- package/dist/commands/init/tools/core/index.js +18 -0
- package/dist/commands/init/tools/docs/index.d.ts.map +1 -1
- package/dist/commands/init/tools/docs/index.js +2 -1
- package/dist/commands/init/tools/ideas/index.d.ts.map +1 -1
- package/dist/commands/init/tools/ideas/index.js +2 -1
- package/dist/commands/init/tools/project-management/index.d.ts.map +1 -1
- package/dist/commands/init/tools/project-management/index.js +19 -0
- package/dist/commands/init/tools/research/index.d.ts.map +1 -1
- package/dist/commands/init/tools/research/index.js +2 -1
- package/dist/commands/init/tools/utility/index.d.ts.map +1 -1
- package/dist/commands/init/tools/utility/index.js +2 -1
- package/dist/commands/init/tools/workflows/index.d.ts.map +1 -1
- package/dist/commands/init/tools/workflows/index.js +2 -1
- package/dist/commands/project/split/evaluate-signals.d.ts.map +1 -1
- package/dist/commands/project/split/evaluate-signals.js +11 -1
- package/dist/commands/project/split/run.d.ts.map +1 -1
- package/dist/commands/project/split/run.js +14 -0
- package/dist/commands/project/split/validate-plan.d.ts.map +1 -1
- package/dist/commands/project/split/validate-plan.js +30 -6
- package/dist/commands/project/validate-plan/index.d.ts +6 -1
- package/dist/commands/project/validate-plan/index.d.ts.map +1 -1
- package/dist/commands/project/validate-plan/index.js +36 -9
- package/dist/commands/providers/inspect/inspect.d.ts.map +1 -1
- package/dist/commands/providers/inspect/inspect.js +2 -1
- package/dist/commands/providers/list/list.d.ts.map +1 -1
- package/dist/commands/providers/list/list.js +2 -1
- package/dist/commands/providers/set/index.d.ts.map +1 -1
- package/dist/commands/providers/set/index.js +6 -2
- package/dist/commands/remove/skill/remove-skill.d.ts.map +1 -1
- package/dist/commands/remove/skill/remove-skill.js +2 -1
- package/dist/commands/remove/skills/remove-skills.d.ts.map +1 -1
- package/dist/commands/remove/skills/remove-skills.js +2 -1
- package/dist/commands/repo/pr-comments/triage-collection/triage-comments.d.ts +1 -0
- package/dist/commands/repo/pr-comments/triage-collection/triage-comments.d.ts.map +1 -1
- package/dist/commands/repo/pr-comments/triage-collection/triage-comments.js +25 -13
- package/dist/commands/shared/scope-option.d.ts +21 -0
- package/dist/commands/shared/scope-option.d.ts.map +1 -0
- package/dist/commands/shared/scope-option.js +24 -0
- package/dist/commands/status/index.d.ts.map +1 -1
- package/dist/commands/status/index.js +2 -1
- package/dist/commands/sync/index.d.ts.map +1 -1
- package/dist/commands/sync/index.js +2 -1
- package/dist/commands/tools/info/index.d.ts.map +1 -1
- package/dist/commands/tools/info/index.js +2 -1
- package/dist/commands/tools/install/index.d.ts.map +1 -1
- package/dist/commands/tools/install/index.js +2 -0
- package/dist/commands/tools/list/index.d.ts.map +1 -1
- package/dist/commands/tools/list/index.js +2 -1
- package/dist/commands/tools/outdated/index.d.ts.map +1 -1
- package/dist/commands/tools/outdated/index.js +2 -1
- package/dist/commands/tools/remove/index.d.ts.map +1 -1
- package/dist/commands/tools/remove/index.js +2 -1
- package/dist/commands/tools/update/index.d.ts.map +1 -1
- package/dist/commands/tools/update/index.js +6 -2
- package/dist/config/oat-config.js +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-program.d.ts","sourceRoot":"","sources":["../../src/app/create-program.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,
|
|
1
|
+
{"version":3,"file":"create-program.d.ts","sourceRoot":"","sources":["../../src/app/create-program.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAMpC,wBAAgB,aAAa,IAAI,OAAO,CASvC"}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { OAT_VERSION } from '../shared/oat-version.js';
|
|
2
|
-
import { Command
|
|
2
|
+
import { Command } from 'commander';
|
|
3
3
|
const PROGRAM_NAME = 'oat';
|
|
4
4
|
const PROGRAM_DESCRIPTION = 'Open Agent Toolkit CLI for provider interoperability';
|
|
5
|
-
const SCOPE_CHOICES = ['project', 'user', 'all'];
|
|
6
5
|
export function createProgram() {
|
|
7
6
|
return new Command()
|
|
8
7
|
.name(PROGRAM_NAME)
|
|
@@ -10,8 +9,6 @@ export function createProgram() {
|
|
|
10
9
|
.version(OAT_VERSION)
|
|
11
10
|
.option('--json', 'Output a single JSON document')
|
|
12
11
|
.option('--verbose', 'Enable verbose debug output')
|
|
13
|
-
.
|
|
14
|
-
.
|
|
15
|
-
.default('all'))
|
|
16
|
-
.option('--cwd <path>', 'Override working directory');
|
|
12
|
+
.option('--cwd <path>', 'Override working directory')
|
|
13
|
+
.configureHelp({ showGlobalOptions: true });
|
|
17
14
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
/**
|
|
3
|
+
* Commander's `.addCommand()` does not inherit the parent's help configuration.
|
|
4
|
+
* This recursive helper applies `showGlobalOptions: true` to every node in the
|
|
5
|
+
* command tree after registration, so that each subcommand's `--help` output
|
|
6
|
+
* includes a "Global Options" section showing `--json`, `--verbose`, and `--cwd`.
|
|
7
|
+
*
|
|
8
|
+
* Must be called once at the end of `registerCommands(program)` after all
|
|
9
|
+
* subcommands have been added via `.addCommand()`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function applyHelpConfiguration(command: Command): void;
|
|
12
|
+
//# sourceMappingURL=help-config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"help-config.d.ts","sourceRoot":"","sources":["../../src/app/help-config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAK7D"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commander's `.addCommand()` does not inherit the parent's help configuration.
|
|
3
|
+
* This recursive helper applies `showGlobalOptions: true` to every node in the
|
|
4
|
+
* command tree after registration, so that each subcommand's `--help` output
|
|
5
|
+
* includes a "Global Options" section showing `--json`, `--verbose`, and `--cwd`.
|
|
6
|
+
*
|
|
7
|
+
* Must be called once at the end of `registerCommands(program)` after all
|
|
8
|
+
* subcommands have been added via `.addCommand()`.
|
|
9
|
+
*/
|
|
10
|
+
export function applyHelpConfiguration(command) {
|
|
11
|
+
command.configureHelp({ showGlobalOptions: true });
|
|
12
|
+
for (const sub of command.commands) {
|
|
13
|
+
applyHelpConfiguration(sub);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/doctor/index.ts"],"names":[],"mappings":"AAWA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/doctor/index.ts"],"names":[],"mappings":"AAWA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,sBAAsB,CAAC;AAO9B,OAAO,EAAiB,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAInE,OAAO,EAAgB,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAM9D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,KAAK,WAAW,EAAuB,MAAM,YAAY,CAAC;AACnE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,UAAU,kBAAkB;IAC1B,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;IAChE,gBAAgB,EAAE,CAChB,KAAK,EAAE,aAAa,EACpB,OAAO,EAAE,cAAc,KACpB,OAAO,CAAC,MAAM,CAAC,CAAC;IACrB,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/C,YAAY,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC1D,mBAAmB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7D,cAAc,EAAE,CACd,SAAS,EAAE,MAAM,KACd,OAAO,CACV,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CACnE,CAAC;IACF,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5C,iBAAiB,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IACzC,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,CAAC,CAAC;IACxD,kBAAkB,EAAE,CAClB,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,gBAAgB,KACvB,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAC5B,kBAAkB,EAAE,CAClB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,KAC3C,OAAO,CAAC,kBAAkB,CAAC,CAAC;CAClC;AAED,UAAU,oBAAoB;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,UAAU,kBAAkB;IAC1B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,0BAA0B,EAAE,MAAM,CAAC;IACnC,cAAc,EAAE,oBAAoB,EAAE,CAAC;CACxC;AAsbD,wBAAgB,mBAAmB,CACjC,SAAS,GAAE,OAAO,CAAC,kBAAkB,CAAM,GAC1C,OAAO,CAcT"}
|
|
@@ -5,6 +5,7 @@ import { buildCommandContext, } from '../../app/command-context.js';
|
|
|
5
5
|
import { compareVersions } from '../init/tools/shared/version.js';
|
|
6
6
|
import { createPjmDisabledCheck, runPjmDoctorChecks, } from '../pjm/doctor.js';
|
|
7
7
|
import { getSkillVersion } from '../shared/frontmatter.js';
|
|
8
|
+
import { withScopeOption } from '../shared/scope-option.js';
|
|
8
9
|
import { readGlobalOptions, resolveConcreteScopes, } from '../shared/shared.utils.js';
|
|
9
10
|
import { readOatConfig } from '../../config/oat-config.js';
|
|
10
11
|
import { resolveAssetsRoot } from '../../fs/assets.js';
|
|
@@ -379,7 +380,7 @@ export function createDoctorCommand(overrides = {}) {
|
|
|
379
380
|
...createDependencies(),
|
|
380
381
|
...overrides,
|
|
381
382
|
};
|
|
382
|
-
return new Command('doctor')
|
|
383
|
+
return withScopeOption(new Command('doctor'))
|
|
383
384
|
.description('Run environment and setup diagnostics')
|
|
384
385
|
.action(async (_options, command) => {
|
|
385
386
|
const context = dependencies.buildCommandContext(readGlobalOptions(command));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/gate/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/gate/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAO9B,OAAO,EAQL,KAAK,UAAU,EAIf,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,KAAK,UAAU,EAEhB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAIL,KAAK,cAAc,EACpB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAOpC,UAAU,uBAAuB;IAC/B,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;IAChE,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,CAAC,CAAC;IACxD,cAAc,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,kBAAkB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;IAClE,mBAAmB,EAAE,CACnB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,cAAc,KACnB,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB,cAAc,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;IAC/D,eAAe,EAAE,CAAC,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9E,sBAAsB,EAAE,CACtB,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,GAAG,EAAE,MAAM,CAAC,UAAU,KACnB,OAAO,CAAC,cAAc,CAAC,CAAC;IAC7B,UAAU,EAAE,CACV,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,EAAE,iBAAiB,KACvB,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC;CAC/B;AA0CD,UAAU,iBAAiB;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,OAAO,EAAE,gBAAgB,GAAG,cAAc,GAAG,SAAS,CAAC;IACvD,KAAK,EAAE,QAAQ,GAAG,SAAS,CAAC;CAC7B;AAED,UAAU,gBAAgB;IACxB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,KAAK,kBAAkB,GAAG,cAAc,GAAG,MAAM,CAAC;AAMlD,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,UAAU,CAAC;CACpB;AAkZD,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,EAC9C,cAAc,EAAE,MAAM,EACtB,KAAK,EAAE,kBAAkB,GACxB,kBAAkB,GAAG,IAAI,CAE3B;AA+wBD,wBAAgB,iBAAiB,CAC/B,SAAS,GAAE,OAAO,CAAC,uBAAuB,CAAM,GAC/C,OAAO,CAwKT"}
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import {
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
4
|
+
import { basename, isAbsolute, join, relative } from 'node:path';
|
|
3
5
|
import { buildCommandContext, } from '../../app/command-context.js';
|
|
6
|
+
import { getFrontmatterBlock, getFrontmatterField, } from '../shared/frontmatter.js';
|
|
4
7
|
import { readGlobalOptions } from '../shared/shared.utils.js';
|
|
5
8
|
import { BUILTIN_EXEC_TARGETS, readOatConfig, readOatLocalConfig, readUserConfig, writeOatConfig, writeOatLocalConfig, writeUserConfig, } from '../../config/oat-config.js';
|
|
6
9
|
import { resolveEffectiveConfig, resolveExecTargets, resolveGate, } from '../../config/resolve.js';
|
|
7
|
-
import {
|
|
10
|
+
import { dirExists, fileExists } from '../../fs/io.js';
|
|
11
|
+
import { normalizeToPosixPath, resolveProjectRoot } from '../../fs/paths.js';
|
|
8
12
|
import { Command } from 'commander';
|
|
13
|
+
import { parseReviewGateVerdict, } from './review-verdict.js';
|
|
9
14
|
const DEFAULT_DEPENDENCIES = {
|
|
10
15
|
buildCommandContext,
|
|
11
16
|
resolveProjectRoot,
|
|
@@ -29,6 +34,16 @@ const VALID_CROSS_PROVIDER_AVOIDS = [
|
|
|
29
34
|
'same-runtime',
|
|
30
35
|
'none',
|
|
31
36
|
];
|
|
37
|
+
const VALID_REVIEW_GATE_THRESHOLDS = [
|
|
38
|
+
'critical',
|
|
39
|
+
'important',
|
|
40
|
+
'medium',
|
|
41
|
+
'minor',
|
|
42
|
+
];
|
|
43
|
+
const REVIEW_GATE_CONTEXT_NOTE = 'This review is gate-originated. If you run `oat-project-review-provide`, set `oat_review_invocation: gate` in the review artifact.';
|
|
44
|
+
function reviewGateProjectContext(projectPath) {
|
|
45
|
+
return `Resolved OAT project path: ${projectPath}. Run the review for this project path.`;
|
|
46
|
+
}
|
|
32
47
|
async function runChildProcess(command, args, options) {
|
|
33
48
|
return new Promise((resolve, reject) => {
|
|
34
49
|
const child = spawn(command, args, {
|
|
@@ -90,6 +105,13 @@ function parseCrossProviderAvoid(value) {
|
|
|
90
105
|
}
|
|
91
106
|
return avoid;
|
|
92
107
|
}
|
|
108
|
+
function parseReviewGateThreshold(value) {
|
|
109
|
+
const threshold = value?.trim() || 'important';
|
|
110
|
+
if (!VALID_REVIEW_GATE_THRESHOLDS.includes(threshold)) {
|
|
111
|
+
throw new Error('--exit-nonzero-on must be one of critical | important | medium | minor.');
|
|
112
|
+
}
|
|
113
|
+
return threshold;
|
|
114
|
+
}
|
|
93
115
|
function parseOnFailure(value) {
|
|
94
116
|
if (!value || !VALID_ON_FAILURE.includes(value)) {
|
|
95
117
|
throw new Error('--on-failure must be one of block | prompt | warn.');
|
|
@@ -149,6 +171,15 @@ function parseGateConfig(options) {
|
|
|
149
171
|
: {}),
|
|
150
172
|
};
|
|
151
173
|
}
|
|
174
|
+
function detectDevBuildGateCommandWarnings(command) {
|
|
175
|
+
const normalized = command.trim();
|
|
176
|
+
if (!/^node\s+(?:"[^"]*\/packages\/cli\/dist\/index\.js"|'[^']*\/packages\/cli\/dist\/index\.js'|\S*\/packages\/cli\/dist\/index\.js)\s+gate(?:\s|$)/.test(normalized)) {
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
return [
|
|
180
|
+
'Durable docs/config should reference `oat gate ...`; absolute dev-build paths are reserved for local development of unmerged behavior.',
|
|
181
|
+
];
|
|
182
|
+
}
|
|
152
183
|
function parseExecTargetConfig(options) {
|
|
153
184
|
if (options.disable) {
|
|
154
185
|
return null;
|
|
@@ -299,6 +330,23 @@ async function selectAvailableExecTarget(registry, currentRuntime, avoid, contex
|
|
|
299
330
|
}
|
|
300
331
|
return null;
|
|
301
332
|
}
|
|
333
|
+
async function resolveSelectedExecTarget(targets, options, context, dependencies) {
|
|
334
|
+
const explicitTarget = options.target?.trim();
|
|
335
|
+
if (explicitTarget) {
|
|
336
|
+
const target = targets[explicitTarget];
|
|
337
|
+
if (!target) {
|
|
338
|
+
throw new Error(`Unknown exec target "${explicitTarget}".`);
|
|
339
|
+
}
|
|
340
|
+
return { id: explicitTarget, target: cloneExecTarget(target) };
|
|
341
|
+
}
|
|
342
|
+
const avoid = parseCrossProviderAvoid(options.avoid);
|
|
343
|
+
const currentRuntime = await resolveCurrentRuntime(options.currentRuntime, context, dependencies);
|
|
344
|
+
const selected = await selectAvailableExecTarget(targets, currentRuntime, avoid, context, dependencies);
|
|
345
|
+
if (!selected) {
|
|
346
|
+
throw new Error(noEligibleTargetMessage(currentRuntime, avoid));
|
|
347
|
+
}
|
|
348
|
+
return selected;
|
|
349
|
+
}
|
|
302
350
|
async function executeTarget(selected, prompt, context, dependencies) {
|
|
303
351
|
const [command, baseArgs] = argvHead(selected.target.baseCommand);
|
|
304
352
|
if (!command) {
|
|
@@ -321,6 +369,217 @@ async function executeTarget(selected, prompt, context, dependencies) {
|
|
|
321
369
|
function noEligibleTargetMessage(currentRuntime, avoid) {
|
|
322
370
|
return `No eligible gate exec target found for current runtime "${currentRuntime}" with --avoid ${avoid}. Install or configure an alternate runtime, rerun with --avoid none, or pin a target with --target <id>.`;
|
|
323
371
|
}
|
|
372
|
+
function normalizeRepoRelativeProjectPath(repoRoot, pathValue) {
|
|
373
|
+
const trimmed = pathValue.trim().replace(/\/+$/, '');
|
|
374
|
+
const withoutState = trimmed.endsWith('/state.md')
|
|
375
|
+
? trimmed.slice(0, -'/state.md'.length)
|
|
376
|
+
: trimmed;
|
|
377
|
+
const repoRelative = isAbsolute(withoutState)
|
|
378
|
+
? relative(repoRoot, withoutState)
|
|
379
|
+
: withoutState;
|
|
380
|
+
const normalized = normalizeToPosixPath(repoRelative).replace(/^\.\//, '');
|
|
381
|
+
if (normalized === '..' || normalized.startsWith('../')) {
|
|
382
|
+
throw new Error(`Project path must resolve inside the current repository: ${pathValue}`);
|
|
383
|
+
}
|
|
384
|
+
return normalized;
|
|
385
|
+
}
|
|
386
|
+
async function listProjectCandidates(repoRoot, projectsRoot) {
|
|
387
|
+
const normalizedProjectsRoot = normalizeRepoRelativeProjectPath(repoRoot, projectsRoot);
|
|
388
|
+
const absoluteProjectsRoot = join(repoRoot, normalizedProjectsRoot);
|
|
389
|
+
let scopeEntries;
|
|
390
|
+
try {
|
|
391
|
+
scopeEntries = await readdir(absoluteProjectsRoot, { withFileTypes: true });
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
if (typeof error === 'object' &&
|
|
395
|
+
error !== null &&
|
|
396
|
+
'code' in error &&
|
|
397
|
+
error.code === 'ENOENT') {
|
|
398
|
+
return [];
|
|
399
|
+
}
|
|
400
|
+
throw error;
|
|
401
|
+
}
|
|
402
|
+
const candidates = (await Promise.all(scopeEntries
|
|
403
|
+
.filter((entry) => entry.isDirectory())
|
|
404
|
+
.map(async (scopeEntry) => {
|
|
405
|
+
const directProjectPath = normalizeToPosixPath(join(normalizedProjectsRoot, scopeEntry.name));
|
|
406
|
+
if (await fileExists(join(repoRoot, directProjectPath, 'state.md'))) {
|
|
407
|
+
return [directProjectPath];
|
|
408
|
+
}
|
|
409
|
+
const scopePath = join(absoluteProjectsRoot, scopeEntry.name);
|
|
410
|
+
const projectEntries = await readdir(scopePath, {
|
|
411
|
+
withFileTypes: true,
|
|
412
|
+
});
|
|
413
|
+
return Promise.all(projectEntries
|
|
414
|
+
.filter((entry) => entry.isDirectory())
|
|
415
|
+
.map(async (projectEntry) => {
|
|
416
|
+
const projectPath = normalizeToPosixPath(join(normalizedProjectsRoot, scopeEntry.name, projectEntry.name));
|
|
417
|
+
return (await fileExists(join(repoRoot, projectPath, 'state.md')))
|
|
418
|
+
? projectPath
|
|
419
|
+
: null;
|
|
420
|
+
}));
|
|
421
|
+
})))
|
|
422
|
+
.flat()
|
|
423
|
+
.filter((candidate) => candidate !== null)
|
|
424
|
+
.sort();
|
|
425
|
+
return candidates;
|
|
426
|
+
}
|
|
427
|
+
async function assertProjectPath(repoRoot, projectPath, source) {
|
|
428
|
+
const normalizedPath = normalizeRepoRelativeProjectPath(repoRoot, projectPath);
|
|
429
|
+
const absolutePath = join(repoRoot, normalizedPath);
|
|
430
|
+
if (!(await dirExists(absolutePath)) ||
|
|
431
|
+
!(await fileExists(join(absolutePath, 'state.md')))) {
|
|
432
|
+
throw new Error(`${source} project "${projectPath}" does not resolve to a project directory containing state.md.`);
|
|
433
|
+
}
|
|
434
|
+
return normalizedPath;
|
|
435
|
+
}
|
|
436
|
+
async function resolveExplicitReviewProject(repoRoot, projectsRoot, projectValue) {
|
|
437
|
+
const trimmed = projectValue.trim();
|
|
438
|
+
if (!trimmed) {
|
|
439
|
+
throw new Error('--project must be a non-empty project path or name.');
|
|
440
|
+
}
|
|
441
|
+
const looksLikePath = trimmed.includes('/') || trimmed.startsWith('.') || isAbsolute(trimmed);
|
|
442
|
+
if (looksLikePath) {
|
|
443
|
+
return assertProjectPath(repoRoot, trimmed, '--project');
|
|
444
|
+
}
|
|
445
|
+
const candidates = await listProjectCandidates(repoRoot, projectsRoot);
|
|
446
|
+
const matches = candidates.filter((candidate) => basename(candidate) === trimmed);
|
|
447
|
+
if (matches.length === 1) {
|
|
448
|
+
return matches[0];
|
|
449
|
+
}
|
|
450
|
+
if (matches.length > 1) {
|
|
451
|
+
throw new Error(`Multiple OAT projects match --project "${trimmed}": ${matches.join(', ')}. Pass a full project path instead.`);
|
|
452
|
+
}
|
|
453
|
+
return assertProjectPath(repoRoot, `${projectsRoot}/${trimmed}`, '--project');
|
|
454
|
+
}
|
|
455
|
+
async function resolveReviewProject(options) {
|
|
456
|
+
const projectsRoot = String(options.effective.resolved['projects.root']?.value ??
|
|
457
|
+
options.effective.shared.projects?.root ??
|
|
458
|
+
'.oat/projects/shared');
|
|
459
|
+
if (options.project !== undefined) {
|
|
460
|
+
return resolveExplicitReviewProject(options.repoRoot, projectsRoot, options.project);
|
|
461
|
+
}
|
|
462
|
+
const activeProject = options.effective.local.activeProject?.trim();
|
|
463
|
+
if (activeProject) {
|
|
464
|
+
return assertProjectPath(options.repoRoot, activeProject, 'Active');
|
|
465
|
+
}
|
|
466
|
+
const candidates = await listProjectCandidates(options.repoRoot, projectsRoot);
|
|
467
|
+
if (candidates.length === 0) {
|
|
468
|
+
throw new Error('No OAT project could be resolved for gate review. Set an active project or pass --project <path-or-name>.');
|
|
469
|
+
}
|
|
470
|
+
if (candidates.length > 1) {
|
|
471
|
+
throw new Error(`Multiple OAT projects could be resolved for gate review: ${candidates.join(', ')}. Pass --project <path-or-name>.`);
|
|
472
|
+
}
|
|
473
|
+
return candidates[0];
|
|
474
|
+
}
|
|
475
|
+
function reviewGateLifecycleRank(scope) {
|
|
476
|
+
const normalizedScope = scope.trim().toLowerCase();
|
|
477
|
+
if (normalizedScope === 'final') {
|
|
478
|
+
return Number.MAX_SAFE_INTEGER;
|
|
479
|
+
}
|
|
480
|
+
const phases = [...normalizedScope.matchAll(/p(\d+)/g)].map((match) => Number.parseInt(match[1] ?? '0', 10));
|
|
481
|
+
if (phases.length === 0) {
|
|
482
|
+
return 0;
|
|
483
|
+
}
|
|
484
|
+
const tasks = [...normalizedScope.matchAll(/(?:^|[-_])t(\d+)/g)].map((match) => Number.parseInt(match[1] ?? '0', 10));
|
|
485
|
+
const latestPhase = Math.max(...phases);
|
|
486
|
+
const latestTask = tasks.length > 0 ? Math.max(...tasks) : 9999;
|
|
487
|
+
return latestPhase * 10_000 + latestTask;
|
|
488
|
+
}
|
|
489
|
+
async function readReviewGateArtifactCandidate(repoRoot, relativePath) {
|
|
490
|
+
const content = await readFile(join(repoRoot, relativePath), 'utf8');
|
|
491
|
+
const frontmatter = getFrontmatterBlock(content);
|
|
492
|
+
if (!frontmatter) {
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
const generatedAt = getFrontmatterField(frontmatter, 'oat_generated_at');
|
|
496
|
+
if (!generatedAt) {
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
const generatedTime = Date.parse(generatedAt);
|
|
500
|
+
if (Number.isNaN(generatedTime)) {
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
const scope = getFrontmatterField(frontmatter, 'oat_review_scope') ?? '';
|
|
504
|
+
return {
|
|
505
|
+
path: relativePath,
|
|
506
|
+
scope,
|
|
507
|
+
generatedAt,
|
|
508
|
+
kind: 'project',
|
|
509
|
+
archived: false,
|
|
510
|
+
actionable: true,
|
|
511
|
+
generatedTime,
|
|
512
|
+
lifecycleRank: reviewGateLifecycleRank(scope),
|
|
513
|
+
signature: createHash('sha256').update(content).digest('hex'),
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
function sortReviewGateArtifacts(left, right) {
|
|
517
|
+
if (left.generatedTime !== right.generatedTime) {
|
|
518
|
+
return right.generatedTime - left.generatedTime;
|
|
519
|
+
}
|
|
520
|
+
if (left.lifecycleRank !== right.lifecycleRank) {
|
|
521
|
+
return right.lifecycleRank - left.lifecycleRank;
|
|
522
|
+
}
|
|
523
|
+
return left.path.localeCompare(right.path);
|
|
524
|
+
}
|
|
525
|
+
async function listActiveProjectReviewCandidates(options) {
|
|
526
|
+
const projectPath = normalizeRepoRelativeProjectPath(options.repoRoot, options.projectPath);
|
|
527
|
+
const reviewsDir = `${projectPath}/reviews`;
|
|
528
|
+
let entries;
|
|
529
|
+
try {
|
|
530
|
+
entries = await readdir(join(options.repoRoot, reviewsDir), {
|
|
531
|
+
withFileTypes: true,
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
catch (error) {
|
|
535
|
+
if (typeof error === 'object' &&
|
|
536
|
+
error !== null &&
|
|
537
|
+
'code' in error &&
|
|
538
|
+
error.code === 'ENOENT') {
|
|
539
|
+
return [];
|
|
540
|
+
}
|
|
541
|
+
throw error;
|
|
542
|
+
}
|
|
543
|
+
return (await Promise.all(entries
|
|
544
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
|
|
545
|
+
.map((entry) => readReviewGateArtifactCandidate(options.repoRoot, normalizeToPosixPath(join(reviewsDir, entry.name))))))
|
|
546
|
+
.filter((candidate) => candidate !== null)
|
|
547
|
+
.sort(sortReviewGateArtifacts);
|
|
548
|
+
}
|
|
549
|
+
function findProducedReviewArtifact(before, after) {
|
|
550
|
+
const beforeSignatures = new Map(before.map((candidate) => [candidate.path, candidate.signature]));
|
|
551
|
+
return (after.find((candidate) => beforeSignatures.get(candidate.path) !== candidate.signature) ?? null);
|
|
552
|
+
}
|
|
553
|
+
function reviewBlocksAtThreshold(verdict, threshold) {
|
|
554
|
+
if (verdict.counts.critical > 0) {
|
|
555
|
+
return true;
|
|
556
|
+
}
|
|
557
|
+
if (threshold === 'critical') {
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
if (verdict.counts.important > 0) {
|
|
561
|
+
return true;
|
|
562
|
+
}
|
|
563
|
+
if (threshold === 'important') {
|
|
564
|
+
return false;
|
|
565
|
+
}
|
|
566
|
+
if (verdict.counts.medium > 0) {
|
|
567
|
+
return true;
|
|
568
|
+
}
|
|
569
|
+
if (threshold === 'medium') {
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
return verdict.counts.minor > 0;
|
|
573
|
+
}
|
|
574
|
+
function writeReviewGateResult(context, payload) {
|
|
575
|
+
if (context.json) {
|
|
576
|
+
context.logger.json(payload);
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
context.logger.info(`Review artifact: ${payload.artifactPath}`);
|
|
580
|
+
context.logger.info(`Verdict: ${payload.status} (critical=${payload.counts.critical}, important=${payload.counts.important}, medium=${payload.counts.medium}, minor=${payload.counts.minor})`);
|
|
581
|
+
context.logger.info(payload.handoff);
|
|
582
|
+
}
|
|
324
583
|
async function updateConfigLayer(context, layer, dependencies, mutate) {
|
|
325
584
|
const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
|
|
326
585
|
const userConfigDir = join(context.home, '.oat');
|
|
@@ -352,8 +611,21 @@ async function runGateSet(skillName, options, context, dependencies) {
|
|
|
352
611
|
const layer = parseLayer(options.layer);
|
|
353
612
|
const normalizedSkill = trimRequired(skillName, '<skill>');
|
|
354
613
|
const gate = parseGateConfig(options);
|
|
614
|
+
const warnings = gate
|
|
615
|
+
? detectDevBuildGateCommandWarnings(gate.command)
|
|
616
|
+
: [];
|
|
355
617
|
await updateConfigLayer(context, layer, dependencies, (config) => setSkillGate(config, normalizedSkill, gate));
|
|
356
|
-
|
|
618
|
+
if (!context.json) {
|
|
619
|
+
for (const warning of warnings) {
|
|
620
|
+
context.logger.warn(warning);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
writeSuccess(context, {
|
|
624
|
+
layer,
|
|
625
|
+
skill: normalizedSkill,
|
|
626
|
+
gate,
|
|
627
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
628
|
+
});
|
|
357
629
|
process.exitCode = 0;
|
|
358
630
|
}
|
|
359
631
|
catch (error) {
|
|
@@ -401,22 +673,71 @@ async function runCrossProviderExec(prompt, options, context, dependencies) {
|
|
|
401
673
|
try {
|
|
402
674
|
const effective = await readEffectiveConfig(context, dependencies);
|
|
403
675
|
const targets = resolveExecTargets(effective);
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
676
|
+
const selected = await resolveSelectedExecTarget(targets, options, context, dependencies);
|
|
677
|
+
process.exitCode = await executeTarget(selected, prompt, context, dependencies);
|
|
678
|
+
}
|
|
679
|
+
catch (error) {
|
|
680
|
+
writeError(context, error);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
async function runReviewGate(prompt, options, context, dependencies) {
|
|
684
|
+
try {
|
|
685
|
+
const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
|
|
686
|
+
const userConfigDir = join(context.home, '.oat');
|
|
687
|
+
const effective = await dependencies.resolveEffectiveConfig(repoRoot, userConfigDir, dependencies.processEnv);
|
|
688
|
+
const projectPath = await resolveReviewProject({
|
|
689
|
+
repoRoot,
|
|
690
|
+
effective,
|
|
691
|
+
project: options.project,
|
|
692
|
+
});
|
|
693
|
+
const targets = resolveExecTargets(effective);
|
|
694
|
+
const selected = await resolveSelectedExecTarget(targets, options, context, dependencies);
|
|
695
|
+
const threshold = parseReviewGateThreshold(options.exitNonzeroOn);
|
|
696
|
+
const before = await listActiveProjectReviewCandidates({
|
|
697
|
+
repoRoot,
|
|
698
|
+
projectPath,
|
|
699
|
+
});
|
|
700
|
+
const reviewPrompt = [
|
|
701
|
+
REVIEW_GATE_CONTEXT_NOTE,
|
|
702
|
+
reviewGateProjectContext(projectPath),
|
|
703
|
+
...(options.reviewType?.trim()
|
|
704
|
+
? [`Review type: ${options.reviewType.trim()}.`]
|
|
705
|
+
: []),
|
|
706
|
+
...(options.reviewScope?.trim()
|
|
707
|
+
? [`Review scope: ${options.reviewScope.trim()}.`]
|
|
708
|
+
: []),
|
|
709
|
+
...prompt,
|
|
710
|
+
];
|
|
711
|
+
const childExitCode = await executeTarget(selected, reviewPrompt, context, dependencies);
|
|
712
|
+
if (childExitCode !== 0) {
|
|
713
|
+
process.exitCode = childExitCode;
|
|
411
714
|
return;
|
|
412
715
|
}
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
716
|
+
const after = await listActiveProjectReviewCandidates({
|
|
717
|
+
repoRoot,
|
|
718
|
+
projectPath,
|
|
719
|
+
});
|
|
720
|
+
const producedArtifact = findProducedReviewArtifact(before, after);
|
|
721
|
+
if (!producedArtifact) {
|
|
722
|
+
throw new Error(`No new review artifact was detected for project ${projectPath}. Ensure the review provider wrote a project review artifact before the gate exits.`);
|
|
418
723
|
}
|
|
419
|
-
|
|
724
|
+
const verdict = await parseReviewGateVerdict(join(repoRoot, producedArtifact.path));
|
|
725
|
+
const blocking = reviewBlocksAtThreshold(verdict, threshold);
|
|
726
|
+
const handoff = `Run oat-project-review-receive for ${producedArtifact.path} before treating this gate review as consumed.`;
|
|
727
|
+
writeReviewGateResult(context, {
|
|
728
|
+
status: blocking ? 'blocked' : 'ok',
|
|
729
|
+
target: selected.id,
|
|
730
|
+
project: projectPath,
|
|
731
|
+
artifactPath: producedArtifact.path,
|
|
732
|
+
threshold,
|
|
733
|
+
blocking,
|
|
734
|
+
counts: verdict.counts,
|
|
735
|
+
reviewType: verdict.reviewType,
|
|
736
|
+
scope: verdict.scope,
|
|
737
|
+
invocation: verdict.invocation,
|
|
738
|
+
handoff,
|
|
739
|
+
});
|
|
740
|
+
process.exitCode = blocking ? 1 : 0;
|
|
420
741
|
}
|
|
421
742
|
catch (error) {
|
|
422
743
|
writeError(context, error);
|
|
@@ -470,6 +791,21 @@ export function createGateCommand(overrides = {}) {
|
|
|
470
791
|
const context = dependencies.buildCommandContext(readGlobalOptions(command));
|
|
471
792
|
await runCrossProviderExec(prompt, options, context, dependencies);
|
|
472
793
|
});
|
|
794
|
+
cmd
|
|
795
|
+
.command('review')
|
|
796
|
+
.description('Run a review gate and map review findings to exit status')
|
|
797
|
+
.option('--target <id>', 'Run this exact exec target')
|
|
798
|
+
.option('--avoid <mode>', 'Avoidance mode: same-runtime or none')
|
|
799
|
+
.option('--current-runtime <runtime>', 'Override detected runtime for testing or manual routing')
|
|
800
|
+
.option('--project <path-or-name>', 'Project path or name to review; defaults to the active project')
|
|
801
|
+
.option('--review-scope <scope>', 'Review scope hint for the provider')
|
|
802
|
+
.option('--review-type <type>', 'Review type hint for the provider')
|
|
803
|
+
.option('--exit-nonzero-on <severity>', 'Lowest severity that exits nonzero: critical, important, medium, or minor')
|
|
804
|
+
.argument('<prompt...>', 'Review prompt arguments appended to the target command')
|
|
805
|
+
.action(async (prompt, options, command) => {
|
|
806
|
+
const context = dependencies.buildCommandContext(readGlobalOptions(command));
|
|
807
|
+
await runReviewGate(prompt, options, context, dependencies);
|
|
808
|
+
});
|
|
473
809
|
const target = new Command('target').description('Manage gate execution targets');
|
|
474
810
|
target
|
|
475
811
|
.command('set')
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface ReviewGateVerdict {
|
|
2
|
+
artifactPath: string;
|
|
3
|
+
reviewType: 'code' | 'artifact' | 'unknown';
|
|
4
|
+
scope: string | null;
|
|
5
|
+
invocation: string | null;
|
|
6
|
+
counts: {
|
|
7
|
+
critical: number;
|
|
8
|
+
important: number;
|
|
9
|
+
medium: number;
|
|
10
|
+
minor: number;
|
|
11
|
+
};
|
|
12
|
+
blocking: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function parseReviewGateVerdict(artifactPath: string): Promise<ReviewGateVerdict>;
|
|
15
|
+
//# sourceMappingURL=review-verdict.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"review-verdict.d.ts","sourceRoot":"","sources":["../../../src/commands/gate/review-verdict.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAAC;IAC5C,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,MAAM,EAAE;QACN,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,MAAM,CAAC;KACf,CAAC;IACF,QAAQ,EAAE,OAAO,CAAC;CACnB;AA2ND,wBAAsB,sBAAsB,CAC1C,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,iBAAiB,CAAC,CAmC5B"}
|