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
@@ -10,19 +10,19 @@ import { claimHarnessGovernanceLoad } from "../lib/extension-load-guard.js";
10
10
  import {
11
11
  rememberSessionWebArtifactDir,
12
12
  resolveWebOutputPath,
13
- webArtifactScopeHint,
14
13
  type WebArtifactScope,
14
+ webArtifactScopeHint,
15
15
  } from "../lib/harness-web/artifacts.js";
16
16
  import {
17
+ type FetchCacheContext,
17
18
  fingerprintFile,
18
19
  formatCacheAge,
19
20
  lookupFetchCache,
20
21
  lookupSearchCache,
21
22
  publishWorkspaceAlias,
23
+ type SearchCacheContext,
22
24
  writeFetchCacheEntry,
23
25
  writeSearchCacheEntry,
24
- type FetchCacheContext,
25
- type SearchCacheContext,
26
26
  } from "../lib/harness-web/cache.js";
27
27
  import {
28
28
  harnessWebContextLine,
@@ -32,7 +32,6 @@ import {
32
32
  summarizeSearchJson,
33
33
  } from "../lib/harness-web/run-cli.js";
34
34
 
35
- // @ts-expect-error pi extensions run as ESM
36
35
  const MODULE_URL = import.meta.url;
37
36
 
38
37
  const WEB_SEARCH_GUIDELINES = [
@@ -135,7 +134,8 @@ const WebSearchSchema = Type.Object({
135
134
  ),
136
135
  limit: Type.Optional(
137
136
  Type.Number({
138
- description: "Max results (tier defaults: instant 5, standard 10, deep 10)",
137
+ description:
138
+ "Max results (tier defaults: instant 5, standard 10, deep 10)",
139
139
  minimum: 1,
140
140
  maximum: 20,
141
141
  }),
@@ -181,10 +181,14 @@ const WebFetchSchema = Type.Object({
181
181
  }),
182
182
  ),
183
183
  highlightQuery: Type.Optional(
184
- Type.String({ description: "Query for highlight scoring (required if highlights)" }),
184
+ Type.String({
185
+ description: "Query for highlight scoring (required if highlights)",
186
+ }),
185
187
  ),
186
188
  highlightsOutput: Type.Optional(
187
- Type.String({ description: "Highlights JSON path (default .web/highlights.json)" }),
189
+ Type.String({
190
+ description: "Highlights JSON path (default .web/highlights.json)",
191
+ }),
188
192
  ),
189
193
  limit: Type.Optional(
190
194
  Type.Number({
@@ -216,7 +220,9 @@ const WebFindSimilarSchema = Type.Object({
216
220
  const WebContentsSchema = Type.Object({
217
221
  webScope: WebScopeSchema,
218
222
  urls: Type.Optional(
219
- Type.Array(Type.String(), { description: "URLs to fetch (or use fromSearch)" }),
223
+ Type.Array(Type.String(), {
224
+ description: "URLs to fetch (or use fromSearch)",
225
+ }),
220
226
  ),
221
227
  fromSearch: Type.Optional(
222
228
  Type.String({
@@ -315,6 +321,586 @@ function resolveTier(params: { tier?: string; bulk?: boolean }): string {
315
321
  return "deep";
316
322
  }
317
323
 
324
+ function executeWebSearchBulk(args: {
325
+ ctx: any;
326
+ cwd: string;
327
+ webScope: string | undefined;
328
+ query: string;
329
+ limit: number | undefined;
330
+ outputParam: unknown;
331
+ }) {
332
+ const bulkScoped = resolveScopedOutput(
333
+ args.ctx,
334
+ "bulk",
335
+ args.outputParam ? `${args.outputParam}` : undefined,
336
+ args.webScope,
337
+ );
338
+ const output = bulkScoped.output.endsWith("/bulk")
339
+ ? bulkScoped.output
340
+ : `${bulkScoped.artifactDir}/bulk`;
341
+ ensureParentDir(args.cwd, output);
342
+ const lim = args.limit ?? 3;
343
+ const argv = [
344
+ "bulk-scrape",
345
+ args.query,
346
+ "-o",
347
+ output,
348
+ "--limit",
349
+ String(lim),
350
+ ];
351
+ const run = runHarnessWeb(MODULE_URL, argv, args.cwd);
352
+ if (!run.ok) {
353
+ return failResult(
354
+ `web_search bulk failed (exit ${run.exitCode}).\n${run.stderr || run.stdout}`,
355
+ );
356
+ }
357
+ return okResult(
358
+ `${run.stdout}\n\noutput: ${output}\nartifactDir: ${bulkScoped.artifactDir}`,
359
+ {
360
+ output,
361
+ artifactDir: bulkScoped.artifactDir,
362
+ query: args.query,
363
+ bulk: true,
364
+ },
365
+ );
366
+ }
367
+
368
+ function resolveAnglesFile(args: {
369
+ params: any;
370
+ ctx: any;
371
+ cwd: string;
372
+ query: string;
373
+ webScope: string | undefined;
374
+ }): string {
375
+ let anglesFile = String(args.params.anglesFile ?? "").trim();
376
+ if (anglesFile && !anglesFile.startsWith("/") && !anglesFile.includes("..")) {
377
+ anglesFile = resolveScopedOutput(
378
+ args.ctx,
379
+ "angles.yaml",
380
+ anglesFile,
381
+ args.webScope,
382
+ ).output;
383
+ }
384
+ if (args.params.angles?.length && !anglesFile) {
385
+ const inline = resolveScopedOutput(
386
+ args.ctx,
387
+ "angles-inline.yaml",
388
+ undefined,
389
+ args.webScope,
390
+ );
391
+ const tmp = resolve(args.cwd, inline.output);
392
+ ensureParentDir(args.cwd, inline.output);
393
+ const yaml =
394
+ `intent: ${JSON.stringify(args.query)}\nangles:\n` +
395
+ args.params.angles
396
+ .map(
397
+ (q: string, i: number) =>
398
+ ` - id: angle_${i + 1}\n query: ${JSON.stringify(q)}`,
399
+ )
400
+ .join("\n") +
401
+ "\n";
402
+ writeFileSync(tmp, yaml, "utf-8");
403
+ anglesFile = inline.output;
404
+ }
405
+ return anglesFile;
406
+ }
407
+
408
+ function tryWebSearchCacheHit(args: {
409
+ refreshCache: boolean;
410
+ cwd: string;
411
+ searchCtx: SearchCacheContext;
412
+ maxAgeSec?: number;
413
+ basename: string;
414
+ scopedArtifactDir: string;
415
+ tier: string;
416
+ query: string;
417
+ engine: string;
418
+ }): ReturnType<typeof okResult> | null {
419
+ if (args.refreshCache) return null;
420
+ const cached = lookupSearchCache(args.cwd, args.searchCtx, {
421
+ maxAgeSec: args.maxAgeSec,
422
+ });
423
+ if (!(cached.hit && !cached.stale)) return null;
424
+ const workspaceOutput = publishWorkspaceAlias(
425
+ args.cwd,
426
+ cached.artifactPath,
427
+ args.basename,
428
+ );
429
+ const parts = [
430
+ `[cache hit] age ${formatCacheAge(cached.ageMs)} · key ${cached.cacheKey}`,
431
+ `cache: ${cached.entryDir}`,
432
+ ];
433
+ const summary =
434
+ args.tier === "deep" || args.tier === "research"
435
+ ? summarizeDeepSearchJson(workspaceOutput, args.cwd)
436
+ : summarizeSearchJson(workspaceOutput, args.cwd);
437
+ if (summary) parts.push("", summary);
438
+ parts.push(
439
+ "",
440
+ `output: ${workspaceOutput}`,
441
+ `artifactDir: ${args.scopedArtifactDir}`,
442
+ `tier: ${args.tier}`,
443
+ );
444
+ parts.push("Read output JSON; web_fetch top URLs with highlights:true.");
445
+ return okResult(parts.join("\n"), {
446
+ output: workspaceOutput,
447
+ artifactDir: args.scopedArtifactDir,
448
+ query: args.query,
449
+ tier: args.tier,
450
+ engine: args.engine,
451
+ cacheHit: true,
452
+ cacheKey: cached.cacheKey,
453
+ cachePath: cached.artifactPath,
454
+ cacheAgeMs: cached.ageMs,
455
+ });
456
+ }
457
+
458
+ function buildWebSearchArgv(args: {
459
+ tier: string;
460
+ query: string;
461
+ output: string;
462
+ resultLimit: number;
463
+ anglesFile: string;
464
+ expandHeuristic: boolean;
465
+ category?: string;
466
+ limit?: number;
467
+ }): string[] {
468
+ if (args.tier === "deep" || args.tier === "research") {
469
+ const argv = [
470
+ "search-deep",
471
+ args.query,
472
+ "-o",
473
+ args.output,
474
+ "--limit",
475
+ String(args.resultLimit),
476
+ ];
477
+ if (args.anglesFile) {
478
+ argv.push("--angles-file", args.anglesFile);
479
+ } else if (args.expandHeuristic) {
480
+ argv.push("--expand-heuristic");
481
+ }
482
+ if (args.category) argv.push("--category", args.category);
483
+ return argv;
484
+ }
485
+ return [
486
+ "search",
487
+ args.query,
488
+ "-o",
489
+ args.output,
490
+ "--tier",
491
+ args.tier,
492
+ ...(args.limit != null ? ["--limit", String(args.limit)] : []),
493
+ ];
494
+ }
495
+
496
+ async function executeWebSearch(params: any, ctx: any) {
497
+ const cwd = sessionCwd(ctx);
498
+ const webScope = String(params.webScope ?? "").trim() || undefined;
499
+ const query = String(params.query ?? "").trim();
500
+ if (!query) return failResult("web_search: query is required.");
501
+
502
+ const tier = resolveTier(params);
503
+ const bulk = params.bulk === true;
504
+ const limit = typeof params.limit === "number" ? params.limit : undefined;
505
+
506
+ if (bulk) {
507
+ return executeWebSearchBulk({
508
+ ctx,
509
+ cwd,
510
+ webScope,
511
+ query,
512
+ limit,
513
+ outputParam: params.output,
514
+ });
515
+ }
516
+
517
+ const basename =
518
+ tier === "deep" || tier === "research" ? "search-deep.json" : "search.json";
519
+ const scoped = resolveScopedOutput(
520
+ ctx,
521
+ basename,
522
+ params.output ? String(params.output) : undefined,
523
+ webScope,
524
+ );
525
+ const output = scoped.output;
526
+ ensureParentDir(cwd, output);
527
+ const { refresh: refreshCache, maxAgeSec } = cacheControlFromParams(params);
528
+ const engine = searchEngineId();
529
+ const resultLimit = limit ?? 10;
530
+ const category = params.category ? String(params.category) : undefined;
531
+
532
+ const anglesFile = resolveAnglesFile({
533
+ params,
534
+ ctx,
535
+ cwd,
536
+ query,
537
+ webScope,
538
+ });
539
+
540
+ if (
541
+ (tier === "deep" || tier === "research") &&
542
+ !anglesFile &&
543
+ params.expandHeuristic !== true &&
544
+ !params.angles?.length
545
+ ) {
546
+ return failResult(
547
+ "web_search tier=deep requires anglesFile (.web/angles.yaml from harness/web-retrieval/web-query-expander) " +
548
+ "or expandHeuristic:true. Invoke web-retrieval skill first.",
549
+ );
550
+ }
551
+
552
+ const anglesFingerprint = anglesFile
553
+ ? fingerprintFile(cwd, anglesFile)
554
+ : undefined;
555
+
556
+ const searchCtx: SearchCacheContext = {
557
+ query,
558
+ tier,
559
+ engine,
560
+ limit: resultLimit,
561
+ category,
562
+ expandHeuristic: params.expandHeuristic === true,
563
+ anglesFingerprint,
564
+ };
565
+
566
+ const cacheHit = tryWebSearchCacheHit({
567
+ refreshCache,
568
+ cwd,
569
+ searchCtx,
570
+ maxAgeSec,
571
+ basename,
572
+ scopedArtifactDir: scoped.artifactDir,
573
+ tier,
574
+ query,
575
+ engine,
576
+ });
577
+ if (cacheHit) return cacheHit;
578
+
579
+ const argv = buildWebSearchArgv({
580
+ tier,
581
+ query,
582
+ output,
583
+ resultLimit,
584
+ anglesFile,
585
+ expandHeuristic: params.expandHeuristic === true,
586
+ category,
587
+ limit,
588
+ });
589
+
590
+ const run = runHarnessWeb(MODULE_URL, argv, cwd);
591
+ if (!run.ok) {
592
+ const hint =
593
+ "\n\nHints: run /harness-setup; for searxng set HARNESS_WEB_SEARXNG_URL; " +
594
+ "enable json in SearXNG search.formats; for deep spawn web-query-expander first.";
595
+ return failResult(
596
+ `web_search failed (exit ${run.exitCode}).\n${run.stderr || run.stdout}${hint}`,
597
+ );
598
+ }
599
+
600
+ const cacheWrite = writeSearchCacheEntry(cwd, searchCtx, output, {
601
+ anglesPath: anglesFile,
602
+ });
603
+ publishWorkspaceAlias(cwd, `${cacheWrite.entryDir}/${basename}`, basename);
604
+
605
+ const parts = [run.stdout];
606
+ const summary =
607
+ tier === "deep" || tier === "research"
608
+ ? summarizeDeepSearchJson(output, cwd)
609
+ : summarizeSearchJson(output, cwd);
610
+ if (summary) parts.push("", summary);
611
+ parts.push(
612
+ "",
613
+ `output: ${output}`,
614
+ `artifactDir: ${scoped.artifactDir}`,
615
+ `tier: ${tier}`,
616
+ `cache: ${cacheWrite.entryDir}`,
617
+ );
618
+ parts.push("Read output JSON; web_fetch top URLs with highlights:true.");
619
+
620
+ return okResult(parts.join("\n"), {
621
+ output,
622
+ artifactDir: scoped.artifactDir,
623
+ query,
624
+ tier,
625
+ engine,
626
+ cacheHit: false,
627
+ cacheKey: cacheWrite.cacheKey,
628
+ cachePath: `${cacheWrite.entryDir}/${basename}`,
629
+ });
630
+ }
631
+
632
+ async function executeWebFetch(params: any, ctx: any) {
633
+ const cwd = sessionCwd(ctx);
634
+ const webScope = String(params.webScope ?? "").trim() || undefined;
635
+ const url = String(params.url ?? "").trim();
636
+ if (!url) return failResult("web_fetch: url is required.");
637
+
638
+ const mode = params.mode === "map" ? "map" : "scrape";
639
+ const fast = params.fast === true;
640
+ const limit = typeof params.limit === "number" ? params.limit : 100;
641
+ const basename = mode === "map" ? "map.json" : "page.md";
642
+ const scoped = resolveScopedOutput(
643
+ ctx,
644
+ basename,
645
+ params.output ? String(params.output) : undefined,
646
+ webScope,
647
+ );
648
+ const output = scoped.output;
649
+ ensureParentDir(cwd, output);
650
+ const highlights = params.highlights === true;
651
+ const hlQuery = String(params.highlightQuery ?? "").trim();
652
+ const { refresh: refreshCache, maxAgeSec } = cacheControlFromParams(params);
653
+
654
+ const hlScoped =
655
+ highlights && !params.highlightsOutput
656
+ ? resolveScopedOutput(ctx, "highlights.json", undefined, webScope)
657
+ : highlights
658
+ ? resolveScopedOutput(
659
+ ctx,
660
+ "highlights.json",
661
+ String(params.highlightsOutput),
662
+ webScope,
663
+ )
664
+ : undefined;
665
+ if (hlScoped) ensureParentDir(cwd, hlScoped.output);
666
+
667
+ const fetchCtx: FetchCacheContext = {
668
+ url,
669
+ mode,
670
+ fast,
671
+ highlightQuery: hlQuery || undefined,
672
+ highlights,
673
+ };
674
+
675
+ if (!refreshCache) {
676
+ const cached = lookupFetchCache(cwd, fetchCtx, { maxAgeSec });
677
+ if (cached.hit && !cached.stale) {
678
+ const workspaceBasename = highlights
679
+ ? "highlights.json"
680
+ : mode === "map"
681
+ ? "map.json"
682
+ : "page.md";
683
+ const workspaceOutput = publishWorkspaceAlias(
684
+ cwd,
685
+ cached.artifactPath,
686
+ workspaceBasename,
687
+ );
688
+ const parts = [
689
+ `[cache hit] age ${formatCacheAge(cached.ageMs)} · key ${cached.cacheKey}`,
690
+ `cache: ${cached.entryDir}`,
691
+ "",
692
+ `output: ${workspaceOutput}`,
693
+ `artifactDir: ${scoped.artifactDir}`,
694
+ ];
695
+ const excerpt = readTextExcerpt(workspaceOutput, cwd);
696
+ if (excerpt) parts.push("", "--- excerpt ---", excerpt);
697
+ return okResult(parts.join("\n"), {
698
+ output: workspaceOutput,
699
+ artifactDir: scoped.artifactDir,
700
+ url,
701
+ mode,
702
+ highlights,
703
+ cacheHit: true,
704
+ cacheKey: cached.cacheKey,
705
+ cachePath: cached.artifactPath,
706
+ });
707
+ }
708
+ }
709
+
710
+ let argv: string[];
711
+ if (mode === "map") {
712
+ argv = [
713
+ "map",
714
+ url,
715
+ "-o",
716
+ output,
717
+ "--limit",
718
+ String(limit),
719
+ ...(fast ? ["--fast"] : []),
720
+ ];
721
+ } else {
722
+ argv = ["scrape", url, "-o", output, ...(fast ? ["--fast"] : [])];
723
+ if (highlights) {
724
+ if (!hlQuery) {
725
+ return failResult(
726
+ "web_fetch: highlightQuery required when highlights=true",
727
+ );
728
+ }
729
+ argv.push("--highlights", "--highlight-query", hlQuery);
730
+ if (hlScoped) argv.push("--highlights-output", hlScoped.output);
731
+ }
732
+ }
733
+
734
+ const run = runHarnessWeb(MODULE_URL, argv, cwd);
735
+ if (!run.ok) {
736
+ return failResult(
737
+ `web_fetch failed (exit ${run.exitCode}).\n${run.stderr || run.stdout}\n` +
738
+ "Try fast:true for static pages, or run harness-cli-verify for Scrapling install.",
739
+ );
740
+ }
741
+
742
+ const cacheArtifact = highlights && hlScoped ? hlScoped.output : output;
743
+ const cacheWrite = writeFetchCacheEntry(cwd, fetchCtx, cacheArtifact, {
744
+ highlightsPath:
745
+ highlights && hlScoped && hlScoped.output !== cacheArtifact
746
+ ? hlScoped.output
747
+ : undefined,
748
+ });
749
+ const workspaceBasename = highlights
750
+ ? "highlights.json"
751
+ : mode === "map"
752
+ ? "map.json"
753
+ : "page.md";
754
+ publishWorkspaceAlias(
755
+ cwd,
756
+ `${cacheWrite.entryDir}/${workspaceBasename}`,
757
+ workspaceBasename,
758
+ );
759
+
760
+ const parts = [
761
+ run.stdout,
762
+ "",
763
+ `output: ${output}`,
764
+ `artifactDir: ${scoped.artifactDir}`,
765
+ `cache: ${cacheWrite.entryDir}`,
766
+ ];
767
+ const excerpt = readTextExcerpt(output, cwd);
768
+ if (excerpt) parts.push("", "--- excerpt ---", excerpt);
769
+
770
+ return okResult(parts.join("\n"), {
771
+ output,
772
+ artifactDir: scoped.artifactDir,
773
+ url,
774
+ mode,
775
+ highlights,
776
+ cacheHit: false,
777
+ cacheKey: cacheWrite.cacheKey,
778
+ cachePath: `${cacheWrite.entryDir}/${workspaceBasename}`,
779
+ });
780
+ }
781
+
782
+ async function executeWebFindSimilar(params: any, ctx: any) {
783
+ const cwd = sessionCwd(ctx);
784
+ const webScope = String(params.webScope ?? "").trim() || undefined;
785
+ const url = String(params.url ?? "").trim();
786
+ if (!url) return failResult("web_find_similar: url is required.");
787
+
788
+ const scoped = resolveScopedOutput(
789
+ ctx,
790
+ "search-deep.json",
791
+ params.output ? String(params.output) : undefined,
792
+ webScope,
793
+ );
794
+ const output = scoped.output;
795
+ ensureParentDir(cwd, output);
796
+ const limit = typeof params.limit === "number" ? params.limit : 10;
797
+ const argv = [
798
+ "find-similar",
799
+ url,
800
+ "-o",
801
+ output,
802
+ "--limit",
803
+ String(limit),
804
+ ...(params.fast !== false ? ["--fast"] : []),
805
+ ];
806
+
807
+ const run = runHarnessWeb(MODULE_URL, argv, cwd);
808
+ if (!run.ok) {
809
+ return failResult(
810
+ `web_find_similar failed (exit ${run.exitCode}).\n${run.stderr || run.stdout}`,
811
+ );
812
+ }
813
+
814
+ const parts = [run.stdout];
815
+ const summary = summarizeDeepSearchJson(output, cwd);
816
+ if (summary) parts.push("", summary);
817
+ parts.push("", `output: ${output}`, `artifactDir: ${scoped.artifactDir}`);
818
+
819
+ return okResult(parts.join("\n"), {
820
+ output,
821
+ artifactDir: scoped.artifactDir,
822
+ url,
823
+ });
824
+ }
825
+
826
+ async function executeWebContents(params: any, ctx: any) {
827
+ const cwd = sessionCwd(ctx);
828
+ const webScope = String(params.webScope ?? "").trim() || undefined;
829
+ const dirScoped = resolveScopedOutput(
830
+ ctx,
831
+ "contents",
832
+ params.outputDir ? String(params.outputDir) : undefined,
833
+ webScope,
834
+ );
835
+ const outputDir = dirScoped.output.endsWith("/contents")
836
+ ? dirScoped.output
837
+ : `${dirScoped.artifactDir}/contents`;
838
+ mkdirSync(resolve(cwd, outputDir), { recursive: true });
839
+ let fromSearch = String(params.fromSearch ?? "").trim();
840
+ if (fromSearch && !fromSearch.startsWith("/") && !fromSearch.includes("..")) {
841
+ fromSearch = resolveScopedOutput(
842
+ ctx,
843
+ "search-deep.json",
844
+ fromSearch,
845
+ webScope,
846
+ ).output;
847
+ }
848
+ const urls = (params.urls ?? [])
849
+ .map((u: unknown) => String(u).trim())
850
+ .filter(Boolean);
851
+ const limit = typeof params.limit === "number" ? params.limit : 5;
852
+ const hlQuery = String(params.highlightQuery ?? "").trim();
853
+
854
+ const argv = [
855
+ "contents-batch",
856
+ "-o",
857
+ outputDir,
858
+ "--limit",
859
+ String(limit),
860
+ ...(params.fast ? ["--fast"] : []),
861
+ ...(params.highlights && hlQuery
862
+ ? ["--highlights", "--highlight-query", hlQuery]
863
+ : []),
864
+ ...urls,
865
+ ];
866
+ if (fromSearch) {
867
+ argv.splice(1, 0, "--from-search", fromSearch);
868
+ }
869
+ let evidencePath: string | undefined;
870
+ if (params.evidenceBundle && fromSearch) {
871
+ const bundleArg = String(params.evidenceBundle);
872
+ evidencePath =
873
+ bundleArg.startsWith("/") || bundleArg.includes("..")
874
+ ? bundleArg
875
+ : resolveScopedOutput(ctx, "evidence-bundle.json", bundleArg, webScope)
876
+ .output;
877
+ ensureParentDir(cwd, evidencePath);
878
+ argv.push("--evidence-bundle", evidencePath);
879
+ }
880
+
881
+ if (!fromSearch && !urls.length) {
882
+ return failResult("web_contents: provide urls or fromSearch");
883
+ }
884
+
885
+ const run = runHarnessWeb(MODULE_URL, argv, cwd);
886
+ if (!run.ok) {
887
+ return failResult(
888
+ `web_contents failed (exit ${run.exitCode}).\n${run.stderr || run.stdout}`,
889
+ );
890
+ }
891
+
892
+ return okResult(
893
+ `${run.stdout}\n\noutputDir: ${outputDir}\nartifactDir: ${dirScoped.artifactDir}` +
894
+ (evidencePath ? `\nevidence: ${evidencePath}` : ""),
895
+ {
896
+ outputDir,
897
+ artifactDir: dirScoped.artifactDir,
898
+ fromSearch,
899
+ evidenceBundle: evidencePath,
900
+ },
901
+ );
902
+ }
903
+
318
904
  export default function harnessWebTools(pi: ExtensionAPI) {
319
905
  if (!claimHarnessGovernanceLoad("harness-web-tools", MODULE_URL)) return;
320
906
  pi.on("before_agent_start", async (event, ctx) => {
@@ -342,210 +928,7 @@ export default function harnessWebTools(pi: ExtensionAPI) {
342
928
  parameters: WebSearchSchema,
343
929
 
344
930
  async execute(_id, params, _signal, _onUpdate, ctx) {
345
- const cwd = sessionCwd(ctx);
346
- const webScope = String(params.webScope ?? "").trim() || undefined;
347
- const query = String(params.query ?? "").trim();
348
- if (!query) return failResult("web_search: query is required.");
349
-
350
- const tier = resolveTier(params);
351
- const bulk = params.bulk === true;
352
- const limit = typeof params.limit === "number" ? params.limit : undefined;
353
-
354
- if (bulk) {
355
- const bulkScoped = resolveScopedOutput(
356
- ctx,
357
- "bulk",
358
- params.output ? `${params.output}` : undefined,
359
- webScope,
360
- );
361
- const output = bulkScoped.output.endsWith("/bulk")
362
- ? bulkScoped.output
363
- : `${bulkScoped.artifactDir}/bulk`;
364
- ensureParentDir(cwd, output);
365
- const lim = limit ?? 3;
366
- const argv = ["bulk-scrape", query, "-o", output, "--limit", String(lim)];
367
- const run = runHarnessWeb(MODULE_URL, argv, cwd);
368
- if (!run.ok) {
369
- return failResult(
370
- `web_search bulk failed (exit ${run.exitCode}).\n${run.stderr || run.stdout}`,
371
- );
372
- }
373
- return okResult(
374
- `${run.stdout}\n\noutput: ${output}\nartifactDir: ${bulkScoped.artifactDir}`,
375
- { output, artifactDir: bulkScoped.artifactDir, query, bulk: true },
376
- );
377
- }
378
-
379
- const basename =
380
- tier === "deep" || tier === "research" ? "search-deep.json" : "search.json";
381
- const scoped = resolveScopedOutput(
382
- ctx,
383
- basename,
384
- params.output ? String(params.output) : undefined,
385
- webScope,
386
- );
387
- const output = scoped.output;
388
- ensureParentDir(cwd, output);
389
- const { refresh: refreshCache, maxAgeSec } = cacheControlFromParams(params);
390
- const engine = searchEngineId();
391
- const resultLimit = limit ?? 10;
392
- const category = params.category ? String(params.category) : undefined;
393
-
394
- let anglesFile = String(params.anglesFile ?? "").trim();
395
- if (anglesFile && !anglesFile.startsWith("/") && !anglesFile.includes("..")) {
396
- anglesFile = resolveScopedOutput(ctx, "angles.yaml", anglesFile, webScope).output;
397
- }
398
- if (params.angles?.length && !anglesFile) {
399
- const inline = resolveScopedOutput(ctx, "angles-inline.yaml", undefined, webScope);
400
- const tmp = resolve(cwd, inline.output);
401
- ensureParentDir(cwd, inline.output);
402
- const yaml =
403
- `intent: ${JSON.stringify(query)}\nangles:\n` +
404
- params.angles
405
- .map(
406
- (q, i) =>
407
- ` - id: angle_${i + 1}\n query: ${JSON.stringify(q)}`,
408
- )
409
- .join("\n") +
410
- "\n";
411
- writeFileSync(tmp, yaml, "utf-8");
412
- anglesFile = inline.output;
413
- }
414
-
415
- if (
416
- (tier === "deep" || tier === "research") &&
417
- !anglesFile &&
418
- params.expandHeuristic !== true &&
419
- !params.angles?.length
420
- ) {
421
- return failResult(
422
- "web_search tier=deep requires anglesFile (.web/angles.yaml from harness/web-retrieval/web-query-expander) " +
423
- "or expandHeuristic:true. Invoke web-retrieval skill first.",
424
- );
425
- }
426
-
427
- const anglesFingerprint = anglesFile
428
- ? fingerprintFile(cwd, anglesFile)
429
- : undefined;
430
-
431
- const searchCtx: SearchCacheContext = {
432
- query,
433
- tier,
434
- engine,
435
- limit: resultLimit,
436
- category,
437
- expandHeuristic: params.expandHeuristic === true,
438
- anglesFingerprint,
439
- };
440
-
441
- if (!refreshCache) {
442
- const cached = lookupSearchCache(cwd, searchCtx, { maxAgeSec });
443
- if (cached.hit && !cached.stale) {
444
- const workspaceOutput = publishWorkspaceAlias(
445
- cwd,
446
- cached.artifactPath,
447
- basename,
448
- );
449
- const parts = [
450
- `[cache hit] age ${formatCacheAge(cached.ageMs)} · key ${cached.cacheKey}`,
451
- `cache: ${cached.entryDir}`,
452
- ];
453
- const summary =
454
- tier === "deep" || tier === "research"
455
- ? summarizeDeepSearchJson(workspaceOutput, cwd)
456
- : summarizeSearchJson(workspaceOutput, cwd);
457
- if (summary) parts.push("", summary);
458
- parts.push(
459
- "",
460
- `output: ${workspaceOutput}`,
461
- `artifactDir: ${scoped.artifactDir}`,
462
- `tier: ${tier}`,
463
- );
464
- parts.push("Read output JSON; web_fetch top URLs with highlights:true.");
465
- return okResult(parts.join("\n"), {
466
- output: workspaceOutput,
467
- artifactDir: scoped.artifactDir,
468
- query,
469
- tier,
470
- engine,
471
- cacheHit: true,
472
- cacheKey: cached.cacheKey,
473
- cachePath: cached.artifactPath,
474
- cacheAgeMs: cached.ageMs,
475
- });
476
- }
477
- }
478
-
479
- let argv: string[];
480
- if (tier === "deep" || tier === "research") {
481
- argv = [
482
- "search-deep",
483
- query,
484
- "-o",
485
- output,
486
- "--limit",
487
- String(resultLimit),
488
- ];
489
- if (anglesFile) {
490
- argv.push("--angles-file", anglesFile);
491
- } else if (params.expandHeuristic === true) {
492
- argv.push("--expand-heuristic");
493
- }
494
- if (category) {
495
- argv.push("--category", category);
496
- }
497
- } else {
498
- argv = [
499
- "search",
500
- query,
501
- "-o",
502
- output,
503
- "--tier",
504
- tier,
505
- ...(limit != null ? ["--limit", String(limit)] : []),
506
- ];
507
- }
508
-
509
- const run = runHarnessWeb(MODULE_URL, argv, cwd);
510
- if (!run.ok) {
511
- const hint =
512
- "\n\nHints: run /harness-setup; for searxng set HARNESS_WEB_SEARXNG_URL; " +
513
- "enable json in SearXNG search.formats; for deep spawn web-query-expander first.";
514
- return failResult(
515
- `web_search failed (exit ${run.exitCode}).\n${run.stderr || run.stdout}${hint}`,
516
- );
517
- }
518
-
519
- const cacheWrite = writeSearchCacheEntry(cwd, searchCtx, output, {
520
- anglesPath: anglesFile,
521
- });
522
- publishWorkspaceAlias(cwd, `${cacheWrite.entryDir}/${basename}`, basename);
523
-
524
- const parts = [run.stdout];
525
- const summary =
526
- tier === "deep" || tier === "research"
527
- ? summarizeDeepSearchJson(output, cwd)
528
- : summarizeSearchJson(output, cwd);
529
- if (summary) parts.push("", summary);
530
- parts.push(
531
- "",
532
- `output: ${output}`,
533
- `artifactDir: ${scoped.artifactDir}`,
534
- `tier: ${tier}`,
535
- `cache: ${cacheWrite.entryDir}`,
536
- );
537
- parts.push("Read output JSON; web_fetch top URLs with highlights:true.");
538
-
539
- return okResult(parts.join("\n"), {
540
- output,
541
- artifactDir: scoped.artifactDir,
542
- query,
543
- tier,
544
- engine,
545
- cacheHit: false,
546
- cacheKey: cacheWrite.cacheKey,
547
- cachePath: `${cacheWrite.entryDir}/${basename}`,
548
- });
931
+ return executeWebSearch(params as Record<string, unknown>, ctx);
549
932
  },
550
933
  });
551
934
 
@@ -559,147 +942,7 @@ export default function harnessWebTools(pi: ExtensionAPI) {
559
942
  parameters: WebFetchSchema,
560
943
 
561
944
  async execute(_id, params, _signal, _onUpdate, ctx) {
562
- const cwd = sessionCwd(ctx);
563
- const webScope = String(params.webScope ?? "").trim() || undefined;
564
- const url = String(params.url ?? "").trim();
565
- if (!url) return failResult("web_fetch: url is required.");
566
-
567
- const mode = params.mode === "map" ? "map" : "scrape";
568
- const fast = params.fast === true;
569
- const limit = typeof params.limit === "number" ? params.limit : 100;
570
- const basename = mode === "map" ? "map.json" : "page.md";
571
- const scoped = resolveScopedOutput(
572
- ctx,
573
- basename,
574
- params.output ? String(params.output) : undefined,
575
- webScope,
576
- );
577
- const output = scoped.output;
578
- ensureParentDir(cwd, output);
579
- const highlights = params.highlights === true;
580
- const hlQuery = String(params.highlightQuery ?? "").trim();
581
- const { refresh: refreshCache, maxAgeSec } = cacheControlFromParams(params);
582
-
583
- const hlScoped =
584
- highlights && !params.highlightsOutput
585
- ? resolveScopedOutput(ctx, "highlights.json", undefined, webScope)
586
- : highlights
587
- ? resolveScopedOutput(
588
- ctx,
589
- "highlights.json",
590
- String(params.highlightsOutput),
591
- webScope,
592
- )
593
- : undefined;
594
- if (hlScoped) ensureParentDir(cwd, hlScoped.output);
595
-
596
- const fetchCtx: FetchCacheContext = {
597
- url,
598
- mode,
599
- fast,
600
- highlightQuery: hlQuery || undefined,
601
- highlights,
602
- };
603
-
604
- if (!refreshCache) {
605
- const cached = lookupFetchCache(cwd, fetchCtx, { maxAgeSec });
606
- if (cached.hit && !cached.stale) {
607
- const workspaceBasename = highlights
608
- ? "highlights.json"
609
- : mode === "map"
610
- ? "map.json"
611
- : "page.md";
612
- const workspaceOutput = publishWorkspaceAlias(
613
- cwd,
614
- cached.artifactPath,
615
- workspaceBasename,
616
- );
617
- const parts = [
618
- `[cache hit] age ${formatCacheAge(cached.ageMs)} · key ${cached.cacheKey}`,
619
- `cache: ${cached.entryDir}`,
620
- "",
621
- `output: ${workspaceOutput}`,
622
- `artifactDir: ${scoped.artifactDir}`,
623
- ];
624
- const excerpt = readTextExcerpt(workspaceOutput, cwd);
625
- if (excerpt) parts.push("", "--- excerpt ---", excerpt);
626
- return okResult(parts.join("\n"), {
627
- output: workspaceOutput,
628
- artifactDir: scoped.artifactDir,
629
- url,
630
- mode,
631
- highlights,
632
- cacheHit: true,
633
- cacheKey: cached.cacheKey,
634
- cachePath: cached.artifactPath,
635
- });
636
- }
637
- }
638
-
639
- let argv: string[];
640
- if (mode === "map") {
641
- argv = [
642
- "map",
643
- url,
644
- "-o",
645
- output,
646
- "--limit",
647
- String(limit),
648
- ...(fast ? ["--fast"] : []),
649
- ];
650
- } else {
651
- argv = ["scrape", url, "-o", output, ...(fast ? ["--fast"] : [])];
652
- if (highlights) {
653
- if (!hlQuery) {
654
- return failResult("web_fetch: highlightQuery required when highlights=true");
655
- }
656
- argv.push("--highlights", "--highlight-query", hlQuery);
657
- if (hlScoped) argv.push("--highlights-output", hlScoped.output);
658
- }
659
- }
660
-
661
- const run = runHarnessWeb(MODULE_URL, argv, cwd);
662
- if (!run.ok) {
663
- return failResult(
664
- `web_fetch failed (exit ${run.exitCode}).\n${run.stderr || run.stdout}\n` +
665
- "Try fast:true for static pages, or run harness-cli-verify for Scrapling install.",
666
- );
667
- }
668
-
669
- const cacheArtifact = highlights && hlScoped ? hlScoped.output : output;
670
- const cacheWrite = writeFetchCacheEntry(cwd, fetchCtx, cacheArtifact, {
671
- highlightsPath:
672
- highlights && hlScoped && hlScoped.output !== cacheArtifact
673
- ? hlScoped.output
674
- : undefined,
675
- });
676
- const workspaceBasename = highlights
677
- ? "highlights.json"
678
- : mode === "map"
679
- ? "map.json"
680
- : "page.md";
681
- publishWorkspaceAlias(cwd, `${cacheWrite.entryDir}/${workspaceBasename}`, workspaceBasename);
682
-
683
- const parts = [
684
- run.stdout,
685
- "",
686
- `output: ${output}`,
687
- `artifactDir: ${scoped.artifactDir}`,
688
- `cache: ${cacheWrite.entryDir}`,
689
- ];
690
- const excerpt = readTextExcerpt(output, cwd);
691
- if (excerpt) parts.push("", "--- excerpt ---", excerpt);
692
-
693
- return okResult(parts.join("\n"), {
694
- output,
695
- artifactDir: scoped.artifactDir,
696
- url,
697
- mode,
698
- highlights,
699
- cacheHit: false,
700
- cacheKey: cacheWrite.cacheKey,
701
- cachePath: `${cacheWrite.entryDir}/${workspaceBasename}`,
702
- });
945
+ return executeWebFetch(params as Record<string, unknown>, ctx);
703
946
  },
704
947
  });
705
948
 
@@ -713,47 +956,7 @@ export default function harnessWebTools(pi: ExtensionAPI) {
713
956
  parameters: WebFindSimilarSchema,
714
957
 
715
958
  async execute(_id, params, _signal, _onUpdate, ctx) {
716
- const cwd = sessionCwd(ctx);
717
- const webScope = String(params.webScope ?? "").trim() || undefined;
718
- const url = String(params.url ?? "").trim();
719
- if (!url) return failResult("web_find_similar: url is required.");
720
-
721
- const scoped = resolveScopedOutput(
722
- ctx,
723
- "search-deep.json",
724
- params.output ? String(params.output) : undefined,
725
- webScope,
726
- );
727
- const output = scoped.output;
728
- ensureParentDir(cwd, output);
729
- const limit = typeof params.limit === "number" ? params.limit : 10;
730
- const argv = [
731
- "find-similar",
732
- url,
733
- "-o",
734
- output,
735
- "--limit",
736
- String(limit),
737
- ...(params.fast !== false ? ["--fast"] : []),
738
- ];
739
-
740
- const run = runHarnessWeb(MODULE_URL, argv, cwd);
741
- if (!run.ok) {
742
- return failResult(
743
- `web_find_similar failed (exit ${run.exitCode}).\n${run.stderr || run.stdout}`,
744
- );
745
- }
746
-
747
- const parts = [run.stdout];
748
- const summary = summarizeDeepSearchJson(output, cwd);
749
- if (summary) parts.push("", summary);
750
- parts.push("", `output: ${output}`, `artifactDir: ${scoped.artifactDir}`);
751
-
752
- return okResult(parts.join("\n"), {
753
- output,
754
- artifactDir: scoped.artifactDir,
755
- url,
756
- });
959
+ return executeWebFindSimilar(params as Record<string, unknown>, ctx);
757
960
  },
758
961
  });
759
962
 
@@ -767,83 +970,7 @@ export default function harnessWebTools(pi: ExtensionAPI) {
767
970
  parameters: WebContentsSchema,
768
971
 
769
972
  async execute(_id, params, _signal, _onUpdate, ctx) {
770
- const cwd = sessionCwd(ctx);
771
- const webScope = String(params.webScope ?? "").trim() || undefined;
772
- const dirScoped = resolveScopedOutput(
773
- ctx,
774
- "contents",
775
- params.outputDir ? String(params.outputDir) : undefined,
776
- webScope,
777
- );
778
- const outputDir = dirScoped.output.endsWith("/contents")
779
- ? dirScoped.output
780
- : `${dirScoped.artifactDir}/contents`;
781
- mkdirSync(resolve(cwd, outputDir), { recursive: true });
782
- let fromSearch = String(params.fromSearch ?? "").trim();
783
- if (fromSearch && !fromSearch.startsWith("/") && !fromSearch.includes("..")) {
784
- fromSearch = resolveScopedOutput(
785
- ctx,
786
- "search-deep.json",
787
- fromSearch,
788
- webScope,
789
- ).output;
790
- }
791
- const urls = (params.urls ?? []).map((u) => String(u).trim()).filter(Boolean);
792
- const limit = typeof params.limit === "number" ? params.limit : 5;
793
- const hlQuery = String(params.highlightQuery ?? "").trim();
794
-
795
- const argv = [
796
- "contents-batch",
797
- "-o",
798
- outputDir,
799
- "--limit",
800
- String(limit),
801
- ...(params.fast ? ["--fast"] : []),
802
- ...(params.highlights && hlQuery
803
- ? ["--highlights", "--highlight-query", hlQuery]
804
- : []),
805
- ...urls,
806
- ];
807
- if (fromSearch) {
808
- argv.splice(1, 0, "--from-search", fromSearch);
809
- }
810
- let evidencePath: string | undefined;
811
- if (params.evidenceBundle && fromSearch) {
812
- const bundleArg = String(params.evidenceBundle);
813
- evidencePath =
814
- bundleArg.startsWith("/") || bundleArg.includes("..")
815
- ? bundleArg
816
- : resolveScopedOutput(
817
- ctx,
818
- "evidence-bundle.json",
819
- bundleArg,
820
- webScope,
821
- ).output;
822
- ensureParentDir(cwd, evidencePath);
823
- argv.push("--evidence-bundle", evidencePath);
824
- }
825
-
826
- if (!fromSearch && !urls.length) {
827
- return failResult("web_contents: provide urls or fromSearch");
828
- }
829
-
830
- const run = runHarnessWeb(MODULE_URL, argv, cwd);
831
- if (!run.ok) {
832
- return failResult(
833
- `web_contents failed (exit ${run.exitCode}).\n${run.stderr || run.stdout}`,
834
- );
835
- }
836
-
837
- return okResult(
838
- `${run.stdout}\n\noutputDir: ${outputDir}\nartifactDir: ${dirScoped.artifactDir}` +
839
- (evidencePath ? `\nevidence: ${evidencePath}` : ""),
840
- {
841
- outputDir,
842
- artifactDir: dirScoped.artifactDir,
843
- fromSearch,
844
- evidenceBundle: evidencePath,
845
- },
846
- );
973
+ return executeWebContents(params as Record<string, unknown>, ctx);
847
974
  },
848
975
  });
849
976
  }