ultimate-pi 0.19.0 → 0.20.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/web-retrieval/SKILL.md +163 -0
- package/.agents/skills/wiki-autoresearch/SKILL.md +6 -6
- package/.pi/SYSTEM.md +30 -12
- package/.pi/agents/harness/planning/implementation-researcher.md +1 -1
- package/.pi/agents/harness/planning/stack-researcher.md +5 -1
- package/.pi/agents/harness/running/executor.md +42 -1
- package/.pi/agents/harness/web-retrieval/web-answerer.md +35 -0
- package/.pi/agents/harness/web-retrieval/web-criteria-verifier.md +28 -0
- package/.pi/agents/harness/web-retrieval/web-gap-analyzer.md +31 -0
- package/.pi/agents/harness/web-retrieval/web-query-expander-fast.md +34 -0
- package/.pi/agents/harness/web-retrieval/web-query-expander.md +60 -0
- package/.pi/agents/harness/web-retrieval/web-summarizer.md +18 -0
- package/.pi/extensions/harness-anchored-edit.ts +141 -0
- package/.pi/extensions/harness-web-guard.ts +2 -1
- package/.pi/extensions/harness-web-tools.ts +689 -51
- package/.pi/harness/agents.manifest.json +30 -6
- package/.pi/harness/agents.policy.yaml +37 -4
- package/.pi/harness/docs/adrs/0050-agentic-web-retrieval-stack.md +46 -0
- package/.pi/harness/docs/adrs/0051-hash-anchored-executor-edits.md +41 -0
- package/.pi/harness/docs/adrs/README.md +2 -0
- package/.pi/harness/docs/harness-web-search.md +97 -0
- package/.pi/harness/docs/practice-map.md +11 -0
- package/.pi/harness/env.harness.template +9 -1
- package/.pi/harness/examples/web-heuristic-angles.project.yaml +22 -0
- package/.pi/harness/web-heuristic-angles.json +278 -0
- package/.pi/harness/web-heuristic-angles.yaml +182 -0
- package/.pi/lib/agents-policy.d.mts +4 -0
- package/.pi/lib/agents-policy.mjs +49 -1
- package/.pi/lib/agents-policy.ts +1 -0
- 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/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-lens/clients/anchored-edit-autopatch.ts +158 -0
- package/.pi/lib/harness-lens/index.ts +24 -7
- package/.pi/lib/harness-subagent-auth.ts +39 -9
- package/.pi/lib/harness-subagents-bridge.ts +24 -1
- package/.pi/lib/harness-web/artifacts.ts +200 -0
- package/.pi/lib/harness-web/cache.ts +369 -0
- package/.pi/lib/harness-web/run-cli.ts +42 -2
- package/.pi/prompts/harness-plan.md +1 -0
- package/.pi/prompts/harness-setup.md +3 -1
- package/.pi/prompts/harness-steer.md +1 -1
- package/.pi/scripts/gen-web-heuristic-angles-json.mjs +24 -0
- package/.pi/scripts/harness-anchored-edit-smoke.mjs +45 -0
- package/.pi/scripts/harness-cli-verify.sh +5 -0
- package/.pi/scripts/harness-verify.mjs +145 -0
- package/.pi/scripts/harness-web-policy-guard.mjs +1 -1
- package/.pi/scripts/harness-web.py +218 -15
- package/.pi/scripts/harness_web/deep_search.py +55 -0
- package/.pi/scripts/harness_web/evidence_bundle.py +47 -0
- package/.pi/scripts/harness_web/find_similar.py +88 -0
- package/.pi/scripts/harness_web/heuristic_angles_shipped.py +85 -0
- package/.pi/scripts/harness_web/heuristic_config.py +251 -0
- package/.pi/scripts/harness_web/highlights.py +47 -0
- package/.pi/scripts/harness_web/multi_search.py +59 -0
- package/.pi/scripts/harness_web/output.py +24 -0
- package/.pi/scripts/harness_web/query_angles.py +116 -0
- package/.pi/scripts/harness_web/rank.py +163 -0
- package/.pi/scripts/harness_web/scrape.py +30 -0
- package/.pi/scripts/run-tests.mjs +64 -0
- package/.pi/scripts/tests/test_harness_web_heuristic_config.py +132 -0
- package/.pi/scripts/tests/test_harness_web_query_angles.py +45 -0
- package/.pi/scripts/tests/test_harness_web_rank.py +56 -0
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +12 -0
- package/THIRD_PARTY_NOTICES.md +7 -0
- package/package.json +7 -4
- package/vendor/pi-subagents/src/agents.ts +5 -0
- package/vendor/pi-subagents/src/subagents.ts +22 -3
- package/.agents/skills/scrapling-web/SKILL.md +0 -98
- package/.pi/extensions/00-posthog-network-bootstrap.ts +0 -11
- package/.pi/scripts/harness_web/__pycache__/__init__.cpython-314.pyc +0 -0
- package/.pi/scripts/harness_web/__pycache__/config.cpython-314.pyc +0 -0
- package/.pi/scripts/harness_web/__pycache__/output.cpython-314.pyc +0 -0
- package/.pi/scripts/harness_web/__pycache__/scrape.cpython-314.pyc +0 -0
- package/.pi/scripts/harness_web/__pycache__/search.cpython-314.pyc +0 -0
- package/.pi/scripts/harness_web/__pycache__/search_ddg.cpython-314.pyc +0 -0
- package/.pi/scripts/harness_web/__pycache__/search_searxng.cpython-314.pyc +0 -0
- package/.pi/scripts/release.sh +0 -338
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""URL normalization and RRF fusion for multi-angle SERP results."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any
|
|
8
|
+
from urllib.parse import parse_qs, urlparse, urlunparse
|
|
9
|
+
|
|
10
|
+
RRF_K = 60
|
|
11
|
+
|
|
12
|
+
_TRACKING_PARAMS = frozenset(
|
|
13
|
+
{
|
|
14
|
+
"utm_source",
|
|
15
|
+
"utm_medium",
|
|
16
|
+
"utm_campaign",
|
|
17
|
+
"utm_term",
|
|
18
|
+
"utm_content",
|
|
19
|
+
"fbclid",
|
|
20
|
+
"gclid",
|
|
21
|
+
"mc_cid",
|
|
22
|
+
"mc_eid",
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class RankedHit:
|
|
29
|
+
url: str
|
|
30
|
+
title: str
|
|
31
|
+
description: str
|
|
32
|
+
score: float
|
|
33
|
+
angle_ids: list[str] = field(default_factory=list)
|
|
34
|
+
ranks: dict[str, int] = field(default_factory=dict)
|
|
35
|
+
|
|
36
|
+
def to_web_dict(self) -> dict[str, Any]:
|
|
37
|
+
return {
|
|
38
|
+
"url": self.url,
|
|
39
|
+
"title": self.title,
|
|
40
|
+
"description": self.description,
|
|
41
|
+
"score": round(self.score, 6),
|
|
42
|
+
"angle_ids": list(self.angle_ids),
|
|
43
|
+
"ranks": dict(self.ranks),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def normalize_url(url: str) -> str:
|
|
48
|
+
u = url.strip()
|
|
49
|
+
if not u:
|
|
50
|
+
return ""
|
|
51
|
+
parsed = urlparse(u)
|
|
52
|
+
scheme = (parsed.scheme or "https").lower()
|
|
53
|
+
host = (parsed.hostname or "").lower()
|
|
54
|
+
if not host:
|
|
55
|
+
return u
|
|
56
|
+
port = parsed.port
|
|
57
|
+
netloc = host
|
|
58
|
+
if port and not ((scheme == "http" and port == 80) or (scheme == "https" and port == 443)):
|
|
59
|
+
netloc = f"{host}:{port}"
|
|
60
|
+
path = parsed.path or "/"
|
|
61
|
+
if path != "/" and path.endswith("/"):
|
|
62
|
+
path = path.rstrip("/")
|
|
63
|
+
qs = parse_qs(parsed.query, keep_blank_values=False)
|
|
64
|
+
filtered = []
|
|
65
|
+
for key in sorted(qs.keys()):
|
|
66
|
+
if key.lower() in _TRACKING_PARAMS:
|
|
67
|
+
continue
|
|
68
|
+
for val in qs[key]:
|
|
69
|
+
filtered.append(f"{key}={val}")
|
|
70
|
+
query = "&".join(filtered)
|
|
71
|
+
return urlunparse((scheme, netloc, path, "", query, ""))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def tokenize(text: str) -> set[str]:
|
|
75
|
+
return {t for t in re.findall(r"[a-z0-9]{3,}", text.lower()) if len(t) >= 3}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def lexical_rerank(hits: list[RankedHit], intent: str) -> list[RankedHit]:
|
|
79
|
+
"""Lightweight O3 boost when HARNESS_WEB_RERANK=lexical."""
|
|
80
|
+
intent_tokens = tokenize(intent)
|
|
81
|
+
if not intent_tokens:
|
|
82
|
+
return hits
|
|
83
|
+
|
|
84
|
+
def lex_score(h: RankedHit) -> float:
|
|
85
|
+
blob = f"{h.title} {h.description}".lower()
|
|
86
|
+
tokens = tokenize(blob)
|
|
87
|
+
if not tokens:
|
|
88
|
+
return 0.0
|
|
89
|
+
overlap = len(intent_tokens & tokens) / max(len(intent_tokens), 1)
|
|
90
|
+
return overlap
|
|
91
|
+
|
|
92
|
+
scored = [(h, h.score + 0.15 * lex_score(h)) for h in hits]
|
|
93
|
+
scored.sort(key=lambda x: x[1], reverse=True)
|
|
94
|
+
out: list[RankedHit] = []
|
|
95
|
+
for h, s in scored:
|
|
96
|
+
out.append(
|
|
97
|
+
RankedHit(
|
|
98
|
+
url=h.url,
|
|
99
|
+
title=h.title,
|
|
100
|
+
description=h.description,
|
|
101
|
+
score=s,
|
|
102
|
+
angle_ids=h.angle_ids,
|
|
103
|
+
ranks=h.ranks,
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
return out
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def fuse_angle_results(
|
|
110
|
+
per_angle: dict[str, list[dict[str, str]]],
|
|
111
|
+
*,
|
|
112
|
+
final_limit: int = 10,
|
|
113
|
+
intent: str = "",
|
|
114
|
+
rerank_mode: str = "off",
|
|
115
|
+
) -> list[RankedHit]:
|
|
116
|
+
"""Reciprocal Rank Fusion across angle result lists."""
|
|
117
|
+
accum: dict[str, dict[str, Any]] = {}
|
|
118
|
+
|
|
119
|
+
for angle_id, results in per_angle.items():
|
|
120
|
+
for rank_1based, item in enumerate(results, start=1):
|
|
121
|
+
raw_url = (item.get("url") or "").strip()
|
|
122
|
+
norm = normalize_url(raw_url)
|
|
123
|
+
if not norm or not norm.startswith("http"):
|
|
124
|
+
continue
|
|
125
|
+
entry = accum.setdefault(
|
|
126
|
+
norm,
|
|
127
|
+
{
|
|
128
|
+
"url": raw_url,
|
|
129
|
+
"title": "",
|
|
130
|
+
"description": "",
|
|
131
|
+
"score": 0.0,
|
|
132
|
+
"angle_ids": [],
|
|
133
|
+
"ranks": {},
|
|
134
|
+
},
|
|
135
|
+
)
|
|
136
|
+
entry["score"] += 1.0 / (RRF_K + rank_1based)
|
|
137
|
+
if angle_id not in entry["angle_ids"]:
|
|
138
|
+
entry["angle_ids"].append(angle_id)
|
|
139
|
+
entry["ranks"][angle_id] = rank_1based
|
|
140
|
+
title = (item.get("title") or "").strip()
|
|
141
|
+
desc = (item.get("description") or "").strip()
|
|
142
|
+
if title and not entry["title"]:
|
|
143
|
+
entry["title"] = title
|
|
144
|
+
if desc and (not entry["description"] or len(desc) > len(entry["description"])):
|
|
145
|
+
entry["description"] = desc
|
|
146
|
+
|
|
147
|
+
hits = [
|
|
148
|
+
RankedHit(
|
|
149
|
+
url=e["url"],
|
|
150
|
+
title=e["title"],
|
|
151
|
+
description=e["description"],
|
|
152
|
+
score=e["score"],
|
|
153
|
+
angle_ids=e["angle_ids"],
|
|
154
|
+
ranks=e["ranks"],
|
|
155
|
+
)
|
|
156
|
+
for e in accum.values()
|
|
157
|
+
]
|
|
158
|
+
hits.sort(key=lambda h: (-h.score, -len(h.angle_ids), min(h.ranks.values()) if h.ranks else 999))
|
|
159
|
+
|
|
160
|
+
if rerank_mode == "lexical" and intent:
|
|
161
|
+
hits = lexical_rerank(hits, intent)
|
|
162
|
+
|
|
163
|
+
return hits[:final_limit]
|
|
@@ -41,6 +41,36 @@ def scrape_url(
|
|
|
41
41
|
write_page_markdown(Path(output), page, main_content_only=True)
|
|
42
42
|
|
|
43
43
|
|
|
44
|
+
def scrape_url_with_highlights(
|
|
45
|
+
url: str,
|
|
46
|
+
markdown_output: str,
|
|
47
|
+
highlights_output: str | None,
|
|
48
|
+
*,
|
|
49
|
+
config: HarnessWebConfig,
|
|
50
|
+
fast: bool,
|
|
51
|
+
wait_ms: int | None,
|
|
52
|
+
highlight_query: str,
|
|
53
|
+
) -> None:
|
|
54
|
+
import json
|
|
55
|
+
from pathlib import Path
|
|
56
|
+
|
|
57
|
+
from .highlights import extract_highlights
|
|
58
|
+
|
|
59
|
+
page = fetch_page(url, config=config, fast=fast, wait_ms=wait_ms)
|
|
60
|
+
md_path = Path(markdown_output)
|
|
61
|
+
write_page_markdown(md_path, page, main_content_only=True)
|
|
62
|
+
if highlights_output and highlight_query.strip():
|
|
63
|
+
text = md_path.read_text(encoding="utf-8")
|
|
64
|
+
spans = extract_highlights(text, highlight_query)
|
|
65
|
+
hp = Path(highlights_output)
|
|
66
|
+
hp.parent.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
hp.write_text(
|
|
68
|
+
json.dumps({"url": url, "query": highlight_query, "highlights": spans}, indent=2)
|
|
69
|
+
+ "\n",
|
|
70
|
+
encoding="utf-8",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
44
74
|
def map_url(
|
|
45
75
|
url: str,
|
|
46
76
|
output: str,
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readdirSync } from 'node:fs';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
|
|
5
|
+
const TEST_DIR = 'test';
|
|
6
|
+
|
|
7
|
+
const NODE_ONLY_TESTS = [
|
|
8
|
+
'harness-verify.test.mjs',
|
|
9
|
+
'harness-ask-user.test.mjs',
|
|
10
|
+
'harness-subagents-loader.test.mjs',
|
|
11
|
+
'harness-subagent-precheck.test.mjs',
|
|
12
|
+
'sentrux-rules-sync.test.mjs',
|
|
13
|
+
'harness-budget-guard.test.mjs',
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const DEFAULT_SUITE_EXCLUDES = new Set([
|
|
17
|
+
'graphify-kb-updater.test.mjs',
|
|
18
|
+
'harness-artifact-gate.test.mjs',
|
|
19
|
+
'harness-spawn-critical-path.test.mjs',
|
|
20
|
+
'harness-yaml.test.mjs',
|
|
21
|
+
'plan-debate-lanes.test.mjs',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
function listTests() {
|
|
25
|
+
const entries = readdirSync(TEST_DIR, { withFileTypes: true });
|
|
26
|
+
const files = entries.filter((entry) => entry.isFile()).map((entry) => entry.name);
|
|
27
|
+
const nodeOnlySet = new Set(NODE_ONLY_TESTS);
|
|
28
|
+
|
|
29
|
+
const nodeTests = NODE_ONLY_TESTS.filter((name) => files.includes(name)).map((name) => `${TEST_DIR}/${name}`);
|
|
30
|
+
const tsxTests = files
|
|
31
|
+
.filter(
|
|
32
|
+
(name) =>
|
|
33
|
+
(name.endsWith('.test.mjs') || name.endsWith('.test.ts')) &&
|
|
34
|
+
!name.endsWith('.integration.test.ts') &&
|
|
35
|
+
!nodeOnlySet.has(name) &&
|
|
36
|
+
!DEFAULT_SUITE_EXCLUDES.has(name),
|
|
37
|
+
)
|
|
38
|
+
.map((name) => `${TEST_DIR}/${name}`)
|
|
39
|
+
.sort();
|
|
40
|
+
|
|
41
|
+
return { nodeTests, tsxTests };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function run(cmd, args) {
|
|
45
|
+
const result = spawnSync(cmd, args, { stdio: 'inherit' });
|
|
46
|
+
if (result.error) {
|
|
47
|
+
throw result.error;
|
|
48
|
+
}
|
|
49
|
+
if (result.status !== 0) {
|
|
50
|
+
process.exit(result.status ?? 1);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const { nodeTests, tsxTests } = listTests();
|
|
55
|
+
|
|
56
|
+
if (nodeTests.length > 0) {
|
|
57
|
+
run('node', ['--test', ...nodeTests]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
run('node', ['.pi/harness/evals/smoke/smoke-harness-plan.mjs', '--fixture']);
|
|
61
|
+
|
|
62
|
+
if (tsxTests.length > 0) {
|
|
63
|
+
run('npx', ['-y', 'tsx', '--test', ...tsxTests]);
|
|
64
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Unit tests for harness_web.heuristic_config."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import tempfile
|
|
8
|
+
import unittest
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from harness_web.heuristic_config import (
|
|
12
|
+
_embedded_builtin_dict,
|
|
13
|
+
_merge_config_dict,
|
|
14
|
+
build_heuristic_angles,
|
|
15
|
+
clear_heuristic_config_cache,
|
|
16
|
+
heuristic_config_from_merged,
|
|
17
|
+
load_heuristic_angles_config_cached,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TestHeuristicConfig(unittest.TestCase):
|
|
22
|
+
def tearDown(self) -> None:
|
|
23
|
+
clear_heuristic_config_cache()
|
|
24
|
+
|
|
25
|
+
def test_builtin_code_includes_stackoverflow(self) -> None:
|
|
26
|
+
cfg = heuristic_config_from_merged(_embedded_builtin_dict())
|
|
27
|
+
angles = build_heuristic_angles("rust async", category="code", config=cfg)
|
|
28
|
+
ids = {a.id for a in angles}
|
|
29
|
+
self.assertIn("stackoverflow", ids)
|
|
30
|
+
self.assertIn("github", ids)
|
|
31
|
+
self.assertTrue(any("site:stackoverflow.com" in a.query for a in angles))
|
|
32
|
+
|
|
33
|
+
def test_shipped_code_includes_mdn_and_registries(self) -> None:
|
|
34
|
+
pkg = Path(__file__).resolve().parents[2] / "harness" / "web-heuristic-angles.yaml"
|
|
35
|
+
if not pkg.is_file():
|
|
36
|
+
self.skipTest("package yaml missing")
|
|
37
|
+
clear_heuristic_config_cache()
|
|
38
|
+
cfg = load_heuristic_angles_config_cached((str(pkg),))
|
|
39
|
+
angles = build_heuristic_angles("websocket api", category="code", config=cfg)
|
|
40
|
+
ids = {a.id for a in angles}
|
|
41
|
+
self.assertIn("mdn", ids)
|
|
42
|
+
self.assertIn("package_registries", ids)
|
|
43
|
+
self.assertLessEqual(len(angles), cfg.max_angles)
|
|
44
|
+
|
|
45
|
+
def test_shipped_security_category(self) -> None:
|
|
46
|
+
pkg = Path(__file__).resolve().parents[2] / "harness" / "web-heuristic-angles.yaml"
|
|
47
|
+
if not pkg.is_file():
|
|
48
|
+
self.skipTest("package yaml missing")
|
|
49
|
+
clear_heuristic_config_cache()
|
|
50
|
+
cfg = load_heuristic_angles_config_cached((str(pkg),))
|
|
51
|
+
angles = build_heuristic_angles("jwt validation", category="security", config=cfg)
|
|
52
|
+
ids = {a.id for a in angles}
|
|
53
|
+
self.assertIn("owasp", ids)
|
|
54
|
+
self.assertIn("cve_nvd", ids)
|
|
55
|
+
|
|
56
|
+
def test_merge_extends_code_category(self) -> None:
|
|
57
|
+
merged = _merge_config_dict(
|
|
58
|
+
_embedded_builtin_dict(),
|
|
59
|
+
{
|
|
60
|
+
"max_angles": 12,
|
|
61
|
+
"categories": {
|
|
62
|
+
"code": [
|
|
63
|
+
{
|
|
64
|
+
"id": "docs_rs",
|
|
65
|
+
"query": "{query} site:docs.rs",
|
|
66
|
+
"rationale": "Rust docs",
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
)
|
|
72
|
+
cfg = heuristic_config_from_merged(merged)
|
|
73
|
+
merged_ids = [a["id"] for a in merged["categories"]["code"]]
|
|
74
|
+
self.assertIn("docs_rs", merged_ids)
|
|
75
|
+
angles = build_heuristic_angles("tokio", category="code", config=cfg)
|
|
76
|
+
ids = {a.id for a in angles}
|
|
77
|
+
self.assertIn("stackoverflow", ids)
|
|
78
|
+
self.assertIn("github", ids)
|
|
79
|
+
|
|
80
|
+
def test_merge_adds_new_category(self) -> None:
|
|
81
|
+
merged = _merge_config_dict(
|
|
82
|
+
_embedded_builtin_dict(),
|
|
83
|
+
{
|
|
84
|
+
"categories": {
|
|
85
|
+
"security": [
|
|
86
|
+
{"id": "cve", "query": "{query} CVE", "rationale": "vulns"},
|
|
87
|
+
{
|
|
88
|
+
"id": "owasp",
|
|
89
|
+
"query": "{query} site:owasp.org",
|
|
90
|
+
"rationale": "guidance",
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
)
|
|
96
|
+
cfg = heuristic_config_from_merged(merged)
|
|
97
|
+
angles = build_heuristic_angles("jwt auth", category="security", config=cfg)
|
|
98
|
+
ids = {a.id for a in angles}
|
|
99
|
+
self.assertIn("cve", ids)
|
|
100
|
+
self.assertIn("owasp", ids)
|
|
101
|
+
|
|
102
|
+
def test_json_project_file_merges(self) -> None:
|
|
103
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
104
|
+
proj = Path(tmp)
|
|
105
|
+
harness_dir = proj / ".pi" / "harness"
|
|
106
|
+
harness_dir.mkdir(parents=True)
|
|
107
|
+
proj_file = harness_dir / "web-heuristic-angles.json"
|
|
108
|
+
proj_file.write_text(
|
|
109
|
+
json.dumps(
|
|
110
|
+
{
|
|
111
|
+
"categories": {
|
|
112
|
+
"code": [
|
|
113
|
+
{
|
|
114
|
+
"id": "crates_io",
|
|
115
|
+
"query": "{query} site:crates.io",
|
|
116
|
+
"rationale": "crates",
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
),
|
|
122
|
+
encoding="utf-8",
|
|
123
|
+
)
|
|
124
|
+
clear_heuristic_config_cache()
|
|
125
|
+
cfg = load_heuristic_angles_config_cached((str(proj_file),))
|
|
126
|
+
angles = build_heuristic_angles("serde", category="code", config=cfg)
|
|
127
|
+
ids = {a.id for a in angles}
|
|
128
|
+
self.assertIn("crates_io", ids)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if __name__ == "__main__":
|
|
132
|
+
unittest.main()
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Unit tests for harness_web.query_angles (no network)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import tempfile
|
|
7
|
+
import unittest
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from harness_web.query_angles import AnglesPlan, load_angles_file, resolve_angles
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestResolveAngles(unittest.TestCase):
|
|
14
|
+
def test_heuristic_code_category(self) -> None:
|
|
15
|
+
plan = resolve_angles("rust async", expand_heuristic=True, category="code")
|
|
16
|
+
ids = {a.id for a in plan.angles}
|
|
17
|
+
self.assertIn("github", ids)
|
|
18
|
+
self.assertGreaterEqual(len(plan.angles), 2)
|
|
19
|
+
self.assertLessEqual(len(plan.angles), 5)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TestLoadFile(unittest.TestCase):
|
|
23
|
+
def test_load_json_file(self) -> None:
|
|
24
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
25
|
+
p = Path(tmp) / "angles.json"
|
|
26
|
+
p.write_text(
|
|
27
|
+
json.dumps(
|
|
28
|
+
{
|
|
29
|
+
"intent": "load test",
|
|
30
|
+
"angles": [
|
|
31
|
+
{"id": "a", "query": "first angle query"},
|
|
32
|
+
{"id": "b", "query": "second angle query"},
|
|
33
|
+
],
|
|
34
|
+
}
|
|
35
|
+
),
|
|
36
|
+
encoding="utf-8",
|
|
37
|
+
)
|
|
38
|
+
plan = load_angles_file(p)
|
|
39
|
+
self.assertIsInstance(plan, AnglesPlan)
|
|
40
|
+
self.assertEqual(plan.intent, "load test")
|
|
41
|
+
self.assertEqual(len(plan.angles), 2)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
if __name__ == "__main__":
|
|
45
|
+
unittest.main()
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Unit tests for harness_web.rank (no network)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import unittest
|
|
6
|
+
|
|
7
|
+
from harness_web.rank import RankedHit, fuse_angle_results, lexical_rerank, normalize_url, tokenize
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestNormalizeUrl(unittest.TestCase):
|
|
11
|
+
def test_strips_tracking(self) -> None:
|
|
12
|
+
a = normalize_url("https://Example.com/path?utm_source=x&id=1")
|
|
13
|
+
b = normalize_url("https://example.com/path?id=1")
|
|
14
|
+
self.assertEqual(a, b)
|
|
15
|
+
|
|
16
|
+
def test_trailing_slash(self) -> None:
|
|
17
|
+
self.assertEqual(
|
|
18
|
+
normalize_url("https://example.com/foo/"),
|
|
19
|
+
normalize_url("https://example.com/foo"),
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TestRrfFusion(unittest.TestCase):
|
|
24
|
+
def test_merges_duplicate_urls(self) -> None:
|
|
25
|
+
angle_results = {
|
|
26
|
+
"a": [
|
|
27
|
+
{"url": "https://x.com/1", "title": "T1", "description": "d1"},
|
|
28
|
+
{"url": "https://x.com/2", "title": "T2", "description": "d2"},
|
|
29
|
+
],
|
|
30
|
+
"b": [
|
|
31
|
+
{"url": "https://x.com/1", "title": "T1b", "description": "d1b"},
|
|
32
|
+
],
|
|
33
|
+
}
|
|
34
|
+
fused = fuse_angle_results(angle_results, final_limit=5)
|
|
35
|
+
self.assertEqual(len(fused), 2)
|
|
36
|
+
top = fused[0]
|
|
37
|
+
self.assertEqual(top.url, "https://x.com/1")
|
|
38
|
+
self.assertIn("a", top.angle_ids)
|
|
39
|
+
self.assertIn("b", top.angle_ids)
|
|
40
|
+
self.assertGreater(top.score, fused[1].score)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TestLexicalRerank(unittest.TestCase):
|
|
44
|
+
def test_boosts_intent_overlap(self) -> None:
|
|
45
|
+
hits = [
|
|
46
|
+
RankedHit("https://a", "unrelated", "noise", 0.52, ["a"]),
|
|
47
|
+
RankedHit("https://b", "kubernetes architecture", "how kubernetes works", 0.50, ["b"]),
|
|
48
|
+
]
|
|
49
|
+
reranked = lexical_rerank(hits, "kubernetes architecture")
|
|
50
|
+
self.assertEqual(reranked[0].url, "https://b")
|
|
51
|
+
self.assertGreater(reranked[0].score, reranked[1].score)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class TestTokenize(unittest.TestCase):
|
|
55
|
+
def test_min_length(self) -> None:
|
|
56
|
+
self.assertIn("hello", tokenize("hello hi"))
|
package/AGENTS.md
CHANGED
|
@@ -16,7 +16,7 @@ Created: 2026-05-14
|
|
|
16
16
|
- docs/adr/ → Repo-level Architectural Decision Records
|
|
17
17
|
- .pi/harness/docs/adrs/ → Harness ADRs (team-shared; [index](.pi/harness/docs/adrs/README.md))
|
|
18
18
|
- .pi/harness/docs/practice-map.md → Phase → practice → agent spawn topology for `/harness-plan`, `/harness-run`, `/harness-review`
|
|
19
|
-
- .pi/skills/ → Agent skills
|
|
19
|
+
- .pi/skills/ → Agent skills (harness skills symlink to `.agents/skills/`, e.g. `web-retrieval`)
|
|
20
20
|
- .pi/agents/ → Specialized agents
|
|
21
21
|
|
|
22
22
|
## Graphify-First Workflow
|
|
@@ -36,7 +36,7 @@ Created: 2026-05-14
|
|
|
36
36
|
- Harness context: **context-mode only** — never lean-ctx on harness paths (see harness-context skill)
|
|
37
37
|
- `graphify update .` after significant code changes
|
|
38
38
|
- ast-grep (`sg`) is the default code search tool — use `sg -p 'pattern'` for structural search, never grep for code
|
|
39
|
-
-
|
|
39
|
+
- Non-API web: invoke **`web-retrieval`** skill (WRS tiers; default `tier=deep` with `web-query-expander` → `anglesFile`). CLI: `python3 "$UP_PKG/.pi/scripts/harness-web.py"`
|
|
40
40
|
|
|
41
41
|
## graphify
|
|
42
42
|
|
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,18 @@ All notable changes to this project are documented in this file.
|
|
|
9
9
|
- **Harness lens:** Integrate selected pi-lens capabilities through a harness-owned extension, store lens state under `.pi/harness/.lens`, and route lens findings through harness PostHog telemetry instead of standalone lens health/telemetry surfaces.
|
|
10
10
|
- **Graphify KB updater:** Productize conservative daily discovery/promotion with explicit repo/release taxonomy, allowlist source-class gates, operator review queue reporting, scheduler smoke validation, and safe Graphify refresh controls.
|
|
11
11
|
|
|
12
|
+
## [v0.20.0] — 2026-05-26
|
|
13
|
+
|
|
14
|
+
### ✨ Features
|
|
15
|
+
|
|
16
|
+
- **Harness:** Add hash-anchored executor edit flow.
|
|
17
|
+
|
|
18
|
+
## [v0.19.1] — 2026-05-26
|
|
19
|
+
|
|
20
|
+
### 🔧 Chores
|
|
21
|
+
|
|
22
|
+
- Prepare web retrieval and harness updates for release.
|
|
23
|
+
|
|
12
24
|
## [v0.19.0] — 2026-05-24
|
|
13
25
|
|
|
14
26
|
### ✨ Features
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## Dirac hash-anchored edit (vendored subset)
|
|
2
|
+
|
|
3
|
+
- **Project:** https://github.com/dirac-run/dirac
|
|
4
|
+
- **License:** Apache-2.0
|
|
5
|
+
- **Location:** [`.pi/lib/harness-anchored-edit/`](.pi/lib/harness-anchored-edit/) (anchor state, line protocol, edit resolve/apply)
|
|
6
|
+
- **Integration:** [`.pi/extensions/harness-anchored-edit.ts`](.pi/extensions/harness-anchored-edit.ts) — first-class harness `read`/`edit` (always on when harness extensions load). harness-lens autopatches anchored `edit.text` at `tool_call`; native apply via `applyAnchoredEditsToFile`.
|
|
7
|
+
|
|
1
8
|
## pi-vcc (vendored)
|
|
2
9
|
|
|
3
10
|
- **Project:** https://github.com/sting8k/pi-vcc
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultimate-pi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Ultimate AI coding harness for pi.dev — extensible skills, Obsidian wiki knowledge layer, compressed context, deterministic output",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -56,7 +56,10 @@
|
|
|
56
56
|
".pi/harness/agents.manifest.json",
|
|
57
57
|
".pi/harness/agents.policy.yaml",
|
|
58
58
|
".pi/harness/examples",
|
|
59
|
+
".pi/harness/web-heuristic-angles.yaml",
|
|
60
|
+
".pi/harness/web-heuristic-angles.json",
|
|
59
61
|
".pi/lib/harness-lens",
|
|
62
|
+
".pi/lib/harness-anchored-edit",
|
|
60
63
|
".pi/harness/README.md",
|
|
61
64
|
".pi/npm/package.json",
|
|
62
65
|
".pi/npm/.gitignore",
|
|
@@ -76,16 +79,15 @@
|
|
|
76
79
|
"@earendil-works/pi-coding-agent": "*"
|
|
77
80
|
},
|
|
78
81
|
"scripts": {
|
|
79
|
-
"check:ts": "tsc
|
|
82
|
+
"check:ts": "tsc -p tsconfig.check.json",
|
|
80
83
|
"vendor:sync-vcc": "bash .pi/scripts/vendor-sync-pi-vcc.sh",
|
|
81
84
|
"vendor:sync-subagents": "bash .pi/scripts/vendor-sync-pi-subagents.sh",
|
|
82
|
-
"release": "bash .pi/scripts/release.sh",
|
|
83
85
|
"lint": "biome check",
|
|
84
86
|
"lint:fix": "biome check --fix",
|
|
85
87
|
"format": "biome format --write",
|
|
86
88
|
"format:check": "biome format",
|
|
87
89
|
"prepare": "lefthook install",
|
|
88
|
-
"test": "node
|
|
90
|
+
"test": "node .pi/scripts/run-tests.mjs",
|
|
89
91
|
"test:vcc": "npx -y tsx --test vendor/pi-vcc/tests/*.test.ts",
|
|
90
92
|
"harness:sentrux-bootstrap": "node .pi/scripts/harness-sentrux-bootstrap.mjs",
|
|
91
93
|
"harness:sentrux-sync": "node .pi/scripts/sentrux-rules-sync.mjs --force",
|
|
@@ -109,6 +111,7 @@
|
|
|
109
111
|
"ajv": "^8.17.1",
|
|
110
112
|
"ajv-formats": "^3.0.1",
|
|
111
113
|
"croner": "^9.0.0",
|
|
114
|
+
"diff": "^8.0.4",
|
|
112
115
|
"jimp": "^1.6.1",
|
|
113
116
|
"minimatch": "^10.2.5",
|
|
114
117
|
"nanoid": "^5.1.5",
|
|
@@ -19,6 +19,11 @@ export interface AgentConfig {
|
|
|
19
19
|
thinking?: string;
|
|
20
20
|
maxTurns?: number;
|
|
21
21
|
extensionsOff?: boolean;
|
|
22
|
+
/** Curated subprocess extensions (agents.policy.yaml `extension_bundle`). */
|
|
23
|
+
extensionBundle?: string;
|
|
24
|
+
extensionsFull?: boolean;
|
|
25
|
+
/** When true, subprocess uses --no-builtin-tools so extension read/edit replace builtins. */
|
|
26
|
+
noBuiltinTools?: boolean;
|
|
22
27
|
skillsOff?: boolean;
|
|
23
28
|
systemPrompt: string;
|
|
24
29
|
source: AgentSource;
|
|
@@ -46,6 +46,10 @@ export interface HarnessSubagentsOptions {
|
|
|
46
46
|
subprocessGovernanceExtensionPath?: string;
|
|
47
47
|
/** @deprecated Use subprocessGovernanceExtensionPath */
|
|
48
48
|
harnessSubprocessExtensionPath?: string;
|
|
49
|
+
/** Resolve curated `-e` extension paths for agents.policy `extension_bundle`. */
|
|
50
|
+
resolveExtensionBundlePaths?: (
|
|
51
|
+
bundleName: string,
|
|
52
|
+
) => string[];
|
|
49
53
|
/** Extra env vars per subprocess (e.g. HARNESS_RUN_ID, HARNESS_RUN_DIR). */
|
|
50
54
|
resolveSubprocessEnv?: (
|
|
51
55
|
task: string,
|
|
@@ -453,13 +457,28 @@ function buildAgentArgs(
|
|
|
453
457
|
agent.extensionsOff &&
|
|
454
458
|
(subagentsOptions?.subprocessGovernanceExtensionPath ??
|
|
455
459
|
subagentsOptions?.harnessSubprocessExtensionPath);
|
|
456
|
-
|
|
460
|
+
const bundlePaths =
|
|
461
|
+
agent.extensionBundle && subagentsOptions?.resolveExtensionBundlePaths
|
|
462
|
+
? subagentsOptions.resolveExtensionBundlePaths(agent.extensionBundle)
|
|
463
|
+
: [];
|
|
464
|
+
|
|
465
|
+
if (agent.extensionBundle && bundlePaths.length > 0) {
|
|
466
|
+
args.push("--no-extensions");
|
|
467
|
+
for (const extPath of bundlePaths) {
|
|
468
|
+
args.push("-e", extPath);
|
|
469
|
+
}
|
|
470
|
+
if (agent.skillsOff) args.push("--no-skills");
|
|
471
|
+
} else if (agent.extensionsOff) {
|
|
457
472
|
args.push("--no-extensions");
|
|
458
473
|
if (governanceExt) args.push("-e", governanceExt);
|
|
459
474
|
if (agent.skillsOff) args.push("--no-skills");
|
|
460
475
|
}
|
|
461
|
-
if (agent.tools?.length)
|
|
462
|
-
|
|
476
|
+
if (agent.tools?.length) {
|
|
477
|
+
args.push("--tools", agent.tools.join(","));
|
|
478
|
+
if (agent.noBuiltinTools) args.push("--no-builtin-tools");
|
|
479
|
+
} else if (agent.extensionsOff || agent.extensionBundle) {
|
|
480
|
+
args.push("--no-tools");
|
|
481
|
+
}
|
|
463
482
|
return args;
|
|
464
483
|
}
|
|
465
484
|
|