@gethmy/harness 1.1.1 → 1.2.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/dist/cli.js +213 -51
- package/dist/index.js +372 -49
- 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 +117 -20
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,26 @@ var init_constants = __esm(() => {
|
|
|
73
81
|
QUERY_GC_TIME: 1000 * 60 * 60 * 24
|
|
74
82
|
};
|
|
75
83
|
});
|
|
84
|
+
// ../harmony-shared/dist/gateConfigError.js
|
|
85
|
+
function gateConfigErrorReason(evaluation) {
|
|
86
|
+
if (!evaluation || evaluation.passed)
|
|
87
|
+
return null;
|
|
88
|
+
const structured = evaluation.structured;
|
|
89
|
+
if (!structured || typeof structured !== "object")
|
|
90
|
+
return null;
|
|
91
|
+
if (!Object.hasOwn(structured, GATE_CONFIG_ERROR_KEY))
|
|
92
|
+
return null;
|
|
93
|
+
if (structured[GATE_CONFIG_ERROR_KEY] !== true) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const reason = structured.reason;
|
|
97
|
+
return typeof reason === "string" && reason.trim().length > 0 ? reason.trim() : "the gate cannot be measured as configured";
|
|
98
|
+
}
|
|
99
|
+
var GATE_CONFIG_ERROR_KEY = "configError", GATE_CONFIG_ERROR_MARK;
|
|
100
|
+
var init_gateConfigError = __esm(() => {
|
|
101
|
+
GATE_CONFIG_ERROR_MARK = Object.freeze({ [GATE_CONFIG_ERROR_KEY]: true });
|
|
102
|
+
});
|
|
103
|
+
|
|
76
104
|
// ../harmony-shared/dist/gateEvaluate.js
|
|
77
105
|
function isGateKind(value) {
|
|
78
106
|
return typeof value === "string" && GATE_KINDS.includes(value);
|
|
@@ -468,6 +496,7 @@ var init_dist = __esm(() => {
|
|
|
468
496
|
init_columnSort();
|
|
469
497
|
init_commentSerializer();
|
|
470
498
|
init_constants();
|
|
499
|
+
init_gateConfigError();
|
|
471
500
|
init_gateEvaluate();
|
|
472
501
|
init_logger();
|
|
473
502
|
init_playbookAutoBind();
|
|
@@ -579,17 +608,18 @@ var RETIRED_MODEL = /^claude-[23][.-]/i;
|
|
|
579
608
|
function clampWithdrawn(model) {
|
|
580
609
|
return RETIRED_MODEL.test(model) ? MAX_IMPLEMENT_MODEL : model;
|
|
581
610
|
}
|
|
582
|
-
function chooseImplementModel(claude, card, attempts) {
|
|
611
|
+
function chooseImplementModel(claude, card, attempts, sized) {
|
|
583
612
|
if (card.model_override) {
|
|
613
|
+
const pinned = isModelTier(card.model_override) ? claude.tiers?.[card.model_override] || claude.model : card.model_override;
|
|
584
614
|
return {
|
|
585
|
-
model: clampWithdrawn(
|
|
615
|
+
model: clampWithdrawn(pinned),
|
|
586
616
|
escalated: false,
|
|
587
617
|
source: "override"
|
|
588
618
|
};
|
|
589
619
|
}
|
|
590
|
-
if (isModelTier(
|
|
620
|
+
if (sized && isModelTier(sized.tier)) {
|
|
591
621
|
const retry = attempts >= claude.escalateAfterAttempts;
|
|
592
|
-
const tier = retry ? escalateTier(
|
|
622
|
+
const tier = retry ? escalateTier(sized.tier) : sized.tier;
|
|
593
623
|
const mapped = claude.tiers?.[tier];
|
|
594
624
|
return {
|
|
595
625
|
model: clampWithdrawn(mapped && mapped.length > 0 ? mapped : claude.model),
|
|
@@ -821,13 +851,15 @@ class SdkAgentRunner {
|
|
|
821
851
|
};
|
|
822
852
|
const allowed = this.cfg.allowedTools ?? SDK_ALLOWED_TOOLS;
|
|
823
853
|
const builtinTools = allowed.filter((t) => !t.startsWith("mcp__") && !t.includes("*"));
|
|
854
|
+
const gateEach = this.cfg.gateEveryToolCall === true;
|
|
824
855
|
const options = {
|
|
825
856
|
cwd: input.cwd,
|
|
826
857
|
model: input.model ?? this.cfg.model,
|
|
827
|
-
allowedTools: allowed,
|
|
858
|
+
...gateEach ? {} : { allowedTools: allowed },
|
|
828
859
|
...this.cfg.disallowedTools && this.cfg.disallowedTools.length > 0 ? { disallowedTools: this.cfg.disallowedTools } : {},
|
|
860
|
+
...this.cfg.canUseTool ? { canUseTool: this.cfg.canUseTool } : {},
|
|
829
861
|
tools: builtinTools,
|
|
830
|
-
permissionMode: "dontAsk",
|
|
862
|
+
permissionMode: gateEach ? "default" : "dontAsk",
|
|
831
863
|
maxTurns: this.cfg.maxTurns,
|
|
832
864
|
abortController: this.abort,
|
|
833
865
|
...resumeSessionId ? { resume: resumeSessionId } : {},
|
|
@@ -1250,22 +1282,7 @@ init_dist();
|
|
|
1250
1282
|
var DEFAULT_METRIC_TIMEOUT_MS = 300000;
|
|
1251
1283
|
|
|
1252
1284
|
// 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
|
-
}
|
|
1285
|
+
init_dist();
|
|
1269
1286
|
|
|
1270
1287
|
// src/command-metric.ts
|
|
1271
1288
|
init_log();
|
|
@@ -1523,6 +1540,7 @@ init_log();
|
|
|
1523
1540
|
|
|
1524
1541
|
// src/oracle-collector.ts
|
|
1525
1542
|
init_log();
|
|
1543
|
+
import { createHash } from "node:crypto";
|
|
1526
1544
|
var TAG4 = "oracle-collector";
|
|
1527
1545
|
|
|
1528
1546
|
class OracleCollector {
|
|
@@ -1545,6 +1563,10 @@ class OracleCollector {
|
|
|
1545
1563
|
return await this.runHeld(oracle);
|
|
1546
1564
|
}
|
|
1547
1565
|
async runHeld(oracle) {
|
|
1566
|
+
const identity = {
|
|
1567
|
+
oracleId: oracle.id ?? null,
|
|
1568
|
+
contentHash: createHash("sha256").update(oracle.content).digest("hex")
|
|
1569
|
+
};
|
|
1548
1570
|
await this.deps.place(this.deps.repoPath, oracle);
|
|
1549
1571
|
try {
|
|
1550
1572
|
const { exitCode, output } = await this.deps.run(this.deps.repoPath, oracle);
|
|
@@ -1561,6 +1583,7 @@ ${output}`;
|
|
|
1561
1583
|
oracle: {
|
|
1562
1584
|
exitCode,
|
|
1563
1585
|
path: oracle.path,
|
|
1586
|
+
...identity,
|
|
1564
1587
|
output: "withheld — oracle_passed is a secrecy gate; see the motor's local log"
|
|
1565
1588
|
}
|
|
1566
1589
|
}
|
|
@@ -1570,7 +1593,10 @@ ${output}`;
|
|
|
1570
1593
|
log.warn(TAG4, `Oracle run threw: ${message} — blocked`);
|
|
1571
1594
|
return {
|
|
1572
1595
|
result: "blocked",
|
|
1573
|
-
structured: {
|
|
1596
|
+
structured: {
|
|
1597
|
+
oracle: { path: oracle.path, ...identity },
|
|
1598
|
+
error: message
|
|
1599
|
+
}
|
|
1574
1600
|
};
|
|
1575
1601
|
} finally {
|
|
1576
1602
|
await this.removeBestEffort(oracle);
|
|
@@ -2495,6 +2521,7 @@ class HarmonyClient {
|
|
|
2495
2521
|
}
|
|
2496
2522
|
const body = await response.json();
|
|
2497
2523
|
return {
|
|
2524
|
+
id: body.id ?? null,
|
|
2498
2525
|
path: body.path,
|
|
2499
2526
|
content: body.content,
|
|
2500
2527
|
runnerHint: body.runnerHint ?? null
|
|
@@ -2516,6 +2543,60 @@ async function detail(response) {
|
|
|
2516
2543
|
// src/cli.ts
|
|
2517
2544
|
init_log();
|
|
2518
2545
|
|
|
2546
|
+
// src/motor-stream.ts
|
|
2547
|
+
var RELAYED_KINDS = new Set([
|
|
2548
|
+
"run_started",
|
|
2549
|
+
"assistant_text",
|
|
2550
|
+
"tool_started",
|
|
2551
|
+
"tool_ended",
|
|
2552
|
+
"cost_updated",
|
|
2553
|
+
"error",
|
|
2554
|
+
"run_finished"
|
|
2555
|
+
]);
|
|
2556
|
+
var MOTOR_TOOL_INPUT_VALUE_MAX = 400;
|
|
2557
|
+
var MOTOR_TOOL_INPUT_KEY_MAX = 24;
|
|
2558
|
+
function boundToolInput(input) {
|
|
2559
|
+
if (typeof input === "string") {
|
|
2560
|
+
return input.slice(0, MOTOR_TOOL_INPUT_VALUE_MAX);
|
|
2561
|
+
}
|
|
2562
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
2563
|
+
return typeof input === "number" || typeof input === "boolean" ? input : undefined;
|
|
2564
|
+
}
|
|
2565
|
+
const bounded = {};
|
|
2566
|
+
let kept = 0;
|
|
2567
|
+
for (const [key, value] of Object.entries(input)) {
|
|
2568
|
+
if (kept >= MOTOR_TOOL_INPUT_KEY_MAX)
|
|
2569
|
+
break;
|
|
2570
|
+
const boundedKey = key.slice(0, MOTOR_TOOL_INPUT_VALUE_MAX);
|
|
2571
|
+
if (typeof value === "string") {
|
|
2572
|
+
bounded[boundedKey] = value.slice(0, MOTOR_TOOL_INPUT_VALUE_MAX);
|
|
2573
|
+
} else if (typeof value === "number" || typeof value === "boolean") {
|
|
2574
|
+
bounded[boundedKey] = value;
|
|
2575
|
+
} else {
|
|
2576
|
+
continue;
|
|
2577
|
+
}
|
|
2578
|
+
kept++;
|
|
2579
|
+
}
|
|
2580
|
+
return bounded;
|
|
2581
|
+
}
|
|
2582
|
+
function relayAgentEvent(draft) {
|
|
2583
|
+
if (!RELAYED_KINDS.has(draft.kind))
|
|
2584
|
+
return null;
|
|
2585
|
+
if (draft.kind === "tool_started") {
|
|
2586
|
+
return {
|
|
2587
|
+
type: "agent_event",
|
|
2588
|
+
event: {
|
|
2589
|
+
...draft,
|
|
2590
|
+
payload: {
|
|
2591
|
+
...draft.payload,
|
|
2592
|
+
input: boundToolInput(draft.payload.input)
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
};
|
|
2596
|
+
}
|
|
2597
|
+
return { type: "agent_event", event: draft };
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2519
2600
|
// src/oracle.ts
|
|
2520
2601
|
import { lstat, mkdir, realpath, rm, writeFile } from "node:fs/promises";
|
|
2521
2602
|
import { dirname, isAbsolute, resolve, sep } from "node:path";
|
|
@@ -2670,6 +2751,10 @@ function mayHoldCredentials(role) {
|
|
|
2670
2751
|
function credentialReadDeny() {
|
|
2671
2752
|
return `Read(/${getConfigDir()}/**)`;
|
|
2672
2753
|
}
|
|
2754
|
+
function credentialAccessDeny() {
|
|
2755
|
+
const dir = `/${getConfigDir()}/**`;
|
|
2756
|
+
return [`Read(${dir})`, `Grep(${dir})`, `Glob(${dir})`];
|
|
2757
|
+
}
|
|
2673
2758
|
function buildRoleLaunch(args) {
|
|
2674
2759
|
const role = normalizeStageRole(args.role);
|
|
2675
2760
|
const keep = mayHoldCredentials(role);
|
|
@@ -2780,7 +2865,9 @@ function oracleWriteAddendum(input) {
|
|
|
2780
2865
|
}, null, 2),
|
|
2781
2866
|
"```",
|
|
2782
2867
|
"",
|
|
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."
|
|
2868
|
+
"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.",
|
|
2869
|
+
"",
|
|
2870
|
+
'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
2871
|
].join(`
|
|
2785
2872
|
`);
|
|
2786
2873
|
}
|
|
@@ -2791,7 +2878,7 @@ function buildStagePrompt(args) {
|
|
|
2791
2878
|
`## Playbook stage: ${stageName}`,
|
|
2792
2879
|
`You are running the "${stageName}" stage for Harmony card ${args.cardId}.`,
|
|
2793
2880
|
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.",
|
|
2881
|
+
"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
2882
|
STAGE_SCOPE_LINE
|
|
2796
2883
|
];
|
|
2797
2884
|
if (args.stage?.role === "author" && args.oracleTargetStageId && args.sessionId) {
|
|
@@ -2843,24 +2930,46 @@ function buildStageRunnerConfig(args) {
|
|
|
2843
2930
|
prompt: launch.prompt,
|
|
2844
2931
|
cwd: launch.repoPath,
|
|
2845
2932
|
config: {
|
|
2846
|
-
disallowedTools: launch.disallowedTools,
|
|
2933
|
+
disallowedTools: [...launch.disallowedTools, ...STAGE_DAEMON_OWNED_TOOLS],
|
|
2847
2934
|
stripEnvKeys: envKeysDroppedByLaunch(args.parentEnv, launch)
|
|
2848
2935
|
}
|
|
2849
2936
|
};
|
|
2850
2937
|
}
|
|
2938
|
+
var DEFAULT_STAGE_TIMEOUT_MS = 2700000;
|
|
2939
|
+
var MAX_TIMER_MS = 2147483647;
|
|
2940
|
+
function stageTimeoutMs(env) {
|
|
2941
|
+
const raw = env.HARMONY_HARNESS_STAGE_TIMEOUT_MS;
|
|
2942
|
+
if (raw === undefined || raw.trim() === "")
|
|
2943
|
+
return DEFAULT_STAGE_TIMEOUT_MS;
|
|
2944
|
+
const parsed = Number(raw);
|
|
2945
|
+
if (!Number.isFinite(parsed))
|
|
2946
|
+
return DEFAULT_STAGE_TIMEOUT_MS;
|
|
2947
|
+
return Math.min(parsed, MAX_TIMER_MS);
|
|
2948
|
+
}
|
|
2949
|
+
function describeShutdown(signal, hasRunner) {
|
|
2950
|
+
return {
|
|
2951
|
+
message: hasRunner ? `the harness motor received ${signal} and stopped its stage subagent` : `the harness motor received ${signal} with no stage subagent in flight`,
|
|
2952
|
+
stopSubagent: hasRunner
|
|
2953
|
+
};
|
|
2954
|
+
}
|
|
2851
2955
|
|
|
2852
2956
|
// src/stage-run.ts
|
|
2853
2957
|
async function runStage(request, deps) {
|
|
2854
|
-
const events = [
|
|
2855
|
-
|
|
2856
|
-
|
|
2958
|
+
const events = [];
|
|
2959
|
+
const emit2 = (event) => {
|
|
2960
|
+
events.push(event);
|
|
2961
|
+
try {
|
|
2962
|
+
deps.emit?.(event);
|
|
2963
|
+
} catch {}
|
|
2964
|
+
};
|
|
2965
|
+
emit2({ type: "stage_entered", stageId: request.stageId });
|
|
2857
2966
|
const gate = await deps.resolveGate(request);
|
|
2858
2967
|
await deps.runRole(request);
|
|
2859
2968
|
if (!gate) {
|
|
2860
2969
|
return { stageId: request.stageId, gateKind: null, evidence: null, events };
|
|
2861
2970
|
}
|
|
2862
2971
|
const evidence = await deps.collect(request, gate);
|
|
2863
|
-
|
|
2972
|
+
emit2({
|
|
2864
2973
|
type: "gate_evaluated",
|
|
2865
2974
|
stageId: request.stageId,
|
|
2866
2975
|
gateKind: gate.kind,
|
|
@@ -2872,7 +2981,27 @@ async function runStage(request, deps) {
|
|
|
2872
2981
|
// src/cli.ts
|
|
2873
2982
|
var TAG11 = "cli";
|
|
2874
2983
|
var GATE_VERIFICATION_TIMEOUT_MS = 600000;
|
|
2875
|
-
|
|
2984
|
+
var SHUTDOWN_HARD_EXIT_MS = 15000;
|
|
2985
|
+
var activeRunner = null;
|
|
2986
|
+
var shuttingDown = false;
|
|
2987
|
+
async function shutdown(signal) {
|
|
2988
|
+
if (shuttingDown)
|
|
2989
|
+
return;
|
|
2990
|
+
shuttingDown = true;
|
|
2991
|
+
const runner = activeRunner;
|
|
2992
|
+
const plan = describeShutdown(signal, runner !== null);
|
|
2993
|
+
process.stderr.write(`${JSON.stringify({ type: "error", message: plan.message })}
|
|
2994
|
+
`);
|
|
2995
|
+
const hardExit = setTimeout(() => process.exit(1), SHUTDOWN_HARD_EXIT_MS);
|
|
2996
|
+
hardExit.unref?.();
|
|
2997
|
+
if (plan.stopSubagent) {
|
|
2998
|
+
try {
|
|
2999
|
+
await runner?.stop("shutdown");
|
|
3000
|
+
} catch {}
|
|
3001
|
+
}
|
|
3002
|
+
process.exit(1);
|
|
3003
|
+
}
|
|
3004
|
+
async function runRole(request, prompt, emit2) {
|
|
2876
3005
|
const launch = buildStageRunnerConfig({
|
|
2877
3006
|
role: request.role,
|
|
2878
3007
|
prompt,
|
|
@@ -2880,21 +3009,50 @@ async function runRole(request, prompt) {
|
|
|
2880
3009
|
parentEnv: process.env
|
|
2881
3010
|
});
|
|
2882
3011
|
const runner = new SdkAgentRunner(launch.config);
|
|
3012
|
+
activeRunner = runner;
|
|
2883
3013
|
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
|
-
|
|
3014
|
+
const timeoutMs = stageTimeoutMs(process.env);
|
|
3015
|
+
let timedOut = false;
|
|
3016
|
+
const clock = timeoutMs > 0 ? setTimeout(() => {
|
|
3017
|
+
timedOut = true;
|
|
3018
|
+
log.warn(TAG11, `stage ${request.stageId} exceeded ${timeoutMs}ms — stopping the subagent`);
|
|
3019
|
+
runner.stop("timeout");
|
|
3020
|
+
}, timeoutMs) : null;
|
|
3021
|
+
clock?.unref?.();
|
|
3022
|
+
try {
|
|
3023
|
+
for await (const event of runner.start({
|
|
3024
|
+
sessionId: request.sessionId,
|
|
3025
|
+
cardId: request.cardId,
|
|
3026
|
+
workspaceId: request.workspaceId,
|
|
3027
|
+
prompt: launch.prompt,
|
|
3028
|
+
cwd: launch.cwd
|
|
3029
|
+
})) {
|
|
3030
|
+
const relayed = relayAgentEvent(event);
|
|
3031
|
+
if (relayed)
|
|
3032
|
+
emit2(relayed);
|
|
3033
|
+
if (event.kind === "error") {
|
|
3034
|
+
log.warn(TAG11, `subagent error: ${event.payload.message}`);
|
|
3035
|
+
} else {
|
|
3036
|
+
log.event(TAG11, `subagent ${event.kind}`);
|
|
3037
|
+
}
|
|
2895
3038
|
}
|
|
3039
|
+
} finally {
|
|
3040
|
+
if (clock)
|
|
3041
|
+
clearTimeout(clock);
|
|
3042
|
+
activeRunner = null;
|
|
3043
|
+
}
|
|
3044
|
+
if (timedOut) {
|
|
3045
|
+
throw new Error(`stage ${request.stageId} exceeded its ${timeoutMs}ms wall-clock bound and its subagent was stopped`);
|
|
2896
3046
|
}
|
|
2897
3047
|
}
|
|
3048
|
+
function emitLine(line) {
|
|
3049
|
+
try {
|
|
3050
|
+
process.stdout.write(`${JSON.stringify(line)}
|
|
3051
|
+
`);
|
|
3052
|
+
} catch {}
|
|
3053
|
+
}
|
|
3054
|
+
process.stdout.on("error", () => {});
|
|
3055
|
+
process.stderr.on("error", () => {});
|
|
2898
3056
|
async function main() {
|
|
2899
3057
|
const parsed = parseStageRunArgs(process.argv.slice(2));
|
|
2900
3058
|
if (!parsed.ok) {
|
|
@@ -2937,8 +3095,9 @@ ${STAGE_RUN_USAGE}
|
|
|
2937
3095
|
role: pinned.stage.role ?? null
|
|
2938
3096
|
};
|
|
2939
3097
|
const result = await runStage(request, {
|
|
3098
|
+
emit: emitLine,
|
|
2940
3099
|
resolveGate: async () => pinned.gate,
|
|
2941
|
-
runRole: (req) => runRole(req, prompt),
|
|
3100
|
+
runRole: (req) => runRole(req, prompt, emitLine),
|
|
2942
3101
|
collect: async (req, gate) => {
|
|
2943
3102
|
const registry = buildGateCollectorRegistry({
|
|
2944
3103
|
build: {
|
|
@@ -2967,10 +3126,6 @@ ${STAGE_RUN_USAGE}
|
|
|
2967
3126
|
});
|
|
2968
3127
|
}
|
|
2969
3128
|
});
|
|
2970
|
-
for (const event of result.events) {
|
|
2971
|
-
process.stdout.write(`${JSON.stringify(event)}
|
|
2972
|
-
`);
|
|
2973
|
-
}
|
|
2974
3129
|
if (result.evidence && pinned.gate) {
|
|
2975
3130
|
const context = {
|
|
2976
3131
|
cardId,
|
|
@@ -2980,11 +3135,18 @@ ${STAGE_RUN_USAGE}
|
|
|
2980
3135
|
};
|
|
2981
3136
|
const evaluation = gateEvaluate(pinned.gate, result.evidence);
|
|
2982
3137
|
await client.recordStageGateEvidence(toStageGateEvidenceInsert(context, result.evidence));
|
|
2983
|
-
|
|
2984
|
-
|
|
3138
|
+
emitLine({
|
|
3139
|
+
type: "gate_verdict",
|
|
3140
|
+
passed: evaluation.passed,
|
|
3141
|
+
findings: evaluation.findings
|
|
3142
|
+
});
|
|
2985
3143
|
}
|
|
2986
|
-
|
|
2987
|
-
|
|
3144
|
+
emitLine({ type: "result", ...result });
|
|
3145
|
+
}
|
|
3146
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
3147
|
+
process.on(signal, () => {
|
|
3148
|
+
shutdown(signal);
|
|
3149
|
+
});
|
|
2988
3150
|
}
|
|
2989
3151
|
main().catch((err) => {
|
|
2990
3152
|
const message = err instanceof Error ? err.message : String(err);
|