ultimate-pi 0.3.0 → 0.4.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 (116) hide show
  1. package/.agents/skills/harness-decisions/SKILL.md +37 -0
  2. package/.agents/skills/harness-governor/SKILL.md +1 -1
  3. package/.agents/skills/harness-orchestration/SKILL.md +54 -0
  4. package/.agents/skills/harness-plan/SKILL.md +4 -3
  5. package/.agents/skills/harness-sentrux-setup/SKILL.md +57 -0
  6. package/.agents/skills/scrapling-web/SKILL.md +93 -0
  7. package/.pi/PACKAGING.md +2 -2
  8. package/.pi/SYSTEM.md +13 -15
  9. package/.pi/agents/harness/adversary.md +3 -0
  10. package/.pi/agents/harness/evaluator.md +3 -0
  11. package/.pi/agents/harness/executor.md +4 -1
  12. package/.pi/agents/harness/meta-optimizer.md +2 -1
  13. package/.pi/agents/harness/planner.md +22 -1
  14. package/.pi/agents/harness/sentrux-bootstrap.md +42 -0
  15. package/.pi/agents/harness/tie-breaker.md +2 -0
  16. package/.pi/extensions/harness-ask-user.ts +74 -0
  17. package/.pi/extensions/harness-subagents.ts +9 -0
  18. package/.pi/extensions/lib/ask-user/dialog.ts +260 -0
  19. package/.pi/extensions/lib/ask-user/fallback.ts +78 -0
  20. package/.pi/extensions/lib/ask-user/render.ts +66 -0
  21. package/.pi/extensions/lib/ask-user/schema.ts +69 -0
  22. package/.pi/extensions/lib/ask-user/types.ts +41 -0
  23. package/.pi/extensions/lib/ask-user/validate-core.mjs +79 -0
  24. package/.pi/extensions/lib/ask-user/validate.ts +92 -0
  25. package/.pi/extensions/lib/harness-subagents/agent-loader.ts +126 -0
  26. package/.pi/extensions/lib/harness-subagents/agent-manifest.ts +119 -0
  27. package/.pi/extensions/lib/harness-subagents/agent-parser.ts +87 -0
  28. package/.pi/extensions/lib/harness-subagents/blackboard-tool.ts +118 -0
  29. package/.pi/extensions/lib/harness-subagents/blackboard.ts +175 -0
  30. package/.pi/extensions/lib/harness-subagents/spawn-policy.ts +27 -0
  31. package/.pi/extensions/lib/harness-subagents/types-blackboard.ts +27 -0
  32. package/.pi/extensions/lib/harness-subagents/vendored/agent-manager.ts +553 -0
  33. package/.pi/extensions/lib/harness-subagents/vendored/agent-runner.ts +637 -0
  34. package/.pi/extensions/lib/harness-subagents/vendored/agent-types.ts +175 -0
  35. package/.pi/extensions/lib/harness-subagents/vendored/context.ts +59 -0
  36. package/.pi/extensions/lib/harness-subagents/vendored/cross-extension-rpc.ts +134 -0
  37. package/.pi/extensions/lib/harness-subagents/vendored/custom-agents.ts +5 -0
  38. package/.pi/extensions/lib/harness-subagents/vendored/default-agents.ts +123 -0
  39. package/.pi/extensions/lib/harness-subagents/vendored/env.ts +43 -0
  40. package/.pi/extensions/lib/harness-subagents/vendored/group-join.ts +144 -0
  41. package/.pi/extensions/lib/harness-subagents/vendored/index.ts +2447 -0
  42. package/.pi/extensions/lib/harness-subagents/vendored/invocation-config.ts +52 -0
  43. package/.pi/extensions/lib/harness-subagents/vendored/memory.ts +182 -0
  44. package/.pi/extensions/lib/harness-subagents/vendored/model-resolver.ts +92 -0
  45. package/.pi/extensions/lib/harness-subagents/vendored/output-file.ts +115 -0
  46. package/.pi/extensions/lib/harness-subagents/vendored/prompts.ts +103 -0
  47. package/.pi/extensions/lib/harness-subagents/vendored/schedule-store.ts +177 -0
  48. package/.pi/extensions/lib/harness-subagents/vendored/schedule.ts +416 -0
  49. package/.pi/extensions/lib/harness-subagents/vendored/settings.ts +210 -0
  50. package/.pi/extensions/lib/harness-subagents/vendored/skill-loader.ts +108 -0
  51. package/.pi/extensions/lib/harness-subagents/vendored/types.ts +187 -0
  52. package/.pi/extensions/lib/harness-subagents/vendored/ui/agent-widget.ts +637 -0
  53. package/.pi/extensions/lib/harness-subagents/vendored/ui/conversation-viewer.ts +324 -0
  54. package/.pi/extensions/lib/harness-subagents/vendored/ui/schedule-menu.ts +110 -0
  55. package/.pi/extensions/lib/harness-subagents/vendored/usage.ts +71 -0
  56. package/.pi/extensions/lib/harness-subagents/vendored/worktree.ts +195 -0
  57. package/.pi/extensions/policy-gate.ts +18 -0
  58. package/.pi/extensions/provider-payload-sanitize.ts +66 -0
  59. package/.pi/harness/README.md +2 -1
  60. package/.pi/harness/agents.manifest.json +80 -0
  61. package/.pi/harness/docs/adrs/0009-sentrux-rules-lifecycle.md +9 -5
  62. package/.pi/harness/env.harness.template +28 -0
  63. package/.pi/harness/sentrux/architecture.manifest.json +6 -1
  64. package/.pi/prompts/harness-auto.md +2 -2
  65. package/.pi/prompts/harness-plan.md +2 -2
  66. package/.pi/prompts/harness-router-tune.md +2 -2
  67. package/.pi/prompts/harness-run.md +1 -0
  68. package/.pi/prompts/harness-setup.md +182 -339
  69. package/.pi/scripts/README.md +6 -1
  70. package/.pi/scripts/harness-agents-manifest.mjs +123 -0
  71. package/.pi/scripts/harness-cli-verify.sh +60 -11
  72. package/.pi/scripts/harness-generate-model-router.mjs +242 -0
  73. package/.pi/scripts/harness-graphify-bootstrap.sh +1 -6
  74. package/.pi/scripts/harness-resolve-up-pkg.mjs +71 -0
  75. package/.pi/scripts/harness-seed-project-contracts.mjs +81 -0
  76. package/.pi/scripts/harness-sentrux-bootstrap.mjs +146 -0
  77. package/.pi/scripts/harness-sync-env.mjs +148 -0
  78. package/.pi/scripts/harness-verify.mjs +19 -0
  79. package/.pi/scripts/harness-web-search.md +33 -0
  80. package/.pi/scripts/harness-web.py +177 -0
  81. package/.pi/scripts/harness_web/__init__.py +1 -0
  82. package/.pi/scripts/harness_web/config.py +80 -0
  83. package/.pi/scripts/harness_web/output.py +55 -0
  84. package/.pi/scripts/harness_web/scrape.py +120 -0
  85. package/.pi/scripts/harness_web/search_ddg.py +106 -0
  86. package/.pi/scripts/release.sh +338 -0
  87. package/.pi/scripts/sentrux-rules-sync.mjs +29 -7
  88. package/.pi/settings.example.json +0 -1
  89. package/.sentrux/rules.toml +1 -1
  90. package/AGENTS.md +1 -1
  91. package/CHANGELOG.md +20 -0
  92. package/THIRD_PARTY_NOTICES.md +22 -0
  93. package/package.json +12 -9
  94. package/.agents/skills/firecrawl/SKILL.md +0 -150
  95. package/.agents/skills/firecrawl/rules/install.md +0 -82
  96. package/.agents/skills/firecrawl/rules/security.md +0 -26
  97. package/.agents/skills/firecrawl-agent/SKILL.md +0 -57
  98. package/.agents/skills/firecrawl-build-interact/SKILL.md +0 -67
  99. package/.agents/skills/firecrawl-build-onboarding/SKILL.md +0 -102
  100. package/.agents/skills/firecrawl-build-onboarding/references/auth-flow.md +0 -39
  101. package/.agents/skills/firecrawl-build-onboarding/references/project-setup.md +0 -20
  102. package/.agents/skills/firecrawl-build-onboarding/references/sdk-installation.md +0 -17
  103. package/.agents/skills/firecrawl-build-scrape/SKILL.md +0 -68
  104. package/.agents/skills/firecrawl-build-search/SKILL.md +0 -68
  105. package/.agents/skills/firecrawl-crawl/SKILL.md +0 -58
  106. package/.agents/skills/firecrawl-download/SKILL.md +0 -69
  107. package/.agents/skills/firecrawl-interact/SKILL.md +0 -83
  108. package/.agents/skills/firecrawl-map/SKILL.md +0 -50
  109. package/.agents/skills/firecrawl-parse/SKILL.md +0 -61
  110. package/.agents/skills/firecrawl-scrape/SKILL.md +0 -68
  111. package/.agents/skills/firecrawl-search/SKILL.md +0 -59
  112. package/firecrawl/.env.template +0 -62
  113. package/firecrawl/README.md +0 -49
  114. package/firecrawl/docker-compose.yaml +0 -201
  115. package/firecrawl/searxng/searxng.env +0 -3
  116. package/firecrawl/searxng/settings.yml +0 -85
@@ -0,0 +1,120 @@
1
+ """Scrape and map URLs via Scrapling fetchers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from urllib.parse import urljoin, urlparse
7
+
8
+ from scrapling.fetchers import Fetcher, StealthyFetcher, StealthySession
9
+
10
+ from .config import HarnessWebConfig
11
+ from .output import write_page_markdown
12
+
13
+
14
+ def _fetch_kwargs(config: HarnessWebConfig, *, wait_ms: int | None = None) -> dict:
15
+ kw: dict = {"timeout": config.timeout_ms}
16
+ if config.proxy:
17
+ kw["proxy"] = config.proxy
18
+ if wait_ms is not None:
19
+ kw["wait"] = wait_ms
20
+ return kw
21
+
22
+
23
+ def fetch_page(url: str, *, config: HarnessWebConfig, fast: bool, wait_ms: int | None):
24
+ kw = _fetch_kwargs(config, wait_ms=wait_ms)
25
+ if fast:
26
+ return Fetcher.get(url, timeout=config.timeout_sec, proxy=config.proxy)
27
+ return StealthyFetcher.fetch(url, **kw)
28
+
29
+
30
+ def scrape_url(
31
+ url: str,
32
+ output: str,
33
+ *,
34
+ config: HarnessWebConfig,
35
+ fast: bool,
36
+ wait_ms: int | None,
37
+ ) -> None:
38
+ from pathlib import Path
39
+
40
+ page = fetch_page(url, config=config, fast=fast, wait_ms=wait_ms)
41
+ write_page_markdown(Path(output), page, main_content_only=True)
42
+
43
+
44
+ def map_url(
45
+ url: str,
46
+ output: str,
47
+ *,
48
+ config: HarnessWebConfig,
49
+ fast: bool,
50
+ limit: int,
51
+ ) -> None:
52
+ import json
53
+ from pathlib import Path
54
+
55
+ page = fetch_page(url, config=config, fast=fast, wait_ms=None)
56
+ base = urlparse(url)
57
+ host = (base.hostname or "").lower()
58
+ links: list[dict[str, str]] = []
59
+ seen: set[str] = set()
60
+
61
+ for el in page.css("a[href]"):
62
+ if len(links) >= limit:
63
+ break
64
+ href = (el.attrib.get("href") or "").strip()
65
+ if not href or href.startswith("#") or href.lower().startswith("javascript:"):
66
+ continue
67
+ absolute = urljoin(url, href)
68
+ parsed = urlparse(absolute)
69
+ if (parsed.hostname or "").lower() != host:
70
+ continue
71
+ if absolute in seen:
72
+ continue
73
+ seen.add(absolute)
74
+ title = (el.get_all_text(strip=True) or "").strip()
75
+ links.append({"url": absolute, "title": title})
76
+
77
+ out = Path(output)
78
+ out.parent.mkdir(parents=True, exist_ok=True)
79
+ out.write_text(json.dumps({"url": url, "links": links}, indent=2) + "\n", encoding="utf-8")
80
+
81
+
82
+ def bulk_scrape(
83
+ urls: list[str],
84
+ out_dir: str,
85
+ *,
86
+ config: HarnessWebConfig,
87
+ fast: bool,
88
+ sleep_sec: float,
89
+ ) -> list[str]:
90
+ from pathlib import Path
91
+
92
+ root = Path(out_dir)
93
+ root.mkdir(parents=True, exist_ok=True)
94
+ failures: list[str] = []
95
+
96
+ if fast:
97
+ for i, url in enumerate(urls):
98
+ if i and sleep_sec > 0:
99
+ time.sleep(sleep_sec)
100
+ safe = urlparse(url).netloc.replace(".", "_") + ".md"
101
+ dest = root / safe
102
+ try:
103
+ scrape_url(url, str(dest), config=config, fast=True, wait_ms=None)
104
+ except Exception as err: # noqa: BLE001
105
+ failures.append(f"{url}: {err}")
106
+ return failures
107
+
108
+ kw = _fetch_kwargs(config)
109
+ with StealthySession(headless=True, **kw) as session:
110
+ for i, url in enumerate(urls):
111
+ if i and sleep_sec > 0:
112
+ time.sleep(sleep_sec)
113
+ safe = urlparse(url).netloc.replace(".", "_") + ".md"
114
+ dest = root / safe
115
+ try:
116
+ page = session.fetch(url)
117
+ write_page_markdown(dest, page, main_content_only=True)
118
+ except Exception as err: # noqa: BLE001
119
+ failures.append(f"{url}: {err}")
120
+ return failures
@@ -0,0 +1,106 @@
1
+ """DuckDuckGo HTML SERP search via HTTP Fetcher."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from urllib.parse import parse_qs, unquote, urlparse
6
+
7
+ from scrapling.fetchers import Fetcher, StealthyFetcher
8
+
9
+ from .config import HarnessWebConfig
10
+
11
+ DDG_HTML_URL = "https://html.duckduckgo.com/html/"
12
+ CHALLENGE_MARKERS = (
13
+ "anomaly-modal",
14
+ "bots use duckduckgo",
15
+ "please complete the following challenge",
16
+ "duckduckgo.com/y.js",
17
+ )
18
+
19
+
20
+ def _unwrap_ddg_href(href: str) -> str:
21
+ if not href:
22
+ return ""
23
+ if href.startswith("//"):
24
+ href = "https:" + href
25
+ parsed = urlparse(href)
26
+ if "duckduckgo.com" in (parsed.hostname or "") and parsed.path.startswith("/l/"):
27
+ qs = parse_qs(parsed.query)
28
+ if "uddg" in qs and qs["uddg"]:
29
+ return unquote(qs["uddg"][0])
30
+ return href
31
+
32
+
33
+ def _looks_like_challenge(html: str) -> bool:
34
+ lower = html.lower()
35
+ return any(m in lower for m in CHALLENGE_MARKERS)
36
+
37
+
38
+ def _parse_serp(page: Any, limit: int) -> list[dict[str, str]]:
39
+ results: list[dict[str, str]] = []
40
+ for block in page.css(".result"):
41
+ if len(results) >= limit:
42
+ break
43
+ links = block.css(".result__a")
44
+ if not links:
45
+ continue
46
+ link = links[0]
47
+ href = _unwrap_ddg_href(link.attrib.get("href", ""))
48
+ if not href.startswith("http"):
49
+ continue
50
+ title = (link.text or "").strip()
51
+ snippet_el = block.css(".result__snippet")
52
+ description = ""
53
+ if snippet_el:
54
+ description = (snippet_el[0].get_all_text(strip=True) or "").strip()
55
+ results.append({"url": href, "title": title, "description": description})
56
+ return results
57
+
58
+
59
+ def search_ddg(
60
+ query: str,
61
+ *,
62
+ limit: int,
63
+ config: HarnessWebConfig,
64
+ impersonate: bool = True,
65
+ ) -> list[dict[str, str]]:
66
+ if config.search_engine != "ddg_html":
67
+ raise SystemExit(
68
+ f"Unsupported HARNESS_WEB_SEARCH_ENGINE={config.search_engine!r} (only ddg_html)"
69
+ )
70
+
71
+ kwargs: dict = {
72
+ "params": {"q": query},
73
+ "timeout": config.timeout_sec,
74
+ }
75
+ if config.proxy:
76
+ kwargs["proxy"] = config.proxy
77
+ if impersonate:
78
+ kwargs["impersonate"] = "chrome"
79
+
80
+ page = Fetcher.get(DDG_HTML_URL, **kwargs)
81
+ status = getattr(page, "status", 200)
82
+ html = getattr(page, "html_content", "") or ""
83
+
84
+ if status == 403 or _looks_like_challenge(html):
85
+ page = StealthyFetcher.fetch(
86
+ DDG_HTML_URL,
87
+ params={"q": query},
88
+ timeout=config.timeout_ms,
89
+ proxy=config.proxy,
90
+ headless=True,
91
+ )
92
+ html = getattr(page, "html_content", "") or ""
93
+ if _looks_like_challenge(html):
94
+ raise SystemExit(
95
+ "Search engine blocked (403/challenge page). "
96
+ "Try later, set HARNESS_WEB_PROXY, or reduce query rate."
97
+ )
98
+
99
+ parsed = _parse_serp(page, limit)
100
+ if not parsed and html:
101
+ # Empty selector set — likely HTML shape change
102
+ if "result__a" not in html:
103
+ raise SystemExit(
104
+ "Could not parse DuckDuckGo HTML results (page layout may have changed)."
105
+ )
106
+ return parsed
@@ -0,0 +1,338 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # release.sh — Version bump, changelog, tag, and push
4
+ # Usage: ./.pi/scripts/release.sh [patch|minor|major] [--dry-run]
5
+ #
6
+ set -euo pipefail
7
+
8
+ # ─── Helpers ──────────────────────────────────────────────────────────────────
9
+ warn() { echo "⚠ $*" >&2; }
10
+ abort() { echo "✗ $*" >&2; exit 1; }
11
+ ok() { echo "✓ $*"; }
12
+
13
+ # ─── Step 0 — Parse arguments ─────────────────────────────────────────────────
14
+ BUMP_TYPE=""
15
+ DRY_RUN=false
16
+
17
+ for arg in "$@"; do
18
+ case "$arg" in
19
+ patch|minor|major) BUMP_TYPE="$arg" ;;
20
+ --dry-run) DRY_RUN=true ;;
21
+ *) abort "Unknown argument: $arg" ;;
22
+ esac
23
+ done
24
+
25
+ # ─── Step 1 — Infer bump type from commits if not provided ────────────────────
26
+ LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
27
+
28
+ if [ -z "$BUMP_TYPE" ]; then
29
+ ok "No bump type provided. Scanning commits since last tag…"
30
+
31
+ if [ -z "$LAST_TAG" ]; then
32
+ COMMIT_LOG=$(git log --format="%s" HEAD 2>/dev/null || true)
33
+ else
34
+ COMMIT_LOG=$(git log --format="%s" "${LAST_TAG}..HEAD" 2>/dev/null || true)
35
+ fi
36
+
37
+ if [ -z "$COMMIT_LOG" ]; then
38
+ abort "No commits since last tag. Nothing to release."
39
+ fi
40
+
41
+ # Inference rules
42
+ if echo "$COMMIT_LOG" | grep -qE '^feat!:|BREAKING CHANGE'; then
43
+ BUMP_TYPE="major"
44
+ elif echo "$COMMIT_LOG" | grep -qE '^feat:'; then
45
+ BUMP_TYPE="minor"
46
+ else
47
+ BUMP_TYPE="patch"
48
+ fi
49
+
50
+ ok "Inferred bump type: $BUMP_TYPE"
51
+ fi
52
+
53
+ # ─── Step 2 — Read current version and validate semver ────────────────────────
54
+ CURRENT_VERSION=$(node -e "console.log(require('./package.json').version)" 2>/dev/null) \
55
+ || abort "Failed to read version from package.json"
56
+
57
+ if ! [[ "$CURRENT_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
58
+ abort "Invalid semver in package.json: $CURRENT_VERSION"
59
+ fi
60
+
61
+ NEW_VERSION=$(node -e "
62
+ const [maj, min, pat] = '$CURRENT_VERSION'.split('.').map(Number);
63
+ const bump = '$BUMP_TYPE';
64
+ if (bump === 'major') console.log((maj + 1) + '.0.0');
65
+ else if (bump === 'minor') console.log(maj + '.' + (min + 1) + '.0');
66
+ else console.log(maj + '.' + min + '.' + (pat + 1));
67
+ ")
68
+
69
+ ok "Version: $CURRENT_VERSION → $NEW_VERSION"
70
+
71
+ # ─── Step 3 — Pre-flight checks ───────────────────────────────────────────────
72
+
73
+ # Must be in a git repo
74
+ git rev-parse --is-inside-work-tree >/dev/null 2>&1 \
75
+ || abort "Not a git repository."
76
+
77
+ # Must have origin remote
78
+ git remote -v | grep -q origin \
79
+ || abort "No 'origin' remote configured."
80
+
81
+ # Must be on a branch (not detached HEAD)
82
+ BRANCH=$(git symbolic-ref -q HEAD 2>/dev/null | sed 's|^refs/heads/||') \
83
+ || abort "Detached HEAD. Switch to a branch first."
84
+
85
+ # Must have clean working tree (warn only in dry-run)
86
+ git diff --quiet && git diff --cached --quiet
87
+ if [ $? -ne 0 ]; then
88
+ if [ "$DRY_RUN" = true ]; then
89
+ warn "Working tree is dirty — actual release would be blocked."
90
+ else
91
+ abort "Working tree is dirty. Commit or stash changes first."
92
+ fi
93
+ fi
94
+
95
+ # No duplicate tag locally or on remote
96
+ if git rev-parse "v$NEW_VERSION" >/dev/null 2>&1; then
97
+ abort "Tag v$NEW_VERSION already exists locally."
98
+ fi
99
+ if git ls-remote --tags origin "refs/tags/v$NEW_VERSION" >/dev/null 2>&1; then
100
+ abort "Tag v$NEW_VERSION already exists on remote."
101
+ fi
102
+
103
+ # ─── Step 4 — Gather commits since last tag ───────────────────────────────────
104
+ if [ -z "$LAST_TAG" ]; then
105
+ COMMITS=$(git log --oneline --no-merges HEAD)
106
+ else
107
+ COMMITS=$(git log --oneline --no-merges "${LAST_TAG}..HEAD")
108
+ fi
109
+
110
+ COMMIT_COUNT=$(echo "$COMMITS" | grep -c '^' || echo 0)
111
+
112
+ if [ "$COMMIT_COUNT" -eq 0 ]; then
113
+ abort "No commits since last tag. Nothing to release."
114
+ fi
115
+
116
+ # ─── Step 5 — Generate changelog entry ────────────────────────────────────────
117
+
118
+ # Map conventional commit prefix → section
119
+ map_prefix() {
120
+ local msg="$1"
121
+ case "$msg" in
122
+ feat!:*|*"BREAKING CHANGE"*) echo "breaking" ;;
123
+ feat:*) echo "features" ;;
124
+ fix:*) echo "fixes" ;;
125
+ perf:*) echo "perf" ;;
126
+ refactor:*) echo "refactor" ;;
127
+ docs:*) echo "docs" ;;
128
+ style:*) echo "style" ;;
129
+ test:*) echo "tests" ;;
130
+ chore:*) echo "chores" ;;
131
+ ci:*) echo "ci" ;;
132
+ build:*) echo "build" ;;
133
+ *) echo "chores" ;;
134
+ esac
135
+ }
136
+
137
+ declare -A SECTIONS=(
138
+ [breaking]="⚠️ Breaking Changes"
139
+ [features]="✨ Features"
140
+ [fixes]="🐛 Fixes"
141
+ [perf]="⚡ Performance"
142
+ [refactor]="♻️ Refactoring"
143
+ [docs]="📖 Documentation"
144
+ [style]="🎨 Style"
145
+ [tests]="✅ Tests"
146
+ [chores]="🔧 Chores"
147
+ [ci]="🔄 CI/CD"
148
+ [build]="📦 Build"
149
+ )
150
+
151
+ # Build per-section entries
152
+ entries_breaking=""
153
+ entries_features=""
154
+ entries_fixes=""
155
+ entries_perf=""
156
+ entries_refactor=""
157
+ entries_docs=""
158
+ entries_style=""
159
+ entries_tests=""
160
+ entries_chores=""
161
+ entries_ci=""
162
+ entries_build=""
163
+
164
+ while IFS= read -r line; do
165
+ [ -z "$line" ] && continue
166
+ # Strip the short sha prefix (first word)
167
+ msg="${line#* }"
168
+ # Strip conventional commit prefix for display
169
+ display="$msg"
170
+ display=$(echo "$display" | sed -E 's/^[a-z]+(\([a-z0-9_-]+\))?!?:\s*//')
171
+ prefix=$(map_prefix "$msg")
172
+ case "$prefix" in
173
+ breaking) entries_breaking="${entries_breaking}- $display
174
+ " ;;
175
+ features) entries_features="${entries_features}- $display
176
+ " ;;
177
+ fixes) entries_fixes="${entries_fixes}- $display
178
+ " ;;
179
+ perf) entries_perf="${entries_perf}- $display
180
+ " ;;
181
+ refactor) entries_refactor="${entries_refactor}- $display
182
+ " ;;
183
+ docs) entries_docs="${entries_docs}- $display
184
+ " ;;
185
+ style) entries_style="${entries_style}- $display
186
+ " ;;
187
+ tests) entries_tests="${entries_tests}- $display
188
+ " ;;
189
+ ci) entries_ci="${entries_ci}- $display
190
+ " ;;
191
+ build) entries_build="${entries_build}- $display
192
+ " ;;
193
+ *) entries_chores="${entries_chores}- $display
194
+ " ;;
195
+ esac
196
+ done <<< "$COMMITS"
197
+
198
+ # Assemble the changelog block
199
+ TODAY=$(date +%Y-%m-%d)
200
+ CHANGELOG_BLOCK="## [v$NEW_VERSION] — $TODAY
201
+ "
202
+
203
+ for key in breaking features fixes perf refactor docs style tests ci build chores; do
204
+ eval "content=\"\$entries_$key\""
205
+ if [ -n "$content" ]; then
206
+ CHANGELOG_BLOCK="${CHANGELOG_BLOCK}
207
+ ### ${SECTIONS[$key]}
208
+
209
+ $content"
210
+ fi
211
+ done
212
+
213
+ # ─── Step 6 — Dry run check ───────────────────────────────────────────────────
214
+ if [ "$DRY_RUN" = true ]; then
215
+ echo ""
216
+ echo "═══════════════════════════════════════════════════════════════"
217
+ echo " DRY RUN — no changes made"
218
+ echo "═══════════════════════════════════════════════════════════════"
219
+ echo " Version: $CURRENT_VERSION → $NEW_VERSION"
220
+ echo " Bump: $BUMP_TYPE"
221
+ echo " Commits: $COMMIT_COUNT since ${LAST_TAG:-<none>}"
222
+ echo " Branch: $BRANCH"
223
+ echo ""
224
+ echo " Files that would change:"
225
+ echo " - package.json (version → $NEW_VERSION)"
226
+ echo " - CHANGELOG.md (new entry below)"
227
+ echo ""
228
+ echo " Tag that would be created: v$NEW_VERSION"
229
+ echo ""
230
+ echo " Changelog entry:"
231
+ echo "───────────────────────────────────────────────────────────────"
232
+ echo "$CHANGELOG_BLOCK"
233
+ echo "───────────────────────────────────────────────────────────────"
234
+ exit 0
235
+ fi
236
+
237
+ # ─── Step 7 — Bump version in package.json ────────────────────────────────────
238
+ npm pkg set version="$NEW_VERSION"
239
+
240
+ node -e "
241
+ const v = require('./package.json').version;
242
+ if (v !== '$NEW_VERSION') {
243
+ console.error('✗ version mismatch: expected $NEW_VERSION, got ' + v);
244
+ process.exit(1);
245
+ }
246
+ console.log('✓ version bumped to $NEW_VERSION');
247
+ "
248
+
249
+ # ─── Step 8 — Write CHANGELOG.md ──────────────────────────────────────────────
250
+ if [ -f CHANGELOG.md ]; then
251
+ # Prepend after the first heading line
252
+ {
253
+ head -n 1 CHANGELOG.md
254
+ echo ""
255
+ echo "$CHANGELOG_BLOCK"
256
+ tail -n +2 CHANGELOG.md
257
+ } > CHANGELOG.md.tmp && mv CHANGELOG.md.tmp CHANGELOG.md
258
+ else
259
+ {
260
+ echo "# Changelog"
261
+ echo ""
262
+ echo "All notable changes to this project are documented in this file."
263
+ echo ""
264
+ echo "$CHANGELOG_BLOCK"
265
+ } > CHANGELOG.md
266
+ fi
267
+
268
+ ok "CHANGELOG.md updated"
269
+
270
+ # ─── Step 9 — Read co-author config ───────────────────────────────────────────
271
+ CO_AUTHOR="pi-mono <261679550+pi-mono@users.noreply.github.com>"
272
+ if [ -f .pi/auto-commit.json ]; then
273
+ CO_AUTHOR=$(node -e "
274
+ const fs = require('fs');
275
+ const cfg = JSON.parse(fs.readFileSync('.pi/auto-commit.json', 'utf8'));
276
+ const ca = cfg.coAuthor || {};
277
+ console.log((ca.login || 'pi-mono') + ' <' + (ca.email || '261679550+pi-mono@users.noreply.github.com') + '>');
278
+ " 2>/dev/null) || true
279
+ fi
280
+
281
+ # ─── Step 10 — Commit ─────────────────────────────────────────────────────────
282
+ git add package.json CHANGELOG.md
283
+
284
+ COMMIT_BODY=$(cat <<EOF
285
+ - Bump version in package.json
286
+ - Add changelog entry for v$NEW_VERSION
287
+
288
+ Commits included:
289
+ $(echo "$COMMITS" | sed 's/^/- /')
290
+ EOF
291
+ )
292
+
293
+ git commit -m "chore(release): bump to v$NEW_VERSION" \
294
+ -m "$COMMIT_BODY" \
295
+ -m "Co-authored-by: $CO_AUTHOR"
296
+
297
+ ok "Committed version bump + changelog"
298
+
299
+ # ─── Step 11 — Create and push tag ────────────────────────────────────────────
300
+ TAG_BODY=$(cat <<EOF
301
+ Release v$NEW_VERSION — $BUMP_TYPE bump
302
+
303
+ $COMMITS
304
+ EOF
305
+ )
306
+
307
+ git tag -a "v$NEW_VERSION" -m "$TAG_BODY"
308
+ ok "Created tag v$NEW_VERSION"
309
+
310
+ git push origin "v$NEW_VERSION"
311
+ ok "Pushed tag v$NEW_VERSION to origin"
312
+
313
+ # ─── Step 12 — Optionally push branch commit ──────────────────────────────────
314
+ echo ""
315
+ read -rp "Push the version-bump commit to the current branch ($BRANCH) too? [Y/n] " PUSH_BRANCH
316
+ if [[ "$PUSH_BRANCH" =~ ^[Yy]?$ ]]; then
317
+ git push origin "$BRANCH"
318
+ ok "Pushed commit to $BRANCH"
319
+ else
320
+ echo "Skipped branch push."
321
+ fi
322
+
323
+ # ─── Step 13 — Report ─────────────────────────────────────────────────────────
324
+ echo ""
325
+ echo "═══════════════════════════════════════════════════════════════"
326
+ echo " ✓ Released v$NEW_VERSION ($BUMP_TYPE)"
327
+ echo "═══════════════════════════════════════════════════════════════"
328
+ echo " Tag: v$NEW_VERSION — pushed to origin"
329
+ echo " Commit: $(git rev-parse --short HEAD)"
330
+ echo " Branch: $BRANCH"
331
+ echo ""
332
+ echo " Workflows triggered:"
333
+ echo " - .github/workflows/publish-github-packages.yml"
334
+ echo " - .github/workflows/publish-npm.yml"
335
+ echo ""
336
+ echo " Changelog: CHANGELOG.md updated"
337
+ echo " Monitor: https://github.com/aryaniyaps/ultimate-pi/actions"
338
+ echo "═══════════════════════════════════════════════════════════════"
@@ -13,16 +13,26 @@ import { fileURLToPath } from "node:url";
13
13
  import { createHash } from "node:crypto";
14
14
  import { spawn } from "node:child_process";
15
15
 
16
- const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
16
+ const UP_PKG = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
17
+ /** Target project root (consumer repo). Default: process.cwd(). */
18
+ const PROJECT_ROOT =
19
+ process.argv.find((a, i) => i >= 2 && !a.startsWith("-")) || process.cwd();
17
20
  const MANIFEST = join(
18
- ROOT,
21
+ PROJECT_ROOT,
19
22
  ".pi",
20
23
  "harness",
21
24
  "sentrux",
22
25
  "architecture.manifest.json",
23
26
  );
24
- const RULES_PATH = join(ROOT, ".sentrux", "rules.toml");
25
- const META_PATH = join(ROOT, ".sentrux", ".harness-rules-meta.json");
27
+ const MANIFEST_TEMPLATE = join(
28
+ UP_PKG,
29
+ ".pi",
30
+ "harness",
31
+ "sentrux",
32
+ "architecture.manifest.json",
33
+ );
34
+ const RULES_PATH = join(PROJECT_ROOT, ".sentrux", "rules.toml");
35
+ const META_PATH = join(PROJECT_ROOT, ".sentrux", ".harness-rules-meta.json");
26
36
 
27
37
  const MANAGED_START = "# --- harness:managed:start ---";
28
38
  const MANAGED_END = "# --- harness:managed:end ---";
@@ -113,7 +123,7 @@ async function fileExists(path) {
113
123
  async function runSentruxCheck() {
114
124
  return new Promise((resolve) => {
115
125
  const child = spawn("sentrux", ["check", "."], {
116
- cwd: ROOT,
126
+ cwd: PROJECT_ROOT,
117
127
  stdio: ["ignore", "pipe", "pipe"],
118
128
  });
119
129
  let out = "";
@@ -136,7 +146,19 @@ async function main() {
136
146
  const strict = process.argv.includes("--strict");
137
147
 
138
148
  if (!(await fileExists(MANIFEST))) {
139
- fail(`missing manifest ${MANIFEST}`);
149
+ if (await fileExists(MANIFEST_TEMPLATE)) {
150
+ await mkdir(dirname(MANIFEST), { recursive: true });
151
+ await writeFile(
152
+ MANIFEST,
153
+ await readFile(MANIFEST_TEMPLATE, "utf-8"),
154
+ "utf-8",
155
+ );
156
+ console.log(
157
+ `sentrux-rules-sync: seeded manifest from package -> ${MANIFEST}`,
158
+ );
159
+ } else {
160
+ fail(`missing manifest ${MANIFEST} (and no template in package)`);
161
+ }
140
162
  }
141
163
 
142
164
  const manifestRaw = await readFile(MANIFEST, "utf-8");
@@ -171,7 +193,7 @@ async function main() {
171
193
  "rules.toml out of date — run node \"$UP_PKG/.pi/scripts/sentrux-rules-sync.mjs\" --force (see .pi/scripts/README.md for UP_PKG)",
172
194
  );
173
195
  } else {
174
- await mkdir(join(ROOT, ".sentrux"), { recursive: true });
196
+ await mkdir(join(PROJECT_ROOT, ".sentrux"), { recursive: true });
175
197
  const next = mergeRules(existing, managedBlock);
176
198
  await writeFile(RULES_PATH, next, "utf-8");
177
199
  meta = {
@@ -7,7 +7,6 @@
7
7
  "npm:ultimate-pi",
8
8
  "npm:@posthog/pi",
9
9
  "npm:context-mode",
10
- "npm:@tintinweb/pi-subagents",
11
10
  "npm:@sting8k/pi-vcc"
12
11
  ]
13
12
  }
@@ -35,7 +35,7 @@ order = 2
35
35
 
36
36
  [[layers]]
37
37
  name = "agents"
38
- paths = [".pi/agents/*", ".pi/prompts/*", ".agents/skills/*"]
38
+ paths = [".pi/agents/*", ".pi/harness/agents.manifest.json", ".pi/prompts/*", ".agents/skills/*"]
39
39
  order = 3
40
40
  # Agent definitions, prompts, and skills
41
41
 
package/AGENTS.md CHANGED
@@ -30,7 +30,7 @@ Created: 2026-05-14
30
30
  - Harness context: **context-mode only** — never lean-ctx on harness paths (see harness-context skill)
31
31
  - `graphify update .` after significant code changes
32
32
  - ast-grep (`sg`) is the default code search tool — use `sg -p 'pattern'` for structural search, never grep for code
33
- - Self-hosted Firecrawl at http://localhost:3002 (FIRECRAWL_API_URL)
33
+ - Web fetch/search via `python3 "$UP_PKG/.pi/scripts/harness-web.py"` (Scrapling; see scrapling-web skill)
34
34
 
35
35
  ## graphify
36
36
 
package/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## [v0.4.0] — 2026-05-16
6
+
7
+ ### ⚠️ Breaking Changes
8
+
9
+ - **Firecrawl removed**: deleted self-hosted `firecrawl/` Docker stack and all `firecrawl*` agent skills. Web fetch/search now uses **`harness-web`** (Scrapling). Artifact dir is **`.web/`** (not `.firecrawl/`). Remove `FIRECRAWL_API_KEY` / `FIRECRAWL_API_URL`; use `uv tool install "scrapling[fetchers]"` and `scrapling install` per `/harness-setup`.
10
+ - **Harness web CLI**: `python3 "$UP_PKG/.pi/scripts/harness-web.py" search|scrape|map|bulk-scrape` — see `.agents/skills/scrapling-web/SKILL.md`.
11
+
12
+ ### ✨ Features
13
+
14
+ - **harness-web**: Scrapling-backed search (DuckDuckGo HTML SERP) and scrape (`StealthyFetcher` default; `--fast` for static HTTP). Modules under `.pi/scripts/harness_web/`.
15
+ - **Harness subagents**: vendored harness-subagents extension and Sentrux bootstrap (`#125`).
16
+
17
+ ## [v0.3.1] — 2026-05-15
18
+
19
+ ### 🐛 Fixes
20
+
21
+ - **External `/harness-setup`**: policy gate no longer forces **plan** phase because the setup doc mentions `harness-plan` (e.g. `gh label create "harness-plan"`).
22
+ - **Harness specs in consumer repos**: copy `*.schema.json` and specs `README` from the package via `harness-seed-project-contracts.mjs` as part of setup (so `plan-packet.schema.json` exists before planning).
23
+ - **Strict LLM gateways**: new `provider-payload-sanitize` extension removes disallowed top-level fields (`reasoning`, etc.) from `messages` before provider requests (avoids 400 “Extra inputs … reasoning” on some OpenAI-compatible APIs).
24
+
5
25
  ## [v0.3.0] — 2026-05-15
6
26
 
7
27
  ### 📦 Release