ultimate-pi 0.19.0 → 0.19.1
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/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-web-guard.ts +2 -1
- package/.pi/extensions/harness-web-tools.ts +689 -51
- package/.pi/harness/agents.manifest.json +29 -5
- package/.pi/harness/agents.policy.yaml +34 -0
- package/.pi/harness/docs/adrs/0050-agentic-web-retrieval-stack.md +46 -0
- package/.pi/harness/docs/harness-web-search.md +97 -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.mjs +6 -0
- package/.pi/lib/harness-subagent-auth.ts +39 -9
- package/.pi/lib/harness-subagents-bridge.ts +21 -0
- 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/scripts/gen-web-heuristic-angles-json.mjs +24 -0
- package/.pi/scripts/harness-cli-verify.sh +5 -0
- package/.pi/scripts/harness-verify.mjs +78 -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/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 +6 -0
- package/package.json +5 -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
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# WRS emergency heuristic angles (--expand-heuristic / expandHeuristic:true).
|
|
2
|
+
# Package defaults (ultimate-pi). External projects: copy
|
|
3
|
+
# .pi/harness/examples/web-heuristic-angles.project.yaml to
|
|
4
|
+
# <your-project>/.pi/harness/web-heuristic-angles.yaml and edit.
|
|
5
|
+
#
|
|
6
|
+
# Placeholders: {query} → user search string
|
|
7
|
+
# Order matters: base angles run first, then category (deduped by id, capped at max_angles).
|
|
8
|
+
# JSON mirror (no PyYAML): run `node .pi/scripts/gen-web-heuristic-angles-json.mjs` after edits.
|
|
9
|
+
# Stdlib fallback: .pi/scripts/harness_web/heuristic_angles_shipped.py
|
|
10
|
+
|
|
11
|
+
version: 1
|
|
12
|
+
max_angles: 8
|
|
13
|
+
|
|
14
|
+
base:
|
|
15
|
+
- id: definitional
|
|
16
|
+
query: "{query}"
|
|
17
|
+
rationale: Core intent phrasing
|
|
18
|
+
- id: authoritative
|
|
19
|
+
query: "{query} official documentation OR specification OR RFC"
|
|
20
|
+
rationale: Primary specs and vendor docs
|
|
21
|
+
|
|
22
|
+
categories:
|
|
23
|
+
code:
|
|
24
|
+
- id: github
|
|
25
|
+
query: "{query} site:github.com"
|
|
26
|
+
rationale: Source, issues, discussions
|
|
27
|
+
- id: stackoverflow
|
|
28
|
+
query: "{query} site:stackoverflow.com"
|
|
29
|
+
rationale: Debugging and API usage Q&A
|
|
30
|
+
- id: stackexchange
|
|
31
|
+
query: "{query} site:stackexchange.com"
|
|
32
|
+
rationale: Broader SE network (Super User, Server Fault, etc.)
|
|
33
|
+
- id: readthedocs
|
|
34
|
+
query: "{query} site:readthedocs.io"
|
|
35
|
+
rationale: OSS library documentation
|
|
36
|
+
- id: mdn
|
|
37
|
+
query: "{query} site:developer.mozilla.org"
|
|
38
|
+
rationale: Web platform and browser APIs
|
|
39
|
+
- id: package_registries
|
|
40
|
+
query: "{query} site:npmjs.com OR site:pypi.org OR site:pkg.go.dev OR site:crates.io"
|
|
41
|
+
rationale: Package metadata across major ecosystems
|
|
42
|
+
- id: microsoft_learn
|
|
43
|
+
query: "{query} site:learn.microsoft.com"
|
|
44
|
+
rationale: .NET, Azure, Windows, and enterprise stacks
|
|
45
|
+
- id: hacker_news
|
|
46
|
+
query: "{query} site:news.ycombinator.com"
|
|
47
|
+
rationale: High-signal practitioner discussion
|
|
48
|
+
- id: gitlab
|
|
49
|
+
query: "{query} site:gitlab.com"
|
|
50
|
+
rationale: Alternate host and CI-visible code
|
|
51
|
+
- id: devto
|
|
52
|
+
query: "{query} site:dev.to OR site:medium.com"
|
|
53
|
+
rationale: Tutorials and implementation writeups
|
|
54
|
+
|
|
55
|
+
paper:
|
|
56
|
+
- id: arxiv
|
|
57
|
+
query: "{query} site:arxiv.org"
|
|
58
|
+
rationale: Preprints and latest ML/CS uploads
|
|
59
|
+
- id: semantic_scholar
|
|
60
|
+
query: "{query} site:semanticscholar.org"
|
|
61
|
+
rationale: Citations, influences, and PDF links
|
|
62
|
+
- id: google_scholar
|
|
63
|
+
query: "{query} site:scholar.google.com"
|
|
64
|
+
rationale: Broad academic discovery
|
|
65
|
+
- id: papers_with_code
|
|
66
|
+
query: "{query} site:paperswithcode.com"
|
|
67
|
+
rationale: Benchmarks tied to implementations
|
|
68
|
+
- id: openreview
|
|
69
|
+
query: "{query} site:openreview.net"
|
|
70
|
+
rationale: Peer reviews and ML conference submissions
|
|
71
|
+
- id: acl_anthology
|
|
72
|
+
query: "{query} site:aclanthology.org"
|
|
73
|
+
rationale: NLP and computational linguistics
|
|
74
|
+
- id: acm_dl
|
|
75
|
+
query: "{query} site:dl.acm.org"
|
|
76
|
+
rationale: ACM proceedings and journals
|
|
77
|
+
- id: pubmed
|
|
78
|
+
query: "{query} site:pubmed.ncbi.nlm.nih.gov"
|
|
79
|
+
rationale: Biomedical and life-sciences literature
|
|
80
|
+
|
|
81
|
+
news:
|
|
82
|
+
- id: recent
|
|
83
|
+
query: "{query} news 2025 2026"
|
|
84
|
+
rationale: Recency-biased open web
|
|
85
|
+
- id: wire_reuters
|
|
86
|
+
query: "{query} site:reuters.com"
|
|
87
|
+
rationale: Wire-service reporting
|
|
88
|
+
- id: wire_ap
|
|
89
|
+
query: "{query} site:apnews.com"
|
|
90
|
+
rationale: Associated Press coverage
|
|
91
|
+
- id: tech_press
|
|
92
|
+
query: "{query} site:techcrunch.com OR site:theverge.com OR site:arstechnica.com"
|
|
93
|
+
rationale: Technology industry news
|
|
94
|
+
- id: business_press
|
|
95
|
+
query: "{query} site:bloomberg.com OR site:ft.com OR site:wsj.com"
|
|
96
|
+
rationale: Markets and business context
|
|
97
|
+
- id: analysis
|
|
98
|
+
query: "{query} in-depth analysis explainer"
|
|
99
|
+
rationale: Long-form journalism and explainers
|
|
100
|
+
- id: bbc
|
|
101
|
+
query: "{query} site:bbc.com/news"
|
|
102
|
+
rationale: International general news desk
|
|
103
|
+
|
|
104
|
+
company:
|
|
105
|
+
- id: official_site
|
|
106
|
+
query: "{query} official website"
|
|
107
|
+
rationale: Company-controlled messaging
|
|
108
|
+
- id: crunchbase
|
|
109
|
+
query: "{query} site:crunchbase.com"
|
|
110
|
+
rationale: Funding, investors, and competitors
|
|
111
|
+
- id: linkedin_company
|
|
112
|
+
query: "{query} site:linkedin.com/company"
|
|
113
|
+
rationale: Headcount, hiring, and positioning
|
|
114
|
+
- id: sec_filings
|
|
115
|
+
query: "{query} site:sec.gov 10-K OR 10-Q OR S-1"
|
|
116
|
+
rationale: US public-company disclosures
|
|
117
|
+
- id: g2_reviews
|
|
118
|
+
query: "{query} site:g2.com OR site:capterra.com"
|
|
119
|
+
rationale: B2B software reviews and comparisons
|
|
120
|
+
- id: company_news
|
|
121
|
+
query: "{query} company announcement press release"
|
|
122
|
+
rationale: Launches, partnerships, and earnings
|
|
123
|
+
- id: glassdoor
|
|
124
|
+
query: "{query} site:glassdoor.com"
|
|
125
|
+
rationale: Employee sentiment and culture signals
|
|
126
|
+
|
|
127
|
+
people:
|
|
128
|
+
- id: linkedin
|
|
129
|
+
query: "{query} site:linkedin.com/in"
|
|
130
|
+
rationale: Professional profiles
|
|
131
|
+
- id: github_person
|
|
132
|
+
query: "{query} site:github.com"
|
|
133
|
+
rationale: Open-source footprint for builders
|
|
134
|
+
- id: wikipedia
|
|
135
|
+
query: "{query} site:en.wikipedia.org"
|
|
136
|
+
rationale: Neutral biographical baseline
|
|
137
|
+
- id: scholar_person
|
|
138
|
+
query: "{query} site:scholar.google.com"
|
|
139
|
+
rationale: Publication record for researchers
|
|
140
|
+
- id: interviews
|
|
141
|
+
query: "{query} interview podcast keynote"
|
|
142
|
+
rationale: First-person statements and talks
|
|
143
|
+
- id: twitter_x
|
|
144
|
+
query: "{query} site:x.com OR site:twitter.com"
|
|
145
|
+
rationale: Public statements and discourse
|
|
146
|
+
|
|
147
|
+
security:
|
|
148
|
+
- id: cve_nvd
|
|
149
|
+
query: "{query} CVE site:nvd.nist.gov"
|
|
150
|
+
rationale: National Vulnerability Database
|
|
151
|
+
- id: owasp
|
|
152
|
+
query: "{query} site:owasp.org"
|
|
153
|
+
rationale: AppSec standards and cheat sheets
|
|
154
|
+
- id: cwe
|
|
155
|
+
query: "{query} site:cwe.mitre.org"
|
|
156
|
+
rationale: Weakness taxonomy
|
|
157
|
+
- id: github_advisories
|
|
158
|
+
query: "{query} site:github.com/advisories OR dependabot"
|
|
159
|
+
rationale: Ecosystem security advisories
|
|
160
|
+
- id: snyk_blog
|
|
161
|
+
query: "{query} site:snyk.io/blog OR vulnerability"
|
|
162
|
+
rationale: Practitioner security writeups
|
|
163
|
+
|
|
164
|
+
default:
|
|
165
|
+
- id: technical
|
|
166
|
+
query: "{query} how it works architecture internals"
|
|
167
|
+
rationale: Mechanism and design
|
|
168
|
+
- id: criticism
|
|
169
|
+
query: "{query} limitations criticism drawbacks"
|
|
170
|
+
rationale: Counterpoints and failure modes
|
|
171
|
+
- id: wikipedia
|
|
172
|
+
query: "{query} site:en.wikipedia.org"
|
|
173
|
+
rationale: Structured overview
|
|
174
|
+
- id: comparison
|
|
175
|
+
query: "{query} vs alternatives comparison benchmark"
|
|
176
|
+
rationale: Competitive landscape
|
|
177
|
+
- id: reddit
|
|
178
|
+
query: "{query} site:reddit.com"
|
|
179
|
+
rationale: Community experience reports
|
|
180
|
+
- id: hn_default
|
|
181
|
+
query: "{query} site:news.ycombinator.com"
|
|
182
|
+
rationale: Practitioner threads when category unknown
|
|
@@ -67,6 +67,8 @@ function normalizeKindEntry(raw) {
|
|
|
67
67
|
typeof raw.thinking === "string" && raw.thinking.trim()
|
|
68
68
|
? raw.thinking.trim()
|
|
69
69
|
: undefined,
|
|
70
|
+
model:
|
|
71
|
+
typeof raw.model === "string" && raw.model.trim() ? raw.model.trim() : undefined,
|
|
70
72
|
};
|
|
71
73
|
}
|
|
72
74
|
|
|
@@ -99,6 +101,8 @@ function normalizeAgentEntry(raw) {
|
|
|
99
101
|
typeof raw.thinking === "string" && raw.thinking.trim()
|
|
100
102
|
? raw.thinking.trim()
|
|
101
103
|
: undefined,
|
|
104
|
+
model:
|
|
105
|
+
typeof raw.model === "string" && raw.model.trim() ? raw.model.trim() : undefined,
|
|
102
106
|
};
|
|
103
107
|
}
|
|
104
108
|
|
|
@@ -165,6 +169,7 @@ export function resolveEffectiveTools(agentId, merged) {
|
|
|
165
169
|
readOnly: kind.readOnly,
|
|
166
170
|
maxTurns: entry.maxTurns ?? kind.maxTurns,
|
|
167
171
|
thinking: entry.thinking ?? kind.thinking,
|
|
172
|
+
model: entry.model ?? kind.model,
|
|
168
173
|
submitTool: entry.submitTool,
|
|
169
174
|
};
|
|
170
175
|
}
|
|
@@ -301,6 +306,7 @@ export function applyAgentPolicyToConfig(agent, packageRoot, projectRoot) {
|
|
|
301
306
|
extensionsOff: spec.extensionsOff,
|
|
302
307
|
maxTurns: spec.maxTurns ?? agent.maxTurns,
|
|
303
308
|
thinking: spec.thinking ?? agent.thinking,
|
|
309
|
+
model: spec.model ?? agent.model,
|
|
304
310
|
};
|
|
305
311
|
}
|
|
306
312
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Resolve concrete LLM credentials for harness subagent subprocesses.
|
|
3
3
|
*
|
|
4
4
|
* Harness subprocesses run with `--no-extensions`, so auth forwarding only uses
|
|
5
|
-
* concrete provider/model references from
|
|
5
|
+
* concrete provider/model references from env, agent config, or parent session.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { AgentConfig } from "../../vendor/pi-subagents/src/agents.js";
|
|
@@ -30,22 +30,52 @@ export interface ConcreteSubagentModel {
|
|
|
30
30
|
modelId: string;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
function toConcrete(ref: string): ConcreteSubagentModel | undefined {
|
|
34
|
+
const parsed = parseModelRef(ref);
|
|
35
|
+
if (!parsed) return undefined;
|
|
36
|
+
return { modelRef: ref, ...parsed };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const WEB_FAST_AGENT_IDS = new Set([
|
|
40
|
+
"harness/web-retrieval/web-query-expander-fast",
|
|
41
|
+
"harness/web-retrieval/web-summarizer",
|
|
42
|
+
"harness/web-retrieval/web-gap-analyzer",
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
const WEB_QUALITY_AGENT_IDS = new Set([
|
|
46
|
+
"harness/web-retrieval/web-answerer",
|
|
47
|
+
"harness/web-retrieval/web-criteria-verifier",
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
function envModelRef(varName: string): string | undefined {
|
|
51
|
+
const v = process.env[varName]?.trim();
|
|
52
|
+
return v && parseModelRef(v) ? v : undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function modelFromEnv(agentName: string): ConcreteSubagentModel | undefined {
|
|
56
|
+
const fast = envModelRef("HARNESS_WEB_FAST_MODEL");
|
|
57
|
+
if (fast && WEB_FAST_AGENT_IDS.has(agentName)) return toConcrete(fast);
|
|
58
|
+
const expander = envModelRef("HARNESS_WEB_EXPANDER_MODEL");
|
|
59
|
+
if (expander && agentName === "harness/web-retrieval/web-query-expander") return toConcrete(expander);
|
|
60
|
+
const quality = envModelRef("HARNESS_WEB_QUALITY_MODEL");
|
|
61
|
+
if (quality && WEB_QUALITY_AGENT_IDS.has(agentName)) return toConcrete(quality);
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
33
65
|
export function resolveConcreteSubagentModel(
|
|
34
66
|
_parentCwd: string,
|
|
35
67
|
parentModel: { provider: string; id: string } | undefined,
|
|
36
68
|
agent: AgentConfig,
|
|
37
69
|
_taskSnippet?: string,
|
|
38
70
|
): ConcreteSubagentModel | undefined {
|
|
71
|
+
const envOverride = modelFromEnv(agent.name);
|
|
72
|
+
if (envOverride) return envOverride;
|
|
73
|
+
|
|
39
74
|
if (agent.model) {
|
|
40
|
-
const
|
|
41
|
-
if (
|
|
42
|
-
return { modelRef: agent.model, ...parsed };
|
|
43
|
-
}
|
|
75
|
+
const concrete = toConcrete(agent.model);
|
|
76
|
+
if (concrete) return concrete;
|
|
44
77
|
}
|
|
45
78
|
|
|
46
79
|
if (!parentModel || parentModel.provider === "router") return undefined;
|
|
47
|
-
|
|
48
|
-
const parsed = parseModelRef(modelRef);
|
|
49
|
-
if (!parsed) return undefined;
|
|
50
|
-
return { modelRef, ...parsed };
|
|
80
|
+
return toConcrete(`${parentModel.provider}/${parentModel.id}`);
|
|
51
81
|
}
|
|
@@ -35,6 +35,10 @@ import {
|
|
|
35
35
|
recordSpawnStart,
|
|
36
36
|
} from "./harness-spawn-budget.js";
|
|
37
37
|
import { parseSpawnContextFromTask } from "./harness-spawn-parse.js";
|
|
38
|
+
import {
|
|
39
|
+
getRememberedSessionWebArtifactDir,
|
|
40
|
+
resolveWebArtifactScope,
|
|
41
|
+
} from "./harness-web/artifacts.js";
|
|
38
42
|
import {
|
|
39
43
|
isUsableApiKey,
|
|
40
44
|
resolveConcreteSubagentModel,
|
|
@@ -130,6 +134,23 @@ export function createHarnessSubagentsExtension(
|
|
|
130
134
|
HARNESS_PKG_ROOT: packageRoot,
|
|
131
135
|
HARNESS_PROJECT_ROOT: projectRoot,
|
|
132
136
|
};
|
|
137
|
+
if (agent.name.startsWith("harness/web-retrieval/")) {
|
|
138
|
+
const ctx = parseSpawnContextFromTask(task);
|
|
139
|
+
const remembered = getRememberedSessionWebArtifactDir(lastSessionId);
|
|
140
|
+
if (remembered) {
|
|
141
|
+
base.HARNESS_WEB_ARTIFACT_DIR = remembered;
|
|
142
|
+
} else if (ctx?.run_id) {
|
|
143
|
+
base.HARNESS_WEB_ARTIFACT_DIR = resolveWebArtifactScope({
|
|
144
|
+
projectRoot,
|
|
145
|
+
explicitArtifactDir: `.web/runs/${ctx.run_id}`,
|
|
146
|
+
}).artifactDir;
|
|
147
|
+
} else {
|
|
148
|
+
base.HARNESS_WEB_ARTIFACT_DIR = resolveWebArtifactScope({
|
|
149
|
+
projectRoot,
|
|
150
|
+
piSessionId: lastSessionId,
|
|
151
|
+
}).artifactDir;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
133
154
|
const ctx = parseSpawnContextFromTask(task);
|
|
134
155
|
if (!ctx?.run_id) return base;
|
|
135
156
|
if (spawnCircuitOpen(ctx.run_id)) {
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WRS workspace paths — flat `.web/` aliases + optional per-run/session isolation.
|
|
3
|
+
* Search/fetch payloads are pooled under `.web/cache/` (see cache.ts).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { activeRunPointerPath } from "../harness-run-context.js";
|
|
9
|
+
import { WEB_ROOT, webCacheHint } from "./cache.js";
|
|
10
|
+
|
|
11
|
+
export type WebArtifactScopeSource =
|
|
12
|
+
| "explicit"
|
|
13
|
+
| "run"
|
|
14
|
+
| "session"
|
|
15
|
+
| "workspace";
|
|
16
|
+
|
|
17
|
+
export interface WebArtifactScope {
|
|
18
|
+
/** Relative path under repo root, e.g. `.web` or `.web/runs/abc` */
|
|
19
|
+
artifactDir: string;
|
|
20
|
+
scopeId: string;
|
|
21
|
+
source: WebArtifactScopeSource;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function webIsolateEnabled(): boolean {
|
|
25
|
+
return (
|
|
26
|
+
process.env.HARNESS_WEB_ISOLATE === "1" ||
|
|
27
|
+
process.env.HARNESS_WEB_LEGACY_SCOPE === "1"
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Parent session → last resolved artifact dir (for web-retrieval subagent env). */
|
|
32
|
+
const sessionArtifactDirs = new Map<string, string>();
|
|
33
|
+
|
|
34
|
+
const CANONICAL_BASENAMES = new Set([
|
|
35
|
+
"angles.yaml",
|
|
36
|
+
"angles-inline.yaml",
|
|
37
|
+
"search-deep.json",
|
|
38
|
+
"search.json",
|
|
39
|
+
"evidence-bundle.json",
|
|
40
|
+
"answer.md",
|
|
41
|
+
"highlights.json",
|
|
42
|
+
"page.md",
|
|
43
|
+
"map.json",
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
export function sanitizeWebScopeId(id: string): string {
|
|
47
|
+
return id.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 120);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function isScopedWebArtifactPath(path: string): boolean {
|
|
51
|
+
const n = path.replace(/\\/g, "/");
|
|
52
|
+
if (!n.startsWith(`${WEB_ROOT}/`)) return false;
|
|
53
|
+
const rest = n.slice(`${WEB_ROOT}/`.length);
|
|
54
|
+
const top = rest.split("/")[0];
|
|
55
|
+
return top === "runs" || top === "sessions";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readActiveHarnessRunId(projectRoot: string): string | null {
|
|
59
|
+
const pointerPath = activeRunPointerPath(projectRoot);
|
|
60
|
+
if (!existsSync(pointerPath)) return null;
|
|
61
|
+
try {
|
|
62
|
+
const raw = readFileSync(pointerPath, "utf-8");
|
|
63
|
+
const data = JSON.parse(raw) as { run_id?: string };
|
|
64
|
+
const runId = data.run_id?.trim();
|
|
65
|
+
return runId || null;
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function resolveWebArtifactScope(options: {
|
|
72
|
+
projectRoot: string;
|
|
73
|
+
piSessionId?: string;
|
|
74
|
+
explicitScope?: string;
|
|
75
|
+
explicitArtifactDir?: string;
|
|
76
|
+
}): WebArtifactScope {
|
|
77
|
+
const explicitDir =
|
|
78
|
+
options.explicitArtifactDir?.trim() ||
|
|
79
|
+
options.explicitScope?.trim() ||
|
|
80
|
+
process.env.HARNESS_WEB_ARTIFACT_DIR?.trim() ||
|
|
81
|
+
process.env.HARNESS_WEB_SCOPE?.trim();
|
|
82
|
+
if (explicitDir) {
|
|
83
|
+
const normalized = normalizeArtifactDir(explicitDir);
|
|
84
|
+
return {
|
|
85
|
+
artifactDir: normalized,
|
|
86
|
+
scopeId: normalized.split("/").pop() ?? normalized,
|
|
87
|
+
source: "explicit",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (webIsolateEnabled()) {
|
|
92
|
+
const runId =
|
|
93
|
+
process.env.HARNESS_RUN_ID?.trim() ||
|
|
94
|
+
readActiveHarnessRunId(options.projectRoot);
|
|
95
|
+
if (runId) {
|
|
96
|
+
const id = sanitizeWebScopeId(runId);
|
|
97
|
+
return {
|
|
98
|
+
artifactDir: `${WEB_ROOT}/runs/${id}`,
|
|
99
|
+
scopeId: id,
|
|
100
|
+
source: "run",
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const sessionId = options.piSessionId?.trim();
|
|
105
|
+
if (sessionId) {
|
|
106
|
+
const id = sanitizeWebScopeId(sessionId);
|
|
107
|
+
return {
|
|
108
|
+
artifactDir: `${WEB_ROOT}/sessions/${id}`,
|
|
109
|
+
scopeId: id,
|
|
110
|
+
source: "session",
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
artifactDir: WEB_ROOT,
|
|
117
|
+
scopeId: "workspace",
|
|
118
|
+
source: "workspace",
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function normalizeArtifactDir(dir: string): string {
|
|
123
|
+
let n = dir.replace(/\\/g, "/").trim();
|
|
124
|
+
if (n.startsWith("./")) n = n.slice(2);
|
|
125
|
+
if (n === WEB_ROOT || n === `${WEB_ROOT}/`) return WEB_ROOT;
|
|
126
|
+
if (!n.startsWith(`${WEB_ROOT}/`)) {
|
|
127
|
+
n = `${WEB_ROOT}/${n.replace(/^\/+/, "")}`;
|
|
128
|
+
}
|
|
129
|
+
return n.replace(/\/+$/, "");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function scopedWebArtifactPath(
|
|
133
|
+
artifactDir: string,
|
|
134
|
+
basename: string,
|
|
135
|
+
): string {
|
|
136
|
+
const base = normalizeArtifactDir(artifactDir);
|
|
137
|
+
if (base === WEB_ROOT) return `${WEB_ROOT}/${basename}`;
|
|
138
|
+
return `${base}/${basename}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Resolve output path: honor explicit paths; optional isolation rewrites flat canonical names.
|
|
143
|
+
*/
|
|
144
|
+
export function resolveWebOutputPath(options: {
|
|
145
|
+
projectRoot: string;
|
|
146
|
+
piSessionId?: string;
|
|
147
|
+
basename: string;
|
|
148
|
+
explicitOutput?: string;
|
|
149
|
+
webScope?: string;
|
|
150
|
+
}): { path: string; artifactDir: string; scope: WebArtifactScope } {
|
|
151
|
+
const scope = resolveWebArtifactScope({
|
|
152
|
+
projectRoot: options.projectRoot,
|
|
153
|
+
piSessionId: options.piSessionId,
|
|
154
|
+
explicitScope: options.webScope,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const explicit = options.explicitOutput?.trim();
|
|
158
|
+
if (explicit) {
|
|
159
|
+
const norm = explicit.replace(/\\/g, "/");
|
|
160
|
+
if (isScopedWebArtifactPath(norm)) {
|
|
161
|
+
const artifactDir = norm.slice(0, norm.lastIndexOf("/"));
|
|
162
|
+
return { path: norm, artifactDir, scope };
|
|
163
|
+
}
|
|
164
|
+
const base = norm.split("/").pop() ?? norm;
|
|
165
|
+
if (
|
|
166
|
+
webIsolateEnabled() &&
|
|
167
|
+
scope.source !== "workspace" &&
|
|
168
|
+
norm.startsWith(`${WEB_ROOT}/`) &&
|
|
169
|
+
CANONICAL_BASENAMES.has(base)
|
|
170
|
+
) {
|
|
171
|
+
const path = scopedWebArtifactPath(scope.artifactDir, base);
|
|
172
|
+
return { path, artifactDir: scope.artifactDir, scope };
|
|
173
|
+
}
|
|
174
|
+
return { path: norm, artifactDir: scope.artifactDir, scope };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const path = scopedWebArtifactPath(scope.artifactDir, options.basename);
|
|
178
|
+
return { path, artifactDir: scope.artifactDir, scope };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function rememberSessionWebArtifactDir(
|
|
182
|
+
sessionId: string,
|
|
183
|
+
artifactDir: string,
|
|
184
|
+
): void {
|
|
185
|
+
if (!sessionId?.trim() || !artifactDir?.trim()) return;
|
|
186
|
+
sessionArtifactDirs.set(sessionId.trim(), normalizeArtifactDir(artifactDir));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function getRememberedSessionWebArtifactDir(
|
|
190
|
+
sessionId: string,
|
|
191
|
+
): string | undefined {
|
|
192
|
+
return sessionArtifactDirs.get(sessionId.trim());
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function webArtifactScopeHint(scope: WebArtifactScope): string {
|
|
196
|
+
const isolateNote = webIsolateEnabled()
|
|
197
|
+
? `Isolation on (${scope.artifactDir}/). Set HARNESS_WEB_ISOLATE=0 for shared workspace only.`
|
|
198
|
+
: `Shared workspace ${scope.artifactDir}/ for angles, search-deep, answer.md. Set HARNESS_WEB_ISOLATE=1 to isolate per session/run.`;
|
|
199
|
+
return `[WRS workspace] ${isolateNote} ${webCacheHint()}`;
|
|
200
|
+
}
|