@gethmy/harness 1.1.1 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +219 -51
- package/dist/index.js +393 -50
- package/package.json +2 -2
- package/src/cli.ts +171 -26
- package/src/confine-to-repo.test.ts +144 -0
- package/src/confine-to-repo.ts +113 -0
- package/src/gate-config-error.ts +19 -69
- package/src/harmony-client.ts +3 -0
- package/src/index.ts +3 -0
- package/src/model-tier.test.ts +88 -24
- package/src/model-tier.ts +45 -15
- package/src/motor-stream.ts +120 -0
- package/src/oracle-collector.ts +15 -1
- package/src/oracle.ts +7 -0
- package/src/run-sizing.test.ts +321 -0
- package/src/run-sizing.ts +393 -0
- package/src/runner.ts +16 -0
- package/src/sdk-agent-runner.ts +44 -3
- package/src/stage-cli.ts +94 -2
- package/src/stage-run.ts +32 -4
- package/src/worktree.ts +147 -21
package/dist/cli.js
CHANGED
|
@@ -36,6 +36,14 @@ var init_branchRef = __esm(() => {
|
|
|
36
36
|
// ../harmony-shared/dist/cardLinks.js
|
|
37
37
|
var init_cardLinks = () => {};
|
|
38
38
|
// ../harmony-shared/dist/classification.js
|
|
39
|
+
function tierFromScore(score) {
|
|
40
|
+
const s = Math.max(0, Math.min(10, Math.round(score)));
|
|
41
|
+
if (s <= 2)
|
|
42
|
+
return "simple";
|
|
43
|
+
if (s <= 6)
|
|
44
|
+
return "advanced";
|
|
45
|
+
return "research";
|
|
46
|
+
}
|
|
39
47
|
function escalateTier(tier) {
|
|
40
48
|
const i = MODEL_TIERS.indexOf(tier);
|
|
41
49
|
return MODEL_TIERS[Math.min(i + 1, MODEL_TIERS.length - 1)];
|
|
@@ -73,6 +81,31 @@ var init_constants = __esm(() => {
|
|
|
73
81
|
QUERY_GC_TIME: 1000 * 60 * 60 * 24
|
|
74
82
|
};
|
|
75
83
|
});
|
|
84
|
+
// ../harmony-shared/dist/fanoutSource.js
|
|
85
|
+
var FANOUT_KEY_MARKER = "harmony:fanout-item", FANOUT_KEY_RE;
|
|
86
|
+
var init_fanoutSource = __esm(() => {
|
|
87
|
+
FANOUT_KEY_RE = new RegExp(`^\\[${FANOUT_KEY_MARKER}\\]:\\s*#(\\S+)\\s*$`, "m");
|
|
88
|
+
});
|
|
89
|
+
// ../harmony-shared/dist/gateConfigError.js
|
|
90
|
+
function gateConfigErrorReason(evaluation) {
|
|
91
|
+
if (!evaluation || evaluation.passed)
|
|
92
|
+
return null;
|
|
93
|
+
const structured = evaluation.structured;
|
|
94
|
+
if (!structured || typeof structured !== "object")
|
|
95
|
+
return null;
|
|
96
|
+
if (!Object.hasOwn(structured, GATE_CONFIG_ERROR_KEY))
|
|
97
|
+
return null;
|
|
98
|
+
if (structured[GATE_CONFIG_ERROR_KEY] !== true) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
const reason = structured.reason;
|
|
102
|
+
return typeof reason === "string" && reason.trim().length > 0 ? reason.trim() : "the gate cannot be measured as configured";
|
|
103
|
+
}
|
|
104
|
+
var GATE_CONFIG_ERROR_KEY = "configError", GATE_CONFIG_ERROR_MARK;
|
|
105
|
+
var init_gateConfigError = __esm(() => {
|
|
106
|
+
GATE_CONFIG_ERROR_MARK = Object.freeze({ [GATE_CONFIG_ERROR_KEY]: true });
|
|
107
|
+
});
|
|
108
|
+
|
|
76
109
|
// ../harmony-shared/dist/gateEvaluate.js
|
|
77
110
|
function isGateKind(value) {
|
|
78
111
|
return typeof value === "string" && GATE_KINDS.includes(value);
|
|
@@ -468,6 +501,8 @@ var init_dist = __esm(() => {
|
|
|
468
501
|
init_columnSort();
|
|
469
502
|
init_commentSerializer();
|
|
470
503
|
init_constants();
|
|
504
|
+
init_fanoutSource();
|
|
505
|
+
init_gateConfigError();
|
|
471
506
|
init_gateEvaluate();
|
|
472
507
|
init_logger();
|
|
473
508
|
init_playbookAutoBind();
|
|
@@ -579,17 +614,18 @@ var RETIRED_MODEL = /^claude-[23][.-]/i;
|
|
|
579
614
|
function clampWithdrawn(model) {
|
|
580
615
|
return RETIRED_MODEL.test(model) ? MAX_IMPLEMENT_MODEL : model;
|
|
581
616
|
}
|
|
582
|
-
function chooseImplementModel(claude, card, attempts) {
|
|
617
|
+
function chooseImplementModel(claude, card, attempts, sized) {
|
|
583
618
|
if (card.model_override) {
|
|
619
|
+
const pinned = isModelTier(card.model_override) ? claude.tiers?.[card.model_override] || claude.model : card.model_override;
|
|
584
620
|
return {
|
|
585
|
-
model: clampWithdrawn(
|
|
621
|
+
model: clampWithdrawn(pinned),
|
|
586
622
|
escalated: false,
|
|
587
623
|
source: "override"
|
|
588
624
|
};
|
|
589
625
|
}
|
|
590
|
-
if (isModelTier(
|
|
626
|
+
if (sized && isModelTier(sized.tier)) {
|
|
591
627
|
const retry = attempts >= claude.escalateAfterAttempts;
|
|
592
|
-
const tier = retry ? escalateTier(
|
|
628
|
+
const tier = retry ? escalateTier(sized.tier) : sized.tier;
|
|
593
629
|
const mapped = claude.tiers?.[tier];
|
|
594
630
|
return {
|
|
595
631
|
model: clampWithdrawn(mapped && mapped.length > 0 ? mapped : claude.model),
|
|
@@ -821,13 +857,15 @@ class SdkAgentRunner {
|
|
|
821
857
|
};
|
|
822
858
|
const allowed = this.cfg.allowedTools ?? SDK_ALLOWED_TOOLS;
|
|
823
859
|
const builtinTools = allowed.filter((t) => !t.startsWith("mcp__") && !t.includes("*"));
|
|
860
|
+
const gateEach = this.cfg.gateEveryToolCall === true;
|
|
824
861
|
const options = {
|
|
825
862
|
cwd: input.cwd,
|
|
826
863
|
model: input.model ?? this.cfg.model,
|
|
827
|
-
allowedTools: allowed,
|
|
864
|
+
...gateEach ? {} : { allowedTools: allowed },
|
|
828
865
|
...this.cfg.disallowedTools && this.cfg.disallowedTools.length > 0 ? { disallowedTools: this.cfg.disallowedTools } : {},
|
|
866
|
+
...this.cfg.canUseTool ? { canUseTool: this.cfg.canUseTool } : {},
|
|
829
867
|
tools: builtinTools,
|
|
830
|
-
permissionMode: "dontAsk",
|
|
868
|
+
permissionMode: gateEach ? "default" : "dontAsk",
|
|
831
869
|
maxTurns: this.cfg.maxTurns,
|
|
832
870
|
abortController: this.abort,
|
|
833
871
|
...resumeSessionId ? { resume: resumeSessionId } : {},
|
|
@@ -1250,22 +1288,7 @@ init_dist();
|
|
|
1250
1288
|
var DEFAULT_METRIC_TIMEOUT_MS = 300000;
|
|
1251
1289
|
|
|
1252
1290
|
// src/gate-config-error.ts
|
|
1253
|
-
|
|
1254
|
-
var GATE_CONFIG_ERROR_MARK = Object.freeze({ [GATE_CONFIG_ERROR_KEY]: true });
|
|
1255
|
-
function gateConfigErrorReason(evaluation) {
|
|
1256
|
-
if (!evaluation || evaluation.passed)
|
|
1257
|
-
return null;
|
|
1258
|
-
const structured = evaluation.structured;
|
|
1259
|
-
if (!structured || typeof structured !== "object")
|
|
1260
|
-
return null;
|
|
1261
|
-
if (!Object.hasOwn(structured, GATE_CONFIG_ERROR_KEY))
|
|
1262
|
-
return null;
|
|
1263
|
-
if (structured[GATE_CONFIG_ERROR_KEY] !== true) {
|
|
1264
|
-
return null;
|
|
1265
|
-
}
|
|
1266
|
-
const reason = structured.reason;
|
|
1267
|
-
return typeof reason === "string" && reason.trim().length > 0 ? reason.trim() : "the gate cannot be measured as configured";
|
|
1268
|
-
}
|
|
1291
|
+
init_dist();
|
|
1269
1292
|
|
|
1270
1293
|
// src/command-metric.ts
|
|
1271
1294
|
init_log();
|
|
@@ -1523,6 +1546,7 @@ init_log();
|
|
|
1523
1546
|
|
|
1524
1547
|
// src/oracle-collector.ts
|
|
1525
1548
|
init_log();
|
|
1549
|
+
import { createHash } from "node:crypto";
|
|
1526
1550
|
var TAG4 = "oracle-collector";
|
|
1527
1551
|
|
|
1528
1552
|
class OracleCollector {
|
|
@@ -1545,6 +1569,10 @@ class OracleCollector {
|
|
|
1545
1569
|
return await this.runHeld(oracle);
|
|
1546
1570
|
}
|
|
1547
1571
|
async runHeld(oracle) {
|
|
1572
|
+
const identity = {
|
|
1573
|
+
oracleId: oracle.id ?? null,
|
|
1574
|
+
contentHash: createHash("sha256").update(oracle.content).digest("hex")
|
|
1575
|
+
};
|
|
1548
1576
|
await this.deps.place(this.deps.repoPath, oracle);
|
|
1549
1577
|
try {
|
|
1550
1578
|
const { exitCode, output } = await this.deps.run(this.deps.repoPath, oracle);
|
|
@@ -1561,6 +1589,7 @@ ${output}`;
|
|
|
1561
1589
|
oracle: {
|
|
1562
1590
|
exitCode,
|
|
1563
1591
|
path: oracle.path,
|
|
1592
|
+
...identity,
|
|
1564
1593
|
output: "withheld — oracle_passed is a secrecy gate; see the motor's local log"
|
|
1565
1594
|
}
|
|
1566
1595
|
}
|
|
@@ -1570,7 +1599,10 @@ ${output}`;
|
|
|
1570
1599
|
log.warn(TAG4, `Oracle run threw: ${message} — blocked`);
|
|
1571
1600
|
return {
|
|
1572
1601
|
result: "blocked",
|
|
1573
|
-
structured: {
|
|
1602
|
+
structured: {
|
|
1603
|
+
oracle: { path: oracle.path, ...identity },
|
|
1604
|
+
error: message
|
|
1605
|
+
}
|
|
1574
1606
|
};
|
|
1575
1607
|
} finally {
|
|
1576
1608
|
await this.removeBestEffort(oracle);
|
|
@@ -2495,6 +2527,7 @@ class HarmonyClient {
|
|
|
2495
2527
|
}
|
|
2496
2528
|
const body = await response.json();
|
|
2497
2529
|
return {
|
|
2530
|
+
id: body.id ?? null,
|
|
2498
2531
|
path: body.path,
|
|
2499
2532
|
content: body.content,
|
|
2500
2533
|
runnerHint: body.runnerHint ?? null
|
|
@@ -2516,6 +2549,60 @@ async function detail(response) {
|
|
|
2516
2549
|
// src/cli.ts
|
|
2517
2550
|
init_log();
|
|
2518
2551
|
|
|
2552
|
+
// src/motor-stream.ts
|
|
2553
|
+
var RELAYED_KINDS = new Set([
|
|
2554
|
+
"run_started",
|
|
2555
|
+
"assistant_text",
|
|
2556
|
+
"tool_started",
|
|
2557
|
+
"tool_ended",
|
|
2558
|
+
"cost_updated",
|
|
2559
|
+
"error",
|
|
2560
|
+
"run_finished"
|
|
2561
|
+
]);
|
|
2562
|
+
var MOTOR_TOOL_INPUT_VALUE_MAX = 400;
|
|
2563
|
+
var MOTOR_TOOL_INPUT_KEY_MAX = 24;
|
|
2564
|
+
function boundToolInput(input) {
|
|
2565
|
+
if (typeof input === "string") {
|
|
2566
|
+
return input.slice(0, MOTOR_TOOL_INPUT_VALUE_MAX);
|
|
2567
|
+
}
|
|
2568
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
2569
|
+
return typeof input === "number" || typeof input === "boolean" ? input : undefined;
|
|
2570
|
+
}
|
|
2571
|
+
const bounded = {};
|
|
2572
|
+
let kept = 0;
|
|
2573
|
+
for (const [key, value] of Object.entries(input)) {
|
|
2574
|
+
if (kept >= MOTOR_TOOL_INPUT_KEY_MAX)
|
|
2575
|
+
break;
|
|
2576
|
+
const boundedKey = key.slice(0, MOTOR_TOOL_INPUT_VALUE_MAX);
|
|
2577
|
+
if (typeof value === "string") {
|
|
2578
|
+
bounded[boundedKey] = value.slice(0, MOTOR_TOOL_INPUT_VALUE_MAX);
|
|
2579
|
+
} else if (typeof value === "number" || typeof value === "boolean") {
|
|
2580
|
+
bounded[boundedKey] = value;
|
|
2581
|
+
} else {
|
|
2582
|
+
continue;
|
|
2583
|
+
}
|
|
2584
|
+
kept++;
|
|
2585
|
+
}
|
|
2586
|
+
return bounded;
|
|
2587
|
+
}
|
|
2588
|
+
function relayAgentEvent(draft) {
|
|
2589
|
+
if (!RELAYED_KINDS.has(draft.kind))
|
|
2590
|
+
return null;
|
|
2591
|
+
if (draft.kind === "tool_started") {
|
|
2592
|
+
return {
|
|
2593
|
+
type: "agent_event",
|
|
2594
|
+
event: {
|
|
2595
|
+
...draft,
|
|
2596
|
+
payload: {
|
|
2597
|
+
...draft.payload,
|
|
2598
|
+
input: boundToolInput(draft.payload.input)
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
};
|
|
2602
|
+
}
|
|
2603
|
+
return { type: "agent_event", event: draft };
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2519
2606
|
// src/oracle.ts
|
|
2520
2607
|
import { lstat, mkdir, realpath, rm, writeFile } from "node:fs/promises";
|
|
2521
2608
|
import { dirname, isAbsolute, resolve, sep } from "node:path";
|
|
@@ -2670,6 +2757,10 @@ function mayHoldCredentials(role) {
|
|
|
2670
2757
|
function credentialReadDeny() {
|
|
2671
2758
|
return `Read(/${getConfigDir()}/**)`;
|
|
2672
2759
|
}
|
|
2760
|
+
function credentialAccessDeny() {
|
|
2761
|
+
const dir = `/${getConfigDir()}/**`;
|
|
2762
|
+
return [`Read(${dir})`, `Grep(${dir})`, `Glob(${dir})`];
|
|
2763
|
+
}
|
|
2673
2764
|
function buildRoleLaunch(args) {
|
|
2674
2765
|
const role = normalizeStageRole(args.role);
|
|
2675
2766
|
const keep = mayHoldCredentials(role);
|
|
@@ -2780,7 +2871,9 @@ function oracleWriteAddendum(input) {
|
|
|
2780
2871
|
}, null, 2),
|
|
2781
2872
|
"```",
|
|
2782
2873
|
"",
|
|
2783
|
-
"Constraints: `path` is repo-relative ([A-Za-z0-9._-] per segment, no `..`, no dot-prefixed segment, no absolute path); `content` <= 100 KB; `runnerHint` is `bun` or `vitest`. Do NOT commit the oracle file — the motor places and removes it at the gated stage."
|
|
2874
|
+
"Constraints: `path` is repo-relative ([A-Za-z0-9._-] per segment, no `..`, no dot-prefixed segment, no absolute path); `content` <= 100 KB; `runnerHint` is `bun` or `vitest`. Do NOT commit the oracle file — the motor places and removes it at the gated stage.",
|
|
2875
|
+
"",
|
|
2876
|
+
'If the POST answers 409, an oracle for that stage is already held — another author wrote it, and replacing it changes the contract the implementer is graded against. Only if replacing it is genuinely this stage\'s instruction (e.g. a deliberate re-author), re-send the same body plus `"replace": true`; the replacement is recorded on the card, naming both authors. Otherwise stop and report the conflict instead of replacing.'
|
|
2784
2877
|
].join(`
|
|
2785
2878
|
`);
|
|
2786
2879
|
}
|
|
@@ -2791,7 +2884,7 @@ function buildStagePrompt(args) {
|
|
|
2791
2884
|
`## Playbook stage: ${stageName}`,
|
|
2792
2885
|
`You are running the "${stageName}" stage for Harmony card ${args.cardId}.`,
|
|
2793
2886
|
entryAction ? `Stage skill / entry action: \`${entryAction}\`. Follow that skill's method for this stage.` : "Read the card with the Harmony MCP tools (`harmony_get_card`) and do this stage's work for it.",
|
|
2794
|
-
"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.",
|
|
2887
|
+
"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. Those tools are disabled for this run, so attempting them only wastes turns.",
|
|
2795
2888
|
STAGE_SCOPE_LINE
|
|
2796
2889
|
];
|
|
2797
2890
|
if (args.stage?.role === "author" && args.oracleTargetStageId && args.sessionId) {
|
|
@@ -2843,24 +2936,46 @@ function buildStageRunnerConfig(args) {
|
|
|
2843
2936
|
prompt: launch.prompt,
|
|
2844
2937
|
cwd: launch.repoPath,
|
|
2845
2938
|
config: {
|
|
2846
|
-
disallowedTools: launch.disallowedTools,
|
|
2939
|
+
disallowedTools: [...launch.disallowedTools, ...STAGE_DAEMON_OWNED_TOOLS],
|
|
2847
2940
|
stripEnvKeys: envKeysDroppedByLaunch(args.parentEnv, launch)
|
|
2848
2941
|
}
|
|
2849
2942
|
};
|
|
2850
2943
|
}
|
|
2944
|
+
var DEFAULT_STAGE_TIMEOUT_MS = 2700000;
|
|
2945
|
+
var MAX_TIMER_MS = 2147483647;
|
|
2946
|
+
function stageTimeoutMs(env) {
|
|
2947
|
+
const raw = env.HARMONY_HARNESS_STAGE_TIMEOUT_MS;
|
|
2948
|
+
if (raw === undefined || raw.trim() === "")
|
|
2949
|
+
return DEFAULT_STAGE_TIMEOUT_MS;
|
|
2950
|
+
const parsed = Number(raw);
|
|
2951
|
+
if (!Number.isFinite(parsed))
|
|
2952
|
+
return DEFAULT_STAGE_TIMEOUT_MS;
|
|
2953
|
+
return Math.min(parsed, MAX_TIMER_MS);
|
|
2954
|
+
}
|
|
2955
|
+
function describeShutdown(signal, hasRunner) {
|
|
2956
|
+
return {
|
|
2957
|
+
message: hasRunner ? `the harness motor received ${signal} and stopped its stage subagent` : `the harness motor received ${signal} with no stage subagent in flight`,
|
|
2958
|
+
stopSubagent: hasRunner
|
|
2959
|
+
};
|
|
2960
|
+
}
|
|
2851
2961
|
|
|
2852
2962
|
// src/stage-run.ts
|
|
2853
2963
|
async function runStage(request, deps) {
|
|
2854
|
-
const events = [
|
|
2855
|
-
|
|
2856
|
-
|
|
2964
|
+
const events = [];
|
|
2965
|
+
const emit2 = (event) => {
|
|
2966
|
+
events.push(event);
|
|
2967
|
+
try {
|
|
2968
|
+
deps.emit?.(event);
|
|
2969
|
+
} catch {}
|
|
2970
|
+
};
|
|
2971
|
+
emit2({ type: "stage_entered", stageId: request.stageId });
|
|
2857
2972
|
const gate = await deps.resolveGate(request);
|
|
2858
2973
|
await deps.runRole(request);
|
|
2859
2974
|
if (!gate) {
|
|
2860
2975
|
return { stageId: request.stageId, gateKind: null, evidence: null, events };
|
|
2861
2976
|
}
|
|
2862
2977
|
const evidence = await deps.collect(request, gate);
|
|
2863
|
-
|
|
2978
|
+
emit2({
|
|
2864
2979
|
type: "gate_evaluated",
|
|
2865
2980
|
stageId: request.stageId,
|
|
2866
2981
|
gateKind: gate.kind,
|
|
@@ -2872,7 +2987,27 @@ async function runStage(request, deps) {
|
|
|
2872
2987
|
// src/cli.ts
|
|
2873
2988
|
var TAG11 = "cli";
|
|
2874
2989
|
var GATE_VERIFICATION_TIMEOUT_MS = 600000;
|
|
2875
|
-
|
|
2990
|
+
var SHUTDOWN_HARD_EXIT_MS = 15000;
|
|
2991
|
+
var activeRunner = null;
|
|
2992
|
+
var shuttingDown = false;
|
|
2993
|
+
async function shutdown(signal) {
|
|
2994
|
+
if (shuttingDown)
|
|
2995
|
+
return;
|
|
2996
|
+
shuttingDown = true;
|
|
2997
|
+
const runner = activeRunner;
|
|
2998
|
+
const plan = describeShutdown(signal, runner !== null);
|
|
2999
|
+
process.stderr.write(`${JSON.stringify({ type: "error", message: plan.message })}
|
|
3000
|
+
`);
|
|
3001
|
+
const hardExit = setTimeout(() => process.exit(1), SHUTDOWN_HARD_EXIT_MS);
|
|
3002
|
+
hardExit.unref?.();
|
|
3003
|
+
if (plan.stopSubagent) {
|
|
3004
|
+
try {
|
|
3005
|
+
await runner?.stop("shutdown");
|
|
3006
|
+
} catch {}
|
|
3007
|
+
}
|
|
3008
|
+
process.exit(1);
|
|
3009
|
+
}
|
|
3010
|
+
async function runRole(request, prompt, emit2) {
|
|
2876
3011
|
const launch = buildStageRunnerConfig({
|
|
2877
3012
|
role: request.role,
|
|
2878
3013
|
prompt,
|
|
@@ -2880,21 +3015,50 @@ async function runRole(request, prompt) {
|
|
|
2880
3015
|
parentEnv: process.env
|
|
2881
3016
|
});
|
|
2882
3017
|
const runner = new SdkAgentRunner(launch.config);
|
|
3018
|
+
activeRunner = runner;
|
|
2883
3019
|
log.info(TAG11, `Running stage ${request.stageId} as role ${launch.role ?? "(none — fail-closed)"}`);
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
})
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
3020
|
+
const timeoutMs = stageTimeoutMs(process.env);
|
|
3021
|
+
let timedOut = false;
|
|
3022
|
+
const clock = timeoutMs > 0 ? setTimeout(() => {
|
|
3023
|
+
timedOut = true;
|
|
3024
|
+
log.warn(TAG11, `stage ${request.stageId} exceeded ${timeoutMs}ms — stopping the subagent`);
|
|
3025
|
+
runner.stop("timeout");
|
|
3026
|
+
}, timeoutMs) : null;
|
|
3027
|
+
clock?.unref?.();
|
|
3028
|
+
try {
|
|
3029
|
+
for await (const event of runner.start({
|
|
3030
|
+
sessionId: request.sessionId,
|
|
3031
|
+
cardId: request.cardId,
|
|
3032
|
+
workspaceId: request.workspaceId,
|
|
3033
|
+
prompt: launch.prompt,
|
|
3034
|
+
cwd: launch.cwd
|
|
3035
|
+
})) {
|
|
3036
|
+
const relayed = relayAgentEvent(event);
|
|
3037
|
+
if (relayed)
|
|
3038
|
+
emit2(relayed);
|
|
3039
|
+
if (event.kind === "error") {
|
|
3040
|
+
log.warn(TAG11, `subagent error: ${event.payload.message}`);
|
|
3041
|
+
} else {
|
|
3042
|
+
log.event(TAG11, `subagent ${event.kind}`);
|
|
3043
|
+
}
|
|
2895
3044
|
}
|
|
3045
|
+
} finally {
|
|
3046
|
+
if (clock)
|
|
3047
|
+
clearTimeout(clock);
|
|
3048
|
+
activeRunner = null;
|
|
3049
|
+
}
|
|
3050
|
+
if (timedOut) {
|
|
3051
|
+
throw new Error(`stage ${request.stageId} exceeded its ${timeoutMs}ms wall-clock bound and its subagent was stopped`);
|
|
2896
3052
|
}
|
|
2897
3053
|
}
|
|
3054
|
+
function emitLine(line) {
|
|
3055
|
+
try {
|
|
3056
|
+
process.stdout.write(`${JSON.stringify(line)}
|
|
3057
|
+
`);
|
|
3058
|
+
} catch {}
|
|
3059
|
+
}
|
|
3060
|
+
process.stdout.on("error", () => {});
|
|
3061
|
+
process.stderr.on("error", () => {});
|
|
2898
3062
|
async function main() {
|
|
2899
3063
|
const parsed = parseStageRunArgs(process.argv.slice(2));
|
|
2900
3064
|
if (!parsed.ok) {
|
|
@@ -2937,8 +3101,9 @@ ${STAGE_RUN_USAGE}
|
|
|
2937
3101
|
role: pinned.stage.role ?? null
|
|
2938
3102
|
};
|
|
2939
3103
|
const result = await runStage(request, {
|
|
3104
|
+
emit: emitLine,
|
|
2940
3105
|
resolveGate: async () => pinned.gate,
|
|
2941
|
-
runRole: (req) => runRole(req, prompt),
|
|
3106
|
+
runRole: (req) => runRole(req, prompt, emitLine),
|
|
2942
3107
|
collect: async (req, gate) => {
|
|
2943
3108
|
const registry = buildGateCollectorRegistry({
|
|
2944
3109
|
build: {
|
|
@@ -2967,10 +3132,6 @@ ${STAGE_RUN_USAGE}
|
|
|
2967
3132
|
});
|
|
2968
3133
|
}
|
|
2969
3134
|
});
|
|
2970
|
-
for (const event of result.events) {
|
|
2971
|
-
process.stdout.write(`${JSON.stringify(event)}
|
|
2972
|
-
`);
|
|
2973
|
-
}
|
|
2974
3135
|
if (result.evidence && pinned.gate) {
|
|
2975
3136
|
const context = {
|
|
2976
3137
|
cardId,
|
|
@@ -2980,11 +3141,18 @@ ${STAGE_RUN_USAGE}
|
|
|
2980
3141
|
};
|
|
2981
3142
|
const evaluation = gateEvaluate(pinned.gate, result.evidence);
|
|
2982
3143
|
await client.recordStageGateEvidence(toStageGateEvidenceInsert(context, result.evidence));
|
|
2983
|
-
|
|
2984
|
-
|
|
3144
|
+
emitLine({
|
|
3145
|
+
type: "gate_verdict",
|
|
3146
|
+
passed: evaluation.passed,
|
|
3147
|
+
findings: evaluation.findings
|
|
3148
|
+
});
|
|
2985
3149
|
}
|
|
2986
|
-
|
|
2987
|
-
|
|
3150
|
+
emitLine({ type: "result", ...result });
|
|
3151
|
+
}
|
|
3152
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
3153
|
+
process.on(signal, () => {
|
|
3154
|
+
shutdown(signal);
|
|
3155
|
+
});
|
|
2988
3156
|
}
|
|
2989
3157
|
main().catch((err) => {
|
|
2990
3158
|
const message = err instanceof Error ? err.message : String(err);
|