@gethmy/harness 1.1.0 → 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 +219 -51
- package/dist/index.js +378 -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);
|
|
@@ -388,6 +416,11 @@ function toStageGateEvidenceInsert(context, evidence) {
|
|
|
388
416
|
|
|
389
417
|
// ../harmony-shared/dist/logger.js
|
|
390
418
|
var init_logger = () => {};
|
|
419
|
+
// ../harmony-shared/dist/playbookAutoBind.js
|
|
420
|
+
var init_playbookAutoBind = __esm(() => {
|
|
421
|
+
init_gateEvaluate();
|
|
422
|
+
});
|
|
423
|
+
|
|
391
424
|
// ../harmony-shared/dist/playbookCatalog.js
|
|
392
425
|
var init_playbookCatalog = () => {};
|
|
393
426
|
|
|
@@ -463,8 +496,10 @@ var init_dist = __esm(() => {
|
|
|
463
496
|
init_columnSort();
|
|
464
497
|
init_commentSerializer();
|
|
465
498
|
init_constants();
|
|
499
|
+
init_gateConfigError();
|
|
466
500
|
init_gateEvaluate();
|
|
467
501
|
init_logger();
|
|
502
|
+
init_playbookAutoBind();
|
|
468
503
|
init_playbookCatalog();
|
|
469
504
|
init_playbookStage();
|
|
470
505
|
init_projectTemplates();
|
|
@@ -573,17 +608,18 @@ var RETIRED_MODEL = /^claude-[23][.-]/i;
|
|
|
573
608
|
function clampWithdrawn(model) {
|
|
574
609
|
return RETIRED_MODEL.test(model) ? MAX_IMPLEMENT_MODEL : model;
|
|
575
610
|
}
|
|
576
|
-
function chooseImplementModel(claude, card, attempts) {
|
|
611
|
+
function chooseImplementModel(claude, card, attempts, sized) {
|
|
577
612
|
if (card.model_override) {
|
|
613
|
+
const pinned = isModelTier(card.model_override) ? claude.tiers?.[card.model_override] || claude.model : card.model_override;
|
|
578
614
|
return {
|
|
579
|
-
model: clampWithdrawn(
|
|
615
|
+
model: clampWithdrawn(pinned),
|
|
580
616
|
escalated: false,
|
|
581
617
|
source: "override"
|
|
582
618
|
};
|
|
583
619
|
}
|
|
584
|
-
if (isModelTier(
|
|
620
|
+
if (sized && isModelTier(sized.tier)) {
|
|
585
621
|
const retry = attempts >= claude.escalateAfterAttempts;
|
|
586
|
-
const tier = retry ? escalateTier(
|
|
622
|
+
const tier = retry ? escalateTier(sized.tier) : sized.tier;
|
|
587
623
|
const mapped = claude.tiers?.[tier];
|
|
588
624
|
return {
|
|
589
625
|
model: clampWithdrawn(mapped && mapped.length > 0 ? mapped : claude.model),
|
|
@@ -815,13 +851,15 @@ class SdkAgentRunner {
|
|
|
815
851
|
};
|
|
816
852
|
const allowed = this.cfg.allowedTools ?? SDK_ALLOWED_TOOLS;
|
|
817
853
|
const builtinTools = allowed.filter((t) => !t.startsWith("mcp__") && !t.includes("*"));
|
|
854
|
+
const gateEach = this.cfg.gateEveryToolCall === true;
|
|
818
855
|
const options = {
|
|
819
856
|
cwd: input.cwd,
|
|
820
857
|
model: input.model ?? this.cfg.model,
|
|
821
|
-
allowedTools: allowed,
|
|
858
|
+
...gateEach ? {} : { allowedTools: allowed },
|
|
822
859
|
...this.cfg.disallowedTools && this.cfg.disallowedTools.length > 0 ? { disallowedTools: this.cfg.disallowedTools } : {},
|
|
860
|
+
...this.cfg.canUseTool ? { canUseTool: this.cfg.canUseTool } : {},
|
|
823
861
|
tools: builtinTools,
|
|
824
|
-
permissionMode: "dontAsk",
|
|
862
|
+
permissionMode: gateEach ? "default" : "dontAsk",
|
|
825
863
|
maxTurns: this.cfg.maxTurns,
|
|
826
864
|
abortController: this.abort,
|
|
827
865
|
...resumeSessionId ? { resume: resumeSessionId } : {},
|
|
@@ -1244,22 +1282,7 @@ init_dist();
|
|
|
1244
1282
|
var DEFAULT_METRIC_TIMEOUT_MS = 300000;
|
|
1245
1283
|
|
|
1246
1284
|
// src/gate-config-error.ts
|
|
1247
|
-
|
|
1248
|
-
var GATE_CONFIG_ERROR_MARK = Object.freeze({ [GATE_CONFIG_ERROR_KEY]: true });
|
|
1249
|
-
function gateConfigErrorReason(evaluation) {
|
|
1250
|
-
if (!evaluation || evaluation.passed)
|
|
1251
|
-
return null;
|
|
1252
|
-
const structured = evaluation.structured;
|
|
1253
|
-
if (!structured || typeof structured !== "object")
|
|
1254
|
-
return null;
|
|
1255
|
-
if (!Object.hasOwn(structured, GATE_CONFIG_ERROR_KEY))
|
|
1256
|
-
return null;
|
|
1257
|
-
if (structured[GATE_CONFIG_ERROR_KEY] !== true) {
|
|
1258
|
-
return null;
|
|
1259
|
-
}
|
|
1260
|
-
const reason = structured.reason;
|
|
1261
|
-
return typeof reason === "string" && reason.trim().length > 0 ? reason.trim() : "the gate cannot be measured as configured";
|
|
1262
|
-
}
|
|
1285
|
+
init_dist();
|
|
1263
1286
|
|
|
1264
1287
|
// src/command-metric.ts
|
|
1265
1288
|
init_log();
|
|
@@ -1517,6 +1540,7 @@ init_log();
|
|
|
1517
1540
|
|
|
1518
1541
|
// src/oracle-collector.ts
|
|
1519
1542
|
init_log();
|
|
1543
|
+
import { createHash } from "node:crypto";
|
|
1520
1544
|
var TAG4 = "oracle-collector";
|
|
1521
1545
|
|
|
1522
1546
|
class OracleCollector {
|
|
@@ -1539,6 +1563,10 @@ class OracleCollector {
|
|
|
1539
1563
|
return await this.runHeld(oracle);
|
|
1540
1564
|
}
|
|
1541
1565
|
async runHeld(oracle) {
|
|
1566
|
+
const identity = {
|
|
1567
|
+
oracleId: oracle.id ?? null,
|
|
1568
|
+
contentHash: createHash("sha256").update(oracle.content).digest("hex")
|
|
1569
|
+
};
|
|
1542
1570
|
await this.deps.place(this.deps.repoPath, oracle);
|
|
1543
1571
|
try {
|
|
1544
1572
|
const { exitCode, output } = await this.deps.run(this.deps.repoPath, oracle);
|
|
@@ -1555,6 +1583,7 @@ ${output}`;
|
|
|
1555
1583
|
oracle: {
|
|
1556
1584
|
exitCode,
|
|
1557
1585
|
path: oracle.path,
|
|
1586
|
+
...identity,
|
|
1558
1587
|
output: "withheld — oracle_passed is a secrecy gate; see the motor's local log"
|
|
1559
1588
|
}
|
|
1560
1589
|
}
|
|
@@ -1564,7 +1593,10 @@ ${output}`;
|
|
|
1564
1593
|
log.warn(TAG4, `Oracle run threw: ${message} — blocked`);
|
|
1565
1594
|
return {
|
|
1566
1595
|
result: "blocked",
|
|
1567
|
-
structured: {
|
|
1596
|
+
structured: {
|
|
1597
|
+
oracle: { path: oracle.path, ...identity },
|
|
1598
|
+
error: message
|
|
1599
|
+
}
|
|
1568
1600
|
};
|
|
1569
1601
|
} finally {
|
|
1570
1602
|
await this.removeBestEffort(oracle);
|
|
@@ -2489,6 +2521,7 @@ class HarmonyClient {
|
|
|
2489
2521
|
}
|
|
2490
2522
|
const body = await response.json();
|
|
2491
2523
|
return {
|
|
2524
|
+
id: body.id ?? null,
|
|
2492
2525
|
path: body.path,
|
|
2493
2526
|
content: body.content,
|
|
2494
2527
|
runnerHint: body.runnerHint ?? null
|
|
@@ -2510,6 +2543,60 @@ async function detail(response) {
|
|
|
2510
2543
|
// src/cli.ts
|
|
2511
2544
|
init_log();
|
|
2512
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
|
+
|
|
2513
2600
|
// src/oracle.ts
|
|
2514
2601
|
import { lstat, mkdir, realpath, rm, writeFile } from "node:fs/promises";
|
|
2515
2602
|
import { dirname, isAbsolute, resolve, sep } from "node:path";
|
|
@@ -2664,6 +2751,10 @@ function mayHoldCredentials(role) {
|
|
|
2664
2751
|
function credentialReadDeny() {
|
|
2665
2752
|
return `Read(/${getConfigDir()}/**)`;
|
|
2666
2753
|
}
|
|
2754
|
+
function credentialAccessDeny() {
|
|
2755
|
+
const dir = `/${getConfigDir()}/**`;
|
|
2756
|
+
return [`Read(${dir})`, `Grep(${dir})`, `Glob(${dir})`];
|
|
2757
|
+
}
|
|
2667
2758
|
function buildRoleLaunch(args) {
|
|
2668
2759
|
const role = normalizeStageRole(args.role);
|
|
2669
2760
|
const keep = mayHoldCredentials(role);
|
|
@@ -2774,7 +2865,9 @@ function oracleWriteAddendum(input) {
|
|
|
2774
2865
|
}, null, 2),
|
|
2775
2866
|
"```",
|
|
2776
2867
|
"",
|
|
2777
|
-
"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.'
|
|
2778
2871
|
].join(`
|
|
2779
2872
|
`);
|
|
2780
2873
|
}
|
|
@@ -2785,7 +2878,7 @@ function buildStagePrompt(args) {
|
|
|
2785
2878
|
`## Playbook stage: ${stageName}`,
|
|
2786
2879
|
`You are running the "${stageName}" stage for Harmony card ${args.cardId}.`,
|
|
2787
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.",
|
|
2788
|
-
"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.",
|
|
2789
2882
|
STAGE_SCOPE_LINE
|
|
2790
2883
|
];
|
|
2791
2884
|
if (args.stage?.role === "author" && args.oracleTargetStageId && args.sessionId) {
|
|
@@ -2837,24 +2930,46 @@ function buildStageRunnerConfig(args) {
|
|
|
2837
2930
|
prompt: launch.prompt,
|
|
2838
2931
|
cwd: launch.repoPath,
|
|
2839
2932
|
config: {
|
|
2840
|
-
disallowedTools: launch.disallowedTools,
|
|
2933
|
+
disallowedTools: [...launch.disallowedTools, ...STAGE_DAEMON_OWNED_TOOLS],
|
|
2841
2934
|
stripEnvKeys: envKeysDroppedByLaunch(args.parentEnv, launch)
|
|
2842
2935
|
}
|
|
2843
2936
|
};
|
|
2844
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
|
+
}
|
|
2845
2955
|
|
|
2846
2956
|
// src/stage-run.ts
|
|
2847
2957
|
async function runStage(request, deps) {
|
|
2848
|
-
const events = [
|
|
2849
|
-
|
|
2850
|
-
|
|
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 });
|
|
2851
2966
|
const gate = await deps.resolveGate(request);
|
|
2852
2967
|
await deps.runRole(request);
|
|
2853
2968
|
if (!gate) {
|
|
2854
2969
|
return { stageId: request.stageId, gateKind: null, evidence: null, events };
|
|
2855
2970
|
}
|
|
2856
2971
|
const evidence = await deps.collect(request, gate);
|
|
2857
|
-
|
|
2972
|
+
emit2({
|
|
2858
2973
|
type: "gate_evaluated",
|
|
2859
2974
|
stageId: request.stageId,
|
|
2860
2975
|
gateKind: gate.kind,
|
|
@@ -2866,7 +2981,27 @@ async function runStage(request, deps) {
|
|
|
2866
2981
|
// src/cli.ts
|
|
2867
2982
|
var TAG11 = "cli";
|
|
2868
2983
|
var GATE_VERIFICATION_TIMEOUT_MS = 600000;
|
|
2869
|
-
|
|
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) {
|
|
2870
3005
|
const launch = buildStageRunnerConfig({
|
|
2871
3006
|
role: request.role,
|
|
2872
3007
|
prompt,
|
|
@@ -2874,21 +3009,50 @@ async function runRole(request, prompt) {
|
|
|
2874
3009
|
parentEnv: process.env
|
|
2875
3010
|
});
|
|
2876
3011
|
const runner = new SdkAgentRunner(launch.config);
|
|
3012
|
+
activeRunner = runner;
|
|
2877
3013
|
log.info(TAG11, `Running stage ${request.stageId} as role ${launch.role ?? "(none — fail-closed)"}`);
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
})
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
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
|
+
}
|
|
2889
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`);
|
|
2890
3046
|
}
|
|
2891
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", () => {});
|
|
2892
3056
|
async function main() {
|
|
2893
3057
|
const parsed = parseStageRunArgs(process.argv.slice(2));
|
|
2894
3058
|
if (!parsed.ok) {
|
|
@@ -2931,8 +3095,9 @@ ${STAGE_RUN_USAGE}
|
|
|
2931
3095
|
role: pinned.stage.role ?? null
|
|
2932
3096
|
};
|
|
2933
3097
|
const result = await runStage(request, {
|
|
3098
|
+
emit: emitLine,
|
|
2934
3099
|
resolveGate: async () => pinned.gate,
|
|
2935
|
-
runRole: (req) => runRole(req, prompt),
|
|
3100
|
+
runRole: (req) => runRole(req, prompt, emitLine),
|
|
2936
3101
|
collect: async (req, gate) => {
|
|
2937
3102
|
const registry = buildGateCollectorRegistry({
|
|
2938
3103
|
build: {
|
|
@@ -2961,10 +3126,6 @@ ${STAGE_RUN_USAGE}
|
|
|
2961
3126
|
});
|
|
2962
3127
|
}
|
|
2963
3128
|
});
|
|
2964
|
-
for (const event of result.events) {
|
|
2965
|
-
process.stdout.write(`${JSON.stringify(event)}
|
|
2966
|
-
`);
|
|
2967
|
-
}
|
|
2968
3129
|
if (result.evidence && pinned.gate) {
|
|
2969
3130
|
const context = {
|
|
2970
3131
|
cardId,
|
|
@@ -2974,11 +3135,18 @@ ${STAGE_RUN_USAGE}
|
|
|
2974
3135
|
};
|
|
2975
3136
|
const evaluation = gateEvaluate(pinned.gate, result.evidence);
|
|
2976
3137
|
await client.recordStageGateEvidence(toStageGateEvidenceInsert(context, result.evidence));
|
|
2977
|
-
|
|
2978
|
-
|
|
3138
|
+
emitLine({
|
|
3139
|
+
type: "gate_verdict",
|
|
3140
|
+
passed: evaluation.passed,
|
|
3141
|
+
findings: evaluation.findings
|
|
3142
|
+
});
|
|
2979
3143
|
}
|
|
2980
|
-
|
|
2981
|
-
|
|
3144
|
+
emitLine({ type: "result", ...result });
|
|
3145
|
+
}
|
|
3146
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
3147
|
+
process.on(signal, () => {
|
|
3148
|
+
shutdown(signal);
|
|
3149
|
+
});
|
|
2982
3150
|
}
|
|
2983
3151
|
main().catch((err) => {
|
|
2984
3152
|
const message = err instanceof Error ? err.message : String(err);
|