ultimate-pi 0.19.1 → 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.
- package/.agents/skills/harness-decisions/SKILL.md +68 -2
- package/.agents/skills/harness-git-commit/SKILL.md +72 -0
- package/.agents/skills/harness-governor/SKILL.md +2 -2
- package/.agents/skills/harness-ls-lint-setup/SKILL.md +59 -0
- package/.agents/skills/harness-plan/SKILL.md +13 -11
- package/.agents/skills/harness-review/SKILL.md +1 -1
- package/.agents/skills/harness-sentrux-repair/SKILL.md +48 -0
- package/.agents/skills/sentrux/SKILL.md +4 -2
- package/.agents/skills/wiki-save/SKILL.md +1 -1
- package/.pi/PACKAGING.md +6 -0
- package/.pi/SYSTEM.md +21 -3
- package/.pi/agents/harness/ls-lint-steward.md +49 -0
- package/.pi/agents/harness/planning/decompose.md +4 -4
- package/.pi/agents/harness/reviewing/evaluator.md +1 -1
- package/.pi/agents/harness/running/executor.md +43 -2
- package/.pi/agents/harness/sentrux-repair-advisor.md +50 -0
- package/.pi/agents/pi-pi/prompt-expert.md +17 -2
- package/.pi/auto-commit.json +9 -2
- package/.pi/extensions/debate-orchestrator.ts +3 -0
- package/.pi/extensions/harness-anchored-edit.ts +139 -0
- package/.pi/extensions/harness-ask-user.ts +13 -34
- package/.pi/extensions/harness-debate-tools.ts +43 -4
- package/.pi/extensions/harness-live-widget.ts +28 -19
- package/.pi/extensions/harness-run-context.ts +278 -115
- package/.pi/extensions/harness-web-tools.ts +598 -471
- package/.pi/extensions/ls-lint-rules-sync.ts +103 -0
- package/.pi/extensions/observation-bus.ts +4 -0
- package/.pi/extensions/policy-gate.ts +270 -229
- package/.pi/extensions/sentrux-rules-sync.ts +2 -0
- package/.pi/extensions/soundboard.ts +48 -48
- package/.pi/harness/README.md +4 -0
- package/.pi/harness/agents.manifest.json +15 -7
- package/.pi/harness/agents.policy.yaml +47 -81
- package/.pi/harness/docs/adrs/0051-hash-anchored-executor-edits.md +41 -0
- package/.pi/harness/docs/adrs/0052-ls-lint-naming-lifecycle.md +45 -0
- package/.pi/harness/docs/adrs/0052-sentrux-structured-repair.md +38 -0
- package/.pi/harness/docs/adrs/0053-plan-task-clarification-gate.md +39 -0
- package/.pi/harness/docs/adrs/0054-harness-native-ask-user.md +40 -0
- package/.pi/harness/docs/adrs/0055-auto-commit-coauthor-lifecycle.md +40 -0
- package/.pi/harness/docs/adrs/README.md +7 -0
- package/.pi/harness/docs/practice-map.md +21 -5
- package/.pi/harness/evals/smoke/ls-lint-stub.json +10 -0
- package/.pi/harness/evolution/self-healing-rules.json +16 -0
- package/.pi/harness/ls-lint/naming.manifest.json +128 -0
- package/.pi/harness/sentrux/architecture.manifest.json +1 -1
- package/.pi/harness/specs/auto-commit.schema.json +63 -0
- package/.pi/harness/specs/ls-lint-manifest-proposal.schema.json +80 -0
- package/.pi/harness/specs/ls-lint-signal.schema.json +47 -0
- package/.pi/harness/specs/naming-manifest.schema.json +54 -0
- package/.pi/harness/specs/plan-task-clarification.schema.json +88 -0
- package/.pi/harness/specs/sentrux-diagnostics.schema.json +173 -0
- package/.pi/harness/specs/sentrux-repair-plan.schema.json +133 -0
- package/.pi/harness/specs/sentrux-report.schema.json +119 -0
- package/.pi/harness/specs/sentrux-signal.schema.json +34 -1
- package/.pi/lib/agents-policy.d.mts +26 -47
- package/.pi/lib/agents-policy.mjs +84 -29
- package/.pi/lib/agents-policy.ts +1 -0
- package/.pi/lib/agt/build-evaluation-context.ts +136 -64
- package/.pi/lib/ask-user/constants.mjs +3 -0
- package/.pi/lib/ask-user/constants.ts +4 -0
- package/.pi/lib/ask-user/contracts/glimpse-parse.ts +56 -0
- package/.pi/lib/ask-user/contracts/glimpse-payload-build.ts +58 -0
- package/.pi/lib/ask-user/contracts/glimpse-payload.ts +38 -0
- package/.pi/lib/ask-user/core/questionnaire.ts +74 -0
- package/.pi/lib/ask-user/dialog.ts +2 -314
- package/.pi/lib/ask-user/fallback.ts +2 -78
- package/.pi/lib/ask-user/format.ts +85 -0
- package/.pi/lib/ask-user/glimpseui.d.ts +10 -0
- package/.pi/lib/ask-user/index.ts +114 -0
- package/.pi/lib/ask-user/merge-task-clarification.ts +98 -0
- package/.pi/lib/ask-user/policy.mjs +43 -0
- package/.pi/lib/ask-user/policy.ts +104 -0
- package/.pi/lib/ask-user/presenters/glimpse.ts +130 -0
- package/.pi/lib/ask-user/presenters/headless.ts +131 -0
- package/.pi/lib/ask-user/presenters/select.ts +60 -0
- package/.pi/lib/ask-user/presenters/tui.ts +373 -0
- package/.pi/lib/ask-user/presenters/types.ts +13 -0
- package/.pi/lib/ask-user/render.ts +40 -9
- package/.pi/lib/ask-user/schema.ts +66 -13
- package/.pi/lib/ask-user/types.ts +60 -3
- package/.pi/lib/ask-user/validate-core.mjs +193 -7
- package/.pi/lib/ask-user/validate.ts +53 -34
- package/.pi/lib/harness-anchored-edit/.hash_anchors +1721 -0
- package/.pi/lib/harness-anchored-edit/anchor-state.ts +320 -0
- package/.pi/lib/harness-anchored-edit/apply-anchored-edits.ts +161 -0
- package/.pi/lib/harness-anchored-edit/edit-executor.ts +146 -0
- package/.pi/lib/harness-anchored-edit/index.ts +9 -0
- package/.pi/lib/harness-anchored-edit/line-protocol.ts +38 -0
- package/.pi/lib/harness-anchored-edit/package.json +3 -0
- package/.pi/lib/harness-anchored-edit/settings.ts +1 -0
- package/.pi/lib/harness-anchored-edit/task-id.ts +8 -0
- package/.pi/lib/harness-anchored-edit/types.ts +19 -0
- package/.pi/lib/harness-artifact-gate.ts +75 -21
- package/.pi/lib/harness-auto-commit-config.mjs +321 -0
- package/.pi/lib/harness-lens/clients/anchored-edit-autopatch.ts +158 -0
- package/.pi/lib/harness-lens/clients/lsp/client.ts +62 -39
- package/.pi/lib/harness-lens/clients/tool-policy.ts +73 -181
- package/.pi/lib/harness-lens/index.ts +246 -96
- package/.pi/lib/harness-lens/tools/lsp-navigation.ts +10 -8
- package/.pi/lib/harness-repair-brief.ts +84 -25
- package/.pi/lib/harness-run-context.ts +42 -52
- package/.pi/lib/harness-sentrux-parse.mjs +272 -0
- package/.pi/lib/harness-sentrux-root.mjs +78 -0
- package/.pi/lib/harness-slash-completions.ts +116 -0
- package/.pi/lib/harness-spawn-topology.ts +121 -87
- package/.pi/lib/harness-subagent-submit-registry.ts +10 -0
- package/.pi/lib/harness-subagents-bridge.ts +11 -6
- package/.pi/lib/harness-ui-state.ts +95 -48
- package/.pi/lib/plan-approval/dialog.ts +5 -0
- package/.pi/lib/plan-approval/validate.ts +1 -1
- package/.pi/lib/plan-approval-readiness.ts +32 -0
- package/.pi/lib/plan-debate-gate.ts +154 -114
- package/.pi/lib/plan-task-clarification.ts +158 -0
- package/.pi/prompts/harness-auto.md +2 -2
- package/.pi/prompts/harness-ls-lint-steward.md +43 -0
- package/.pi/prompts/harness-plan.md +58 -8
- package/.pi/prompts/harness-review.md +40 -6
- package/.pi/prompts/harness-run.md +33 -11
- package/.pi/prompts/harness-setup.md +72 -3
- package/.pi/prompts/harness-steer.md +3 -2
- package/.pi/prompts/wiki-save.md +5 -4
- package/.pi/scripts/README.md +8 -0
- package/.pi/scripts/generate-agents-policy-yaml.mjs +14 -2
- package/.pi/scripts/harness-anchored-edit-smoke.mjs +45 -0
- package/.pi/scripts/harness-auto-commit-bootstrap.mjs +96 -0
- package/.pi/scripts/harness-cli-verify.sh +47 -0
- package/.pi/scripts/harness-git-churn.mjs +77 -0
- package/.pi/scripts/harness-git-commit.mjs +173 -0
- package/.pi/scripts/harness-ls-lint-bootstrap.mjs +142 -0
- package/.pi/scripts/harness-ls-lint-cli.mjs +184 -0
- package/.pi/scripts/harness-seed-project-contracts.mjs +47 -0
- package/.pi/scripts/harness-sentrux-diagnostics.mjs +230 -0
- package/.pi/scripts/harness-sentrux-report.mjs +256 -0
- package/.pi/scripts/harness-verify.mjs +347 -117
- package/.pi/scripts/ls-lint-rules-sync.mjs +265 -0
- package/.pi/scripts/run-tests.mjs +65 -0
- package/.pi/settings.example.json +1 -0
- package/.sentrux/rules.toml +1 -1
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +31 -0
- package/README.md +13 -4
- package/THIRD_PARTY_NOTICES.md +7 -0
- package/package.json +8 -3
- package/vendor/pi-subagents/src/agents.ts +5 -0
- package/vendor/pi-subagents/src/subagents.ts +22 -3
- package/vendor/pi-vcc/src/hooks/before-compact.ts +86 -60
- package/.pi/scripts/release.sh +0 -338
|
@@ -8,8 +8,15 @@
|
|
|
8
8
|
|
|
9
9
|
import { mkdir, readFile, realpath, writeFile } from "node:fs/promises";
|
|
10
10
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
11
|
+
import {
|
|
12
|
+
isPlanApprovalAskUser,
|
|
13
|
+
PLAN_APPROVE_OPTION,
|
|
14
|
+
PLAN_CANCEL_OPTION,
|
|
15
|
+
} from "./ask-user/policy.js";
|
|
11
16
|
import { readYamlFile, writeYamlFile } from "./harness-yaml.js";
|
|
12
17
|
|
|
18
|
+
export { isPlanApprovalAskUser } from "./ask-user/policy.js";
|
|
19
|
+
|
|
13
20
|
export type HarnessPhase =
|
|
14
21
|
| "plan"
|
|
15
22
|
| "execute"
|
|
@@ -171,11 +178,6 @@ export function steerMaxAttemptsFromEnv(): number {
|
|
|
171
178
|
|
|
172
179
|
const MUTATING_FILE_TOOLS = new Set(["write", "edit"]);
|
|
173
180
|
|
|
174
|
-
const PLAN_APPROVE_OPTION =
|
|
175
|
-
/^(approve(d)?(\s+plan)?|yes,?\s+proceed|looks\s+good)$/i;
|
|
176
|
-
const PLAN_CANCEL_OPTION =
|
|
177
|
-
/^(cancel(led)?|revise|request\s+changes|needs?\s+clarification)$/i;
|
|
178
|
-
|
|
179
181
|
export interface PlanUserApproval {
|
|
180
182
|
plan_id: string | null;
|
|
181
183
|
approved_at: string;
|
|
@@ -702,28 +704,6 @@ export function hasApprovedPlanSignalFromUserPrompt(prompt: string): boolean {
|
|
|
702
704
|
return false;
|
|
703
705
|
}
|
|
704
706
|
|
|
705
|
-
/** Detect parent-session ask_user calls that duplicate planner plan approval. */
|
|
706
|
-
export function isPlanApprovalAskUser(input: {
|
|
707
|
-
question?: string;
|
|
708
|
-
options?: unknown[];
|
|
709
|
-
}): boolean {
|
|
710
|
-
const q = String(input.question ?? "").trim();
|
|
711
|
-
const opts = Array.isArray(input.options) ? input.options : [];
|
|
712
|
-
const titles = opts.map((o) => {
|
|
713
|
-
if (typeof o === "string") return o.trim();
|
|
714
|
-
if (o && typeof o === "object" && "title" in o) {
|
|
715
|
-
return String((o as { title?: string }).title ?? "").trim();
|
|
716
|
-
}
|
|
717
|
-
return "";
|
|
718
|
-
});
|
|
719
|
-
const hasPlanOptions =
|
|
720
|
-
titles.some(
|
|
721
|
-
(t) => PLAN_APPROVE_OPTION.test(t) || PLAN_CANCEL_OPTION.test(t),
|
|
722
|
-
) || PLAN_APPROVE_OPTION.test(q);
|
|
723
|
-
if (!hasPlanOptions) return false;
|
|
724
|
-
return /plan|approve/i.test(q);
|
|
725
|
-
}
|
|
726
|
-
|
|
727
707
|
export function appendPlanApprovalIfNew(
|
|
728
708
|
appendEntry: (customType: string, data: unknown) => void,
|
|
729
709
|
parentEntries: unknown[],
|
|
@@ -1667,6 +1647,34 @@ export async function readReviewOutcomeFromRun(
|
|
|
1667
1647
|
}
|
|
1668
1648
|
}
|
|
1669
1649
|
|
|
1650
|
+
function nextStepForEvaluateLikePhase(input: {
|
|
1651
|
+
adversaryComplete?: boolean;
|
|
1652
|
+
remediation: string;
|
|
1653
|
+
evalStatus: string;
|
|
1654
|
+
steerAttempt: number;
|
|
1655
|
+
steerMax: number;
|
|
1656
|
+
}): string {
|
|
1657
|
+
if (input.remediation === "pass" || input.evalStatus === "pass") {
|
|
1658
|
+
if (input.adversaryComplete) return "/harness-policy-status";
|
|
1659
|
+
return "/harness-review";
|
|
1660
|
+
}
|
|
1661
|
+
if (input.remediation === "rollback") return "/harness-incident";
|
|
1662
|
+
if (input.remediation === "plan_gap") return "/harness-plan (mode: revise)";
|
|
1663
|
+
if (
|
|
1664
|
+
input.remediation === "implementation_gap" ||
|
|
1665
|
+
(input.remediation === "inconclusive" && input.evalStatus === "fail")
|
|
1666
|
+
) {
|
|
1667
|
+
if (input.steerAttempt < input.steerMax) return "/harness-steer";
|
|
1668
|
+
return "/harness-plan (mode: revise) or /harness-abort";
|
|
1669
|
+
}
|
|
1670
|
+
if (input.evalStatus === "fail") {
|
|
1671
|
+
if (input.steerAttempt < input.steerMax) return "/harness-steer";
|
|
1672
|
+
return "/harness-plan (mode: revise) or /harness-incident";
|
|
1673
|
+
}
|
|
1674
|
+
if (input.adversaryComplete) return "/harness-policy-status";
|
|
1675
|
+
return "/harness-review";
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1670
1678
|
export function nextStepAfterOutcome(input: {
|
|
1671
1679
|
phase: HarnessPhase;
|
|
1672
1680
|
planStatus?: string | null;
|
|
@@ -1730,31 +1738,13 @@ export function nextStepAfterOutcome(input: {
|
|
|
1730
1738
|
}
|
|
1731
1739
|
|
|
1732
1740
|
if (input.phase === "evaluate" || input.phase === "adversary") {
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
}
|
|
1740
|
-
if (remediation === "plan_gap") {
|
|
1741
|
-
return "/harness-plan (mode: revise)";
|
|
1742
|
-
}
|
|
1743
|
-
if (
|
|
1744
|
-
remediation === "implementation_gap" ||
|
|
1745
|
-
(remediation === "inconclusive" && evalSt === "fail")
|
|
1746
|
-
) {
|
|
1747
|
-
if (steerAttempt < steerMax) {
|
|
1748
|
-
return "/harness-steer";
|
|
1749
|
-
}
|
|
1750
|
-
return "/harness-plan (mode: revise) or /harness-abort";
|
|
1751
|
-
}
|
|
1752
|
-
if (evalSt === "fail") {
|
|
1753
|
-
if (steerAttempt < steerMax) return "/harness-steer";
|
|
1754
|
-
return "/harness-plan (mode: revise) or /harness-incident";
|
|
1755
|
-
}
|
|
1756
|
-
if (input.adversaryComplete) return "/harness-policy-status";
|
|
1757
|
-
return "/harness-review";
|
|
1741
|
+
return nextStepForEvaluateLikePhase({
|
|
1742
|
+
adversaryComplete: input.adversaryComplete,
|
|
1743
|
+
remediation,
|
|
1744
|
+
evalStatus: evalSt,
|
|
1745
|
+
steerAttempt,
|
|
1746
|
+
steerMax,
|
|
1747
|
+
});
|
|
1758
1748
|
}
|
|
1759
1749
|
|
|
1760
1750
|
if (input.phase === "merge") return "/harness-policy-status";
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse sentrux check / gate CLI stdout (and optional upstream JSON).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
|
|
7
|
+
export const PARSER_VERSION = "1.0.0";
|
|
8
|
+
|
|
9
|
+
const PROGRESS_LINE =
|
|
10
|
+
/^\[(?:scan|build_project_map|resolve|resolve_imports|build_graphs)\]/;
|
|
11
|
+
|
|
12
|
+
const VIOLATION_HEAD =
|
|
13
|
+
/^([✗⚠])\s+\[(\w+)\]\s+([\w_]+):\s+(.+)$/;
|
|
14
|
+
const QUALITY_LINE = /^Quality:\s*(\d+)\s*$/;
|
|
15
|
+
const RULES_CHECKED = /^sentrux check —\s*(\d+)\s+rules checked/;
|
|
16
|
+
const GATE_QUALITY =
|
|
17
|
+
/^Quality:\s*(\d+)\s*->\s*(\d+)\s*$/;
|
|
18
|
+
const GATE_METRIC = /^(\w[\w\s]*?):\s*(.+?)\s*→\s*(.+?)\s*$/;
|
|
19
|
+
const GATE_DEGRADED_LINE = /^✗\s+DEGRADED\s*$/i;
|
|
20
|
+
const GATE_PASS_LINE = /^✓\s+No degradation detected/i;
|
|
21
|
+
const GATE_VIOLATION = /^\s+✗\s+(.+)$/;
|
|
22
|
+
|
|
23
|
+
/** Strip scan progress noise; keep user-facing lines. */
|
|
24
|
+
export function filterSentruxOutputLines(text) {
|
|
25
|
+
return text
|
|
26
|
+
.split(/\r?\n/)
|
|
27
|
+
.filter((line) => line.trim() && !PROGRESS_LINE.test(line.trim()))
|
|
28
|
+
.filter((line) => !line.startsWith("Scanning "));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function sha256(text) {
|
|
32
|
+
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {string} text — filtered check stdout
|
|
37
|
+
*/
|
|
38
|
+
export function parseCheckOutput(text) {
|
|
39
|
+
const lines = filterSentruxOutputLines(text);
|
|
40
|
+
const result = {
|
|
41
|
+
parse_ok: true,
|
|
42
|
+
parse_errors: [],
|
|
43
|
+
rules_checked: null,
|
|
44
|
+
quality_signal: null,
|
|
45
|
+
check_pass: true,
|
|
46
|
+
violations: [],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
const rulesM = line.match(RULES_CHECKED);
|
|
51
|
+
if (rulesM) {
|
|
52
|
+
result.rules_checked = Number.parseInt(rulesM[1], 10);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const qM = line.match(QUALITY_LINE);
|
|
56
|
+
if (qM) {
|
|
57
|
+
result.quality_signal = Number.parseInt(qM[1], 10);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (line.includes("violation(s) found")) {
|
|
61
|
+
result.check_pass = false;
|
|
62
|
+
}
|
|
63
|
+
if (line.includes("All rules pass")) {
|
|
64
|
+
result.check_pass = true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const rawViolations = [];
|
|
69
|
+
let current = null;
|
|
70
|
+
for (const line of lines) {
|
|
71
|
+
const head = line.match(VIOLATION_HEAD);
|
|
72
|
+
if (head) {
|
|
73
|
+
if (current) rawViolations.push(current);
|
|
74
|
+
current = {
|
|
75
|
+
severity: head[2].toLowerCase(),
|
|
76
|
+
rule: head[3],
|
|
77
|
+
message: head[4].trim(),
|
|
78
|
+
files: [],
|
|
79
|
+
};
|
|
80
|
+
if (head[1] === "✗") result.check_pass = false;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (current && /^\s{4}\S/.test(line)) {
|
|
84
|
+
current.files.push(line.trim());
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (current) rawViolations.push(current);
|
|
88
|
+
|
|
89
|
+
result.violations = dedupeViolations(rawViolations);
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const BOUNDARY_PAIR = new Set(["layer_direction", "boundary"]);
|
|
94
|
+
|
|
95
|
+
function filesKey(v) {
|
|
96
|
+
return [...v.files].sort().join("|");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function violationKey(v) {
|
|
100
|
+
return `${v.rule}::${filesKey(v)}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function dedupeViolations(raw) {
|
|
104
|
+
const byKey = new Map();
|
|
105
|
+
for (const v of raw) {
|
|
106
|
+
let key = violationKey(v);
|
|
107
|
+
if (BOUNDARY_PAIR.has(v.rule)) {
|
|
108
|
+
const fk = filesKey(v);
|
|
109
|
+
for (const [k, existing] of byKey) {
|
|
110
|
+
if (
|
|
111
|
+
BOUNDARY_PAIR.has(existing.rule) &&
|
|
112
|
+
filesKey(existing) === fk
|
|
113
|
+
) {
|
|
114
|
+
key = k;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const existing = byKey.get(key);
|
|
120
|
+
if (!existing) {
|
|
121
|
+
byKey.set(key, { ...v, related_rules: [] });
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (existing.rule !== v.rule && !existing.related_rules.includes(v.rule)) {
|
|
125
|
+
existing.related_rules.push(v.rule);
|
|
126
|
+
}
|
|
127
|
+
if (v.severity === "error") existing.severity = "error";
|
|
128
|
+
}
|
|
129
|
+
return [...byKey.values()];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* @param {string} text — filtered gate stdout
|
|
134
|
+
*/
|
|
135
|
+
export function parseGateOutput(text) {
|
|
136
|
+
const lines = filterSentruxOutputLines(text);
|
|
137
|
+
const result = {
|
|
138
|
+
parse_ok: true,
|
|
139
|
+
parse_errors: [],
|
|
140
|
+
status: "pass",
|
|
141
|
+
quality_before: null,
|
|
142
|
+
quality_after: null,
|
|
143
|
+
metrics: [],
|
|
144
|
+
degraded_reasons: [],
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
for (const line of lines) {
|
|
148
|
+
const qM = line.match(GATE_QUALITY);
|
|
149
|
+
if (qM) {
|
|
150
|
+
result.quality_before = Number.parseInt(qM[1], 10);
|
|
151
|
+
result.quality_after = Number.parseInt(qM[2], 10);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const mM = line.match(GATE_METRIC);
|
|
155
|
+
if (mM) {
|
|
156
|
+
result.metrics.push({
|
|
157
|
+
name: mM[1].trim().toLowerCase().replace(/\s+/g, "_"),
|
|
158
|
+
before: mM[2].trim(),
|
|
159
|
+
after: mM[3].trim(),
|
|
160
|
+
});
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (GATE_DEGRADED_LINE.test(line)) {
|
|
164
|
+
result.status = "degraded";
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (GATE_PASS_LINE.test(line)) {
|
|
168
|
+
result.status = "pass";
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const vM = line.match(GATE_VIOLATION);
|
|
172
|
+
if (vM) {
|
|
173
|
+
result.degraded_reasons.push(vM[1].trim());
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (
|
|
178
|
+
result.quality_before != null &&
|
|
179
|
+
result.quality_after != null &&
|
|
180
|
+
result.quality_after < result.quality_before - 200
|
|
181
|
+
) {
|
|
182
|
+
if (!result.degraded_reasons.some((r) => /quality/i.test(r))) {
|
|
183
|
+
result.degraded_reasons.push(
|
|
184
|
+
`Quality signal dropped: ${result.quality_before} -> ${result.quality_after}`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Map violation rules → inferred bottleneck root-cause bucket. */
|
|
193
|
+
export function inferBottleneck(violations, gate) {
|
|
194
|
+
const rules = new Set(violations.map((v) => v.rule));
|
|
195
|
+
if (rules.has("boundary") || rules.has("layer_direction")) {
|
|
196
|
+
return { bottleneck: "modularity", bottleneck_inferred: true };
|
|
197
|
+
}
|
|
198
|
+
if (rules.has("max_cc") || rules.has("max_fn_lines")) {
|
|
199
|
+
return { bottleneck: "equality", bottleneck_inferred: true };
|
|
200
|
+
}
|
|
201
|
+
if (rules.has("max_cycles")) {
|
|
202
|
+
return { bottleneck: "acyclicity", bottleneck_inferred: true };
|
|
203
|
+
}
|
|
204
|
+
if (gate?.degraded_reasons?.some((r) => /coupling/i.test(r))) {
|
|
205
|
+
return { bottleneck: "modularity", bottleneck_inferred: true };
|
|
206
|
+
}
|
|
207
|
+
if (gate?.degraded_reasons?.some((r) => /complex function/i.test(r))) {
|
|
208
|
+
return { bottleneck: "equality", bottleneck_inferred: true };
|
|
209
|
+
}
|
|
210
|
+
return { bottleneck: "modularity", bottleneck_inferred: true };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Parse `path:func (cc=N)` from max_cc violation file lines. */
|
|
214
|
+
export function parseComplexFunctionEntries(violations) {
|
|
215
|
+
const out = [];
|
|
216
|
+
for (const v of violations) {
|
|
217
|
+
if (v.rule !== "max_cc") continue;
|
|
218
|
+
for (const f of v.files) {
|
|
219
|
+
const m = f.match(/^(.+?):([^(\s]+)\s*\(cc=(\d+)\)\s*$/);
|
|
220
|
+
if (m) {
|
|
221
|
+
out.push({
|
|
222
|
+
file: m[1],
|
|
223
|
+
func: m[2],
|
|
224
|
+
cc: Number.parseInt(m[3], 10),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return out;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Parse god file lines from no_god_files violations. */
|
|
233
|
+
export function parseGodFileEntries(violations) {
|
|
234
|
+
const out = [];
|
|
235
|
+
for (const v of violations) {
|
|
236
|
+
if (v.rule !== "no_god_files") continue;
|
|
237
|
+
for (const f of v.files) {
|
|
238
|
+
const m = f.match(/^(.+?)\s*\(fan-out=(\d+)\)/);
|
|
239
|
+
if (m) {
|
|
240
|
+
out.push({ path: m[1], fan_out: Number.parseInt(m[2], 10) });
|
|
241
|
+
} else {
|
|
242
|
+
out.push({ path: f, fan_out: null });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return out;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Try upstream `sentrux check --format json` payload (future / optional).
|
|
251
|
+
* @param {unknown} json
|
|
252
|
+
*/
|
|
253
|
+
export function normalizeUpstreamCheckJson(json) {
|
|
254
|
+
if (!json || typeof json !== "object") return null;
|
|
255
|
+
const doc = /** @type {Record<string, unknown>} */ (json);
|
|
256
|
+
if (doc.format !== "json" && !doc.violations && !doc.diagnostics) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
source: "upstream",
|
|
261
|
+
check_pass: doc.check_pass !== false && !(doc.violation_count > 0),
|
|
262
|
+
quality_signal:
|
|
263
|
+
typeof doc.quality_signal === "number" ? doc.quality_signal : null,
|
|
264
|
+
rules_checked:
|
|
265
|
+
typeof doc.rules_checked === "number" ? doc.rules_checked : null,
|
|
266
|
+
violations: Array.isArray(doc.violations) ? doc.violations : [],
|
|
267
|
+
bottleneck: typeof doc.bottleneck === "string" ? doc.bottleneck : null,
|
|
268
|
+
root_causes: doc.root_causes ?? null,
|
|
269
|
+
diagnostics: doc.diagnostics ?? null,
|
|
270
|
+
bottleneck_inferred: false,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve harness project root for Sentrux (.sentrux/rules.toml or architecture manifest).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { access } from "node:fs/promises";
|
|
6
|
+
import { constants } from "node:fs";
|
|
7
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
8
|
+
|
|
9
|
+
export const SENTRUX_ROOT_MARKERS = [
|
|
10
|
+
join(".sentrux", "rules.toml"),
|
|
11
|
+
join(".pi", "harness", "sentrux", "architecture.manifest.json"),
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export async function fileExists(path) {
|
|
15
|
+
try {
|
|
16
|
+
await access(path, constants.R_OK);
|
|
17
|
+
return true;
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function hasSentruxRootMarker(dir) {
|
|
24
|
+
for (const marker of SENTRUX_ROOT_MARKERS) {
|
|
25
|
+
if (await fileExists(join(dir, marker))) return true;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function findSentruxProjectRoot(startDir) {
|
|
31
|
+
let dir = resolve(startDir || process.cwd());
|
|
32
|
+
while (true) {
|
|
33
|
+
if (await hasSentruxRootMarker(dir)) return dir;
|
|
34
|
+
const parent = dirname(dir);
|
|
35
|
+
if (parent === dir) return null;
|
|
36
|
+
dir = parent;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function takeRootArg(args) {
|
|
41
|
+
const next = [];
|
|
42
|
+
let explicitRoot = process.env.HARNESS_PROJECT_ROOT || "";
|
|
43
|
+
for (let i = 0; i < args.length; i++) {
|
|
44
|
+
const arg = args[i];
|
|
45
|
+
if (arg === "--root") {
|
|
46
|
+
explicitRoot = args[i + 1] || "";
|
|
47
|
+
i++;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (arg.startsWith("--root=")) {
|
|
51
|
+
explicitRoot = arg.slice("--root=".length);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
next.push(arg);
|
|
55
|
+
}
|
|
56
|
+
return { args: next, explicitRoot };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function resolveSentruxProjectRoot(explicitRoot) {
|
|
60
|
+
if (explicitRoot) {
|
|
61
|
+
const root = isAbsolute(explicitRoot)
|
|
62
|
+
? resolve(explicitRoot)
|
|
63
|
+
: resolve(process.cwd(), explicitRoot);
|
|
64
|
+
if (!(await hasSentruxRootMarker(root))) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`${root} has no .sentrux/rules.toml or .pi/harness/sentrux/architecture.manifest.json`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return root;
|
|
70
|
+
}
|
|
71
|
+
const root = await findSentruxProjectRoot(process.cwd());
|
|
72
|
+
if (!root) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
"could not find a harness project root above the current directory",
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return root;
|
|
78
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared slash-command argument completions for harness extension commands.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import type { AutocompleteItem } from "@earendil-works/pi-tui";
|
|
8
|
+
import {
|
|
9
|
+
harnessRunsRoot,
|
|
10
|
+
RUN_CONTEXT_BASENAME,
|
|
11
|
+
} from "./harness-run-context.js";
|
|
12
|
+
|
|
13
|
+
const MAX_RUN_SUGGESTIONS = 20;
|
|
14
|
+
|
|
15
|
+
export function filterPrefix(
|
|
16
|
+
items: AutocompleteItem[],
|
|
17
|
+
prefix: string,
|
|
18
|
+
): AutocompleteItem[] | null {
|
|
19
|
+
const p = prefix.trim();
|
|
20
|
+
if (!p) return items.length > 0 ? items : null;
|
|
21
|
+
const filtered = items.filter(
|
|
22
|
+
(item) =>
|
|
23
|
+
item.value.startsWith(p) ||
|
|
24
|
+
(item.label !== undefined && item.label.startsWith(p)),
|
|
25
|
+
);
|
|
26
|
+
return filtered.length > 0 ? filtered : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function completeFlags(
|
|
30
|
+
prefix: string,
|
|
31
|
+
flags: string[],
|
|
32
|
+
): AutocompleteItem[] | null {
|
|
33
|
+
const items = flags.map((flag) => ({ value: flag, label: flag }));
|
|
34
|
+
return filterPrefix(items, prefix);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function completeEnum(
|
|
38
|
+
prefix: string,
|
|
39
|
+
values: string[],
|
|
40
|
+
): AutocompleteItem[] | null {
|
|
41
|
+
return completeFlags(prefix, values);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function completeRunIds(
|
|
45
|
+
prefix: string,
|
|
46
|
+
projectRoot: string,
|
|
47
|
+
): Promise<AutocompleteItem[] | null> {
|
|
48
|
+
const runsDir = harnessRunsRoot(projectRoot);
|
|
49
|
+
let names: string[];
|
|
50
|
+
try {
|
|
51
|
+
names = await readdir(runsDir);
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const entries: { id: string; mtime: number; phase?: string }[] = [];
|
|
57
|
+
for (const name of names) {
|
|
58
|
+
if (name.startsWith(".")) continue;
|
|
59
|
+
const dir = join(runsDir, name);
|
|
60
|
+
try {
|
|
61
|
+
const st = await stat(dir);
|
|
62
|
+
if (!st.isDirectory()) continue;
|
|
63
|
+
let phase: string | undefined;
|
|
64
|
+
try {
|
|
65
|
+
const raw = await readFile(join(dir, RUN_CONTEXT_BASENAME), "utf-8");
|
|
66
|
+
const match = raw.match(/^phase:\s*(\S+)/m);
|
|
67
|
+
if (match) phase = match[1];
|
|
68
|
+
} catch {
|
|
69
|
+
/* optional */
|
|
70
|
+
}
|
|
71
|
+
entries.push({ id: name, mtime: st.mtimeMs, phase });
|
|
72
|
+
} catch {}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
entries.sort((a, b) => b.mtime - a.mtime);
|
|
76
|
+
const items: AutocompleteItem[] = entries
|
|
77
|
+
.slice(0, MAX_RUN_SUGGESTIONS)
|
|
78
|
+
.map((entry) => ({
|
|
79
|
+
value: entry.id,
|
|
80
|
+
label: entry.id,
|
|
81
|
+
description: entry.phase ? `phase ${entry.phase}` : undefined,
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
return filterPrefix(items, prefix);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function completeHarnessUseRun(
|
|
88
|
+
prefix: string,
|
|
89
|
+
projectRoot: string,
|
|
90
|
+
): Promise<AutocompleteItem[] | null> {
|
|
91
|
+
const p = prefix.trim();
|
|
92
|
+
if (p.startsWith("-")) {
|
|
93
|
+
return completeFlags(p, ["--claim", "--readonly"]);
|
|
94
|
+
}
|
|
95
|
+
return completeRunIds(p, projectRoot);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function completeStrictFlag(prefix: string): AutocompleteItem[] | null {
|
|
99
|
+
return completeFlags(prefix, ["--strict"]);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function completeDebateOpen(
|
|
103
|
+
prefix: string,
|
|
104
|
+
projectRoot: string,
|
|
105
|
+
): Promise<AutocompleteItem[] | null> {
|
|
106
|
+
const runs = await completeRunIds("", projectRoot);
|
|
107
|
+
if (!runs?.length) {
|
|
108
|
+
return filterPrefix([{ value: "plan-", label: "plan-<run-id>" }], prefix);
|
|
109
|
+
}
|
|
110
|
+
const items = runs.map((run) => ({
|
|
111
|
+
value: `plan-${run.value}`,
|
|
112
|
+
label: `plan-${run.value}`,
|
|
113
|
+
description: run.description,
|
|
114
|
+
}));
|
|
115
|
+
return filterPrefix(items, prefix);
|
|
116
|
+
}
|