harnery 0.36.0 → 0.37.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/commander.js +4 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +87 -46
- package/dist/commands/artifacts.d.ts.map +1 -1
- package/dist/commands/artifacts.js +26 -1
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +11 -0
- package/dist/commands/diagnostics.d.ts +4 -0
- package/dist/commands/diagnostics.d.ts.map +1 -0
- package/dist/commands/diagnostics.js +229 -0
- package/dist/commands/files.d.ts +12 -0
- package/dist/commands/files.d.ts.map +1 -1
- package/dist/commands/files.js +54 -1
- package/dist/commands/governor.d.ts.map +1 -1
- package/dist/commands/governor.js +4 -0
- package/dist/commands/ledger-v3.d.ts.map +1 -1
- package/dist/commands/ledger-v3.js +71 -2
- package/dist/commands/qa-run.d.ts +4 -0
- package/dist/commands/qa-run.d.ts.map +1 -0
- package/dist/commands/qa-run.js +141 -0
- package/dist/commands/resources.d.ts +4 -0
- package/dist/commands/resources.d.ts.map +1 -0
- package/dist/commands/resources.js +29 -0
- package/dist/commands/supervisor.d.ts +4 -0
- package/dist/commands/supervisor.d.ts.map +1 -0
- package/dist/commands/supervisor.js +101 -0
- package/dist/commands/web.d.ts.map +1 -1
- package/dist/commands/web.js +17 -0
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +4 -0
- package/dist/core/agents/cli.js +7 -3
- package/dist/core/agents/health.d.ts +17 -0
- package/dist/core/agents/health.d.ts.map +1 -0
- package/dist/core/agents/health.js +109 -0
- package/dist/core/agents/mailbox.d.ts +92 -0
- package/dist/core/agents/mailbox.d.ts.map +1 -0
- package/dist/core/agents/mailbox.js +325 -0
- package/dist/core/agents/reconcile-coordination-v3.d.ts +44 -0
- package/dist/core/agents/reconcile-coordination-v3.d.ts.map +1 -0
- package/dist/core/agents/reconcile-coordination-v3.js +58 -0
- package/dist/core/agents/render/prompt-context.d.ts +5 -0
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +34 -13
- package/dist/core/agents/render/session-context.d.ts +8 -0
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +34 -3
- package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/commit-conflict.js +50 -1
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +6 -10
- package/dist/core/agents/session-finalizer-v3.js +4 -1
- package/dist/core/artifacts/index.d.ts +26 -1
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +146 -6
- package/dist/core/config.d.ts +12 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +43 -0
- package/dist/core/diagnostics/advice.d.ts +9 -0
- package/dist/core/diagnostics/advice.d.ts.map +1 -0
- package/dist/core/diagnostics/advice.js +112 -0
- package/dist/core/diagnostics/bundle.d.ts +35 -0
- package/dist/core/diagnostics/bundle.d.ts.map +1 -0
- package/dist/core/diagnostics/bundle.js +874 -0
- package/dist/core/diagnostics/comparison.d.ts +5 -0
- package/dist/core/diagnostics/comparison.d.ts.map +1 -0
- package/dist/core/diagnostics/comparison.js +444 -0
- package/dist/core/diagnostics/contract.d.ts +245 -0
- package/dist/core/diagnostics/contract.d.ts.map +1 -0
- package/dist/core/diagnostics/contract.js +18 -0
- package/dist/core/diagnostics/identity.d.ts +3 -0
- package/dist/core/diagnostics/identity.d.ts.map +1 -0
- package/dist/core/diagnostics/identity.js +10 -0
- package/dist/core/diagnostics/index.d.ts +8 -0
- package/dist/core/diagnostics/index.d.ts.map +1 -0
- package/dist/core/diagnostics/index.js +7 -0
- package/dist/core/diagnostics/replay.d.ts +8 -0
- package/dist/core/diagnostics/replay.d.ts.map +1 -0
- package/dist/core/diagnostics/replay.js +213 -0
- package/dist/core/diagnostics/sanitize.d.ts +9 -0
- package/dist/core/diagnostics/sanitize.d.ts.map +1 -0
- package/dist/core/diagnostics/sanitize.js +84 -0
- package/dist/core/events/legacy-storage/compression.d.ts +13 -0
- package/dist/core/events/legacy-storage/compression.d.ts.map +1 -0
- package/dist/core/events/legacy-storage/compression.js +107 -0
- package/dist/core/events/legacy-storage/index.d.ts +1 -0
- package/dist/core/events/legacy-storage/index.d.ts.map +1 -1
- package/dist/core/events/legacy-storage/index.js +1 -0
- package/dist/core/events/v3/archive-retention.d.ts +33 -0
- package/dist/core/events/v3/archive-retention.d.ts.map +1 -0
- package/dist/core/events/v3/archive-retention.js +195 -0
- package/dist/core/events/v3/capabilities.d.ts +1 -1
- package/dist/core/events/v3/capabilities.d.ts.map +1 -1
- package/dist/core/events/v3/capabilities.js +1 -0
- package/dist/core/events/v3/index.d.ts +1 -0
- package/dist/core/events/v3/index.d.ts.map +1 -1
- package/dist/core/events/v3/index.js +1 -0
- package/dist/core/events/v3/live-routing.d.ts.map +1 -1
- package/dist/core/events/v3/live-routing.js +1 -0
- package/dist/core/events/v3/producers/hook-base.d.ts +1 -1
- package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -1
- package/dist/core/events/v3/producers/hook-base.js +13 -2
- package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
- package/dist/core/events/v3/producers/intake.js +1 -0
- package/dist/core/events/v3/producers/recorder.d.ts +16 -0
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +88 -1
- package/dist/core/hooks/adapter/output.d.ts +8 -0
- package/dist/core/hooks/adapter/output.d.ts.map +1 -1
- package/dist/core/hooks/adapter/output.js +11 -1
- package/dist/core/hooks/cli.js +183 -192
- package/dist/core/hooks/health.d.ts +59 -0
- package/dist/core/hooks/health.d.ts.map +1 -0
- package/dist/core/hooks/health.js +84 -0
- package/dist/core/resources/contract.d.ts +90 -0
- package/dist/core/resources/contract.d.ts.map +1 -0
- package/dist/core/resources/contract.js +2 -0
- package/dist/core/resources/index.d.ts +6 -0
- package/dist/core/resources/index.d.ts.map +1 -0
- package/dist/core/resources/index.js +5 -0
- package/dist/core/resources/sampler.d.ts +27 -0
- package/dist/core/resources/sampler.d.ts.map +1 -0
- package/dist/core/resources/sampler.js +357 -0
- package/dist/core/resources/service-status.d.ts +3 -0
- package/dist/core/resources/service-status.d.ts.map +1 -0
- package/dist/core/resources/service-status.js +48 -0
- package/dist/core/resources/service.d.ts +26 -0
- package/dist/core/resources/service.d.ts.map +1 -0
- package/dist/core/resources/service.js +292 -0
- package/dist/core/resources/storage.d.ts +13 -0
- package/dist/core/resources/storage.d.ts.map +1 -0
- package/dist/core/resources/storage.js +30 -0
- package/dist/core/storage/atomic-json.d.ts +3 -0
- package/dist/core/storage/atomic-json.d.ts.map +1 -0
- package/dist/core/storage/atomic-json.js +15 -0
- package/dist/core/storage/builtins.d.ts.map +1 -1
- package/dist/core/storage/builtins.js +45 -6
- package/dist/core/storage/logger.d.ts +2 -0
- package/dist/core/storage/logger.d.ts.map +1 -1
- package/dist/core/storage/logger.js +2 -0
- package/dist/core/storage/query.d.ts +14 -0
- package/dist/core/storage/query.d.ts.map +1 -1
- package/dist/core/storage/query.js +51 -0
- package/dist/core/supervisor/activity.d.ts +4 -0
- package/dist/core/supervisor/activity.d.ts.map +1 -0
- package/dist/core/supervisor/activity.js +141 -0
- package/dist/core/supervisor/contract.d.ts +268 -0
- package/dist/core/supervisor/contract.d.ts.map +1 -0
- package/dist/core/supervisor/contract.js +35 -0
- package/dist/core/supervisor/explanations.d.ts +4 -0
- package/dist/core/supervisor/explanations.d.ts.map +1 -0
- package/dist/core/supervisor/explanations.js +71 -0
- package/dist/core/supervisor/findings.d.ts +20 -0
- package/dist/core/supervisor/findings.d.ts.map +1 -0
- package/dist/core/supervisor/findings.js +399 -0
- package/dist/core/supervisor/history.d.ts +9 -0
- package/dist/core/supervisor/history.d.ts.map +1 -0
- package/dist/core/supervisor/history.js +46 -0
- package/dist/core/supervisor/hook-health-alerts.d.ts +14 -0
- package/dist/core/supervisor/hook-health-alerts.d.ts.map +1 -0
- package/dist/core/supervisor/hook-health-alerts.js +70 -0
- package/dist/core/supervisor/hook-health-storage.d.ts +5 -0
- package/dist/core/supervisor/hook-health-storage.d.ts.map +1 -0
- package/dist/core/supervisor/hook-health-storage.js +24 -0
- package/dist/core/supervisor/hook-health.d.ts +72 -0
- package/dist/core/supervisor/hook-health.d.ts.map +1 -0
- package/dist/core/supervisor/hook-health.js +203 -0
- package/dist/core/supervisor/hooks.d.ts +10 -0
- package/dist/core/supervisor/hooks.d.ts.map +1 -0
- package/dist/core/supervisor/hooks.js +28 -0
- package/dist/core/supervisor/index.d.ts +15 -0
- package/dist/core/supervisor/index.d.ts.map +1 -0
- package/dist/core/supervisor/index.js +14 -0
- package/dist/core/supervisor/log-feed.d.ts +9 -0
- package/dist/core/supervisor/log-feed.d.ts.map +1 -0
- package/dist/core/supervisor/log-feed.js +103 -0
- package/dist/core/supervisor/service.d.ts +29 -0
- package/dist/core/supervisor/service.d.ts.map +1 -0
- package/dist/core/supervisor/service.js +450 -0
- package/dist/core/supervisor/services.d.ts +11 -0
- package/dist/core/supervisor/services.d.ts.map +1 -0
- package/dist/core/supervisor/services.js +155 -0
- package/dist/core/supervisor/status.d.ts +3 -0
- package/dist/core/supervisor/status.d.ts.map +1 -0
- package/dist/core/supervisor/status.js +48 -0
- package/dist/core/supervisor/storage.d.ts +30 -0
- package/dist/core/supervisor/storage.d.ts.map +1 -0
- package/dist/core/supervisor/storage.js +73 -0
- package/dist/core/supervisor/timeline.d.ts +3 -0
- package/dist/core/supervisor/timeline.d.ts.map +1 -0
- package/dist/core/supervisor/timeline.js +87 -0
- package/dist/core/workflow/admission.d.ts +23 -0
- package/dist/core/workflow/admission.d.ts.map +1 -0
- package/dist/core/workflow/admission.js +136 -0
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +77 -2
- package/dist/core/workflow/index.d.ts +3 -2
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/index.js +2 -1
- package/dist/core/workflow/proof.d.ts +2 -1
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +39 -1
- package/dist/core/workflow/run-state.d.ts +3 -2
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +8 -1
- package/dist/core/workflow/types.d.ts +33 -1
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/types.js +2 -1
- package/dist/lib/browser/client.d.ts +5 -0
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +1 -0
- package/dist/lib/browser/index.d.ts +2 -0
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +2 -0
- package/dist/lib/browser/qa-run-contracts.d.ts +173 -0
- package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -0
- package/dist/lib/browser/qa-run-contracts.js +226 -0
- package/dist/lib/browser/qa-run.d.ts +51 -0
- package/dist/lib/browser/qa-run.d.ts.map +1 -0
- package/dist/lib/browser/qa-run.js +684 -0
- package/dist/lib/coord-root-id.d.ts +9 -0
- package/dist/lib/coord-root-id.d.ts.map +1 -0
- package/dist/lib/coord-root-id.js +17 -0
- package/dist/lib/instructions/apply.d.ts +3 -1
- package/dist/lib/instructions/apply.d.ts.map +1 -1
- package/dist/lib/instructions/apply.js +74 -3
- package/dist/lib/instructions/templates.d.ts.map +1 -1
- package/dist/lib/instructions/templates.js +6 -0
- package/package.json +12 -1
- package/src/commander.ts +26 -0
- package/src/commands/agents.ts +102 -49
- package/src/commands/artifacts.ts +40 -2
- package/src/commands/browse.ts +15 -0
- package/src/commands/diagnostics.ts +271 -0
- package/src/commands/files.ts +102 -2
- package/src/commands/governor.ts +8 -0
- package/src/commands/ledger-v3.ts +82 -0
- package/src/commands/qa-run.ts +184 -0
- package/src/commands/resources.ts +38 -0
- package/src/commands/supervisor.ts +141 -0
- package/src/commands/web.ts +19 -0
- package/src/commands/workflow.ts +8 -0
- package/src/core/agents/cli.ts +7 -3
- package/src/core/agents/health.ts +142 -0
- package/src/core/agents/mailbox.ts +373 -0
- package/src/core/agents/reconcile-coordination-v3.ts +77 -0
- package/src/core/agents/render/prompt-context.ts +42 -12
- package/src/core/agents/render/session-context.ts +37 -3
- package/src/core/agents/rules/commit-conflict.ts +58 -1
- package/src/core/agents/rules/stop-hook.ts +6 -12
- package/src/core/agents/session-finalizer-v3.ts +4 -1
- package/src/core/artifacts/index.ts +196 -10
- package/src/core/config.ts +111 -2
- package/src/core/diagnostics/advice.ts +147 -0
- package/src/core/diagnostics/bundle.ts +1142 -0
- package/src/core/diagnostics/comparison.ts +565 -0
- package/src/core/diagnostics/contract.ts +311 -0
- package/src/core/diagnostics/identity.ts +11 -0
- package/src/core/diagnostics/index.ts +7 -0
- package/src/core/diagnostics/replay.ts +314 -0
- package/src/core/diagnostics/sanitize.ts +97 -0
- package/src/core/events/legacy-storage/compression.ts +130 -0
- package/src/core/events/legacy-storage/index.ts +1 -0
- package/src/core/events/v3/archive-retention.ts +251 -0
- package/src/core/events/v3/capabilities.ts +2 -0
- package/src/core/events/v3/index.ts +9 -0
- package/src/core/events/v3/live-routing.ts +1 -0
- package/src/core/events/v3/producers/hook-base.ts +14 -2
- package/src/core/events/v3/producers/intake.ts +1 -0
- package/src/core/events/v3/producers/recorder.ts +110 -1
- package/src/core/hooks/adapter/output.ts +12 -1
- package/src/core/hooks/cli.ts +192 -215
- package/src/core/hooks/health.ts +146 -0
- package/src/core/resources/contract.ts +98 -0
- package/src/core/resources/index.ts +5 -0
- package/src/core/resources/sampler.ts +458 -0
- package/src/core/resources/service-status.ts +62 -0
- package/src/core/resources/service.ts +337 -0
- package/src/core/resources/storage.ts +44 -0
- package/src/core/storage/atomic-json.ts +15 -0
- package/src/core/storage/builtins.ts +62 -6
- package/src/core/storage/logger.ts +2 -0
- package/src/core/storage/query.ts +58 -0
- package/src/core/supervisor/activity.ts +193 -0
- package/src/core/supervisor/contract.ts +312 -0
- package/src/core/supervisor/explanations.ts +104 -0
- package/src/core/supervisor/findings.ts +788 -0
- package/src/core/supervisor/history.ts +61 -0
- package/src/core/supervisor/hook-health-alerts.ts +108 -0
- package/src/core/supervisor/hook-health-storage.ts +25 -0
- package/src/core/supervisor/hook-health.ts +301 -0
- package/src/core/supervisor/hooks.ts +35 -0
- package/src/core/supervisor/index.ts +14 -0
- package/src/core/supervisor/log-feed.ts +131 -0
- package/src/core/supervisor/service.ts +511 -0
- package/src/core/supervisor/services.ts +203 -0
- package/src/core/supervisor/status.ts +59 -0
- package/src/core/supervisor/storage.ts +126 -0
- package/src/core/supervisor/timeline.ts +133 -0
- package/src/core/workflow/admission.ts +215 -0
- package/src/core/workflow/engine.ts +93 -1
- package/src/core/workflow/index.ts +10 -0
- package/src/core/workflow/proof.ts +54 -1
- package/src/core/workflow/run-state.ts +13 -1
- package/src/core/workflow/types.ts +39 -1
- package/src/lib/browser/client.ts +6 -0
- package/src/lib/browser/index.ts +29 -0
- package/src/lib/browser/qa-run-contracts.ts +380 -0
- package/src/lib/browser/qa-run.ts +784 -0
- package/src/lib/coord-root-id.ts +23 -0
- package/src/lib/instructions/apply.ts +81 -2
- package/src/lib/instructions/templates.ts +6 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// `qa-run <target>`: run the whole page-QA matrix (planner, deterministic
|
|
2
|
+
// gates, interactions, critique, snapshot) in one command. The agent's loop
|
|
3
|
+
// collapses to: build the job, run this, read page-qa-result.json.
|
|
4
|
+
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import { join, resolve } from "node:path";
|
|
7
|
+
import type { Command } from "commander";
|
|
8
|
+
import type { EmitContext } from "../commander.ts";
|
|
9
|
+
import { QA_RUN_RESULT_FILENAME, runQaMatrix } from "../lib/browser/qa-run.ts";
|
|
10
|
+
import {
|
|
11
|
+
QA_RUN_JOB_SCHEMA_VERSION,
|
|
12
|
+
type QaRunJob,
|
|
13
|
+
validateQaRunJob,
|
|
14
|
+
} from "../lib/browser/qa-run-contracts.ts";
|
|
15
|
+
|
|
16
|
+
interface QaRunOpts {
|
|
17
|
+
job?: string;
|
|
18
|
+
mode: string;
|
|
19
|
+
concurrency?: string;
|
|
20
|
+
allowMetered?: boolean;
|
|
21
|
+
outDir?: string;
|
|
22
|
+
json?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function registerQaRunCommand(program: Command, emit: EmitContext): void {
|
|
26
|
+
program
|
|
27
|
+
.command("qa-run <target>")
|
|
28
|
+
.description(
|
|
29
|
+
"Run the whole page-QA matrix in one command: QA planner, deterministic " +
|
|
30
|
+
"gates per viewport/theme/state through a bounded process pool, serial " +
|
|
31
|
+
"interaction assertions, manifest-required vision critique, and the QA " +
|
|
32
|
+
"snapshot. Writes page-qa-result.json with a fail-closed verdict.",
|
|
33
|
+
)
|
|
34
|
+
.option(
|
|
35
|
+
"--job <file>",
|
|
36
|
+
"QA job document (JSON). Validated before any browser starts — secret-bearing " +
|
|
37
|
+
"fields are refused, and the job may widen but never narrow the planner's coverage. " +
|
|
38
|
+
"Absent: a minimal implicit job for <target> is used.",
|
|
39
|
+
)
|
|
40
|
+
.option(
|
|
41
|
+
"--mode <mode>",
|
|
42
|
+
"signoff persists a QA snapshot on a passing run; review never does.",
|
|
43
|
+
"signoff",
|
|
44
|
+
)
|
|
45
|
+
.option(
|
|
46
|
+
"--concurrency <n>",
|
|
47
|
+
"Concurrent deterministic captures (1-8; default 2). Interactions and critique " +
|
|
48
|
+
"always run serially.",
|
|
49
|
+
)
|
|
50
|
+
.option(
|
|
51
|
+
"--allow-metered",
|
|
52
|
+
"Permit the critique provider's metered-API fallback. Default: subscription-backed " +
|
|
53
|
+
"headless harnesses only — exhaustion becomes an incomplete blocker, never a metered call.",
|
|
54
|
+
)
|
|
55
|
+
.option(
|
|
56
|
+
"--out-dir <dir>",
|
|
57
|
+
"Artifact + result directory (default: .qa-run/<timestamp> under the current directory).",
|
|
58
|
+
)
|
|
59
|
+
.option("--json", "Print the full QaRunResult JSON to stdout.")
|
|
60
|
+
.addHelpText(
|
|
61
|
+
"after",
|
|
62
|
+
"\nExit codes: 0 verdict passed · 1 usage or job-validation error · " +
|
|
63
|
+
"2 verdict failed · 4 verdict incomplete.",
|
|
64
|
+
)
|
|
65
|
+
.action(async (target: string, opts: QaRunOpts, command: Command) => {
|
|
66
|
+
if (opts.mode !== "signoff" && opts.mode !== "review") {
|
|
67
|
+
emit.error({
|
|
68
|
+
code: "qa_run_invalid_mode",
|
|
69
|
+
message: '--mode must be "signoff" or "review"',
|
|
70
|
+
});
|
|
71
|
+
process.exitCode = 1;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const modeExplicit = command.getOptionValueSource("mode") === "cli";
|
|
75
|
+
|
|
76
|
+
let jobDocument: unknown;
|
|
77
|
+
if (opts.job) {
|
|
78
|
+
let rawText: string;
|
|
79
|
+
try {
|
|
80
|
+
rawText = readFileSync(opts.job, "utf8");
|
|
81
|
+
} catch (err: unknown) {
|
|
82
|
+
emit.error({
|
|
83
|
+
code: "qa_run_job_unreadable",
|
|
84
|
+
message: `cannot read --job file ${opts.job}: ${err instanceof Error ? err.message : String(err)}`,
|
|
85
|
+
});
|
|
86
|
+
process.exitCode = 1;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
let parsed: unknown;
|
|
90
|
+
try {
|
|
91
|
+
parsed = JSON.parse(rawText);
|
|
92
|
+
} catch (err: unknown) {
|
|
93
|
+
emit.error({
|
|
94
|
+
code: "qa_run_job_invalid_json",
|
|
95
|
+
message: `--job file ${opts.job} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
96
|
+
});
|
|
97
|
+
process.exitCode = 1;
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
101
|
+
const raw = parsed as Record<string, unknown>;
|
|
102
|
+
// The CLI target argument is authoritative; a job file may omit
|
|
103
|
+
// target and mode. An explicit --mode overrides the job's mode.
|
|
104
|
+
jobDocument = {
|
|
105
|
+
...raw,
|
|
106
|
+
target,
|
|
107
|
+
...(raw.mode === undefined || modeExplicit ? { mode: opts.mode } : {}),
|
|
108
|
+
};
|
|
109
|
+
} else {
|
|
110
|
+
jobDocument = parsed;
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
jobDocument = { schema_version: QA_RUN_JOB_SCHEMA_VERSION, target, mode: opts.mode };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// All validation errors print at once, before any browser starts.
|
|
117
|
+
const validation = validateQaRunJob(jobDocument);
|
|
118
|
+
if (!validation.ok) {
|
|
119
|
+
for (const message of validation.errors) emit.log(`job: ${message}`, "error");
|
|
120
|
+
emit.error({
|
|
121
|
+
code: "qa_run_invalid_job",
|
|
122
|
+
message: `job validation failed with ${validation.errors.length} error${validation.errors.length === 1 ? "" : "s"}`,
|
|
123
|
+
});
|
|
124
|
+
process.exitCode = 1;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const policy = { ...validation.job.policy };
|
|
129
|
+
if (opts.concurrency !== undefined) {
|
|
130
|
+
const n = Number.parseInt(opts.concurrency, 10);
|
|
131
|
+
if (!Number.isInteger(n) || n < 1 || n > 8) {
|
|
132
|
+
emit.error({
|
|
133
|
+
code: "qa_run_invalid_concurrency",
|
|
134
|
+
message: "--concurrency must be an integer between 1 and 8",
|
|
135
|
+
});
|
|
136
|
+
process.exitCode = 1;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
policy.command_concurrency = n;
|
|
140
|
+
}
|
|
141
|
+
if (opts.allowMetered) policy.allow_metered_critique = true;
|
|
142
|
+
const job: QaRunJob = { ...validation.job, policy };
|
|
143
|
+
|
|
144
|
+
const outDir = resolve(
|
|
145
|
+
opts.outDir ?? join(".qa-run", new Date().toISOString().replace(/[:.]/g, "-")),
|
|
146
|
+
);
|
|
147
|
+
const cliScript = process.argv[1];
|
|
148
|
+
if (!cliScript) {
|
|
149
|
+
emit.error({
|
|
150
|
+
code: "qa_run_no_cli_script",
|
|
151
|
+
message: "cannot resolve the host CLI script path for child browse invocations",
|
|
152
|
+
});
|
|
153
|
+
process.exitCode = 1;
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// Children run the same host CLI, so they inherit the host's browse
|
|
157
|
+
// wiring (critique provider, cookie jar, profiles) unchanged.
|
|
158
|
+
const browseArgv = [process.execPath, cliScript, "browse"];
|
|
159
|
+
|
|
160
|
+
const result = await runQaMatrix({
|
|
161
|
+
job,
|
|
162
|
+
outDir,
|
|
163
|
+
browseArgv,
|
|
164
|
+
onLog: (message) => emit.log(message, "info"),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
if (opts.json) emit.data(result);
|
|
168
|
+
const resultPath = join(outDir, QA_RUN_RESULT_FILENAME);
|
|
169
|
+
for (const blocker of result.blockers) {
|
|
170
|
+
emit.log(
|
|
171
|
+
`blocker [${blocker.stage}${blocker.context_id ? ` ${blocker.context_id}` : ""}]: ${blocker.reason}`,
|
|
172
|
+
"warn",
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
emit.log(
|
|
176
|
+
`verdict: ${result.verdict} — ${result.contexts.length} context${result.contexts.length === 1 ? "" : "s"}, ` +
|
|
177
|
+
`${result.commands.length} command${result.commands.length === 1 ? "" : "s"}, ` +
|
|
178
|
+
`${result.blockers.length} blocker${result.blockers.length === 1 ? "" : "s"}; result: ${resultPath}`,
|
|
179
|
+
result.verdict === "passed" ? "info" : "warn",
|
|
180
|
+
);
|
|
181
|
+
if (result.verdict === "failed") process.exitCode = 2;
|
|
182
|
+
else if (result.verdict === "incomplete") process.exitCode = 4;
|
|
183
|
+
});
|
|
184
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import type { Command } from "commander";
|
|
3
|
+
import type { EmitContext, HarneryProgramContext } from "../commander.ts";
|
|
4
|
+
import { sampleResources } from "../core/resources/index.ts";
|
|
5
|
+
|
|
6
|
+
export function registerResourcesCommand(
|
|
7
|
+
program: Command,
|
|
8
|
+
emit: EmitContext,
|
|
9
|
+
context?: HarneryProgramContext,
|
|
10
|
+
): void {
|
|
11
|
+
const resources = program
|
|
12
|
+
.command("resources")
|
|
13
|
+
.description("Observe local CPU and memory pressure by agent and process tree");
|
|
14
|
+
|
|
15
|
+
resources
|
|
16
|
+
.command("snapshot")
|
|
17
|
+
.description("Capture one machine and process-resource snapshot")
|
|
18
|
+
.option("--root <path>", "Explicit coordination root")
|
|
19
|
+
.action((options: { root?: string }) => {
|
|
20
|
+
try {
|
|
21
|
+
emit.data(sampleResources(resolve(options.root ?? coordRoot(context))).snapshot);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
fail(emit, "resource_snapshot_failed", error);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function coordRoot(context?: HarneryProgramContext): string {
|
|
29
|
+
return context?.resolveCoordRoot?.() ?? context?.repoRoot ?? process.cwd();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function fail(emit: EmitContext, code: string, error: unknown): void {
|
|
33
|
+
emit.error({
|
|
34
|
+
code,
|
|
35
|
+
message: error instanceof Error ? error.message : String(error),
|
|
36
|
+
});
|
|
37
|
+
emit.setExitCode(1);
|
|
38
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import type { Command } from "commander";
|
|
3
|
+
import type { EmitContext, HarneryProgramContext } from "../commander.ts";
|
|
4
|
+
import { readSupervisorHookHealth } from "../core/supervisor/hook-health-storage.ts";
|
|
5
|
+
import {
|
|
6
|
+
requestSupervisorStop,
|
|
7
|
+
runSupervisor,
|
|
8
|
+
spawnSupervisor,
|
|
9
|
+
} from "../core/supervisor/service.ts";
|
|
10
|
+
import { readSupervisorStatus } from "../core/supervisor/status.ts";
|
|
11
|
+
import {
|
|
12
|
+
readSupervisorFindings,
|
|
13
|
+
readSupervisorHistory,
|
|
14
|
+
readSupervisorLogFeed,
|
|
15
|
+
readSupervisorSnapshot,
|
|
16
|
+
} from "../core/supervisor/storage.ts";
|
|
17
|
+
|
|
18
|
+
export function registerSupervisorCommand(
|
|
19
|
+
program: Command,
|
|
20
|
+
emit: EmitContext,
|
|
21
|
+
context?: HarneryProgramContext,
|
|
22
|
+
): void {
|
|
23
|
+
const supervisor = program
|
|
24
|
+
.command("supervisor")
|
|
25
|
+
.description("Run Harnery's optional local diagnostic supervisor");
|
|
26
|
+
|
|
27
|
+
supervisor
|
|
28
|
+
.command("start")
|
|
29
|
+
.description("Start the detached local supervisor")
|
|
30
|
+
.option("--root <path>", "Explicit coordination root")
|
|
31
|
+
.option("--interval-ms <milliseconds>", "Collection interval from 500 to 60000 ms", integer)
|
|
32
|
+
.option(
|
|
33
|
+
"--idle-exit-ms <milliseconds>",
|
|
34
|
+
"Exit after this long without a live dashboard consumer or attributed agent",
|
|
35
|
+
integer,
|
|
36
|
+
)
|
|
37
|
+
.option("--keep-alive", "Remain resident until explicitly stopped")
|
|
38
|
+
.action(
|
|
39
|
+
async (options: {
|
|
40
|
+
root?: string;
|
|
41
|
+
intervalMs?: number;
|
|
42
|
+
idleExitMs?: number;
|
|
43
|
+
keepAlive?: boolean;
|
|
44
|
+
}) => {
|
|
45
|
+
try {
|
|
46
|
+
emit.data(
|
|
47
|
+
await spawnSupervisor(resolve(options.root ?? coordRoot(context)), {
|
|
48
|
+
intervalMs: options.intervalMs,
|
|
49
|
+
idleExitMs: options.idleExitMs,
|
|
50
|
+
keepAlive: options.keepAlive,
|
|
51
|
+
}),
|
|
52
|
+
);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
fail(emit, "supervisor_start_failed", error);
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
supervisor
|
|
60
|
+
.command("status")
|
|
61
|
+
.description("Show supervisor liveness and its latest bounded projections")
|
|
62
|
+
.option("--root <path>", "Explicit coordination root")
|
|
63
|
+
.action((options: { root?: string }) => {
|
|
64
|
+
try {
|
|
65
|
+
const root = resolve(options.root ?? coordRoot(context));
|
|
66
|
+
emit.data({
|
|
67
|
+
service: readSupervisorStatus(root),
|
|
68
|
+
snapshot: readSupervisorSnapshot(root),
|
|
69
|
+
history: readSupervisorHistory(root),
|
|
70
|
+
findings: readSupervisorFindings(root),
|
|
71
|
+
hook_health: readSupervisorHookHealth(root),
|
|
72
|
+
log_feed: readSupervisorLogFeed(root),
|
|
73
|
+
});
|
|
74
|
+
} catch (error) {
|
|
75
|
+
fail(emit, "supervisor_status_failed", error);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
supervisor
|
|
80
|
+
.command("stop")
|
|
81
|
+
.description("Request a graceful supervisor stop")
|
|
82
|
+
.option("--root <path>", "Explicit coordination root")
|
|
83
|
+
.action(async (options: { root?: string }) => {
|
|
84
|
+
try {
|
|
85
|
+
const root = resolve(options.root ?? coordRoot(context));
|
|
86
|
+
let status = requestSupervisorStop(root);
|
|
87
|
+
const deadline = Date.now() + 5_000;
|
|
88
|
+
while (status.running && Date.now() < deadline) {
|
|
89
|
+
await new Promise((done) => setTimeout(done, 50));
|
|
90
|
+
status = readSupervisorStatus(root);
|
|
91
|
+
}
|
|
92
|
+
emit.data(status);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
fail(emit, "supervisor_stop_failed", error);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
supervisor
|
|
99
|
+
.command("daemon", { hidden: true })
|
|
100
|
+
.description("Internal detached local supervisor entrypoint")
|
|
101
|
+
.option("--root <path>", "Explicit coordination root")
|
|
102
|
+
.option("--interval-ms <milliseconds>", "Collection interval", integer)
|
|
103
|
+
.option("--idle-exit-ms <milliseconds>", "Idle exit interval", integer)
|
|
104
|
+
.option("--keep-alive", "Remain resident until explicitly stopped")
|
|
105
|
+
.action(
|
|
106
|
+
async (options: {
|
|
107
|
+
root?: string;
|
|
108
|
+
intervalMs?: number;
|
|
109
|
+
idleExitMs?: number;
|
|
110
|
+
keepAlive?: boolean;
|
|
111
|
+
}) => {
|
|
112
|
+
try {
|
|
113
|
+
emit.data(
|
|
114
|
+
await runSupervisor({
|
|
115
|
+
coordRoot: resolve(options.root ?? coordRoot(context)),
|
|
116
|
+
intervalMs: options.intervalMs,
|
|
117
|
+
idleExitMs: options.idleExitMs,
|
|
118
|
+
keepAlive: options.keepAlive,
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
fail(emit, "supervisor_daemon_failed", error);
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function coordRoot(context?: HarneryProgramContext): string {
|
|
129
|
+
return context?.resolveCoordRoot?.() ?? context?.repoRoot ?? process.cwd();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function integer(value: string): number {
|
|
133
|
+
const parsed = Number(value);
|
|
134
|
+
if (!Number.isInteger(parsed)) throw new Error(`expected an integer, got ${value}`);
|
|
135
|
+
return parsed;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function fail(emit: EmitContext, code: string, error: unknown): void {
|
|
139
|
+
emit.error({ code, message: error instanceof Error ? error.message : String(error) });
|
|
140
|
+
emit.setExitCode(1);
|
|
141
|
+
}
|
package/src/commands/web.ts
CHANGED
|
@@ -8,6 +8,11 @@ import type { EmitContext } from "../commander.ts";
|
|
|
8
8
|
import { DEFAULT_WEB_PORT, resolveWebPort } from "../core/config.ts";
|
|
9
9
|
import { ensureSemanticServiceRunning } from "../core/semantic/service.ts";
|
|
10
10
|
import { processLogDestination } from "../core/storage/process-log.ts";
|
|
11
|
+
import { ensureSupervisorRunning } from "../core/supervisor/service.ts";
|
|
12
|
+
import {
|
|
13
|
+
registerSupervisorConsumer,
|
|
14
|
+
unregisterSupervisorConsumer,
|
|
15
|
+
} from "../core/supervisor/services.ts";
|
|
11
16
|
import {
|
|
12
17
|
parsePerformanceWindow,
|
|
13
18
|
readWebPerformanceReport,
|
|
@@ -109,6 +114,16 @@ async function startSemanticServiceWithWeb(coordRoot: string, emit: EmitContext)
|
|
|
109
114
|
}
|
|
110
115
|
}
|
|
111
116
|
|
|
117
|
+
async function startSupervisorWithWeb(coordRoot: string, emit: EmitContext): Promise<void> {
|
|
118
|
+
registerSupervisorConsumer(coordRoot, { id: "dashboard" });
|
|
119
|
+
const result = await ensureSupervisorRunning(coordRoot);
|
|
120
|
+
if (result.state === "started") emit.log("local supervisor · started automatically", "info");
|
|
121
|
+
if (result.state === "running") emit.log("local supervisor · already running", "info");
|
|
122
|
+
if (result.state === "unavailable") {
|
|
123
|
+
emit.log(`local supervisor · unavailable (${result.error})`, "warn");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
112
127
|
/**
|
|
113
128
|
* Resolve the web app root. A local `web/` (source checkout / submodule) wins.
|
|
114
129
|
* Otherwise, on an npm install, lazy-fetch it (clone + install) unless the
|
|
@@ -233,6 +248,7 @@ export function registerWebCommand(program: Command, emit: EmitContext): void {
|
|
|
233
248
|
|
|
234
249
|
if (!(await requireAvailablePort(emit, port))) return;
|
|
235
250
|
|
|
251
|
+
await startSupervisorWithWeb(coordRoot, emit);
|
|
236
252
|
await startSemanticServiceWithWeb(coordRoot, emit);
|
|
237
253
|
|
|
238
254
|
const heapMb = resolveMaxOldSpaceMb(opts.maxOldSpace);
|
|
@@ -264,6 +280,7 @@ export function registerWebCommand(program: Command, emit: EmitContext): void {
|
|
|
264
280
|
process.on("SIGTERM", () => cleanup("SIGTERM"));
|
|
265
281
|
|
|
266
282
|
child.on("exit", (code, sig) => {
|
|
283
|
+
unregisterSupervisorConsumer(coordRoot, "dashboard");
|
|
267
284
|
if (sig) {
|
|
268
285
|
emit.log(`web exited on ${sig}`, "info");
|
|
269
286
|
} else if (code !== 0) {
|
|
@@ -326,6 +343,7 @@ export function registerWebCommand(program: Command, emit: EmitContext): void {
|
|
|
326
343
|
return;
|
|
327
344
|
}
|
|
328
345
|
if (!(await requireAvailablePort(emit, port))) return;
|
|
346
|
+
await startSupervisorWithWeb(coordRoot, emit);
|
|
329
347
|
await startSemanticServiceWithWeb(coordRoot, emit);
|
|
330
348
|
const heapMb = resolveMaxOldSpaceMb(opts.maxOldSpace);
|
|
331
349
|
const nodeOptions = nodeOptionsWithWebDiagnostics(root, heapMb);
|
|
@@ -355,6 +373,7 @@ export function registerWebCommand(program: Command, emit: EmitContext): void {
|
|
|
355
373
|
process.on("SIGTERM", () => cleanup("SIGTERM"));
|
|
356
374
|
|
|
357
375
|
child.on("exit", (code) => {
|
|
376
|
+
unregisterSupervisorConsumer(coordRoot, "dashboard");
|
|
358
377
|
process.exitCode = code ?? 0;
|
|
359
378
|
});
|
|
360
379
|
});
|
package/src/commands/workflow.ts
CHANGED
|
@@ -28,6 +28,7 @@ import type { WorkspaceProvider } from "../core/workflow/index.ts";
|
|
|
28
28
|
interface WorkflowRunOpts {
|
|
29
29
|
maxAgents?: string;
|
|
30
30
|
concurrency?: string;
|
|
31
|
+
observePressure?: boolean;
|
|
31
32
|
cwd?: string;
|
|
32
33
|
adapter?: string;
|
|
33
34
|
resumeFrom?: string;
|
|
@@ -98,6 +99,10 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
|
|
|
98
99
|
)
|
|
99
100
|
.option("--max-agents <n>", "Total-agent ceiling for the run (default 50)")
|
|
100
101
|
.option("--concurrency <n>", "Concurrent-subagent cap (default 4)")
|
|
102
|
+
.option(
|
|
103
|
+
"--observe-pressure",
|
|
104
|
+
"Record observer-only local pressure before the first real child dispatch",
|
|
105
|
+
)
|
|
101
106
|
.option("--cwd <dir>", "Working directory children spawn in (default: coord root)")
|
|
102
107
|
.option(
|
|
103
108
|
"--adapter <name>",
|
|
@@ -194,6 +199,9 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
|
|
|
194
199
|
allowApiBilling: opts.allowApiBilling,
|
|
195
200
|
maxAgents: opts.maxAgents ? Number.parseInt(opts.maxAgents, 10) : undefined,
|
|
196
201
|
concurrency: opts.concurrency ? Number.parseInt(opts.concurrency, 10) : undefined,
|
|
202
|
+
diagnosticAdmission: opts.observePressure
|
|
203
|
+
? { schema_version: 1, mode: "shadow" }
|
|
204
|
+
: undefined,
|
|
197
205
|
cwd: opts.cwd,
|
|
198
206
|
...adapterProofInputs(
|
|
199
207
|
registry.list().map((adapter) => adapter.profile),
|
package/src/core/agents/cli.ts
CHANGED
|
@@ -708,8 +708,10 @@ async function handleStaleSweep(root: string, _rest: string[]): Promise<number>
|
|
|
708
708
|
}
|
|
709
709
|
|
|
710
710
|
async function handleReconcileFinalization(root: string): Promise<number> {
|
|
711
|
-
|
|
712
|
-
|
|
711
|
+
// Shares the supervisor composition with `agents reconcile` so session start
|
|
712
|
+
// sweeps the stale cache in the same pass, as ADR 0077 specified.
|
|
713
|
+
const { reconcileCoordinationV3 } = await import("./reconcile-coordination-v3.ts");
|
|
714
|
+
const result = reconcileCoordinationV3(root);
|
|
713
715
|
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
714
716
|
return result.diagnostics.some((item) => item === "ledger_not_authority_safe") ? 2 : 0;
|
|
715
717
|
}
|
|
@@ -759,6 +761,7 @@ async function handlePromptContext(root: string, rest: string[]): Promise<number
|
|
|
759
761
|
const instanceId = args.instance;
|
|
760
762
|
const sessionId = args.session ?? instanceId;
|
|
761
763
|
const agentName = args.name;
|
|
764
|
+
const adapter = args.adapter;
|
|
762
765
|
const sessionNameNudge = args["session-name-nudge"] === "true";
|
|
763
766
|
const taskNudge = args["task-nudge"] === "true";
|
|
764
767
|
const hostPromptReminder = args["host-prompt-reminder"] === "true";
|
|
@@ -769,7 +772,7 @@ async function handlePromptContext(root: string, rest: string[]): Promise<number
|
|
|
769
772
|
args["turn-ritual-nudge"] === "true" ? "generic" : args["turn-ritual-nudge"];
|
|
770
773
|
if (!instanceId) {
|
|
771
774
|
process.stderr.write(
|
|
772
|
-
"agent-coord prompt-context --instance <id> [--session <id>] [--name <agent-name>] [--session-name-nudge] [--task-nudge] [--host-prompt-reminder] [--status-footer-nudge] [--turn-ritual-nudge <adapter>]\n",
|
|
775
|
+
"agent-coord prompt-context --instance <id> [--session <id>] [--name <agent-name>] [--adapter <id>] [--session-name-nudge] [--task-nudge] [--host-prompt-reminder] [--status-footer-nudge] [--turn-ritual-nudge <adapter>]\n",
|
|
773
776
|
);
|
|
774
777
|
return 2;
|
|
775
778
|
}
|
|
@@ -784,6 +787,7 @@ async function handlePromptContext(root: string, rest: string[]): Promise<number
|
|
|
784
787
|
hostPromptReminder,
|
|
785
788
|
statusFooterNudge,
|
|
786
789
|
turnRitualNudge,
|
|
790
|
+
adapter,
|
|
787
791
|
});
|
|
788
792
|
process.stdout.write(text);
|
|
789
793
|
return 0;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { closeSync, existsSync, lstatSync, openSync, readSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import type { SupervisorCapability, SupervisorSourceReference } from "../supervisor/contract.ts";
|
|
4
|
+
|
|
5
|
+
export interface CoordinationHealthDiagnostic {
|
|
6
|
+
code: "malformed_json" | "invalid_contract";
|
|
7
|
+
byte_offset: number;
|
|
8
|
+
segment_ordinal: number;
|
|
9
|
+
event_id?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CoordinationHealthSnapshot {
|
|
13
|
+
observed_at: string;
|
|
14
|
+
capability: SupervisorCapability;
|
|
15
|
+
recent_events: readonly SupervisorSourceReference[];
|
|
16
|
+
diagnostics: readonly CoordinationHealthDiagnostic[];
|
|
17
|
+
omitted_event_count: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const MAX_EVENT_REFERENCES = 64;
|
|
21
|
+
const MAX_DIAGNOSTICS = 16;
|
|
22
|
+
const MAX_TAIL_BYTES = 256 * 1_024;
|
|
23
|
+
|
|
24
|
+
/** Read bounded V3 references without retaining event payloads or the full ledger reader. */
|
|
25
|
+
export function collectCoordinationHealthSnapshot(
|
|
26
|
+
coordRoot: string,
|
|
27
|
+
now = new Date(),
|
|
28
|
+
): CoordinationHealthSnapshot {
|
|
29
|
+
const observedAt = now.toISOString();
|
|
30
|
+
const active = join(resolve(coordRoot), ".harnery", "ledgers", "v3", "active.ndjson");
|
|
31
|
+
if (!existsSync(active)) return unavailable(observedAt, "ledger-missing");
|
|
32
|
+
|
|
33
|
+
let handle: number | undefined;
|
|
34
|
+
try {
|
|
35
|
+
const stat = lstatSync(active);
|
|
36
|
+
if (stat.isSymbolicLink() || !stat.isFile()) return unavailable(observedAt, "ledger-unsafe");
|
|
37
|
+
if (stat.size === 0) return available(observedAt, [], [], 0);
|
|
38
|
+
const start = Math.max(0, stat.size - MAX_TAIL_BYTES);
|
|
39
|
+
const bytes = Buffer.allocUnsafe(stat.size - start);
|
|
40
|
+
handle = openSync(active, "r");
|
|
41
|
+
const count = readSync(handle, bytes, 0, bytes.length, start);
|
|
42
|
+
const lines = bytes.subarray(0, count).toString("utf8").split("\n");
|
|
43
|
+
if (start > 0) lines.shift();
|
|
44
|
+
const completeLines = lines.filter((line) => line.length > 0);
|
|
45
|
+
const diagnostics: CoordinationHealthDiagnostic[] = [];
|
|
46
|
+
const references: SupervisorSourceReference[] = [];
|
|
47
|
+
let byteOffset = start;
|
|
48
|
+
for (const line of completeLines) {
|
|
49
|
+
const reference = parseReference(line);
|
|
50
|
+
if (reference) references.push(reference);
|
|
51
|
+
else if (diagnostics.length < MAX_DIAGNOSTICS) {
|
|
52
|
+
diagnostics.push({
|
|
53
|
+
code: line.trimStart().startsWith("{") ? "invalid_contract" : "malformed_json",
|
|
54
|
+
byte_offset: byteOffset,
|
|
55
|
+
segment_ordinal: 1,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
byteOffset += Buffer.byteLength(line) + 1;
|
|
59
|
+
}
|
|
60
|
+
const recent = references.slice(-MAX_EVENT_REFERENCES);
|
|
61
|
+
return available(
|
|
62
|
+
observedAt,
|
|
63
|
+
recent,
|
|
64
|
+
diagnostics,
|
|
65
|
+
Math.max(0, references.length - recent.length) + (start > 0 ? 1 : 0),
|
|
66
|
+
);
|
|
67
|
+
} catch {
|
|
68
|
+
return unavailable(observedAt, "tail-read-failed", "error");
|
|
69
|
+
} finally {
|
|
70
|
+
if (handle !== undefined) closeSync(handle);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function parseReference(line: string): SupervisorSourceReference | undefined {
|
|
75
|
+
try {
|
|
76
|
+
const value = JSON.parse(line) as Record<string, unknown>;
|
|
77
|
+
const contract = object(value.contract);
|
|
78
|
+
const time = object(value.time);
|
|
79
|
+
const producer = object(value.producer);
|
|
80
|
+
if (
|
|
81
|
+
contract.major !== 3 ||
|
|
82
|
+
typeof value.event_id !== "string" ||
|
|
83
|
+
typeof value.event_type !== "string" ||
|
|
84
|
+
typeof time.observed_at !== "string" ||
|
|
85
|
+
typeof producer.sequence !== "number" ||
|
|
86
|
+
!Number.isSafeInteger(producer.sequence)
|
|
87
|
+
)
|
|
88
|
+
return undefined;
|
|
89
|
+
return {
|
|
90
|
+
id: `v3:${value.event_id}`,
|
|
91
|
+
source_kind: "coordination.v3",
|
|
92
|
+
source_id: value.event_type,
|
|
93
|
+
record_id: value.event_id,
|
|
94
|
+
sequence: producer.sequence,
|
|
95
|
+
schema_version: 3,
|
|
96
|
+
observed_at: time.observed_at,
|
|
97
|
+
capability: "supported",
|
|
98
|
+
};
|
|
99
|
+
} catch {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function object(value: unknown): Record<string, unknown> {
|
|
105
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
106
|
+
? (value as Record<string, unknown>)
|
|
107
|
+
: {};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function available(
|
|
111
|
+
observedAt: string,
|
|
112
|
+
recentEvents: readonly SupervisorSourceReference[],
|
|
113
|
+
diagnostics: readonly CoordinationHealthDiagnostic[],
|
|
114
|
+
omittedEventCount: number,
|
|
115
|
+
): CoordinationHealthSnapshot {
|
|
116
|
+
const state = diagnostics.length === 0 ? "supported" : "partial";
|
|
117
|
+
return {
|
|
118
|
+
observed_at: observedAt,
|
|
119
|
+
capability: {
|
|
120
|
+
source_kind: "coordination.v3.references",
|
|
121
|
+
state,
|
|
122
|
+
...(state === "partial" ? { reason_code: diagnostics[0]?.code ?? "tail-invalid" } : {}),
|
|
123
|
+
},
|
|
124
|
+
recent_events: recentEvents.map((reference) => ({ ...reference, capability: state })),
|
|
125
|
+
diagnostics,
|
|
126
|
+
omitted_event_count: omittedEventCount,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function unavailable(
|
|
131
|
+
observedAt: string,
|
|
132
|
+
reasonCode: string,
|
|
133
|
+
state: "unsupported" | "error" = "unsupported",
|
|
134
|
+
): CoordinationHealthSnapshot {
|
|
135
|
+
return {
|
|
136
|
+
observed_at: observedAt,
|
|
137
|
+
capability: { source_kind: "coordination.v3.references", state, reason_code: reasonCode },
|
|
138
|
+
recent_events: [],
|
|
139
|
+
diagnostics: [],
|
|
140
|
+
omitted_event_count: 0,
|
|
141
|
+
};
|
|
142
|
+
}
|