@gethmy/harness 1.0.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/README.md +66 -0
- package/dist/cli.js +2936 -0
- package/dist/index.js +3734 -0
- package/package.json +65 -0
- package/src/artifact-judge.ts +410 -0
- package/src/cli.ts +272 -0
- package/src/command-metric.ts +594 -0
- package/src/error-classifier.ts +95 -0
- package/src/exec-types.ts +109 -0
- package/src/gate-collectors.ts +431 -0
- package/src/gate-config-error.ts +73 -0
- package/src/git-diff-stat.ts +148 -0
- package/src/git-pr.ts +839 -0
- package/src/harmony-client.ts +197 -0
- package/src/index.ts +37 -0
- package/src/log.ts +129 -0
- package/src/model-tier.test.ts +169 -0
- package/src/model-tier.ts +108 -0
- package/src/oracle-collector.ts +148 -0
- package/src/oracle.ts +434 -0
- package/src/pm.ts +73 -0
- package/src/process-group.ts +149 -0
- package/src/project-type.ts +303 -0
- package/src/revert-guard.ts +99 -0
- package/src/review-types.ts +52 -0
- package/src/runner.ts +184 -0
- package/src/sdk-agent-runner.ts +575 -0
- package/src/stage-cli.ts +302 -0
- package/src/stage-run.ts +91 -0
- package/src/verification.ts +711 -0
- package/src/worktree.ts +639 -0
package/src/stage-cli.ts
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stage-run CLI's decidable parts: argument validation, the fail-loud stage
|
|
3
|
+
* check, the pinned-stage resolution, the subagent's prompt, and the runner
|
|
4
|
+
* config that carries the role's two enforcement fields. No network and no
|
|
5
|
+
* subprocess here — `cli.ts` stays a thin shell around the IO, and every rule
|
|
6
|
+
* below is testable without a live API.
|
|
7
|
+
*
|
|
8
|
+
* `buildStageRunnerConfig` exists as a seam for exactly one reason: the tool
|
|
9
|
+
* deny and the credential strip would otherwise be assembled inline in
|
|
10
|
+
* `cli.ts`, the one file no test executes, where deleting either line leaves
|
|
11
|
+
* the whole suite green while a documented security property quietly stops
|
|
12
|
+
* holding.
|
|
13
|
+
*/
|
|
14
|
+
import {
|
|
15
|
+
type GateSpec,
|
|
16
|
+
isAgentRunnableOwner,
|
|
17
|
+
type PlaybookStageDef,
|
|
18
|
+
type PlaybookStageRole,
|
|
19
|
+
type PlaybookVersionDef,
|
|
20
|
+
resolveStageDef,
|
|
21
|
+
} from "@harmony/shared";
|
|
22
|
+
import { normalizeGateSpec } from "./gate-collectors.js";
|
|
23
|
+
import type { StageCardPin } from "./harmony-client.js";
|
|
24
|
+
import { buildRoleLaunch, envKeysDroppedByLaunch } from "./runner.js";
|
|
25
|
+
import type { SdkRunnerConfig } from "./sdk-agent-runner.js";
|
|
26
|
+
|
|
27
|
+
export const STAGE_RUN_USAGE =
|
|
28
|
+
"usage: harmony-harness stage run --card <id> --stage <id> --workspace <id> --repo <path> --session <id> [--metrics <json-path>]";
|
|
29
|
+
|
|
30
|
+
/** The five identifiers one stage run needs. Every one is required.
|
|
31
|
+
* `metricsPath` is the one optional input: a JSON file holding the DRIVER's
|
|
32
|
+
* metric allowlist for `custom` gates (same shape as the daemon operator's
|
|
33
|
+
* `agent.playbooks.metrics`). The motor itself stays config-free — without the
|
|
34
|
+
* flag a `custom` gate reports "metric not declared" and blocks, exactly as
|
|
35
|
+
* before. */
|
|
36
|
+
export interface StageRunArgs {
|
|
37
|
+
cardId: string;
|
|
38
|
+
stageId: string;
|
|
39
|
+
workspaceId: string;
|
|
40
|
+
repoPath: string;
|
|
41
|
+
sessionId: string;
|
|
42
|
+
metricsPath: string | null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type StageRunArgsResult =
|
|
46
|
+
| { ok: true; args: StageRunArgs }
|
|
47
|
+
| { ok: false; message: string };
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Read one `--flag value` pair. A flag whose value is absent, blank, or another
|
|
51
|
+
* flag is REFUSED rather than defaulted: `--session --repo /tmp/wt` would
|
|
52
|
+
* otherwise send "--repo" (or an empty string) as the agent-session id, and the
|
|
53
|
+
* oracle read route would refuse it with a 403 that names nothing useful.
|
|
54
|
+
*/
|
|
55
|
+
function readFlag(argv: string[], name: string): string | null {
|
|
56
|
+
const index = argv.indexOf(`--${name}`);
|
|
57
|
+
if (index === -1) return null;
|
|
58
|
+
const value = argv[index + 1];
|
|
59
|
+
if (value === undefined || value.startsWith("--")) return null;
|
|
60
|
+
// Return the TRIMMED value: a padded `--card " card-1 "` would otherwise be
|
|
61
|
+
// handed to `encodeURIComponent` and reach the API as "%20card-1%20", which
|
|
62
|
+
// matches no card and reports as a plain 404.
|
|
63
|
+
const trimmed = value.trim();
|
|
64
|
+
return trimmed.length === 0 ? null : trimmed;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Validate an `argv` (already stripped of `node` and the script path). Returns
|
|
69
|
+
* the five identifiers, or a message naming the first flag that is missing —
|
|
70
|
+
* the caller exits 2 with the usage line.
|
|
71
|
+
*/
|
|
72
|
+
export function parseStageRunArgs(argv: string[]): StageRunArgsResult {
|
|
73
|
+
if (argv[0] !== "stage" || argv[1] !== "run") {
|
|
74
|
+
return {
|
|
75
|
+
ok: false,
|
|
76
|
+
message: `unknown command "${argv.slice(0, 2).join(" ")}" — the motor runs exactly one thing: stage run`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const fields: Array<[Exclude<keyof StageRunArgs, "metricsPath">, string]> = [
|
|
81
|
+
["cardId", "card"],
|
|
82
|
+
["stageId", "stage"],
|
|
83
|
+
["workspaceId", "workspace"],
|
|
84
|
+
["repoPath", "repo"],
|
|
85
|
+
["sessionId", "session"],
|
|
86
|
+
];
|
|
87
|
+
const args = { metricsPath: null } as StageRunArgs;
|
|
88
|
+
for (const [field, flag] of fields) {
|
|
89
|
+
const value = readFlag(argv, flag);
|
|
90
|
+
if (value === null) {
|
|
91
|
+
return { ok: false, message: `missing --${flag} <value>` };
|
|
92
|
+
}
|
|
93
|
+
args[field] = value;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Optional: --metrics <path>. Distinguish "absent" (fine — custom gates
|
|
97
|
+
// block with a legible reason) from "present but valueless" (refused — the
|
|
98
|
+
// driver clearly meant to pass an allowlist and silently running without one
|
|
99
|
+
// would turn every custom gate into a block the driver did not expect).
|
|
100
|
+
if (argv.includes("--metrics")) {
|
|
101
|
+
const metricsPath = readFlag(argv, "metrics");
|
|
102
|
+
if (metricsPath === null) {
|
|
103
|
+
return { ok: false, message: "missing value for --metrics <json-path>" };
|
|
104
|
+
}
|
|
105
|
+
args.metricsPath = metricsPath;
|
|
106
|
+
}
|
|
107
|
+
return { ok: true, args };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Parse the driver's metric-allowlist file (`--metrics`). THROWS on an
|
|
112
|
+
* unreadable file, invalid JSON, or a non-object top level — a driver that
|
|
113
|
+
* passed the flag wants the allowlist applied, and degrading to `{}` would
|
|
114
|
+
* silently turn every `custom` gate into a block. Per-metric validation stays
|
|
115
|
+
* where it lives: the command collector fails closed on a malformed
|
|
116
|
+
* declaration and names the key to fix.
|
|
117
|
+
*/
|
|
118
|
+
export function parseMetricsAllowlist(
|
|
119
|
+
raw: string,
|
|
120
|
+
sourcePath: string,
|
|
121
|
+
): Record<string, unknown> {
|
|
122
|
+
let parsed: unknown;
|
|
123
|
+
try {
|
|
124
|
+
parsed = JSON.parse(raw);
|
|
125
|
+
} catch (err) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`--metrics ${sourcePath} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`--metrics ${sourcePath} must hold a JSON object keyed by metric name`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
return parsed as Record<string, unknown>;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Refuse to run when the CLI's `--stage` disagrees with the card's
|
|
140
|
+
* `current_stage`. THROWS — the motor collects nothing in that case.
|
|
141
|
+
*
|
|
142
|
+
* Collecting evidence for a stage the card is not on writes a row that
|
|
143
|
+
* `gateEvaluate` later applies to a DIFFERENT stage. Silently preferring either
|
|
144
|
+
* side would hide a driver/board desync, which is the same acting-vs-named
|
|
145
|
+
* stage confusion the API had to untangle for the oracle routes.
|
|
146
|
+
*/
|
|
147
|
+
export function assertCardOnStage(stageId: string, card: StageCardPin): void {
|
|
148
|
+
if (card.current_stage === stageId) return;
|
|
149
|
+
throw new Error(
|
|
150
|
+
`--stage "${stageId}" does not match the card's current_stage ${
|
|
151
|
+
card.current_stage === null ? "(none)" : `"${card.current_stage}"`
|
|
152
|
+
} — refusing to collect evidence for a stage the card is not on`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Refuse to run a stage a HUMAN owns. THROWS — before any subagent is launched
|
|
158
|
+
* and before anything is written.
|
|
159
|
+
*
|
|
160
|
+
* `owner` is the stage model's answer to "who runs this stage?", and `human`
|
|
161
|
+
* means a person does. Until this check existed nothing under
|
|
162
|
+
* `packages/harmony-harness/src/` read `owner` at all, so a driver that handed
|
|
163
|
+
* the motor a human stage would get a subagent doing that person's work and a
|
|
164
|
+
* gate collected against it — the card would look advanced by a human who never
|
|
165
|
+
* saw it. `fix-a-bug` stage 3 (`Review`) is `owner: "human"`, so this is
|
|
166
|
+
* reachable the moment a driver exists, not a hypothetical.
|
|
167
|
+
*
|
|
168
|
+
* The spec's rule for both drivers is that reaching a human stage is a NORMAL
|
|
169
|
+
* outcome — write `stage_waiting` and stop — so the refusal belongs to the
|
|
170
|
+
* driver's decision, and the motor's job is only to make running one impossible
|
|
171
|
+
* by accident. Same shape as {@link assertCardOnStage}: fail loud, name what
|
|
172
|
+
* disagrees, collect nothing. `either` and `agent` both run
|
|
173
|
+
* (`isAgentRunnableOwner`), which is the same predicate the daemon's stage
|
|
174
|
+
* executor already uses — one rule, not a second opinion.
|
|
175
|
+
*/
|
|
176
|
+
export function assertStageIsAgentRunnable(stage: PlaybookStageDef): void {
|
|
177
|
+
if (isAgentRunnableOwner(stage.owner)) return;
|
|
178
|
+
throw new Error(
|
|
179
|
+
`stage "${stage.id}" ("${stage.name}") is owned by "${stage.owner}" — refusing to run a stage a human owns`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The subagent's prompt. Built from the stage's `entry_action` when the pinned
|
|
185
|
+
* stage names one, otherwise from the card — in which case the agent is told to
|
|
186
|
+
* read the card itself through the Harmony MCP tools, which every role keeps
|
|
187
|
+
* (see runner.ts). The motor does not read board content to build this.
|
|
188
|
+
*
|
|
189
|
+
* The signature is the guard on what can leak: there is no parameter for the
|
|
190
|
+
* agent-session id or for the oracle's path, and neither value is reachable
|
|
191
|
+
* from here. The prompt is free text and the last remaining way to hand the
|
|
192
|
+
* implementer either one.
|
|
193
|
+
*/
|
|
194
|
+
export function buildStagePrompt(args: {
|
|
195
|
+
cardId: string;
|
|
196
|
+
stageId: string;
|
|
197
|
+
stage: PlaybookStageDef | null;
|
|
198
|
+
}): string {
|
|
199
|
+
const stageName = args.stage?.name ?? args.stageId;
|
|
200
|
+
const entryAction = args.stage?.entry_action;
|
|
201
|
+
return [
|
|
202
|
+
`## Playbook stage: ${stageName}`,
|
|
203
|
+
`You are running the "${stageName}" stage for Harmony card ${args.cardId}.`,
|
|
204
|
+
entryAction
|
|
205
|
+
? `Stage skill / entry action: \`${entryAction}\`. Follow that skill's method for this stage.`
|
|
206
|
+
: "Read the card with the Harmony MCP tools (`harmony_get_card`) and do this stage's work for it.",
|
|
207
|
+
"Do only this stage's work, then stop. Do not move the card, do not advance the stage, and do not end your agent session — the driver that invoked this stage owns all three.",
|
|
208
|
+
].join("\n");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The pinned stage this run is bound to, or a refusal. Two OUTCOMES, kept
|
|
213
|
+
* apart on purpose:
|
|
214
|
+
*
|
|
215
|
+
* - `ok: true` with `gate: null` — the stage exists and simply declares no
|
|
216
|
+
* gate. The run proceeds: the stage's `role` and `entry_action` are still
|
|
217
|
+
* in force, and nothing is collected or recorded afterwards.
|
|
218
|
+
* - `ok: false` — the PIN itself is unusable (a legacy macro snapshot, or a
|
|
219
|
+
* stage id that is not in the pinned version). The caller refuses to run.
|
|
220
|
+
*
|
|
221
|
+
* Collapsing those two into one `null`, which is what deriving the stage from
|
|
222
|
+
* `resolveStageGate` did, made an ungated stage lose both its declared role
|
|
223
|
+
* (silently falling back to the fail-closed `implementer` treatment) and its
|
|
224
|
+
* `entry_action` prompt — a degradation invisible at runtime.
|
|
225
|
+
*/
|
|
226
|
+
export type PinnedStageResolution =
|
|
227
|
+
| { ok: true; stage: PlaybookStageDef; gate: GateSpec | null }
|
|
228
|
+
| { ok: false; reason: string };
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Resolve `stageId` against the PINNED `playbook_versions` snapshot and
|
|
232
|
+
* normalize that stage's gate. Pure and total — `resolveStageDef` and
|
|
233
|
+
* `normalizeGateSpec` are both pure, and neither throws.
|
|
234
|
+
*
|
|
235
|
+
* A gate whose `kind` is unknown normalizes to `null` (fail-closed in
|
|
236
|
+
* `normalizeGateSpec`): the stage still runs, and nothing is collected. That
|
|
237
|
+
* is deliberately NOT a refusal — the stage def is intact, only its gate is
|
|
238
|
+
* unreadable.
|
|
239
|
+
*/
|
|
240
|
+
export function resolvePinnedStage(
|
|
241
|
+
version: PlaybookVersionDef,
|
|
242
|
+
stageId: string,
|
|
243
|
+
): PinnedStageResolution {
|
|
244
|
+
const resolution = resolveStageDef(version, stageId);
|
|
245
|
+
if (resolution.kind === "not_stage_model") {
|
|
246
|
+
return {
|
|
247
|
+
ok: false,
|
|
248
|
+
reason: `the pinned playbook version is a legacy macro (steps_version ${version.steps_version}) and declares no stages`,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
if (resolution.kind === "stage_not_found") {
|
|
252
|
+
return {
|
|
253
|
+
ok: false,
|
|
254
|
+
reason: `stage "${stageId}" is not in the pinned playbook version`,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
ok: true,
|
|
259
|
+
stage: resolution.stage,
|
|
260
|
+
gate: normalizeGateSpec(resolution.stage.gate),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** A stage's subagent launch, in the two shapes `SdkAgentRunner` consumes. */
|
|
265
|
+
export interface StageRunnerLaunch {
|
|
266
|
+
/** The NORMALIZED role (`buildRoleLaunch`), never the raw input. */
|
|
267
|
+
role: PlaybookStageRole | null;
|
|
268
|
+
prompt: string;
|
|
269
|
+
cwd: string;
|
|
270
|
+
/**
|
|
271
|
+
* The runner config carrying Task 13's two measures: the scoped `Read` deny
|
|
272
|
+
* over the credential directory, and the environment keys the child must not
|
|
273
|
+
* receive. `stripEnvKeys` is DERIVED from the launch's own omissions, so
|
|
274
|
+
* `HARMONY_CREDENTIAL_KEYS` stays the only credential list in the codebase.
|
|
275
|
+
*/
|
|
276
|
+
config: SdkRunnerConfig;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Build the subagent launch for one stage: the role's env strip and tool deny,
|
|
281
|
+
* plus the prompt and worktree the run input needs.
|
|
282
|
+
*
|
|
283
|
+
* Both enforcement fields are asserted in `stage-cli.test.ts`, so removing
|
|
284
|
+
* either one fails a test rather than silently disarming the role separation.
|
|
285
|
+
*/
|
|
286
|
+
export function buildStageRunnerConfig(args: {
|
|
287
|
+
role: PlaybookStageRole | null;
|
|
288
|
+
prompt: string;
|
|
289
|
+
repoPath: string;
|
|
290
|
+
parentEnv: Record<string, string | undefined>;
|
|
291
|
+
}): StageRunnerLaunch {
|
|
292
|
+
const launch = buildRoleLaunch(args);
|
|
293
|
+
return {
|
|
294
|
+
role: launch.role,
|
|
295
|
+
prompt: launch.prompt,
|
|
296
|
+
cwd: launch.repoPath,
|
|
297
|
+
config: {
|
|
298
|
+
disallowedTools: launch.disallowedTools,
|
|
299
|
+
stripEnvKeys: envKeysDroppedByLaunch(args.parentEnv, launch),
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
}
|
package/src/stage-run.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One stage, one invocation. This is the whole orchestration the motor performs:
|
|
3
|
+
* announce the stage, run the subagent for its role, collect the gate's evidence,
|
|
4
|
+
* report. It deliberately does NOT decide what runs next and does NOT form a
|
|
5
|
+
* verdict — Harmony advances the stage, and `gateEvaluate` in @harmony/shared
|
|
6
|
+
* turns evidence into pass or fail.
|
|
7
|
+
*
|
|
8
|
+
* The deps are injected so the orchestration is testable without a repo, a
|
|
9
|
+
* subagent, or a network. The real wiring lives in cli.ts.
|
|
10
|
+
*/
|
|
11
|
+
import type {
|
|
12
|
+
GateEvidence,
|
|
13
|
+
GateSpec,
|
|
14
|
+
PlaybookStageRole,
|
|
15
|
+
} from "@harmony/shared";
|
|
16
|
+
|
|
17
|
+
export interface StageRunRequest {
|
|
18
|
+
cardId: string;
|
|
19
|
+
stageId: string;
|
|
20
|
+
workspaceId: string;
|
|
21
|
+
repoPath: string;
|
|
22
|
+
/**
|
|
23
|
+
* The card's ACTIVE agent session id (Task 11 review, ruling 27). The oracle
|
|
24
|
+
* read route binds the caller to it, so the driver that claimed the stage
|
|
25
|
+
* threads its own session id in through the CLI's `--session` flag. It is
|
|
26
|
+
* passed to the oracle deps and to the runner's event labels ONLY — it never
|
|
27
|
+
* enters the subagent's prompt or context.
|
|
28
|
+
*/
|
|
29
|
+
sessionId: string;
|
|
30
|
+
/**
|
|
31
|
+
* The stage's role (Task 8). The real `runRole` builds its subagent launch
|
|
32
|
+
* from this via `buildRoleLaunch` (runner.ts). `null` fails closed — treated
|
|
33
|
+
* like `implementer`, not like an unrestricted default.
|
|
34
|
+
*/
|
|
35
|
+
role: PlaybookStageRole | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type MotorEvent =
|
|
39
|
+
| { type: "stage_entered"; stageId: string }
|
|
40
|
+
| {
|
|
41
|
+
type: "gate_evaluated";
|
|
42
|
+
stageId: string;
|
|
43
|
+
gateKind: string;
|
|
44
|
+
result: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export interface StageRunDeps {
|
|
48
|
+
/** Resolve the pinned stage's gate, or null when the stage declares none. */
|
|
49
|
+
resolveGate(request: StageRunRequest): Promise<GateSpec | null>;
|
|
50
|
+
/** Run the subagent for this stage's role. Returns when it has exited. */
|
|
51
|
+
runRole(request: StageRunRequest): Promise<void>;
|
|
52
|
+
/** Collect the gate's evidence. Called only after runRole has resolved. */
|
|
53
|
+
collect(request: StageRunRequest, gate: GateSpec): Promise<GateEvidence>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface StageRunResult {
|
|
57
|
+
stageId: string;
|
|
58
|
+
gateKind: string | null;
|
|
59
|
+
evidence: GateEvidence | null;
|
|
60
|
+
events: MotorEvent[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function runStage(
|
|
64
|
+
request: StageRunRequest,
|
|
65
|
+
deps: StageRunDeps,
|
|
66
|
+
): Promise<StageRunResult> {
|
|
67
|
+
const events: MotorEvent[] = [
|
|
68
|
+
{ type: "stage_entered", stageId: request.stageId },
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
const gate = await deps.resolveGate(request);
|
|
72
|
+
|
|
73
|
+
// The role's subagent always runs to completion BEFORE any gate is collected.
|
|
74
|
+
// Task 12's oracle collector relies on this ordering: the held test and a model
|
|
75
|
+
// must never share a filesystem window.
|
|
76
|
+
await deps.runRole(request);
|
|
77
|
+
|
|
78
|
+
if (!gate) {
|
|
79
|
+
return { stageId: request.stageId, gateKind: null, evidence: null, events };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const evidence = await deps.collect(request, gate);
|
|
83
|
+
events.push({
|
|
84
|
+
type: "gate_evaluated",
|
|
85
|
+
stageId: request.stageId,
|
|
86
|
+
gateKind: gate.kind,
|
|
87
|
+
result: evidence.result,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return { stageId: request.stageId, gateKind: gate.kind, evidence, events };
|
|
91
|
+
}
|