@kestrel-agents/ruhroh 0.5.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +114 -0
- package/assets/ruhroh-badge.png +0 -0
- package/assets/ruhroh-logo.png +0 -0
- package/dist/adapters.d.ts +97 -0
- package/dist/adapters.d.ts.map +1 -0
- package/dist/adapters.js +21 -0
- package/dist/adapters.js.map +1 -0
- package/dist/builtin-scenarios.d.ts +8 -0
- package/dist/builtin-scenarios.d.ts.map +1 -0
- package/dist/builtin-scenarios.js +22 -0
- package/dist/builtin-scenarios.js.map +1 -0
- package/dist/cli.d.ts +30 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +313 -0
- package/dist/cli.js.map +1 -0
- package/dist/env.d.ts +6 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +66 -0
- package/dist/env.js.map +1 -0
- package/dist/generate.d.ts +32 -0
- package/dist/generate.d.ts.map +1 -0
- package/dist/generate.js +231 -0
- package/dist/generate.js.map +1 -0
- package/dist/harbor.d.ts +28 -0
- package/dist/harbor.d.ts.map +1 -0
- package/dist/harbor.js +47 -0
- package/dist/harbor.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/results.d.ts +66 -0
- package/dist/results.d.ts.map +1 -0
- package/dist/results.js +31 -0
- package/dist/results.js.map +1 -0
- package/dist/scenarios.d.ts +61 -0
- package/dist/scenarios.d.ts.map +1 -0
- package/dist/scenarios.js +69 -0
- package/dist/scenarios.js.map +1 -0
- package/package.json +66 -0
- package/python/ruhroh/__init__.py +5 -0
- package/python/ruhroh/harbor_agent.py +345 -0
- package/python/ruhroh/loop_controller.py +783 -0
- package/python/ruhroh/setup.sh +12 -0
- package/scenarios/grocery-budget-planner/instruction.md +1 -0
- package/scenarios/grocery-budget-planner/scenario.json +44 -0
- package/scenarios/nextjs-task-board/instruction.md +1 -0
- package/scenarios/nextjs-task-board/scenario.json +45 -0
- package/scenarios/shift-coverage-planner/assets/prompt-assets/shift-coverage/coverage-rules.json +29 -0
- package/scenarios/shift-coverage-planner/assets/prompt-assets/shift-coverage/employees.csv +8 -0
- package/scenarios/shift-coverage-planner/assets/prompt-assets/shift-coverage/existing-schedule.csv +9 -0
- package/scenarios/shift-coverage-planner/assets/prompt-assets/shift-coverage/shift-requirements.csv +8 -0
- package/scenarios/shift-coverage-planner/assets/prompt-assets/shift-coverage/time-off-requests.csv +5 -0
- package/scenarios/shift-coverage-planner/instruction.md +1 -0
- package/scenarios/shift-coverage-planner/scenario.json +47 -0
- package/scenarios/simple-newsletter/instruction.md +1 -0
- package/scenarios/simple-newsletter/scenario.json +40 -0
- package/scenarios/vite-csv-reconciliation/assets/prompt-assets/csv-reconciliation-people/source-a.csv +9 -0
- package/scenarios/vite-csv-reconciliation/assets/prompt-assets/csv-reconciliation-people/source-b.csv +9 -0
- package/scenarios/vite-csv-reconciliation/instruction.md +1 -0
- package/scenarios/vite-csv-reconciliation/scenario.json +48 -0
- package/scenarios/vite-sprint-planner/instruction.md +1 -0
- package/scenarios/vite-sprint-planner/scenario.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kestrel-agents
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/ruhroh-logo.png" alt="Ruhroh logo" width="220">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# <img src="assets/ruhroh-badge.png" alt="" width="28" align="absmiddle"> Ruhroh
|
|
6
|
+
|
|
7
|
+
Ruhroh is the **Real-User Harness for Repair-Oriented Harbor**.
|
|
8
|
+
|
|
9
|
+
Ruhroh runs real-user task scenarios against coding agents through adapters,
|
|
10
|
+
preserves the full implementation journey, and runs a terminal evaluator over
|
|
11
|
+
the final delivered workspace.
|
|
12
|
+
|
|
13
|
+
Ruhroh is agent-agnostic. Kestrel is one reference run-agent adapter, not the
|
|
14
|
+
benchmark itself. Harbor is the execution substrate.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pnpm add -D @kestrel-agents/ruhroh
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quickstart
|
|
23
|
+
|
|
24
|
+
Create scenarios under `ruhroh/scenarios/<id>/`, or use the bundled scenarios
|
|
25
|
+
under `node_modules/@kestrel-agents/ruhroh/scenarios`, then run:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pnpm ruhroh --list
|
|
29
|
+
pnpm ruhroh --scenario simple-newsletter --generate-only
|
|
30
|
+
pnpm ruhroh --scenario simple-newsletter --adapter ./path/to/agent-adapter --dry-run
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
In this repo, the same package CLI is available with:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pnpm ruhroh --scenario-dir examples/scenarios --list
|
|
37
|
+
pnpm ruhroh --scenario-dir examples/scenarios --scenario simple-newsletter --generate-only
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
This package currently contains the portable TypeScript surfaces:
|
|
41
|
+
|
|
42
|
+
- scenario schema and validation;
|
|
43
|
+
- run-agent adapter interfaces and capability compatibility helpers;
|
|
44
|
+
- eval and final result types;
|
|
45
|
+
- verdict mapping helpers;
|
|
46
|
+
- env forwarding and redaction helpers;
|
|
47
|
+
- Harbor command construction helpers;
|
|
48
|
+
- JSON scenario discovery and Harbor task generation helpers.
|
|
49
|
+
|
|
50
|
+
## Scenario Generation
|
|
51
|
+
|
|
52
|
+
Ruhroh can load JSON scenarios from:
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
ruhroh/scenarios/<id>/
|
|
56
|
+
scenario.json
|
|
57
|
+
instruction.md
|
|
58
|
+
assets/
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
and generate local Harbor task directories under:
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
.generated/ruhroh/harbor/tasks/<scenario-id>/
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The generated Harbor verifier is app-agnostic. It only validates that the
|
|
68
|
+
structured Ruhroh result exists and maps to a passing score/reward; it does not
|
|
69
|
+
perform required-file, route, command, or source-text checks.
|
|
70
|
+
|
|
71
|
+
The public API exports:
|
|
72
|
+
|
|
73
|
+
- `discoverRuhrohScenarios()`
|
|
74
|
+
- `loadRuhrohScenario()`
|
|
75
|
+
- `generateHarborTask()`
|
|
76
|
+
- `generateHarborDataset()`
|
|
77
|
+
|
|
78
|
+
The package CLI exposes this through:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
pnpm ruhroh --scenario-dir ruhroh/scenarios --generate-only
|
|
82
|
+
pnpm ruhroh --scenario-dir ruhroh/scenarios --dry-run
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
This package ships the reusable scenario source under `scenarios/` and the
|
|
86
|
+
package-owned Python Harbor runtime under `python/ruhroh`. Run-agents are wired
|
|
87
|
+
into this runtime as command adapters through
|
|
88
|
+
`RUHROH_RUN_AGENT_COMMAND`; terminal evaluation can be supplied through
|
|
89
|
+
`RUHROH_EVAL_COMMAND` or fixture eval variables.
|
|
90
|
+
|
|
91
|
+
Kestrel is a consumer adapter, not a Ruhroh package dependency. The Harbor
|
|
92
|
+
harness is package-owned for generated Ruhroh tasks.
|
|
93
|
+
|
|
94
|
+
## Docs
|
|
95
|
+
|
|
96
|
+
- Architecture: `docs/architecture.md`
|
|
97
|
+
- Scenario format: `docs/scenario-format.md`
|
|
98
|
+
- Adapter protocol: `docs/adapter-protocol.md`
|
|
99
|
+
- Custom-shell adapter: `docs/custom-shell.md`
|
|
100
|
+
- Harbor: `docs/harbor.md`
|
|
101
|
+
- Eval-agent: `docs/eval-agent.md`
|
|
102
|
+
- Artifacts: `docs/artifacts.md`
|
|
103
|
+
- CI: `docs/ci.md`
|
|
104
|
+
- Security: `docs/security.md`
|
|
105
|
+
- Limitations: `docs/limitations.md`
|
|
106
|
+
- Public repo layout: `docs/public-repo-layout.md`
|
|
107
|
+
|
|
108
|
+
## Security
|
|
109
|
+
|
|
110
|
+
Scenario prompts and assets are untrusted input. Run-agents should mutate only
|
|
111
|
+
benchmark workspaces. Eval-agent inspection should happen against a copied
|
|
112
|
+
workspace. Secrets must pass through allowlisted environment variables, and
|
|
113
|
+
dry-run output must never print secret values. Generated Harbor verifiers do
|
|
114
|
+
not perform app-goal checks.
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
export type RuhrohContinuityLevel = "native_session" | "workspace_plus_transcript" | "workspace_only";
|
|
2
|
+
export type RuhrohCompletionConfidence = "explicit" | "adapter_inferred";
|
|
3
|
+
export type RuhrohCompletionDoneReason = "goal_satisfied";
|
|
4
|
+
export type RuhrohCompletionNotDoneReason = "agent_requested_continue" | "missing_completion_signal" | "context_checkpoint" | "partial_progress";
|
|
5
|
+
export type RuhrohCompletionTerminalFailureReason = "cannot_satisfy" | "policy_blocked" | "out_of_scope" | "runtime_failure" | "infra_failure";
|
|
6
|
+
export interface RuhrohEvidenceRef {
|
|
7
|
+
kind: string;
|
|
8
|
+
ref: string;
|
|
9
|
+
summary: string;
|
|
10
|
+
}
|
|
11
|
+
export type RuhrohCompletionStatus = {
|
|
12
|
+
state: "done";
|
|
13
|
+
reason: RuhrohCompletionDoneReason;
|
|
14
|
+
confidence: RuhrohCompletionConfidence;
|
|
15
|
+
evidenceRefs: RuhrohEvidenceRef[];
|
|
16
|
+
} | {
|
|
17
|
+
state: "not_done";
|
|
18
|
+
reason: RuhrohCompletionNotDoneReason;
|
|
19
|
+
evidenceRefs: RuhrohEvidenceRef[];
|
|
20
|
+
} | {
|
|
21
|
+
state: "terminal_failure";
|
|
22
|
+
reason: RuhrohCompletionTerminalFailureReason;
|
|
23
|
+
evidenceRefs: RuhrohEvidenceRef[];
|
|
24
|
+
};
|
|
25
|
+
export interface PrepareRunAgentInput {
|
|
26
|
+
scenarioId: string;
|
|
27
|
+
workspacePath: string;
|
|
28
|
+
runRootPath: string;
|
|
29
|
+
}
|
|
30
|
+
export interface PrepareRunAgentResult {
|
|
31
|
+
artifactPaths: Record<string, string>;
|
|
32
|
+
}
|
|
33
|
+
export interface StartSessionInput {
|
|
34
|
+
scenarioId: string;
|
|
35
|
+
workspacePath: string;
|
|
36
|
+
}
|
|
37
|
+
export interface StartSessionResult {
|
|
38
|
+
sessionHandle: string;
|
|
39
|
+
artifactPaths: Record<string, string>;
|
|
40
|
+
}
|
|
41
|
+
export interface RunTurnInput {
|
|
42
|
+
iteration: number;
|
|
43
|
+
message: string;
|
|
44
|
+
workspacePath: string;
|
|
45
|
+
}
|
|
46
|
+
export interface RunTurnResult {
|
|
47
|
+
iteration: number;
|
|
48
|
+
status: "completed" | "failed" | "timeout";
|
|
49
|
+
failureKind?: string | undefined;
|
|
50
|
+
runId?: string | undefined;
|
|
51
|
+
threadId?: string | undefined;
|
|
52
|
+
sessionHandle: string;
|
|
53
|
+
transcriptPath?: string | undefined;
|
|
54
|
+
eventLogPath?: string | undefined;
|
|
55
|
+
artifactPaths: Record<string, string>;
|
|
56
|
+
raw: unknown;
|
|
57
|
+
}
|
|
58
|
+
export interface DetectCompletionInput {
|
|
59
|
+
turn: RunTurnResult;
|
|
60
|
+
}
|
|
61
|
+
export interface CollectArtifactsInput {
|
|
62
|
+
sessionHandle: string;
|
|
63
|
+
}
|
|
64
|
+
export interface RunAgentArtifactManifest {
|
|
65
|
+
adapterId: string;
|
|
66
|
+
continuityLevel: RuhrohContinuityLevel;
|
|
67
|
+
sessionHandle: string;
|
|
68
|
+
runIds: string[];
|
|
69
|
+
transcriptPaths: string[];
|
|
70
|
+
eventLogPaths: string[];
|
|
71
|
+
artifactPaths: Record<string, string>;
|
|
72
|
+
}
|
|
73
|
+
export interface CleanupInput {
|
|
74
|
+
sessionHandle: string;
|
|
75
|
+
}
|
|
76
|
+
export interface RuhrohRunAgentAdapter {
|
|
77
|
+
readonly id: string;
|
|
78
|
+
readonly continuityLevel: RuhrohContinuityLevel;
|
|
79
|
+
prepare(input: PrepareRunAgentInput): Promise<PrepareRunAgentResult>;
|
|
80
|
+
startSession(input: StartSessionInput): Promise<StartSessionResult>;
|
|
81
|
+
runTurn(input: RunTurnInput): Promise<RunTurnResult>;
|
|
82
|
+
detectCompletion(input: DetectCompletionInput): Promise<RuhrohCompletionStatus>;
|
|
83
|
+
collectArtifacts(input: CollectArtifactsInput): Promise<RunAgentArtifactManifest>;
|
|
84
|
+
cleanup?(input: CleanupInput): Promise<void>;
|
|
85
|
+
}
|
|
86
|
+
export interface RuhrohRunAgentAdapterCapabilities {
|
|
87
|
+
adapter: string;
|
|
88
|
+
continuity: RuhrohContinuityLevel;
|
|
89
|
+
tools: string[];
|
|
90
|
+
network: boolean;
|
|
91
|
+
}
|
|
92
|
+
export declare function adapterSatisfiesRequirements(adapter: RuhrohRunAgentAdapterCapabilities, requirements: {
|
|
93
|
+
continuity: RuhrohContinuityLevel;
|
|
94
|
+
tools: string[];
|
|
95
|
+
network: boolean;
|
|
96
|
+
}): string[];
|
|
97
|
+
//# sourceMappingURL=adapters.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapters.d.ts","sourceRoot":"","sources":["../src/adapters.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,qBAAqB,GAAG,gBAAgB,GAAG,2BAA2B,GAAG,gBAAgB,CAAC;AACtG,MAAM,MAAM,0BAA0B,GAAG,UAAU,GAAG,kBAAkB,CAAC;AACzE,MAAM,MAAM,0BAA0B,GAAG,gBAAgB,CAAC;AAC1D,MAAM,MAAM,6BAA6B,GACrC,0BAA0B,GAC1B,2BAA2B,GAC3B,oBAAoB,GACpB,kBAAkB,CAAC;AACvB,MAAM,MAAM,qCAAqC,GAC7C,gBAAgB,GAChB,gBAAgB,GAChB,cAAc,GACd,iBAAiB,GACjB,eAAe,CAAC;AAEpB,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,sBAAsB,GAC9B;IACE,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,0BAA0B,CAAC;IACnC,UAAU,EAAE,0BAA0B,CAAC;IACvC,YAAY,EAAE,iBAAiB,EAAE,CAAC;CACnC,GACD;IACE,KAAK,EAAE,UAAU,CAAC;IAClB,MAAM,EAAE,6BAA6B,CAAC;IACtC,YAAY,EAAE,iBAAiB,EAAE,CAAC;CACnC,GACD;IACE,KAAK,EAAE,kBAAkB,CAAC;IAC1B,MAAM,EAAE,qCAAqC,CAAC;IAC9C,YAAY,EAAE,iBAAiB,EAAE,CAAC;CACnC,CAAC;AAEN,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,qBAAqB;IACpC,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACvC;AAED,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,kBAAkB;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACvC;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,WAAW,GAAG,QAAQ,GAAG,SAAS,CAAC;IAC3C,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,GAAG,EAAE,OAAO,CAAC;CACd;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,aAAa,CAAC;CACrB;AAED,MAAM,WAAW,qBAAqB;IACpC,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,qBAAqB,CAAC;IACvC,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACvC;AAED,MAAM,WAAW,YAAY;IAC3B,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,eAAe,EAAE,qBAAqB,CAAC;IAEhD,OAAO,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAAC;IACrE,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACpE,OAAO,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IACrD,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAChF,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,wBAAwB,CAAC,CAAC;IAClF,OAAO,CAAC,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9C;AAED,MAAM,WAAW,iCAAiC;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,qBAAqB,CAAC;IAClC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;CAClB;AAQD,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,iCAAiC,EAC1C,YAAY,EAAE;IACZ,UAAU,EAAE,qBAAqB,CAAC;IAClC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;CAClB,GACA,MAAM,EAAE,CAcV"}
|
package/dist/adapters.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const CONTINUITY_RANK = {
|
|
2
|
+
workspace_only: 1,
|
|
3
|
+
workspace_plus_transcript: 2,
|
|
4
|
+
native_session: 3,
|
|
5
|
+
};
|
|
6
|
+
export function adapterSatisfiesRequirements(adapter, requirements) {
|
|
7
|
+
const errors = [];
|
|
8
|
+
if (CONTINUITY_RANK[adapter.continuity] < CONTINUITY_RANK[requirements.continuity]) {
|
|
9
|
+
errors.push(`adapter ${adapter.adapter} does not satisfy required continuity ${requirements.continuity}`);
|
|
10
|
+
}
|
|
11
|
+
for (const tool of requirements.tools) {
|
|
12
|
+
if (!adapter.tools.includes(tool)) {
|
|
13
|
+
errors.push(`adapter ${adapter.adapter} does not provide required tool ${tool}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (requirements.network && !adapter.network) {
|
|
17
|
+
errors.push(`adapter ${adapter.adapter} does not provide required network access`);
|
|
18
|
+
}
|
|
19
|
+
return errors;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=adapters.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapters.js","sourceRoot":"","sources":["../src/adapters.ts"],"names":[],"mappings":"AAuHA,MAAM,eAAe,GAA0C;IAC7D,cAAc,EAAE,CAAC;IACjB,yBAAyB,EAAE,CAAC;IAC5B,cAAc,EAAE,CAAC;CAClB,CAAC;AAEF,MAAM,UAAU,4BAA4B,CAC1C,OAA0C,EAC1C,YAIC;IAED,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,eAAe,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,eAAe,CAAC,YAAY,CAAC,UAAU,CAAC,EAAE,CAAC;QACnF,MAAM,CAAC,IAAI,CAAC,WAAW,OAAO,CAAC,OAAO,yCAAyC,YAAY,CAAC,UAAU,EAAE,CAAC,CAAC;IAC5G,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,YAAY,CAAC,KAAK,EAAE,CAAC;QACtC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YAClC,MAAM,CAAC,IAAI,CAAC,WAAW,OAAO,CAAC,OAAO,mCAAmC,IAAI,EAAE,CAAC,CAAC;QACnF,CAAC;IACH,CAAC;IACD,IAAI,YAAY,CAAC,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;QAC7C,MAAM,CAAC,IAAI,CAAC,WAAW,OAAO,CAAC,OAAO,2CAA2C,CAAC,CAAC;IACrF,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type LoadedRuhrohScenario } from "./generate.js";
|
|
2
|
+
import { type RuhrohScenario, type RuhrohScenarioTier } from "./scenarios.js";
|
|
3
|
+
export declare function resolveRuhrohPackageRootFromModule(importMetaUrl?: string): string;
|
|
4
|
+
export declare function resolveRuhrohBuiltinScenarioDir(packageRoot?: string): string;
|
|
5
|
+
export declare function loadBuiltinRuhrohScenarios(scenarioDir?: string): LoadedRuhrohScenario[];
|
|
6
|
+
export declare function getBuiltinRuhrohScenarioById(id: string, scenarioDir?: string): RuhrohScenario | undefined;
|
|
7
|
+
export declare function getBuiltinRuhrohScenariosByTier(tier: RuhrohScenarioTier, scenarioDir?: string): RuhrohScenario[];
|
|
8
|
+
//# sourceMappingURL=builtin-scenarios.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"builtin-scenarios.d.ts","sourceRoot":"","sources":["../src/builtin-scenarios.ts"],"names":[],"mappings":"AAGA,OAAO,EAA+C,KAAK,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACvG,OAAO,EAGL,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACxB,MAAM,gBAAgB,CAAC;AAExB,wBAAgB,kCAAkC,CAAC,aAAa,GAAE,MAAwB,GAAG,MAAM,CAIlG;AAED,wBAAgB,+BAA+B,CAAC,WAAW,GAAE,MAA6C,GAAG,MAAM,CAElH;AAED,wBAAgB,0BAA0B,CACxC,WAAW,GAAE,MAA0C,GACtD,oBAAoB,EAAE,CAExB;AAED,wBAAgB,4BAA4B,CAC1C,EAAE,EAAE,MAAM,EACV,WAAW,GAAE,MAA0C,GACtD,cAAc,GAAG,SAAS,CAE5B;AAED,wBAAgB,+BAA+B,CAC7C,IAAI,EAAE,kBAAkB,EACxB,WAAW,GAAE,MAA0C,GACtD,cAAc,EAAE,CAElB"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { discoverRuhrohScenarios, loadRuhrohScenario } from "./generate.js";
|
|
4
|
+
import { getRuhrohScenarioById, getRuhrohScenariosByTier, } from "./scenarios.js";
|
|
5
|
+
export function resolveRuhrohPackageRootFromModule(importMetaUrl = import.meta.url) {
|
|
6
|
+
const moduleDir = path.dirname(fileURLToPath(importMetaUrl));
|
|
7
|
+
const basename = path.basename(moduleDir);
|
|
8
|
+
return basename === "dist" || basename === "src" ? path.dirname(moduleDir) : moduleDir;
|
|
9
|
+
}
|
|
10
|
+
export function resolveRuhrohBuiltinScenarioDir(packageRoot = resolveRuhrohPackageRootFromModule()) {
|
|
11
|
+
return path.join(packageRoot, "scenarios");
|
|
12
|
+
}
|
|
13
|
+
export function loadBuiltinRuhrohScenarios(scenarioDir = resolveRuhrohBuiltinScenarioDir()) {
|
|
14
|
+
return discoverRuhrohScenarios(scenarioDir).map((source) => loadRuhrohScenario(source));
|
|
15
|
+
}
|
|
16
|
+
export function getBuiltinRuhrohScenarioById(id, scenarioDir = resolveRuhrohBuiltinScenarioDir()) {
|
|
17
|
+
return getRuhrohScenarioById(loadBuiltinRuhrohScenarios(scenarioDir).map((loaded) => loaded.scenario), id);
|
|
18
|
+
}
|
|
19
|
+
export function getBuiltinRuhrohScenariosByTier(tier, scenarioDir = resolveRuhrohBuiltinScenarioDir()) {
|
|
20
|
+
return getRuhrohScenariosByTier(loadBuiltinRuhrohScenarios(scenarioDir).map((loaded) => loaded.scenario), tier);
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=builtin-scenarios.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"builtin-scenarios.js","sourceRoot":"","sources":["../src/builtin-scenarios.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,uBAAuB,EAAE,kBAAkB,EAA6B,MAAM,eAAe,CAAC;AACvG,OAAO,EACL,qBAAqB,EACrB,wBAAwB,GAGzB,MAAM,gBAAgB,CAAC;AAExB,MAAM,UAAU,kCAAkC,CAAC,gBAAwB,MAAM,CAAC,IAAI,CAAC,GAAG;IACxF,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC,CAAC;IAC7D,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;IAC1C,OAAO,QAAQ,KAAK,MAAM,IAAI,QAAQ,KAAK,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AACzF,CAAC;AAED,MAAM,UAAU,+BAA+B,CAAC,cAAsB,kCAAkC,EAAE;IACxG,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,0BAA0B,CACxC,cAAsB,+BAA+B,EAAE;IAEvD,OAAO,uBAAuB,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC,CAAC;AAC1F,CAAC;AAED,MAAM,UAAU,4BAA4B,CAC1C,EAAU,EACV,cAAsB,+BAA+B,EAAE;IAEvD,OAAO,qBAAqB,CAAC,0BAA0B,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC,CAAC;AAC7G,CAAC;AAED,MAAM,UAAU,+BAA+B,CAC7C,IAAwB,EACxB,cAAsB,+BAA+B,EAAE;IAEvD,OAAO,wBAAwB,CAAC,0BAA0B,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAC,CAAC;AAClH,CAAC"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import type { RuhrohScenarioTier } from "./scenarios.js";
|
|
4
|
+
interface RuntimeDeps {
|
|
5
|
+
spawn: typeof spawnSync;
|
|
6
|
+
env: NodeJS.ProcessEnv;
|
|
7
|
+
cwd: string;
|
|
8
|
+
stdout: Pick<NodeJS.WriteStream, "write">;
|
|
9
|
+
stderr: Pick<NodeJS.WriteStream, "write">;
|
|
10
|
+
}
|
|
11
|
+
export interface RuhrohCliOptions {
|
|
12
|
+
command: "run" | "generate";
|
|
13
|
+
list: boolean;
|
|
14
|
+
dryRun: boolean;
|
|
15
|
+
generateOnly: boolean;
|
|
16
|
+
harborBin: string;
|
|
17
|
+
scenarioDir: string;
|
|
18
|
+
generatedDir: string;
|
|
19
|
+
scenarioId?: string | undefined;
|
|
20
|
+
tier?: RuhrohScenarioTier | undefined;
|
|
21
|
+
iterations?: number | undefined;
|
|
22
|
+
adapter?: string | undefined;
|
|
23
|
+
}
|
|
24
|
+
export declare function parseRuhrohCliArgs(argv: string[], cwd?: string): RuhrohCliOptions;
|
|
25
|
+
export declare function runRuhrohCli(argv: string[], deps: RuntimeDeps): Promise<number>;
|
|
26
|
+
export declare function resolveRuhrohPackageRoot(): string;
|
|
27
|
+
export declare function resolveRuhrohPythonPath(): string;
|
|
28
|
+
export declare function buildHarborSpawnEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
29
|
+
export {};
|
|
30
|
+
//# sourceMappingURL=cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAyB,MAAM,oBAAoB,CAAC;AAOtE,OAAO,KAAK,EAAkB,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAEzE,UAAU,WAAW;IACnB,KAAK,EAAE,OAAO,SAAS,CAAC;IACxB,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IAC1C,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;CAC3C;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,KAAK,GAAG,UAAU,CAAC;IAC5B,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;IAChB,YAAY,EAAE,OAAO,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,IAAI,CAAC,EAAE,kBAAkB,GAAG,SAAS,CAAC;IACtC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC9B;AAcD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,GAAE,MAAsB,GAAG,gBAAgB,CAgFhG;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAyFrF;AAgED,wBAAgB,wBAAwB,IAAI,MAAM,CAIjD;AAED,wBAAgB,uBAAuB,IAAI,MAAM,CAEhD;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,CAQ7E"}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { realpathSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { RUHROH_ARTIFACTS, RUHROH_HARBOR_AGENT_IMPORT_PATH, buildRuhrohHarborCommands } from "./harbor.js";
|
|
7
|
+
import { discoverRuhrohScenarios, generateHarborDataset, loadRuhrohScenario } from "./generate.js";
|
|
8
|
+
class HelpRequested extends Error {
|
|
9
|
+
}
|
|
10
|
+
export function parseRuhrohCliArgs(argv, cwd = process.cwd()) {
|
|
11
|
+
const options = {
|
|
12
|
+
command: "run",
|
|
13
|
+
list: false,
|
|
14
|
+
dryRun: false,
|
|
15
|
+
generateOnly: false,
|
|
16
|
+
harborBin: "harbor",
|
|
17
|
+
scenarioDir: path.join(resolveRuhrohPackageRoot(), "scenarios"),
|
|
18
|
+
generatedDir: path.resolve(cwd, ".generated", "ruhroh"),
|
|
19
|
+
tier: "smoke",
|
|
20
|
+
};
|
|
21
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
22
|
+
const arg = argv[index];
|
|
23
|
+
if (arg === undefined || arg === "--") {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (index === 0 && (arg === "run" || arg === "generate")) {
|
|
27
|
+
options.command = arg;
|
|
28
|
+
if (arg === "generate") {
|
|
29
|
+
options.generateOnly = true;
|
|
30
|
+
}
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (arg === "--help" || arg === "-h") {
|
|
34
|
+
throw new HelpRequested();
|
|
35
|
+
}
|
|
36
|
+
if (arg === "--list") {
|
|
37
|
+
options.list = true;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (arg === "--dry-run") {
|
|
41
|
+
options.dryRun = true;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (arg === "--generate-only") {
|
|
45
|
+
options.generateOnly = true;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (arg === "--scenario") {
|
|
49
|
+
options.scenarioId = assertSafeScenarioId(readValue(argv, index, arg));
|
|
50
|
+
options.tier = undefined;
|
|
51
|
+
index += 1;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (arg === "--tier") {
|
|
55
|
+
options.tier = parseTier(readValue(argv, index, arg));
|
|
56
|
+
options.scenarioId = undefined;
|
|
57
|
+
index += 1;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (arg === "--scenario-dir") {
|
|
61
|
+
options.scenarioDir = path.resolve(cwd, readValue(argv, index, arg));
|
|
62
|
+
index += 1;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (arg === "--generated-dir") {
|
|
66
|
+
options.generatedDir = path.resolve(cwd, readValue(argv, index, arg));
|
|
67
|
+
index += 1;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (arg === "--iterations") {
|
|
71
|
+
options.iterations = parsePositiveInteger(readValue(argv, index, arg), arg);
|
|
72
|
+
index += 1;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (arg === "--adapter") {
|
|
76
|
+
options.adapter = readValue(argv, index, arg);
|
|
77
|
+
index += 1;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (arg === "--harbor-bin") {
|
|
81
|
+
options.harborBin = readValue(argv, index, arg);
|
|
82
|
+
index += 1;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
86
|
+
}
|
|
87
|
+
return options;
|
|
88
|
+
}
|
|
89
|
+
export async function runRuhrohCli(argv, deps) {
|
|
90
|
+
let options;
|
|
91
|
+
try {
|
|
92
|
+
options = parseRuhrohCliArgs(argv, deps.cwd);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (error instanceof HelpRequested) {
|
|
96
|
+
deps.stdout.write(helpText());
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
deps.stderr.write(`ruhroh failed: ${error instanceof Error ? error.message : String(error)}\n\n`);
|
|
100
|
+
deps.stderr.write(helpText());
|
|
101
|
+
return 1;
|
|
102
|
+
}
|
|
103
|
+
let loaded;
|
|
104
|
+
try {
|
|
105
|
+
loaded = discoverRuhrohScenarios(options.scenarioDir).map((source) => loadRuhrohScenario(source));
|
|
106
|
+
if (loaded.length === 0) {
|
|
107
|
+
throw new Error(`No Ruhroh scenarios found in ${options.scenarioDir}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
deps.stderr.write(`ruhroh failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
112
|
+
return 1;
|
|
113
|
+
}
|
|
114
|
+
if (options.list) {
|
|
115
|
+
for (const { scenario } of loaded) {
|
|
116
|
+
deps.stdout.write(`${scenario.id}\t${scenario.tier}\t${scenario.title}\n`);
|
|
117
|
+
}
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
let selected;
|
|
121
|
+
try {
|
|
122
|
+
selected = selectScenarios(loaded, options);
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
deps.stderr.write(`ruhroh failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
126
|
+
return 1;
|
|
127
|
+
}
|
|
128
|
+
let datasetPath = path.join(options.generatedDir, "harbor");
|
|
129
|
+
if (!options.dryRun || options.generateOnly) {
|
|
130
|
+
const generated = generateHarborDataset({
|
|
131
|
+
scenarios: selected,
|
|
132
|
+
outputRoot: options.generatedDir,
|
|
133
|
+
agentImportPath: RUHROH_HARBOR_AGENT_IMPORT_PATH,
|
|
134
|
+
artifacts: RUHROH_ARTIFACTS,
|
|
135
|
+
});
|
|
136
|
+
datasetPath = generated.datasetPath;
|
|
137
|
+
for (const task of generated.tasks) {
|
|
138
|
+
deps.stdout.write(`[ruhroh] generated ${task.scenarioId}: ${path.relative(deps.cwd, task.taskDir)}\n`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (options.generateOnly) {
|
|
142
|
+
deps.stdout.write("[ruhroh] generate-only complete. No Harbor tasks were started.\n");
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
let adapter;
|
|
146
|
+
try {
|
|
147
|
+
adapter = resolveAdapterSelection(options, deps.cwd, deps.env);
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
deps.stderr.write(`ruhroh failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
151
|
+
return 1;
|
|
152
|
+
}
|
|
153
|
+
const commands = buildCommands(selected.map((item) => item.scenario), options, datasetPath, adapter);
|
|
154
|
+
deps.stdout.write(`[ruhroh] selected=${commands.map((command) => command.scenarioId).join(",")}\n`);
|
|
155
|
+
for (const command of commands) {
|
|
156
|
+
deps.stdout.write(`[ruhroh] harbor: ${formatCommand(options.harborBin, command.args)}\n`);
|
|
157
|
+
}
|
|
158
|
+
if (options.dryRun) {
|
|
159
|
+
deps.stdout.write("[ruhroh] dry run complete. No Harbor tasks were started.\n");
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
162
|
+
let failed = false;
|
|
163
|
+
const harborEnv = buildHarborSpawnEnv(adapter.env);
|
|
164
|
+
for (const command of commands) {
|
|
165
|
+
const result = deps.spawn(options.harborBin, command.args, {
|
|
166
|
+
cwd: deps.cwd,
|
|
167
|
+
env: harborEnv,
|
|
168
|
+
stdio: "inherit",
|
|
169
|
+
});
|
|
170
|
+
if (result.status !== 0 || result.error !== undefined) {
|
|
171
|
+
deps.stderr.write(`[ruhroh] Harbor command failed for ${command.scenarioId}.\n${formatSpawnFailure(result)}`);
|
|
172
|
+
failed = true;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return failed ? 1 : 0;
|
|
176
|
+
}
|
|
177
|
+
function selectScenarios(loaded, options) {
|
|
178
|
+
if (options.scenarioId !== undefined) {
|
|
179
|
+
const scenario = loaded.find((item) => item.scenario.id === options.scenarioId);
|
|
180
|
+
if (scenario === undefined) {
|
|
181
|
+
throw new Error(`Unknown Ruhroh scenario: ${options.scenarioId}`);
|
|
182
|
+
}
|
|
183
|
+
return [scenario];
|
|
184
|
+
}
|
|
185
|
+
return loaded.filter((item) => item.scenario.tier === (options.tier ?? "smoke"));
|
|
186
|
+
}
|
|
187
|
+
function buildCommands(scenarios, options, datasetPath, adapter) {
|
|
188
|
+
return buildRuhrohHarborCommands({
|
|
189
|
+
scenarios,
|
|
190
|
+
adapter: adapter.adapterId,
|
|
191
|
+
datasetPath,
|
|
192
|
+
iterations: options.iterations,
|
|
193
|
+
env: adapter.env,
|
|
194
|
+
agentImportPath: RUHROH_HARBOR_AGENT_IMPORT_PATH,
|
|
195
|
+
artifacts: RUHROH_ARTIFACTS,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
function resolveAdapterSelection(options, cwd, env) {
|
|
199
|
+
const adapter = options.adapter?.trim();
|
|
200
|
+
if (adapter === undefined || adapter.length === 0) {
|
|
201
|
+
throw new Error("--adapter is required for ruhroh run and dry-run commands");
|
|
202
|
+
}
|
|
203
|
+
if (looksLikeAdapterCommand(adapter)) {
|
|
204
|
+
const command = adapter.includes(" ")
|
|
205
|
+
? adapter
|
|
206
|
+
: path.resolve(cwd, adapter);
|
|
207
|
+
const adapterId = adapterIdFromCommand(command);
|
|
208
|
+
return {
|
|
209
|
+
adapterId,
|
|
210
|
+
env: {
|
|
211
|
+
...env,
|
|
212
|
+
RUHROH_RUN_AGENT_COMMAND: command,
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
return { adapterId: adapter, env };
|
|
217
|
+
}
|
|
218
|
+
function looksLikeAdapterCommand(adapter) {
|
|
219
|
+
return /\s/u.test(adapter) || adapter.startsWith(".") || adapter.startsWith("/") || adapter.includes(path.sep);
|
|
220
|
+
}
|
|
221
|
+
function adapterIdFromCommand(command) {
|
|
222
|
+
const firstPathLikePart = command.split(/\s+/u).find((part) => part.includes(path.sep)) ?? command;
|
|
223
|
+
return path.basename(firstPathLikePart).replace(/\.[cm]?[jt]sx?$/u, "") || "command";
|
|
224
|
+
}
|
|
225
|
+
export function resolveRuhrohPackageRoot() {
|
|
226
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
227
|
+
const basename = path.basename(moduleDir);
|
|
228
|
+
return basename === "dist" || basename === "src" ? path.dirname(moduleDir) : moduleDir;
|
|
229
|
+
}
|
|
230
|
+
export function resolveRuhrohPythonPath() {
|
|
231
|
+
return path.join(resolveRuhrohPackageRoot(), "python");
|
|
232
|
+
}
|
|
233
|
+
export function buildHarborSpawnEnv(env) {
|
|
234
|
+
const pythonPath = resolveRuhrohPythonPath();
|
|
235
|
+
return {
|
|
236
|
+
...env,
|
|
237
|
+
PYTHONPATH: env.PYTHONPATH === undefined || env.PYTHONPATH.trim().length === 0
|
|
238
|
+
? pythonPath
|
|
239
|
+
: `${pythonPath}${path.delimiter}${env.PYTHONPATH}`,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function parseTier(value) {
|
|
243
|
+
if (value === "smoke" || value === "nightly" || value === "release") {
|
|
244
|
+
return value;
|
|
245
|
+
}
|
|
246
|
+
throw new Error(`Unknown Ruhroh tier: ${value}`);
|
|
247
|
+
}
|
|
248
|
+
function parsePositiveInteger(value, arg) {
|
|
249
|
+
const parsed = Number.parseInt(value, 10);
|
|
250
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
251
|
+
throw new Error(`${arg} must be a positive integer`);
|
|
252
|
+
}
|
|
253
|
+
return parsed;
|
|
254
|
+
}
|
|
255
|
+
function assertSafeScenarioId(value) {
|
|
256
|
+
if (!/^[a-zA-Z0-9._-]+$/u.test(value) || value === "." || value === "..") {
|
|
257
|
+
throw new Error(`Unsafe Ruhroh scenario id: ${value}`);
|
|
258
|
+
}
|
|
259
|
+
return value;
|
|
260
|
+
}
|
|
261
|
+
function readValue(argv, index, arg) {
|
|
262
|
+
const value = argv[index + 1];
|
|
263
|
+
if (value === undefined || value.length === 0) {
|
|
264
|
+
throw new Error(`${arg} requires a value`);
|
|
265
|
+
}
|
|
266
|
+
return value;
|
|
267
|
+
}
|
|
268
|
+
function formatCommand(command, args) {
|
|
269
|
+
return [command, ...args].map((part) => (/^[a-zA-Z0-9_./:=@-]+$/u.test(part) ? part : JSON.stringify(part))).join(" ");
|
|
270
|
+
}
|
|
271
|
+
function formatSpawnFailure(result) {
|
|
272
|
+
const stdout = result.stdout ?? Buffer.alloc(0);
|
|
273
|
+
const stderr = result.stderr ?? Buffer.alloc(0);
|
|
274
|
+
const parts = [
|
|
275
|
+
`status=${String(result.status)}`,
|
|
276
|
+
result.error === undefined ? "" : `error=${result.error.message}`,
|
|
277
|
+
stdout.length > 0 ? `stdout:\n${stdout.toString("utf8")}` : "",
|
|
278
|
+
stderr.length > 0 ? `stderr:\n${stderr.toString("utf8")}` : "",
|
|
279
|
+
].filter(Boolean);
|
|
280
|
+
return `${parts.join("\n")}\n`;
|
|
281
|
+
}
|
|
282
|
+
function helpText() {
|
|
283
|
+
return `Usage: ruhroh [run|generate] [options]\n\nCommon commands:\n ruhroh --list\n ruhroh --scenario simple-newsletter --generate-only\n ruhroh --scenario simple-newsletter --dry-run\n ruhroh generate --tier smoke\n\nOptions:\n --list List JSON scenarios.\n --scenario <id> Select one scenario.\n --tier <tier> Select scenarios in tier: smoke, nightly, release. Default: smoke.\n --scenario-dir <path> Scenario root. Default: bundled package scenarios.\n --generated-dir <path> Generated output root. Default: .generated/ruhroh.\n --iterations <n> Override implementation iterations.\n --adapter <id-or-command> Select run-agent adapter by id or command path. Required for run/dry-run.\n --generate-only Generate Harbor task directories without running Harbor.\n --harbor-bin <path> Harbor binary. Default: harbor.\n --dry-run Print Harbor commands without writing tasks or running Harbor.\n`;
|
|
284
|
+
}
|
|
285
|
+
async function main() {
|
|
286
|
+
const code = await runRuhrohCli(process.argv.slice(2), {
|
|
287
|
+
spawn: spawnSync,
|
|
288
|
+
env: process.env,
|
|
289
|
+
cwd: process.cwd(),
|
|
290
|
+
stdout: process.stdout,
|
|
291
|
+
stderr: process.stderr,
|
|
292
|
+
});
|
|
293
|
+
process.exitCode = code;
|
|
294
|
+
}
|
|
295
|
+
if (isDirectCliInvocation()) {
|
|
296
|
+
main().catch((error) => {
|
|
297
|
+
process.stderr.write(`ruhroh failed: ${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
|
|
298
|
+
process.exitCode = 1;
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
function isDirectCliInvocation() {
|
|
302
|
+
if (process.argv[1] === undefined) {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
const modulePath = fileURLToPath(import.meta.url);
|
|
306
|
+
try {
|
|
307
|
+
return realpathSync(process.argv[1]) === realpathSync(modulePath);
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
return path.resolve(process.argv[1]) === modulePath;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
//# sourceMappingURL=cli.js.map
|