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.
- 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 +1 -1
- 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 +7 -9
- 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 +49 -82
- 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 +5 -0
- package/.pi/harness/docs/practice-map.md +10 -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 -51
- package/.pi/lib/agents-policy.mjs +41 -28
- 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/package.json +3 -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/lsp/client.ts +62 -39
- package/.pi/lib/harness-lens/clients/tool-policy.ts +73 -181
- package/.pi/lib/harness-lens/index.ts +241 -108
- 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 +4 -1
- 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 +2 -1
- 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-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 +288 -125
- package/.pi/scripts/ls-lint-rules-sync.mjs +265 -0
- package/.pi/scripts/run-tests.mjs +1 -0
- package/.pi/settings.example.json +1 -0
- package/.sentrux/rules.toml +1 -1
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +25 -0
- package/README.md +13 -4
- package/package.json +5 -1
- package/vendor/pi-vcc/src/hooks/before-compact.ts +86 -60
|
@@ -723,6 +723,15 @@ function buildNavigationOutput(args: {
|
|
|
723
723
|
return { output, actionStats, isEmpty, resultCount };
|
|
724
724
|
}
|
|
725
725
|
|
|
726
|
+
function isDirectoryPath(filePath: string): boolean {
|
|
727
|
+
if (!filePath) return false;
|
|
728
|
+
try {
|
|
729
|
+
return nodeFs.statSync(filePath).isDirectory();
|
|
730
|
+
} catch {
|
|
731
|
+
return false;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
726
735
|
export function createLspNavigationTool(
|
|
727
736
|
getFlag: (name: string) => boolean | string | undefined,
|
|
728
737
|
) {
|
|
@@ -1021,14 +1030,7 @@ export function createLspNavigationTool(
|
|
|
1021
1030
|
: path.resolve(ctx.cwd || ".", rawPath)
|
|
1022
1031
|
: "";
|
|
1023
1032
|
|
|
1024
|
-
|
|
1025
|
-
if (filePath) {
|
|
1026
|
-
try {
|
|
1027
|
-
filePathIsDirectory = nodeFs.statSync(filePath).isDirectory();
|
|
1028
|
-
} catch {
|
|
1029
|
-
// non-existent path — existing error paths handle this
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1033
|
+
const filePathIsDirectory = isDirectoryPath(filePath);
|
|
1032
1034
|
|
|
1033
1035
|
const lspService = getLSPService();
|
|
1034
1036
|
if (operation === "workspaceDiagnostics") {
|
|
@@ -64,27 +64,23 @@ export async function synthesizeRepairBrief(
|
|
|
64
64
|
const planRel =
|
|
65
65
|
input.planPacketPath?.replace(/\\/g, "/") ?? "plan-packet.yaml";
|
|
66
66
|
const plan = await readArtifactYaml(runRoot, planRel, "plan-packet");
|
|
67
|
+
const sentruxRepair = await readArtifactYaml(
|
|
68
|
+
runRoot,
|
|
69
|
+
"artifacts/sentrux-repair-plan.yaml",
|
|
70
|
+
"sentrux-repair-plan",
|
|
71
|
+
);
|
|
67
72
|
|
|
68
73
|
const remediation =
|
|
69
74
|
(typeof review?.remediation_class === "string" &&
|
|
70
75
|
review.remediation_class) ||
|
|
71
76
|
"implementation_gap";
|
|
72
77
|
|
|
73
|
-
const sourceArtifacts
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
input.evalVerdictPath ?? "artifacts/eval-verdict.yaml";
|
|
80
|
-
}
|
|
81
|
-
if (adversary) {
|
|
82
|
-
sourceArtifacts["adversary-report"] =
|
|
83
|
-
input.adversaryReportPath ?? "artifacts/adversary-report.yaml";
|
|
84
|
-
}
|
|
85
|
-
if (plan) {
|
|
86
|
-
sourceArtifacts["plan-packet"] = planRel;
|
|
87
|
-
}
|
|
78
|
+
const sourceArtifacts = buildSourceArtifacts(input, planRel, {
|
|
79
|
+
evalDoc,
|
|
80
|
+
adversary,
|
|
81
|
+
plan,
|
|
82
|
+
sentruxRepair,
|
|
83
|
+
});
|
|
88
84
|
|
|
89
85
|
const failedIds = [
|
|
90
86
|
...stringList(review?.failed_acceptance_check_ids),
|
|
@@ -93,7 +89,7 @@ export async function synthesizeRepairBrief(
|
|
|
93
89
|
];
|
|
94
90
|
const uniqueFailed = [...new Set(failedIds)];
|
|
95
91
|
|
|
96
|
-
const fixDirectives: string[] =
|
|
92
|
+
const fixDirectives: string[] = sentruxFixDirectives(sentruxRepair);
|
|
97
93
|
for (const key of [
|
|
98
94
|
"fix_directives",
|
|
99
95
|
"repair_directives",
|
|
@@ -117,15 +113,7 @@ export async function synthesizeRepairBrief(
|
|
|
117
113
|
);
|
|
118
114
|
}
|
|
119
115
|
|
|
120
|
-
const
|
|
121
|
-
const priorityLakeIds = stringList(execPlan?.critical_path_lake_ids);
|
|
122
|
-
if (priorityLakeIds.length === 0) {
|
|
123
|
-
const lakes = Array.isArray(execPlan?.lakes) ? execPlan.lakes : [];
|
|
124
|
-
for (const lake of lakes) {
|
|
125
|
-
const l = asRecord(lake);
|
|
126
|
-
if (l && typeof l.id === "string") priorityLakeIds.push(l.id);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
116
|
+
const priorityLakeIds = collectPriorityLakeIds(plan);
|
|
129
117
|
|
|
130
118
|
const brief: Record<string, unknown> = {
|
|
131
119
|
schema_version: REPAIR_BRIEF_SCHEMA,
|
|
@@ -143,3 +131,74 @@ export async function synthesizeRepairBrief(
|
|
|
143
131
|
}
|
|
144
132
|
return brief;
|
|
145
133
|
}
|
|
134
|
+
|
|
135
|
+
function buildSourceArtifacts(
|
|
136
|
+
input: SynthesizeRepairBriefInput,
|
|
137
|
+
planRel: string,
|
|
138
|
+
docs: {
|
|
139
|
+
evalDoc: Record<string, unknown> | null;
|
|
140
|
+
adversary: Record<string, unknown> | null;
|
|
141
|
+
plan: Record<string, unknown> | null;
|
|
142
|
+
sentruxRepair: Record<string, unknown> | null;
|
|
143
|
+
},
|
|
144
|
+
): Record<string, string> {
|
|
145
|
+
const sourceArtifacts: Record<string, string> = {
|
|
146
|
+
"review-outcome":
|
|
147
|
+
input.reviewOutcomePath ?? "artifacts/review-outcome.yaml",
|
|
148
|
+
};
|
|
149
|
+
if (docs.evalDoc)
|
|
150
|
+
sourceArtifacts["eval-verdict"] =
|
|
151
|
+
input.evalVerdictPath ?? "artifacts/eval-verdict.yaml";
|
|
152
|
+
if (docs.adversary)
|
|
153
|
+
sourceArtifacts["adversary-report"] =
|
|
154
|
+
input.adversaryReportPath ?? "artifacts/adversary-report.yaml";
|
|
155
|
+
if (docs.plan) sourceArtifacts["plan-packet"] = planRel;
|
|
156
|
+
if (docs.sentruxRepair)
|
|
157
|
+
sourceArtifacts["sentrux-repair-plan"] =
|
|
158
|
+
"artifacts/sentrux-repair-plan.yaml";
|
|
159
|
+
return sourceArtifacts;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function sentruxFixDirectives(
|
|
163
|
+
sentruxRepair: Record<string, unknown> | null,
|
|
164
|
+
): string[] {
|
|
165
|
+
if (!sentruxRepair) return [];
|
|
166
|
+
const out: string[] = [];
|
|
167
|
+
const actions = Array.isArray(sentruxRepair.actions)
|
|
168
|
+
? sentruxRepair.actions
|
|
169
|
+
: [];
|
|
170
|
+
for (const raw of actions) {
|
|
171
|
+
const action = asRecord(raw);
|
|
172
|
+
if (!action) continue;
|
|
173
|
+
const id = typeof action.id === "string" ? action.id : "action";
|
|
174
|
+
const target = typeof action.target === "string" ? action.target : "";
|
|
175
|
+
const instruction =
|
|
176
|
+
typeof action.instruction === "string" ? action.instruction : "";
|
|
177
|
+
if (instruction)
|
|
178
|
+
out.push(`[sentrux:${id}] ${target}: ${instruction}`.trim());
|
|
179
|
+
}
|
|
180
|
+
if (
|
|
181
|
+
typeof sentruxRepair.summary === "string" &&
|
|
182
|
+
sentruxRepair.summary.trim()
|
|
183
|
+
) {
|
|
184
|
+
out.unshift(`[sentrux] ${sentruxRepair.summary.trim()}`);
|
|
185
|
+
}
|
|
186
|
+
for (const v of stringList(sentruxRepair.verification)) {
|
|
187
|
+
out.push(`[sentrux:verify] ${v}`);
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function collectPriorityLakeIds(
|
|
193
|
+
plan: Record<string, unknown> | null,
|
|
194
|
+
): string[] {
|
|
195
|
+
const execPlan = asRecord(plan?.execution_plan);
|
|
196
|
+
const ids = stringList(execPlan?.critical_path_lake_ids);
|
|
197
|
+
if (ids.length > 0) return ids;
|
|
198
|
+
const lakes = Array.isArray(execPlan?.lakes) ? execPlan.lakes : [];
|
|
199
|
+
for (const lake of lakes) {
|
|
200
|
+
const record = asRecord(lake);
|
|
201
|
+
if (record && typeof record.id === "string") ids.push(record.id);
|
|
202
|
+
}
|
|
203
|
+
return ids;
|
|
204
|
+
}
|
|
@@ -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
|
+
}
|