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.
Files changed (147) 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 +43 -2
  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 +139 -0
  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 +47 -81
  34. package/.pi/harness/docs/adrs/0051-hash-anchored-executor-edits.md +41 -0
  35. package/.pi/harness/docs/adrs/0052-ls-lint-naming-lifecycle.md +45 -0
  36. package/.pi/harness/docs/adrs/0052-sentrux-structured-repair.md +38 -0
  37. package/.pi/harness/docs/adrs/0053-plan-task-clarification-gate.md +39 -0
  38. package/.pi/harness/docs/adrs/0054-harness-native-ask-user.md +40 -0
  39. package/.pi/harness/docs/adrs/0055-auto-commit-coauthor-lifecycle.md +40 -0
  40. package/.pi/harness/docs/adrs/README.md +7 -0
  41. package/.pi/harness/docs/practice-map.md +21 -5
  42. package/.pi/harness/evals/smoke/ls-lint-stub.json +10 -0
  43. package/.pi/harness/evolution/self-healing-rules.json +16 -0
  44. package/.pi/harness/ls-lint/naming.manifest.json +128 -0
  45. package/.pi/harness/sentrux/architecture.manifest.json +1 -1
  46. package/.pi/harness/specs/auto-commit.schema.json +63 -0
  47. package/.pi/harness/specs/ls-lint-manifest-proposal.schema.json +80 -0
  48. package/.pi/harness/specs/ls-lint-signal.schema.json +47 -0
  49. package/.pi/harness/specs/naming-manifest.schema.json +54 -0
  50. package/.pi/harness/specs/plan-task-clarification.schema.json +88 -0
  51. package/.pi/harness/specs/sentrux-diagnostics.schema.json +173 -0
  52. package/.pi/harness/specs/sentrux-repair-plan.schema.json +133 -0
  53. package/.pi/harness/specs/sentrux-report.schema.json +119 -0
  54. package/.pi/harness/specs/sentrux-signal.schema.json +34 -1
  55. package/.pi/lib/agents-policy.d.mts +26 -47
  56. package/.pi/lib/agents-policy.mjs +84 -29
  57. package/.pi/lib/agents-policy.ts +1 -0
  58. package/.pi/lib/agt/build-evaluation-context.ts +136 -64
  59. package/.pi/lib/ask-user/constants.mjs +3 -0
  60. package/.pi/lib/ask-user/constants.ts +4 -0
  61. package/.pi/lib/ask-user/contracts/glimpse-parse.ts +56 -0
  62. package/.pi/lib/ask-user/contracts/glimpse-payload-build.ts +58 -0
  63. package/.pi/lib/ask-user/contracts/glimpse-payload.ts +38 -0
  64. package/.pi/lib/ask-user/core/questionnaire.ts +74 -0
  65. package/.pi/lib/ask-user/dialog.ts +2 -314
  66. package/.pi/lib/ask-user/fallback.ts +2 -78
  67. package/.pi/lib/ask-user/format.ts +85 -0
  68. package/.pi/lib/ask-user/glimpseui.d.ts +10 -0
  69. package/.pi/lib/ask-user/index.ts +114 -0
  70. package/.pi/lib/ask-user/merge-task-clarification.ts +98 -0
  71. package/.pi/lib/ask-user/policy.mjs +43 -0
  72. package/.pi/lib/ask-user/policy.ts +104 -0
  73. package/.pi/lib/ask-user/presenters/glimpse.ts +130 -0
  74. package/.pi/lib/ask-user/presenters/headless.ts +131 -0
  75. package/.pi/lib/ask-user/presenters/select.ts +60 -0
  76. package/.pi/lib/ask-user/presenters/tui.ts +373 -0
  77. package/.pi/lib/ask-user/presenters/types.ts +13 -0
  78. package/.pi/lib/ask-user/render.ts +40 -9
  79. package/.pi/lib/ask-user/schema.ts +66 -13
  80. package/.pi/lib/ask-user/types.ts +60 -3
  81. package/.pi/lib/ask-user/validate-core.mjs +193 -7
  82. package/.pi/lib/ask-user/validate.ts +53 -34
  83. package/.pi/lib/harness-anchored-edit/.hash_anchors +1721 -0
  84. package/.pi/lib/harness-anchored-edit/anchor-state.ts +320 -0
  85. package/.pi/lib/harness-anchored-edit/apply-anchored-edits.ts +161 -0
  86. package/.pi/lib/harness-anchored-edit/edit-executor.ts +146 -0
  87. package/.pi/lib/harness-anchored-edit/index.ts +9 -0
  88. package/.pi/lib/harness-anchored-edit/line-protocol.ts +38 -0
  89. package/.pi/lib/harness-anchored-edit/package.json +3 -0
  90. package/.pi/lib/harness-anchored-edit/settings.ts +1 -0
  91. package/.pi/lib/harness-anchored-edit/task-id.ts +8 -0
  92. package/.pi/lib/harness-anchored-edit/types.ts +19 -0
  93. package/.pi/lib/harness-artifact-gate.ts +75 -21
  94. package/.pi/lib/harness-auto-commit-config.mjs +321 -0
  95. package/.pi/lib/harness-lens/clients/anchored-edit-autopatch.ts +158 -0
  96. package/.pi/lib/harness-lens/clients/lsp/client.ts +62 -39
  97. package/.pi/lib/harness-lens/clients/tool-policy.ts +73 -181
  98. package/.pi/lib/harness-lens/index.ts +246 -96
  99. package/.pi/lib/harness-lens/tools/lsp-navigation.ts +10 -8
  100. package/.pi/lib/harness-repair-brief.ts +84 -25
  101. package/.pi/lib/harness-run-context.ts +42 -52
  102. package/.pi/lib/harness-sentrux-parse.mjs +272 -0
  103. package/.pi/lib/harness-sentrux-root.mjs +78 -0
  104. package/.pi/lib/harness-slash-completions.ts +116 -0
  105. package/.pi/lib/harness-spawn-topology.ts +121 -87
  106. package/.pi/lib/harness-subagent-submit-registry.ts +10 -0
  107. package/.pi/lib/harness-subagents-bridge.ts +11 -6
  108. package/.pi/lib/harness-ui-state.ts +95 -48
  109. package/.pi/lib/plan-approval/dialog.ts +5 -0
  110. package/.pi/lib/plan-approval/validate.ts +1 -1
  111. package/.pi/lib/plan-approval-readiness.ts +32 -0
  112. package/.pi/lib/plan-debate-gate.ts +154 -114
  113. package/.pi/lib/plan-task-clarification.ts +158 -0
  114. package/.pi/prompts/harness-auto.md +2 -2
  115. package/.pi/prompts/harness-ls-lint-steward.md +43 -0
  116. package/.pi/prompts/harness-plan.md +58 -8
  117. package/.pi/prompts/harness-review.md +40 -6
  118. package/.pi/prompts/harness-run.md +33 -11
  119. package/.pi/prompts/harness-setup.md +72 -3
  120. package/.pi/prompts/harness-steer.md +3 -2
  121. package/.pi/prompts/wiki-save.md +5 -4
  122. package/.pi/scripts/README.md +8 -0
  123. package/.pi/scripts/generate-agents-policy-yaml.mjs +14 -2
  124. package/.pi/scripts/harness-anchored-edit-smoke.mjs +45 -0
  125. package/.pi/scripts/harness-auto-commit-bootstrap.mjs +96 -0
  126. package/.pi/scripts/harness-cli-verify.sh +47 -0
  127. package/.pi/scripts/harness-git-churn.mjs +77 -0
  128. package/.pi/scripts/harness-git-commit.mjs +173 -0
  129. package/.pi/scripts/harness-ls-lint-bootstrap.mjs +142 -0
  130. package/.pi/scripts/harness-ls-lint-cli.mjs +184 -0
  131. package/.pi/scripts/harness-seed-project-contracts.mjs +47 -0
  132. package/.pi/scripts/harness-sentrux-diagnostics.mjs +230 -0
  133. package/.pi/scripts/harness-sentrux-report.mjs +256 -0
  134. package/.pi/scripts/harness-verify.mjs +347 -117
  135. package/.pi/scripts/ls-lint-rules-sync.mjs +265 -0
  136. package/.pi/scripts/run-tests.mjs +65 -0
  137. package/.pi/settings.example.json +1 -0
  138. package/.sentrux/rules.toml +1 -1
  139. package/AGENTS.md +1 -0
  140. package/CHANGELOG.md +31 -0
  141. package/README.md +13 -4
  142. package/THIRD_PARTY_NOTICES.md +7 -0
  143. package/package.json +8 -3
  144. package/vendor/pi-subagents/src/agents.ts +5 -0
  145. package/vendor/pi-subagents/src/subagents.ts +22 -3
  146. package/vendor/pi-vcc/src/hooks/before-compact.ts +86 -60
  147. 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
- if (remediation === "pass" || evalSt === "pass") {
1734
- if (input.adversaryComplete) return "/harness-policy-status";
1735
- return "/harness-review";
1736
- }
1737
- if (remediation === "rollback") {
1738
- return "/harness-incident";
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
+ }