ultimate-pi 0.20.0 → 0.22.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.
Files changed (130) hide show
  1. package/.agents/skills/harness-decisions/SKILL.md +68 -2
  2. package/.agents/skills/harness-git-commit/SKILL.md +72 -0
  3. package/.agents/skills/harness-governor/SKILL.md +2 -2
  4. package/.agents/skills/harness-ls-lint-setup/SKILL.md +59 -0
  5. package/.agents/skills/harness-plan/SKILL.md +13 -11
  6. package/.agents/skills/harness-review/SKILL.md +1 -1
  7. package/.agents/skills/harness-sentrux-repair/SKILL.md +48 -0
  8. package/.agents/skills/sentrux/SKILL.md +4 -2
  9. package/.agents/skills/wiki-save/SKILL.md +1 -1
  10. package/.pi/PACKAGING.md +6 -0
  11. package/.pi/SYSTEM.md +21 -3
  12. package/.pi/agents/harness/ls-lint-steward.md +49 -0
  13. package/.pi/agents/harness/planning/decompose.md +4 -4
  14. package/.pi/agents/harness/reviewing/evaluator.md +1 -1
  15. package/.pi/agents/harness/running/executor.md +1 -1
  16. package/.pi/agents/harness/sentrux-repair-advisor.md +50 -0
  17. package/.pi/agents/pi-pi/prompt-expert.md +17 -2
  18. package/.pi/auto-commit.json +9 -2
  19. package/.pi/extensions/debate-orchestrator.ts +3 -0
  20. package/.pi/extensions/harness-anchored-edit.ts +7 -9
  21. package/.pi/extensions/harness-ask-user.ts +13 -34
  22. package/.pi/extensions/harness-debate-tools.ts +43 -4
  23. package/.pi/extensions/harness-live-widget.ts +28 -19
  24. package/.pi/extensions/harness-run-context.ts +278 -115
  25. package/.pi/extensions/harness-web-tools.ts +598 -471
  26. package/.pi/extensions/ls-lint-rules-sync.ts +103 -0
  27. package/.pi/extensions/observation-bus.ts +4 -0
  28. package/.pi/extensions/policy-gate.ts +270 -229
  29. package/.pi/extensions/sentrux-rules-sync.ts +2 -0
  30. package/.pi/extensions/soundboard.ts +48 -48
  31. package/.pi/harness/README.md +4 -0
  32. package/.pi/harness/agents.manifest.json +15 -7
  33. package/.pi/harness/agents.policy.yaml +49 -82
  34. package/.pi/harness/docs/adrs/0052-ls-lint-naming-lifecycle.md +45 -0
  35. package/.pi/harness/docs/adrs/0052-sentrux-structured-repair.md +38 -0
  36. package/.pi/harness/docs/adrs/0053-plan-task-clarification-gate.md +39 -0
  37. package/.pi/harness/docs/adrs/0054-harness-native-ask-user.md +40 -0
  38. package/.pi/harness/docs/adrs/0055-auto-commit-coauthor-lifecycle.md +40 -0
  39. package/.pi/harness/docs/adrs/README.md +5 -0
  40. package/.pi/harness/docs/practice-map.md +10 -5
  41. package/.pi/harness/evals/smoke/ls-lint-stub.json +10 -0
  42. package/.pi/harness/evolution/self-healing-rules.json +16 -0
  43. package/.pi/harness/ls-lint/naming.manifest.json +128 -0
  44. package/.pi/harness/sentrux/architecture.manifest.json +1 -1
  45. package/.pi/harness/specs/auto-commit.schema.json +63 -0
  46. package/.pi/harness/specs/ls-lint-manifest-proposal.schema.json +80 -0
  47. package/.pi/harness/specs/ls-lint-signal.schema.json +47 -0
  48. package/.pi/harness/specs/naming-manifest.schema.json +54 -0
  49. package/.pi/harness/specs/plan-task-clarification.schema.json +88 -0
  50. package/.pi/harness/specs/sentrux-diagnostics.schema.json +173 -0
  51. package/.pi/harness/specs/sentrux-repair-plan.schema.json +133 -0
  52. package/.pi/harness/specs/sentrux-report.schema.json +119 -0
  53. package/.pi/harness/specs/sentrux-signal.schema.json +34 -1
  54. package/.pi/lib/agents-policy.d.mts +26 -51
  55. package/.pi/lib/agents-policy.mjs +41 -28
  56. package/.pi/lib/agt/build-evaluation-context.ts +136 -64
  57. package/.pi/lib/ask-user/constants.mjs +3 -0
  58. package/.pi/lib/ask-user/constants.ts +4 -0
  59. package/.pi/lib/ask-user/contracts/glimpse-parse.ts +56 -0
  60. package/.pi/lib/ask-user/contracts/glimpse-payload-build.ts +58 -0
  61. package/.pi/lib/ask-user/contracts/glimpse-payload.ts +38 -0
  62. package/.pi/lib/ask-user/core/questionnaire.ts +74 -0
  63. package/.pi/lib/ask-user/dialog.ts +2 -314
  64. package/.pi/lib/ask-user/fallback.ts +2 -78
  65. package/.pi/lib/ask-user/format.ts +85 -0
  66. package/.pi/lib/ask-user/glimpseui.d.ts +10 -0
  67. package/.pi/lib/ask-user/index.ts +114 -0
  68. package/.pi/lib/ask-user/merge-task-clarification.ts +98 -0
  69. package/.pi/lib/ask-user/policy.mjs +43 -0
  70. package/.pi/lib/ask-user/policy.ts +104 -0
  71. package/.pi/lib/ask-user/presenters/glimpse.ts +130 -0
  72. package/.pi/lib/ask-user/presenters/headless.ts +131 -0
  73. package/.pi/lib/ask-user/presenters/select.ts +60 -0
  74. package/.pi/lib/ask-user/presenters/tui.ts +373 -0
  75. package/.pi/lib/ask-user/presenters/types.ts +13 -0
  76. package/.pi/lib/ask-user/render.ts +40 -9
  77. package/.pi/lib/ask-user/schema.ts +66 -13
  78. package/.pi/lib/ask-user/types.ts +60 -3
  79. package/.pi/lib/ask-user/validate-core.mjs +193 -7
  80. package/.pi/lib/ask-user/validate.ts +53 -34
  81. package/.pi/lib/harness-anchored-edit/package.json +3 -0
  82. package/.pi/lib/harness-artifact-gate.ts +75 -21
  83. package/.pi/lib/harness-auto-commit-config.mjs +321 -0
  84. package/.pi/lib/harness-lens/clients/lsp/client.ts +62 -39
  85. package/.pi/lib/harness-lens/clients/tool-policy.ts +73 -181
  86. package/.pi/lib/harness-lens/index.ts +241 -108
  87. package/.pi/lib/harness-lens/tools/lsp-navigation.ts +10 -8
  88. package/.pi/lib/harness-repair-brief.ts +84 -25
  89. package/.pi/lib/harness-run-context.ts +42 -52
  90. package/.pi/lib/harness-sentrux-parse.mjs +272 -0
  91. package/.pi/lib/harness-sentrux-root.mjs +78 -0
  92. package/.pi/lib/harness-slash-completions.ts +116 -0
  93. package/.pi/lib/harness-spawn-topology.ts +121 -87
  94. package/.pi/lib/harness-subagent-submit-registry.ts +10 -0
  95. package/.pi/lib/harness-subagents-bridge.ts +4 -1
  96. package/.pi/lib/harness-ui-state.ts +95 -48
  97. package/.pi/lib/plan-approval/dialog.ts +5 -0
  98. package/.pi/lib/plan-approval/validate.ts +1 -1
  99. package/.pi/lib/plan-approval-readiness.ts +32 -0
  100. package/.pi/lib/plan-debate-gate.ts +154 -114
  101. package/.pi/lib/plan-task-clarification.ts +158 -0
  102. package/.pi/prompts/harness-auto.md +2 -2
  103. package/.pi/prompts/harness-ls-lint-steward.md +43 -0
  104. package/.pi/prompts/harness-plan.md +58 -8
  105. package/.pi/prompts/harness-review.md +40 -6
  106. package/.pi/prompts/harness-run.md +33 -11
  107. package/.pi/prompts/harness-setup.md +72 -3
  108. package/.pi/prompts/harness-steer.md +2 -1
  109. package/.pi/prompts/wiki-save.md +5 -4
  110. package/.pi/scripts/README.md +8 -0
  111. package/.pi/scripts/generate-agents-policy-yaml.mjs +14 -2
  112. package/.pi/scripts/harness-auto-commit-bootstrap.mjs +96 -0
  113. package/.pi/scripts/harness-cli-verify.sh +47 -0
  114. package/.pi/scripts/harness-git-churn.mjs +77 -0
  115. package/.pi/scripts/harness-git-commit.mjs +173 -0
  116. package/.pi/scripts/harness-ls-lint-bootstrap.mjs +142 -0
  117. package/.pi/scripts/harness-ls-lint-cli.mjs +184 -0
  118. package/.pi/scripts/harness-seed-project-contracts.mjs +47 -0
  119. package/.pi/scripts/harness-sentrux-diagnostics.mjs +230 -0
  120. package/.pi/scripts/harness-sentrux-report.mjs +256 -0
  121. package/.pi/scripts/harness-verify.mjs +288 -125
  122. package/.pi/scripts/ls-lint-rules-sync.mjs +265 -0
  123. package/.pi/scripts/run-tests.mjs +1 -0
  124. package/.pi/settings.example.json +1 -0
  125. package/.sentrux/rules.toml +1 -1
  126. package/AGENTS.md +1 -0
  127. package/CHANGELOG.md +25 -0
  128. package/README.md +13 -4
  129. package/package.json +5 -1
  130. package/vendor/pi-vcc/src/hooks/before-compact.ts +86 -60
@@ -0,0 +1,230 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Synthesize Pro-shaped sentrux-diagnostics.json from sentrux-report.json (+ optional git churn).
4
+ *
5
+ * Usage:
6
+ * node harness-sentrux-diagnostics.mjs --report <sentrux-report.json> [--out <DIR>] [--churn]
7
+ */
8
+
9
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
10
+ import { dirname, join } from "node:path";
11
+ import {
12
+ inferBottleneck,
13
+ parseComplexFunctionEntries,
14
+ parseGodFileEntries,
15
+ sha256,
16
+ } from "../lib/harness-sentrux-parse.mjs";
17
+ import { loadGitChurn } from "./harness-git-churn.mjs";
18
+
19
+ function usage() {
20
+ console.error(`Usage: harness-sentrux-diagnostics.mjs --report <path> [--out <dir>] [--churn] [--root <path>]`);
21
+ process.exit(2);
22
+ }
23
+
24
+ function summarizeViolations(violations) {
25
+ const byRule = new Map();
26
+ for (const v of violations) {
27
+ const cur = byRule.get(v.rule) || {
28
+ rule: v.rule,
29
+ count: 0,
30
+ severity: v.severity,
31
+ sample_files: [],
32
+ };
33
+ cur.count += 1;
34
+ for (const f of v.files) {
35
+ if (cur.sample_files.length < 5 && !cur.sample_files.includes(f)) {
36
+ cur.sample_files.push(f);
37
+ }
38
+ }
39
+ byRule.set(v.rule, cur);
40
+ }
41
+ return [...byRule.values()].sort((a, b) => b.count - a.count);
42
+ }
43
+
44
+ function ccPriority(cc) {
45
+ if (cc >= 60) return "critical";
46
+ if (cc >= 45) return "high";
47
+ if (cc >= 35) return "medium";
48
+ return "low";
49
+ }
50
+
51
+ function buildRootCauses(violations, gate, bottleneck) {
52
+ const causes = [];
53
+ for (const v of violations) {
54
+ if (v.rule === "layer_direction" || v.rule === "boundary") {
55
+ causes.push(
56
+ `Layer/boundary violations: ${v.message} (${v.files.length} location(s))`,
57
+ );
58
+ } else if (v.rule === "max_cc") {
59
+ causes.push(`High cyclomatic complexity: ${v.message}`);
60
+ } else {
61
+ causes.push(`${v.rule}: ${v.message}`);
62
+ }
63
+ }
64
+ for (const r of gate.degraded_reasons || []) {
65
+ if (!causes.some((c) => c.includes(r))) causes.push(r);
66
+ }
67
+ if (causes.length === 0) {
68
+ causes.push(
69
+ bottleneck === "equality"
70
+ ? "Structural equality debt (complexity / size)"
71
+ : "Structural modularity debt (coupling / boundaries)",
72
+ );
73
+ }
74
+ return [...new Set(causes)];
75
+ }
76
+
77
+ function scoreHotspots(complexFns, churnMap, violations) {
78
+ const scores = new Map();
79
+ const add = (path, delta, reason) => {
80
+ const cur = scores.get(path) || { path, score: 0, reasons: [] };
81
+ cur.score += delta;
82
+ if (reason && !cur.reasons.includes(reason)) cur.reasons.push(reason);
83
+ scores.set(path, cur);
84
+ };
85
+
86
+ for (const fn of complexFns) {
87
+ add(fn.file, fn.cc / 10, `complex:${fn.func}`);
88
+ }
89
+ for (const v of violations) {
90
+ for (const f of v.files) {
91
+ const path = f.split(":")[0] || f;
92
+ add(path, v.severity === "error" ? 5 : 2, `rule:${v.rule}`);
93
+ }
94
+ }
95
+ if (churnMap) {
96
+ for (const [path, churn] of Object.entries(churnMap)) {
97
+ if (churn > 0) add(path, Math.min(churn / 5, 15), "git-churn");
98
+ }
99
+ }
100
+
101
+ return [...scores.values()]
102
+ .sort((a, b) => b.score - a.score)
103
+ .slice(0, 20)
104
+ .map((h) => ({
105
+ path: h.path,
106
+ score: Math.round(h.score * 10) / 10,
107
+ churn_14d: churnMap?.[h.path] ?? null,
108
+ reason: h.reasons.slice(0, 3).join("; "),
109
+ }));
110
+ }
111
+
112
+ export function synthesizeDiagnostics(report, options = {}) {
113
+ const violations = report.check?.violations ?? [];
114
+ const gate = report.gate ?? { status: "unknown", degraded_reasons: [] };
115
+ const { bottleneck, bottleneck_inferred } = inferBottleneck(violations, gate);
116
+ const root_causes = buildRootCauses(violations, gate, bottleneck);
117
+
118
+ const complex_functions = parseComplexFunctionEntries(violations).map((fn) => ({
119
+ ...fn,
120
+ priority: ccPriority(fn.cc),
121
+ }));
122
+
123
+ const god_files = parseGodFileEntries(violations).map((g) => ({
124
+ ...g,
125
+ reason: "no_god_files rule violation",
126
+ }));
127
+
128
+ const cycles = [];
129
+ for (const v of violations) {
130
+ if (v.rule === "max_cycles") {
131
+ cycles.push({
132
+ members: v.files,
133
+ reason: v.message,
134
+ });
135
+ }
136
+ }
137
+
138
+ const churnMap = options.churnMap ?? null;
139
+ const hotspots = scoreHotspots(complex_functions, churnMap, violations);
140
+
141
+ const graphify_refs = [];
142
+ if (options.graphifyReportPath) {
143
+ graphify_refs.push({
144
+ path: options.graphifyReportPath,
145
+ summary: "GRAPH_REPORT.md god nodes and communities for repair targeting",
146
+ });
147
+ }
148
+
149
+ return {
150
+ schema_version: "1.0.0",
151
+ synthesized_at: new Date().toISOString(),
152
+ project_root: report.project_root,
153
+ report_sha256: sha256(JSON.stringify(report)),
154
+ quality_signal: report.check?.quality_signal ?? null,
155
+ gate_status: gate.status === "degraded" ? "degraded" : gate.status === "pass" ? "pass" : "unknown",
156
+ bottleneck,
157
+ bottleneck_inferred,
158
+ root_causes,
159
+ diagnostics: {
160
+ god_files,
161
+ hotspots,
162
+ complex_functions,
163
+ cycles,
164
+ violations_summary: summarizeViolations(violations),
165
+ gate_degraded_reasons: gate.degraded_reasons ?? [],
166
+ },
167
+ graphify_refs: graphify_refs.length ? graphify_refs : undefined,
168
+ };
169
+ }
170
+
171
+ async function main() {
172
+ const args = process.argv.slice(2);
173
+ let reportPath = "";
174
+ let outDir = "";
175
+ let useChurn = false;
176
+ let projectRoot = "";
177
+
178
+ for (let i = 0; i < args.length; i++) {
179
+ const a = args[i];
180
+ if (a === "--report") reportPath = args[++i] || "";
181
+ else if (a === "--out") outDir = args[++i] || "";
182
+ else if (a === "--churn") useChurn = true;
183
+ else if (a === "--root") projectRoot = args[++i] || "";
184
+ else if (a === "--help" || a === "-h") usage();
185
+ }
186
+
187
+ if (!reportPath) usage();
188
+ const report = JSON.parse(await readFile(reportPath, "utf-8"));
189
+ const root = projectRoot || report.project_root;
190
+
191
+ let churnMap = null;
192
+ if (useChurn && root) {
193
+ try {
194
+ churnMap = await loadGitChurn(root, { days: 14 });
195
+ } catch {
196
+ churnMap = null;
197
+ }
198
+ }
199
+
200
+ const graphifyReportPath = root
201
+ ? join(root, "graphify-out", "GRAPH_REPORT.md")
202
+ : undefined;
203
+
204
+ const diagnostics = synthesizeDiagnostics(report, {
205
+ churnMap,
206
+ graphifyReportPath,
207
+ });
208
+
209
+ const json = `${JSON.stringify(diagnostics, null, 2)}\n`;
210
+ const targetDir =
211
+ outDir || join(dirname(reportPath), ".");
212
+ const outPath = outDir
213
+ ? join(outDir, "artifacts", "sentrux-diagnostics.json")
214
+ : join(dirname(reportPath), "sentrux-diagnostics.json");
215
+
216
+ if (outDir) {
217
+ await mkdir(join(outDir, "artifacts"), { recursive: true });
218
+ }
219
+ await writeFile(outPath, json);
220
+ if (!outDir) process.stdout.write(json);
221
+ }
222
+
223
+ import { pathToFileURL } from "node:url";
224
+
225
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
226
+ main().catch((err) => {
227
+ console.error(err);
228
+ process.exit(1);
229
+ });
230
+ }
@@ -0,0 +1,256 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Single-scan Sentrux check + gate capture → sentrux-report.json (+ optional sentrux-signal.yaml).
4
+ *
5
+ * Usage:
6
+ * node harness-sentrux-report.mjs --out <DIR> [--root <PROJECT_ROOT>] [--run-id <ID>] [--signal]
7
+ * node harness-sentrux-report.mjs --parse-only --check-file <path> [--gate-file <path>]
8
+ */
9
+
10
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
11
+ import { dirname, join } from "node:path";
12
+ import { spawn } from "node:child_process";
13
+ import {
14
+ takeRootArg,
15
+ resolveSentruxProjectRoot,
16
+ } from "../lib/harness-sentrux-root.mjs";
17
+ import {
18
+ PARSER_VERSION,
19
+ filterSentruxOutputLines,
20
+ parseCheckOutput,
21
+ parseGateOutput,
22
+ sha256,
23
+ normalizeUpstreamCheckJson,
24
+ } from "../lib/harness-sentrux-parse.mjs";
25
+ import { stringify as stringifyYaml } from "yaml";
26
+
27
+ function usage() {
28
+ console.error(`Usage:
29
+ harness-sentrux-report.mjs --out <DIR> [--root <PROJECT_ROOT>] [--run-id <ID>] [--signal]
30
+ harness-sentrux-report.mjs --parse-only --check-file <path> [--gate-file <path>] [--out <DIR>]`);
31
+ process.exit(2);
32
+ }
33
+
34
+ function runSentrux(args, projectRoot) {
35
+ return new Promise((resolve, reject) => {
36
+ const child = spawn("sentrux", args, {
37
+ cwd: projectRoot,
38
+ env: process.env,
39
+ });
40
+ let stdout = "";
41
+ let stderr = "";
42
+ child.stdout?.on("data", (c) => {
43
+ stdout += c.toString();
44
+ });
45
+ child.stderr?.on("data", (c) => {
46
+ stderr += c.toString();
47
+ });
48
+ child.on("error", (err) => {
49
+ if (err?.code === "ENOENT") {
50
+ reject(
51
+ Object.assign(new Error("sentrux not installed"), { code: 127 }),
52
+ );
53
+ return;
54
+ }
55
+ reject(err);
56
+ });
57
+ child.on("close", (code) => {
58
+ resolve({ code: code ?? 1, stdout, stderr });
59
+ });
60
+ });
61
+ }
62
+
63
+ async function tryUpstreamJson(projectRoot) {
64
+ try {
65
+ const { code, stdout } = await runSentrux(
66
+ ["check", "--format", "json", projectRoot],
67
+ projectRoot,
68
+ );
69
+ if (code !== 0 && code !== 1) return null;
70
+ const trimmed = stdout.trim();
71
+ if (!trimmed.startsWith("{")) return null;
72
+ const json = JSON.parse(trimmed);
73
+ return normalizeUpstreamCheckJson(json);
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+
79
+ async function getSentruxVersion() {
80
+ try {
81
+ const { stdout } = await runSentrux(["--version"], process.cwd());
82
+ const line = stdout.trim().split(/\r?\n/)[0] || "";
83
+ return line || null;
84
+ } catch {
85
+ return null;
86
+ }
87
+ }
88
+
89
+ function buildSignal(runId, report) {
90
+ const check = report.check;
91
+ const gate = report.gate;
92
+ const summaryParts = [];
93
+ if (check.quality_signal != null) {
94
+ summaryParts.push(`quality=${check.quality_signal}`);
95
+ }
96
+ if (gate.quality_before != null && gate.quality_after != null) {
97
+ summaryParts.push(`gate ${gate.quality_before}->${gate.quality_after}`);
98
+ }
99
+ if (gate.degraded_reasons?.length) {
100
+ summaryParts.push(gate.degraded_reasons.join("; "));
101
+ }
102
+ return {
103
+ schema_version: "1.1.0",
104
+ run_id: runId || "unknown",
105
+ check_pass: check.check_pass,
106
+ gate_status:
107
+ gate.status === "degraded"
108
+ ? "degraded"
109
+ : gate.status === "pass"
110
+ ? "pass"
111
+ : "skipped",
112
+ quality_signal_summary: summaryParts.join(" | ") || undefined,
113
+ recorded_at: report.captured_at,
114
+ phase: "review",
115
+ quality_signal: check.quality_signal ?? undefined,
116
+ violation_count: check.violations?.length ?? 0,
117
+ report_path: "artifacts/sentrux-report.json",
118
+ diagnostics_path: "artifacts/sentrux-diagnostics.json",
119
+ degraded_reasons:
120
+ gate.degraded_reasons?.length > 0
121
+ ? gate.degraded_reasons
122
+ : undefined,
123
+ };
124
+ }
125
+
126
+ async function captureReport(projectRoot) {
127
+ const captured_at = new Date().toISOString();
128
+ const upstream = await tryUpstreamJson(projectRoot);
129
+ const upstream_json_available = upstream != null;
130
+
131
+ const checkRun = await runSentrux(["check", projectRoot], projectRoot);
132
+ const gateRun = await runSentrux(["gate", projectRoot], projectRoot);
133
+
134
+ const checkFiltered = filterSentruxOutputLines(
135
+ `${checkRun.stdout}\n${checkRun.stderr}`,
136
+ ).join("\n");
137
+ const gateFiltered = filterSentruxOutputLines(
138
+ `${gateRun.stdout}\n${gateRun.stderr}`,
139
+ ).join("\n");
140
+
141
+ let check = parseCheckOutput(checkFiltered);
142
+ const gate = parseGateOutput(gateFiltered);
143
+
144
+ if (upstream) {
145
+ check = {
146
+ ...check,
147
+ check_pass: upstream.check_pass,
148
+ quality_signal: upstream.quality_signal ?? check.quality_signal,
149
+ rules_checked: upstream.rules_checked ?? check.rules_checked,
150
+ violations:
151
+ upstream.violations?.length > 0
152
+ ? upstream.violations
153
+ : check.violations,
154
+ upstream: true,
155
+ };
156
+ }
157
+
158
+ check.stdout_sha256 = sha256(checkFiltered);
159
+ gate.stdout_sha256 = sha256(gateFiltered);
160
+
161
+ if (checkRun.code === 127) throw Object.assign(new Error("sentrux not installed"), { code: 127 });
162
+
163
+ return {
164
+ schema_version: "1.0.0",
165
+ captured_at,
166
+ project_root: projectRoot,
167
+ parser_version: PARSER_VERSION,
168
+ sentrux_cli_version: await getSentruxVersion(),
169
+ upstream_json_available,
170
+ check,
171
+ gate,
172
+ exit_codes: { check: checkRun.code, gate: gateRun.code },
173
+ };
174
+ }
175
+
176
+ async function main() {
177
+ const raw = process.argv.slice(2);
178
+ const { args, explicitRoot } = takeRootArg(raw);
179
+
180
+ let outDir = "";
181
+ let runId = "";
182
+ let parseOnly = false;
183
+ let writeSignal = false;
184
+ let checkFile = "";
185
+ let gateFile = "";
186
+
187
+ for (let i = 0; i < args.length; i++) {
188
+ const a = args[i];
189
+ if (a === "--out") outDir = args[++i] || "";
190
+ else if (a === "--run-id") runId = args[++i] || "";
191
+ else if (a === "--parse-only") parseOnly = true;
192
+ else if (a === "--signal") writeSignal = true;
193
+ else if (a === "--check-file") checkFile = args[++i] || "";
194
+ else if (a === "--gate-file") gateFile = args[++i] || "";
195
+ else if (a === "--help" || a === "-h") usage();
196
+ }
197
+
198
+ if (parseOnly) {
199
+ if (!checkFile) usage();
200
+ const checkText = await readFile(checkFile, "utf-8");
201
+ const gateText = gateFile ? await readFile(gateFile, "utf-8") : "";
202
+ const report = {
203
+ schema_version: "1.0.0",
204
+ captured_at: new Date().toISOString(),
205
+ project_root: explicitRoot || process.cwd(),
206
+ parser_version: PARSER_VERSION,
207
+ upstream_json_available: false,
208
+ check: {
209
+ ...parseCheckOutput(checkText),
210
+ stdout_sha256: sha256(checkText),
211
+ },
212
+ gate: {
213
+ ...parseGateOutput(gateText),
214
+ stdout_sha256: sha256(gateText),
215
+ },
216
+ };
217
+ const json = `${JSON.stringify(report, null, 2)}\n`;
218
+ if (outDir) {
219
+ const artifactsDir = join(outDir, "artifacts");
220
+ await mkdir(artifactsDir, { recursive: true });
221
+ await writeFile(join(artifactsDir, "sentrux-report.json"), json);
222
+ } else {
223
+ process.stdout.write(json);
224
+ }
225
+ return;
226
+ }
227
+
228
+ if (!outDir) usage();
229
+ const projectRoot = await resolveSentruxProjectRoot(explicitRoot);
230
+ const report = await captureReport(projectRoot);
231
+
232
+ const artifactsDir = join(outDir, "artifacts");
233
+ await mkdir(artifactsDir, { recursive: true });
234
+ const reportPath = join(artifactsDir, "sentrux-report.json");
235
+ await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`);
236
+
237
+ if (writeSignal) {
238
+ const signal = buildSignal(runId, report);
239
+ await writeFile(
240
+ join(artifactsDir, "sentrux-signal.yaml"),
241
+ stringifyYaml(signal),
242
+ );
243
+ }
244
+
245
+ const exitCode = report.check.check_pass && report.gate.status !== "degraded" ? 0 : 1;
246
+ process.exit(exitCode);
247
+ }
248
+
249
+ main().catch((err) => {
250
+ if (err?.code === 127) {
251
+ console.error("harness-sentrux-report: sentrux not installed");
252
+ process.exit(127);
253
+ }
254
+ console.error(err);
255
+ process.exit(1);
256
+ });