@heretek-ai/epistemic-swarm 0.7.25 → 1.0.2
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/index.ts +1 -0
- package/package.json +63 -139
- package/rpc.ts +1 -0
- package/server.ts +1 -0
- package/src/agents.ts +69 -0
- package/src/continue.ts +40 -0
- package/src/hooks.ts +283 -0
- package/src/panels.tsx +220 -0
- package/src/policy.ts +221 -0
- package/src/pr.ts +37 -0
- package/src/research.ts +63 -0
- package/src/rpc-def.ts +136 -0
- package/src/rpc.ts +331 -0
- package/src/runtime.ts +112 -0
- package/src/server.ts +311 -0
- package/src/session.ts +61 -0
- package/src/tools.ts +61 -0
- package/src/tui.tsx +206 -0
- package/tui.tsx +1 -0
- package/.agents/skills/brainstorming/SKILL.md +0 -19
- package/.agents/skills/code_audit/SKILL.md +0 -19
- package/.agents/skills/darkharvest/SKILL.md +0 -19
- package/.agents/skills/epistemic_search/SKILL.md +0 -19
- package/.agents/skills/factory/SKILL.md +0 -19
- package/.agents/skills/grilling/SKILL.md +0 -19
- package/.agents/skills/oss_scout/SKILL.md +0 -19
- package/.agents/skills/research_cache/SKILL.md +0 -19
- package/.agents/skills/swarm_config/SKILL.md +0 -19
- package/.claude-plugin/marketplace.json +0 -61
- package/.claude-plugin/plugin.json +0 -76
- package/.omp/README.md +0 -39
- package/.omp/SYSTEM.md +0 -17
- package/.omp/commands/audit.md +0 -10
- package/.omp/commands/brainstorming.md +0 -12
- package/.omp/commands/darkharvest.md +0 -11
- package/.omp/commands/domainexpansion.md +0 -9
- package/.omp/commands/factory.md +0 -9
- package/.omp/commands/grill.md +0 -9
- package/.omp/commands/scout.md +0 -10
- package/.omp/commands/swarm-config.md +0 -10
- package/.omp/commands/swarm.md +0 -9
- package/.omp/hooks/post/epistemic-audit.ts +0 -16
- package/.omp/hooks/pre/epistemic-redirect.ts +0 -21
- package/.omp/prompts/brainstorming.md +0 -8
- package/.omp/prompts/swarm.md +0 -8
- package/LICENSE +0 -126
- package/MARKETPLACE.md +0 -130
- package/README.md +0 -403
- package/bin/cli.js +0 -297
- package/config/claude-settings-patch.json +0 -10
- package/config/docker-compose.infra.yml +0 -23
- package/config/domain_packs/biopharma.json +0 -23
- package/config/domain_packs/legal.json +0 -19
- package/config/domain_packs/quant.json +0 -19
- package/config/mcp-research-servers.json +0 -33
- package/config/mcp_launcher.py +0 -153
- package/config/opencode-snippet.json +0 -156
- package/config/searxng_mcp.py +0 -92
- package/extensions/pi/index.js +0 -357
- package/hooks/hooks.json +0 -45
- package/install.sh +0 -473
- package/plugins/antigravity/README.md +0 -72
- package/plugins/antigravity/agents/alpha-thesis.md +0 -36
- package/plugins/antigravity/agents/beta-antithesis.md +0 -35
- package/plugins/antigravity/agents/brainstormer.md +0 -27
- package/plugins/antigravity/agents/code-auditor.md +0 -28
- package/plugins/antigravity/agents/darkharvester.md +0 -30
- package/plugins/antigravity/agents/epistemic-auditor.md +0 -28
- package/plugins/antigravity/agents/manager.md +0 -26
- package/plugins/antigravity/agents/oss-scout.md +0 -27
- package/plugins/antigravity/agents/programmer.md +0 -25
- package/plugins/antigravity/agents/qa-a.md +0 -22
- package/plugins/antigravity/agents/qa-b.md +0 -22
- package/plugins/antigravity/antigravity_hooks.py +0 -265
- package/plugins/antigravity/commands/audit.md +0 -11
- package/plugins/antigravity/commands/brainstorm.md +0 -9
- package/plugins/antigravity/commands/brainstorming.md +0 -10
- package/plugins/antigravity/commands/darkharvest.md +0 -12
- package/plugins/antigravity/commands/domainexpansion.md +0 -12
- package/plugins/antigravity/commands/factory.md +0 -14
- package/plugins/antigravity/commands/grill.md +0 -10
- package/plugins/antigravity/commands/scout.md +0 -11
- package/plugins/antigravity/commands/swarm-config.md +0 -9
- package/plugins/antigravity/commands/swarm.md +0 -12
- package/plugins/antigravity/hook_bus_bridge.js +0 -352
- package/plugins/antigravity/hooks.json +0 -44
- package/plugins/antigravity/mcp_config.json +0 -33
- package/plugins/antigravity/mcp_launcher.py +0 -58
- package/plugins/antigravity/plugin.json +0 -13
- package/plugins/antigravity/rules/AGENTS.md +0 -127
- package/plugins/antigravity/rules/epistemic-integrity.md +0 -16
- package/plugins/antigravity/skills/brainstorming/SKILL.md +0 -19
- package/plugins/antigravity/skills/code_audit/SKILL.md +0 -19
- package/plugins/antigravity/skills/darkharvest/SKILL.md +0 -19
- package/plugins/antigravity/skills/epistemic_search/SKILL.md +0 -19
- package/plugins/antigravity/skills/factory/SKILL.md +0 -19
- package/plugins/antigravity/skills/grilling/SKILL.md +0 -19
- package/plugins/antigravity/skills/oss_scout/SKILL.md +0 -19
- package/plugins/antigravity/skills/research_cache/SKILL.md +0 -19
- package/plugins/antigravity/skills/swarm_config/SKILL.md +0 -19
- package/plugins/codex/AGENTS.md.snippet +0 -10
- package/plugins/codex/README.md +0 -37
- package/plugins/codex/config.toml.snippet +0 -28
- package/plugins/codex/openai.yaml +0 -24
- package/plugins/codex/skills/brainstorming/SKILL.md +0 -19
- package/plugins/codex/skills/code_audit/SKILL.md +0 -19
- package/plugins/codex/skills/darkharvest/SKILL.md +0 -19
- package/plugins/codex/skills/epistemic_search/SKILL.md +0 -19
- package/plugins/codex/skills/factory/SKILL.md +0 -19
- package/plugins/codex/skills/grilling/SKILL.md +0 -19
- package/plugins/codex/skills/oss_scout/SKILL.md +0 -19
- package/plugins/codex/skills/research_cache/SKILL.md +0 -19
- package/plugins/codex/skills/swarm_config/SKILL.md +0 -19
- package/plugins/darkharvest/.claude-plugin/plugin.json +0 -15
- package/plugins/darkharvest/agents/harvest-proponent.md +0 -22
- package/plugins/darkharvest/agents/harvest-redteam.md +0 -20
- package/plugins/darkharvest/evals/teardown-verdict/graders/license-line.md +0 -6
- package/plugins/darkharvest/evals/teardown-verdict/graders/skill-fired.md +0 -5
- package/plugins/darkharvest/evals/teardown-verdict/prompt.md +0 -6
- package/plugins/darkharvest/skills/darkharvest/SKILL.md +0 -70
- package/plugins/darkharvest/skills/darkharvest/scripts/harvest.py +0 -289
- package/plugins/factory/.claude-plugin/plugin.json +0 -15
- package/plugins/factory/agents/factory-manager.md +0 -22
- package/plugins/factory/agents/programmer.md +0 -16
- package/plugins/factory/agents/qa-adversarial.md +0 -17
- package/plugins/factory/agents/qa-functional.md +0 -17
- package/plugins/factory/evals/gate-halt/graders/gates-first.md +0 -6
- package/plugins/factory/evals/gate-halt/graders/skill-fired.md +0 -5
- package/plugins/factory/evals/gate-halt/prompt.md +0 -6
- package/plugins/factory/skills/factory/SKILL.md +0 -61
- package/plugins/factory/skills/factory/scripts/factory.py +0 -300
- package/plugins/gemini/GEMINI.md +0 -16
- package/plugins/gemini/README.md +0 -20
- package/plugins/gemini/commands/audit.toml +0 -6
- package/plugins/gemini/commands/brainstorming.toml +0 -10
- package/plugins/gemini/commands/darkharvest.toml +0 -8
- package/plugins/gemini/commands/domainexpansion.toml +0 -7
- package/plugins/gemini/commands/factory.toml +0 -8
- package/plugins/gemini/commands/grill.toml +0 -6
- package/plugins/gemini/commands/scout.toml +0 -7
- package/plugins/gemini/commands/swarm-config.toml +0 -7
- package/plugins/gemini/commands/swarm.toml +0 -8
- package/plugins/gemini/gemini-extension.json +0 -38
- package/plugins/gemini/hooks/hooks.json +0 -11
- package/plugins/gemini/skills/brainstorming/SKILL.md +0 -19
- package/plugins/gemini/skills/code_audit/SKILL.md +0 -19
- package/plugins/gemini/skills/darkharvest/SKILL.md +0 -19
- package/plugins/gemini/skills/epistemic_search/SKILL.md +0 -19
- package/plugins/gemini/skills/factory/SKILL.md +0 -19
- package/plugins/gemini/skills/grilling/SKILL.md +0 -19
- package/plugins/gemini/skills/oss_scout/SKILL.md +0 -19
- package/plugins/gemini/skills/research_cache/SKILL.md +0 -19
- package/plugins/gemini/skills/swarm_config/SKILL.md +0 -19
- package/plugins/opencode/analysis-bridge.js +0 -620
- package/plugins/opencode/config-io.js +0 -1651
- package/plugins/opencode/dep-health.js +0 -1039
- package/plugins/opencode/hook-bus.js +0 -496
- package/plugins/opencode/index.js +0 -2992
- package/plugins/opencode/tui.js +0 -1551
- package/plugins/opencode/weight-baseline.json +0 -16
- package/plugins/opencode/weight-signals.js +0 -769
- package/plugins/research-cache/.claude-plugin/plugin.json +0 -15
- package/plugins/research-cache/skills/research_cache/SKILL.md +0 -36
- package/plugins/research-cache/skills/research_cache/__init__.py +0 -0
- package/plugins/research-cache/skills/research_cache/hasher.py +0 -302
- package/plugins/socratic-grilling/.claude-plugin/plugin.json +0 -15
- package/plugins/socratic-grilling/skills/grilling/SKILL.md +0 -61
- package/plugins/socratic-grilling/skills/grilling/__init__.py +0 -0
- package/plugins/socratic-grilling/skills/grilling/socratic_tree.py +0 -363
- package/prompts/agent_alpha_thesis.md +0 -70
- package/prompts/agent_beta_antithesis.md +0 -78
- package/prompts/agent_brainstormer.md +0 -143
- package/prompts/agent_code_auditor.md +0 -88
- package/prompts/agent_darkharvest.md +0 -140
- package/prompts/agent_oss_scout.md +0 -78
- package/prompts/antigravity_repo_factcheck.md +0 -143
- package/prompts/base_epistemic_system.md +0 -50
- package/prompts/epistemic_auditor.md +0 -74
- package/prompts/orchestrator.md +0 -70
- package/runner/__init__.py +0 -0
- package/runner/__pycache__/__init__.cpython-311.pyc +0 -0
- package/runner/__pycache__/auctioneer.cpython-311.pyc +0 -0
- package/runner/__pycache__/auditor_engine.cpython-311.pyc +0 -0
- package/runner/__pycache__/claim_store.cpython-311.pyc +0 -0
- package/runner/__pycache__/claim_witness.cpython-311.pyc +0 -0
- package/runner/__pycache__/darkharvest_claims.cpython-311.pyc +0 -0
- package/runner/__pycache__/errors.cpython-311.pyc +0 -0
- package/runner/__pycache__/living_dossiers.cpython-311.pyc +0 -0
- package/runner/__pycache__/mcp_protocol.cpython-311.pyc +0 -0
- package/runner/__pycache__/mcp_server.cpython-311.pyc +0 -0
- package/runner/__pycache__/path_safety.cpython-311.pyc +0 -0
- package/runner/__pycache__/pcrb.cpython-311.pyc +0 -0
- package/runner/__pycache__/pcrb_verify.cpython-311.pyc +0 -0
- package/runner/__pycache__/preflight.cpython-311.pyc +0 -0
- package/runner/__pycache__/refinement.cpython-311.pyc +0 -0
- package/runner/__pycache__/research_swarm.cpython-311.pyc +0 -0
- package/runner/__pycache__/retrieval_log.cpython-311.pyc +0 -0
- package/runner/__pycache__/schema_validate.cpython-311.pyc +0 -0
- package/runner/__pycache__/schemas.cpython-311.pyc +0 -0
- package/runner/__pycache__/state_machine.cpython-311.pyc +0 -0
- package/runner/antigravity_hooks.py +0 -265
- package/runner/auctioneer.py +0 -169
- package/runner/auditor_engine.py +0 -711
- package/runner/claim_store.py +0 -417
- package/runner/claim_witness.py +0 -412
- package/runner/darkharvest_claims.py +0 -358
- package/runner/errors.py +0 -51
- package/runner/living_dossiers.py +0 -395
- package/runner/mcp_protocol.py +0 -188
- package/runner/mcp_server.py +0 -1447
- package/runner/path_safety.py +0 -62
- package/runner/pcrb.py +0 -290
- package/runner/pcrb_verify.py +0 -267
- package/runner/preflight.py +0 -1011
- package/runner/refinement.py +0 -580
- package/runner/research_swarm.py +0 -1848
- package/runner/retrieval_log.py +0 -90
- package/runner/schema_validate.py +0 -193
- package/runner/schemas.py +0 -630
- package/runner/state_machine.py +0 -463
- package/runner/tests/__pycache__/test_analysis_bridge.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_antigravity_plugin.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_auction_order.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_backends.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_bet1_spike.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_claim_store.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_claim_witness.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_claude_plugin.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_config_schema.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_contract_tooling.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_darkharvest_audit.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_dep_health.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_domain_packs.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_dossier_contract.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_factory.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_fleet_seam.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_frontier_concurrency.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_frontier_mirror_standalone.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_grounding.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_hardening_contract.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_hook_bus.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_living_dossiers.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_manifest_concurrency.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_mcp_server.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_nk_hardening.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_opencode_ux.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_opencode_v2_registrars.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_pcrb.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_phase08_waiver_reopens.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_refinement.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_search_anomaly.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_search_provider.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_search_provider_plugin.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_swarm.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_swarm_config.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_sweep_regressions.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_webcache.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_weight_signals.cpython-311.pyc +0 -0
- package/runner/tests/fixtures/auction_objective.json +0 -35
- package/runner/tests/fixtures/borderline_claims.json +0 -27
- package/runner/tests/fixtures/config_validation_cases.json +0 -276
- package/runner/tests/fixtures/ddg_anomaly_page.html +0 -176
- package/runner/tests/fixtures/ddg_results_page.html +0 -28
- package/runner/tests/fixtures/divergence_objectives.json +0 -31
- package/runner/tests/test_analysis_bridge.py +0 -450
- package/runner/tests/test_antigravity_plugin.py +0 -361
- package/runner/tests/test_auction_order.py +0 -173
- package/runner/tests/test_backends.py +0 -457
- package/runner/tests/test_bet1_spike.py +0 -95
- package/runner/tests/test_claim_store.py +0 -162
- package/runner/tests/test_claim_witness.py +0 -186
- package/runner/tests/test_claude_plugin.py +0 -170
- package/runner/tests/test_config_schema.py +0 -879
- package/runner/tests/test_contract_tooling.py +0 -312
- package/runner/tests/test_darkharvest_audit.py +0 -395
- package/runner/tests/test_dep_health.py +0 -595
- package/runner/tests/test_domain_packs.py +0 -217
- package/runner/tests/test_dossier_contract.py +0 -196
- package/runner/tests/test_factory.py +0 -258
- package/runner/tests/test_fleet_seam.py +0 -113
- package/runner/tests/test_frontier_concurrency.py +0 -288
- package/runner/tests/test_frontier_mirror_standalone.py +0 -156
- package/runner/tests/test_grounding.py +0 -183
- package/runner/tests/test_hardening_contract.py +0 -391
- package/runner/tests/test_hook_bus.py +0 -956
- package/runner/tests/test_living_dossiers.py +0 -204
- package/runner/tests/test_manifest_concurrency.py +0 -187
- package/runner/tests/test_mcp_server.py +0 -257
- package/runner/tests/test_nk_hardening.py +0 -1046
- package/runner/tests/test_opencode_ux.py +0 -2438
- package/runner/tests/test_opencode_v2_registrars.py +0 -2157
- package/runner/tests/test_pcrb.py +0 -240
- package/runner/tests/test_phase08_waiver_reopens.py +0 -539
- package/runner/tests/test_refinement.py +0 -255
- package/runner/tests/test_search_anomaly.py +0 -342
- package/runner/tests/test_search_provider.py +0 -1534
- package/runner/tests/test_search_provider_plugin.py +0 -401
- package/runner/tests/test_swarm.py +0 -444
- package/runner/tests/test_swarm_config.py +0 -520
- package/runner/tests/test_sweep_regressions.py +0 -453
- package/runner/tests/test_webcache.py +0 -271
- package/runner/tests/test_weight_signals.py +0 -401
- package/schemas/alpha_dossier.schema.json +0 -204
- package/schemas/audit_report.schema.json +0 -79
- package/schemas/beta_dossier.schema.json +0 -214
- package/schemas/config.schema.json +0 -311
- package/schemas/frontier.schema.json +0 -61
- package/schemas/manifest.schema.json +0 -49
- package/schemas/scope_manifest.schema.json +0 -47
- package/scripts/__pycache__/bet1_advisory_spike.cpython-311.pyc +0 -0
- package/scripts/__pycache__/build_adapters.cpython-311.pyc +0 -0
- package/scripts/__pycache__/check_docs.cpython-311.pyc +0 -0
- package/scripts/__pycache__/divergence_experiment.cpython-311.pyc +0 -0
- package/scripts/__pycache__/gen_schemas.cpython-311.pyc +0 -0
- package/scripts/auction_experiment.py +0 -188
- package/scripts/bet1_advisory_spike.py +0 -442
- package/scripts/build_adapters.py +0 -417
- package/scripts/check_docs.py +0 -66
- package/scripts/divergence_experiment.py +0 -214
- package/scripts/gen_schemas.py +0 -69
- package/skills/brainstorming/SKILL.md +0 -106
- package/skills/brainstorming/__init__.py +0 -1
- package/skills/brainstorming/scripts/brainstorm.py +0 -201
- package/skills/code_audit/SKILL.md +0 -45
- package/skills/darkharvest/SKILL.md +0 -70
- package/skills/darkharvest/scripts/harvest.py +0 -289
- package/skills/epistemic_search/SKILL.md +0 -67
- package/skills/epistemic_search/__init__.py +0 -1
- package/skills/epistemic_search/scripts/__pycache__/search.cpython-311.pyc +0 -0
- package/skills/epistemic_search/scripts/fetch.py +0 -156
- package/skills/epistemic_search/scripts/search.py +0 -551
- package/skills/epistemic_search/scripts/webcache.py +0 -339
- package/skills/factory/SKILL.md +0 -61
- package/skills/factory/scripts/factory.py +0 -300
- package/skills/grilling/SKILL.md +0 -61
- package/skills/grilling/__init__.py +0 -0
- package/skills/grilling/__pycache__/__init__.cpython-311.pyc +0 -0
- package/skills/grilling/__pycache__/socratic_tree.cpython-311.pyc +0 -0
- package/skills/grilling/socratic_tree.py +0 -363
- package/skills/oss_scout/SKILL.md +0 -49
- package/skills/research_cache/SKILL.md +0 -36
- package/skills/research_cache/__init__.py +0 -0
- package/skills/research_cache/__pycache__/__init__.cpython-311.pyc +0 -0
- package/skills/research_cache/__pycache__/hasher.cpython-311.pyc +0 -0
- package/skills/research_cache/hasher.py +0 -302
- package/skills/swarm_config/SKILL.md +0 -178
- package/skills/swarm_config/__init__.py +0 -35
- package/skills/swarm_config/__pycache__/__init__.cpython-311.pyc +0 -0
- package/skills/swarm_config/__pycache__/configure.cpython-311.pyc +0 -0
- package/skills/swarm_config/configure.py +0 -1002
package/src/panels.tsx
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// The four TUI panels: factory dashboard, LSP manager, hook inspector and
|
|
2
|
+
// brainstorm board. Each is a session.panel slot contribution (the host
|
|
3
|
+
// renders the one whose name is open) and reads structured state from the
|
|
4
|
+
// server plugin over RPC — the same human-only channel the dialogs use. While
|
|
5
|
+
// open, a panel refetches on every server `changed` event and every couple of
|
|
6
|
+
// seconds, so it follows a running factory.
|
|
7
|
+
import { createResource, createSignal, For, onCleanup, Show } from "solid-js"
|
|
8
|
+
|
|
9
|
+
export type PanelCall = (method: string, input?: unknown) => Promise<any>
|
|
10
|
+
/** Subscribe to server state changes; returns the unsubscribe function. */
|
|
11
|
+
export type PanelSubscribe = (listener: () => void) => () => void
|
|
12
|
+
|
|
13
|
+
export interface PanelProps {
|
|
14
|
+
readonly call: PanelCall
|
|
15
|
+
readonly subscribe: PanelSubscribe
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const PANEL_REFRESH_MS = 2_000
|
|
19
|
+
|
|
20
|
+
/** Panel data, refetched on change events and on an interval while mounted. */
|
|
21
|
+
function usePanelState(props: PanelProps, method: string): { data: () => any; error: () => string | undefined } {
|
|
22
|
+
const [version, setVersion] = createSignal(0)
|
|
23
|
+
const bump = () => setVersion((value) => value + 1)
|
|
24
|
+
const unsubscribe = props.subscribe(bump)
|
|
25
|
+
const timer = setInterval(bump, PANEL_REFRESH_MS)
|
|
26
|
+
onCleanup(() => {
|
|
27
|
+
unsubscribe()
|
|
28
|
+
clearInterval(timer)
|
|
29
|
+
})
|
|
30
|
+
const [state] = createResource(
|
|
31
|
+
version,
|
|
32
|
+
(): Promise<{ data?: any; error?: string }> =>
|
|
33
|
+
props.call(method).then(
|
|
34
|
+
(data) => ({ data }),
|
|
35
|
+
(error: any) => ({ error: String(error?.data?.reason ?? error?.message ?? error) }),
|
|
36
|
+
),
|
|
37
|
+
)
|
|
38
|
+
return { data: () => state.latest?.data, error: () => state.latest?.error }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const Frame = (props: { title: string; children?: any }) => (
|
|
42
|
+
<box flexDirection="column" padding={1} gap={1}>
|
|
43
|
+
<text>{props.title}</text>
|
|
44
|
+
{props.children}
|
|
45
|
+
</box>
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
const Failed = (props: { error: () => string | undefined }) => (
|
|
49
|
+
<Show when={props.error()}>
|
|
50
|
+
<text>Could not load: {props.error()}</text>
|
|
51
|
+
</Show>
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
const FactoryPanel = (props: PanelProps) => {
|
|
55
|
+
const state = usePanelState(props, "factoryState")
|
|
56
|
+
return (
|
|
57
|
+
<Frame title="Factory dashboard">
|
|
58
|
+
<Failed error={state.error} />
|
|
59
|
+
<Show when={state.data()} fallback={<text>{state.error() ? "" : "Loading factory state…"}</text>}>
|
|
60
|
+
{(data: () => any) => (
|
|
61
|
+
<>
|
|
62
|
+
<text>
|
|
63
|
+
Stage: {data().stage}
|
|
64
|
+
{data().runId ? ` · ${data().runId}` : ""}
|
|
65
|
+
</text>
|
|
66
|
+
<text>
|
|
67
|
+
Spend: ${data().spend.usd.toFixed(2)}
|
|
68
|
+
{data().spend.ceilingUSD ? ` / $${data().spend.ceilingUSD}` : ""}
|
|
69
|
+
{data().spend.estimated ? " (estimated)" : ""}
|
|
70
|
+
</text>
|
|
71
|
+
<Show when={data().halt}>
|
|
72
|
+
<text>HALTED: {data().halt}</text>
|
|
73
|
+
</Show>
|
|
74
|
+
<Show when={data().release}>
|
|
75
|
+
<text>PR: {data().release}</text>
|
|
76
|
+
</Show>
|
|
77
|
+
<Show when={data().pending.length}>
|
|
78
|
+
<text>Waiting on human: {data().pending.join(", ")}</text>
|
|
79
|
+
</Show>
|
|
80
|
+
<text>Phases:</text>
|
|
81
|
+
<For each={data().phases}>
|
|
82
|
+
{(phase: any) => (
|
|
83
|
+
<text>
|
|
84
|
+
{phase.id === data().activePhase ? "▶ " : " "}
|
|
85
|
+
{phase.id} · {phase.status} · failures {phase.failures}
|
|
86
|
+
</text>
|
|
87
|
+
)}
|
|
88
|
+
</For>
|
|
89
|
+
</>
|
|
90
|
+
)}
|
|
91
|
+
</Show>
|
|
92
|
+
</Frame>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const LspPanel = (props: PanelProps) => {
|
|
97
|
+
const state = usePanelState(props, "lspState")
|
|
98
|
+
return (
|
|
99
|
+
<Frame title="Language servers">
|
|
100
|
+
<Failed error={state.error} />
|
|
101
|
+
<Show when={state.data()} fallback={<text>{state.error() ? "" : "Loading language servers…"}</text>}>
|
|
102
|
+
{(data: () => any) => (
|
|
103
|
+
<>
|
|
104
|
+
<text>{data().enabled ? "LSP is on." : "LSP is disabled (lsp: false)."}</text>
|
|
105
|
+
<For each={data().servers}>
|
|
106
|
+
{(server: any) => (
|
|
107
|
+
<text>
|
|
108
|
+
{server.available ? "●" : "○"} {server.id} [{server.extensions.join(" ")}]
|
|
109
|
+
{server.running ? ` · ${server.running} running` : ""}
|
|
110
|
+
{server.available ? "" : " — not installed"}
|
|
111
|
+
</text>
|
|
112
|
+
)}
|
|
113
|
+
</For>
|
|
114
|
+
<For each={data().diagnostics}>{(diagnostic: string) => <text>! {diagnostic}</text>}</For>
|
|
115
|
+
</>
|
|
116
|
+
)}
|
|
117
|
+
</Show>
|
|
118
|
+
</Frame>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const HooksPanel = (props: PanelProps) => {
|
|
123
|
+
const state = usePanelState(props, "hooksState")
|
|
124
|
+
return (
|
|
125
|
+
<Frame title="Hook bridge">
|
|
126
|
+
<Failed error={state.error} />
|
|
127
|
+
<Show when={state.data()} fallback={<text>{state.error() ? "" : "Loading hooks…"}</text>}>
|
|
128
|
+
{(data: () => any) => (
|
|
129
|
+
<>
|
|
130
|
+
<text>
|
|
131
|
+
{data().handlers} handler(s) · {data().projectHandlers} from the project ·{" "}
|
|
132
|
+
{data().projectHandlers === 0 ? "nothing to trust" : data().trusted ? "trusted" : "NOT trusted"}
|
|
133
|
+
</text>
|
|
134
|
+
<For each={data().projectLines}>{(line: string) => <text>{line}</text>}</For>
|
|
135
|
+
<Show when={data().loss.length}>
|
|
136
|
+
<text>Capability loss (advisory rows):</text>
|
|
137
|
+
<For each={data().loss}>
|
|
138
|
+
{(item: any) => (
|
|
139
|
+
<text>
|
|
140
|
+
! {item.event} · {item.handler} — {item.support}: {item.reason}
|
|
141
|
+
</text>
|
|
142
|
+
)}
|
|
143
|
+
</For>
|
|
144
|
+
</Show>
|
|
145
|
+
<For each={data().diagnostics}>{(item: string) => <text>! {item}</text>}</For>
|
|
146
|
+
<For each={data().recent}>{(item: string) => <text>recent error: {item}</text>}</For>
|
|
147
|
+
</>
|
|
148
|
+
)}
|
|
149
|
+
</Show>
|
|
150
|
+
</Frame>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const BrainstormPanel = (props: PanelProps) => {
|
|
155
|
+
const state = usePanelState(props, "brainstormState")
|
|
156
|
+
return (
|
|
157
|
+
<Frame title="Brainstorm board">
|
|
158
|
+
<Failed error={state.error} />
|
|
159
|
+
<Show when={state.data()} fallback={<text>{state.error() ? "" : "Loading brainstorm…"}</text>}>
|
|
160
|
+
{(data: () => any) => (
|
|
161
|
+
<Show when={data().active} fallback={<text>No brainstorm in .factory/brainstorm yet. Run /brainstorm.</text>}>
|
|
162
|
+
<text>Brief: {data().brief}</text>
|
|
163
|
+
<text>
|
|
164
|
+
{data().ideas} idea(s) · {data().duplicates} duplicate(s) · {data().scored} scored ·{" "}
|
|
165
|
+
{data().complete ? "complete" : "in progress"}
|
|
166
|
+
</text>
|
|
167
|
+
<text>
|
|
168
|
+
Coverage:{" "}
|
|
169
|
+
{data()
|
|
170
|
+
.lenses.map((lens: string) => `${lens} ${data().coverage[lens] ?? 0}`)
|
|
171
|
+
.join(" · ")}
|
|
172
|
+
</text>
|
|
173
|
+
<Show when={data().gaps.length}>
|
|
174
|
+
<text>Gaps: {data().gaps.join(", ")}</text>
|
|
175
|
+
</Show>
|
|
176
|
+
<Show when={data().shortlist.length}>
|
|
177
|
+
<text>Shortlist:</text>
|
|
178
|
+
<For each={data().shortlist}>
|
|
179
|
+
{(entry: any) => (
|
|
180
|
+
<text>
|
|
181
|
+
{entry.outlier ? "◇" : "◆"} {entry.id} ({entry.total}/20) — {entry.title}
|
|
182
|
+
{entry.outlier ? " (forced outlier)" : ""}
|
|
183
|
+
</text>
|
|
184
|
+
)}
|
|
185
|
+
</For>
|
|
186
|
+
</Show>
|
|
187
|
+
</Show>
|
|
188
|
+
)}
|
|
189
|
+
</Show>
|
|
190
|
+
</Frame>
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** The panel components by panel name (rendered headlessly in tests). */
|
|
195
|
+
export const PANEL_COMPONENTS: Readonly<Record<string, (props: PanelProps) => any>> = {
|
|
196
|
+
factory: FactoryPanel,
|
|
197
|
+
lsp: LspPanel,
|
|
198
|
+
hooks: HooksPanel,
|
|
199
|
+
brainstorm: BrainstormPanel,
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const PANELS = Object.entries(PANEL_COMPONENTS).map(([name, Panel]) => ({
|
|
203
|
+
name,
|
|
204
|
+
render: (props: PanelProps) => <Panel call={props.call} subscribe={props.subscribe} />,
|
|
205
|
+
}))
|
|
206
|
+
|
|
207
|
+
/** Claim the session.panel slot once per panel; each claim filters by name. */
|
|
208
|
+
export function registerPanels(context: any, call: PanelCall, subscribe: PanelSubscribe): () => void {
|
|
209
|
+
const disposers = PANELS.map((panel) =>
|
|
210
|
+
context.slot({
|
|
211
|
+
append: "session.panel",
|
|
212
|
+
render: (input: { name: string }) => (input.name === panel.name ? panel.render({ call, subscribe }) : null),
|
|
213
|
+
}),
|
|
214
|
+
)
|
|
215
|
+
return () => {
|
|
216
|
+
for (const dispose of disposers) dispose()
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export const PANEL_NAMES = PANELS.map((panel) => panel.name)
|
package/src/policy.ts
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// Mechanical enforcement in the host: every write, edit, patch and shell call
|
|
2
|
+
// from any agent passes through core's policy (canonical paths, control files,
|
|
3
|
+
// seat scopes); factory seats additionally stop on STOP or a halted run, run
|
|
4
|
+
// read-only shells under bubblewrap, and get fast gate feedback after edits.
|
|
5
|
+
import path from "node:path"
|
|
6
|
+
import {
|
|
7
|
+
bwrapAvailable,
|
|
8
|
+
checkStopFile,
|
|
9
|
+
evaluateRead,
|
|
10
|
+
evaluateShell,
|
|
11
|
+
evaluateWrite,
|
|
12
|
+
formatDiagnostics,
|
|
13
|
+
formatReport,
|
|
14
|
+
loadGatesConfig,
|
|
15
|
+
readonlyWrap,
|
|
16
|
+
runGates,
|
|
17
|
+
seatOf,
|
|
18
|
+
shellQuote,
|
|
19
|
+
treeFingerprint,
|
|
20
|
+
} from "@heretek-ai/es-core"
|
|
21
|
+
import { Error as ToolError } from "@opencode/plugin/promise/tool"
|
|
22
|
+
import type { Runtime } from "./runtime.ts"
|
|
23
|
+
|
|
24
|
+
const WRITE_TOOLS = new Set(["write", "edit"])
|
|
25
|
+
|
|
26
|
+
const PATCH_HEADERS = ["*** Add File: ", "*** Update File: ", "*** Delete File: ", "*** Move to: "]
|
|
27
|
+
|
|
28
|
+
/** Files a v2 `patch` touches (add, update, delete and move targets). */
|
|
29
|
+
export function patchTargets(patchText: string): string[] {
|
|
30
|
+
const targets: string[] = []
|
|
31
|
+
for (const line of patchText.split("\n")) {
|
|
32
|
+
const header = PATCH_HEADERS.find((prefix) => line.startsWith(prefix))
|
|
33
|
+
const target = header ? line.slice(header.length).trimEnd() : ""
|
|
34
|
+
if (target) targets.push(target)
|
|
35
|
+
}
|
|
36
|
+
return targets
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const deny = (message: string): never => {
|
|
40
|
+
throw new ToolError({ message })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const appendContent = (content: unknown, text: string) =>
|
|
44
|
+
typeof content === "string"
|
|
45
|
+
? `${content}\n\n${text}`
|
|
46
|
+
: Array.isArray(content)
|
|
47
|
+
? [...content, { type: "text", text }]
|
|
48
|
+
: text
|
|
49
|
+
|
|
50
|
+
export function createPolicyHooks(runtime: Runtime) {
|
|
51
|
+
const fingerprints = new Map<string, { dir: string; print: string }>()
|
|
52
|
+
|
|
53
|
+
const before = async (event: { tool: string; agent: string; id: string; input: unknown }) => {
|
|
54
|
+
const seat = seatOf(event.agent)
|
|
55
|
+
const input = (event.input ?? {}) as Record<string, any>
|
|
56
|
+
if (seat || event.tool.startsWith("es_")) {
|
|
57
|
+
const stop = await checkStopFile(runtime.root)
|
|
58
|
+
if (stop.stopped)
|
|
59
|
+
deny(`The factory is stopped (.factory/STOP: ${stop.reason}). A human must remove it and resume.`)
|
|
60
|
+
}
|
|
61
|
+
if (seat && !event.tool.startsWith("es_")) {
|
|
62
|
+
const state = await runtime.factory.read().catch(() => undefined)
|
|
63
|
+
if (state?.stage === "HALTED")
|
|
64
|
+
deny(`The factory is halted: ${state.halt?.reason}. Stop and report; a human must resume.`)
|
|
65
|
+
}
|
|
66
|
+
const context = await runtime.policy()
|
|
67
|
+
if (WRITE_TOOLS.has(event.tool) && typeof input.path === "string") {
|
|
68
|
+
const decision = evaluateWrite(context, event.agent, input.path)
|
|
69
|
+
if (decision.effect === "deny") deny(decision.reason)
|
|
70
|
+
}
|
|
71
|
+
if (event.tool === "patch" && typeof input.patchText === "string")
|
|
72
|
+
for (const target of patchTargets(input.patchText)) {
|
|
73
|
+
const decision = evaluateWrite(context, event.agent, target)
|
|
74
|
+
if (decision.effect === "deny") deny(decision.reason)
|
|
75
|
+
}
|
|
76
|
+
if (event.tool === "read" && typeof input.path === "string") {
|
|
77
|
+
const decision = evaluateRead(context, event.agent, input.path)
|
|
78
|
+
if (decision.effect === "deny") deny(decision.reason)
|
|
79
|
+
}
|
|
80
|
+
if (event.tool === "shell" && typeof input.command === "string") {
|
|
81
|
+
const decision = evaluateShell(context, event.agent, input.command, { sandboxAvailable: bwrapAvailable() })
|
|
82
|
+
if (decision.effect === "deny") deny(decision.reason)
|
|
83
|
+
const cwd =
|
|
84
|
+
typeof input.workdir === "string"
|
|
85
|
+
? path.resolve(runtime.root, input.workdir)
|
|
86
|
+
: (context.worktree ?? runtime.root)
|
|
87
|
+
if (decision.effect === "allow" && decision.mode === "readonly-sandbox") {
|
|
88
|
+
event.input = { ...input, command: readonlyWrap(["sh", "-c", input.command], cwd).map(shellQuote).join(" ") }
|
|
89
|
+
} else if (decision.effect === "allow" && decision.mode === "readonly-checked") {
|
|
90
|
+
fingerprints.set(event.id, { dir: cwd, print: await treeFingerprint(cwd) })
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const after = async (event: {
|
|
96
|
+
tool: string
|
|
97
|
+
agent: string
|
|
98
|
+
id: string
|
|
99
|
+
input: unknown
|
|
100
|
+
status: string
|
|
101
|
+
result?: any
|
|
102
|
+
}) => {
|
|
103
|
+
const seat = seatOf(event.agent)
|
|
104
|
+
const checked = fingerprints.get(event.id)
|
|
105
|
+
if (checked) {
|
|
106
|
+
fingerprints.delete(event.id)
|
|
107
|
+
if ((await treeFingerprint(checked.dir)) !== checked.print) {
|
|
108
|
+
await runtime.factory
|
|
109
|
+
.halt(`agent:${event.agent}`, `read-only seat ${event.agent} changed files via the shell`)
|
|
110
|
+
.catch(() => undefined)
|
|
111
|
+
if (event.status === "completed")
|
|
112
|
+
event.result = {
|
|
113
|
+
...event.result,
|
|
114
|
+
content: appendContent(
|
|
115
|
+
event.result?.content,
|
|
116
|
+
"VIOLATION: this read-only seat modified the checkout. The factory has been halted.",
|
|
117
|
+
),
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (event.status !== "completed") return
|
|
122
|
+
const input = (event.input ?? {}) as Record<string, any>
|
|
123
|
+
const files =
|
|
124
|
+
WRITE_TOOLS.has(event.tool) && typeof input.path === "string"
|
|
125
|
+
? [input.path]
|
|
126
|
+
: event.tool === "patch" && typeof input.patchText === "string"
|
|
127
|
+
? patchTargets(input.patchText)
|
|
128
|
+
: []
|
|
129
|
+
if (!files.length) return
|
|
130
|
+
if (runtime.options.lspAfterEdit !== false) {
|
|
131
|
+
const lines: string[] = []
|
|
132
|
+
for (const file of files) {
|
|
133
|
+
const absolute = path.resolve(runtime.root, file)
|
|
134
|
+
const result = await runtime.lsp.diagnostics(absolute, { maxWaitMs: 4_000 }).catch(() => undefined)
|
|
135
|
+
if (Array.isArray(result)) lines.push(...formatDiagnostics(runtime.root, absolute, result, 1))
|
|
136
|
+
}
|
|
137
|
+
if (lines.length)
|
|
138
|
+
event.result = {
|
|
139
|
+
...event.result,
|
|
140
|
+
content: appendContent(event.result?.content, `<diagnostics>\n${lines.join("\n")}\n</diagnostics>`),
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (seat !== "programmer" || runtime.options.afterEdit === "off") return
|
|
144
|
+
const worktree = (await runtime.policy()).worktree
|
|
145
|
+
if (!worktree) return
|
|
146
|
+
const { config } = await loadGatesConfig(runtime.root, worktree)
|
|
147
|
+
const touched = files
|
|
148
|
+
.map((file) => path.relative(worktree, path.resolve(runtime.root, file)))
|
|
149
|
+
.filter((file) => !file.startsWith(".."))
|
|
150
|
+
const report = await runGates({
|
|
151
|
+
root: runtime.root,
|
|
152
|
+
dir: worktree,
|
|
153
|
+
scope: "touched",
|
|
154
|
+
touched,
|
|
155
|
+
config: {
|
|
156
|
+
...config,
|
|
157
|
+
commands: config.commands.filter((check) => check.kind === "format" || check.kind === "lint"),
|
|
158
|
+
},
|
|
159
|
+
stateDir: runtime.stateDir,
|
|
160
|
+
})
|
|
161
|
+
if (report.findings.length || !report.passed)
|
|
162
|
+
event.result = {
|
|
163
|
+
...event.result,
|
|
164
|
+
content: appendContent(event.result?.content, `Gates after edit:\n${formatReport(report)}`),
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const evaluate = async (event: {
|
|
169
|
+
agent?: string
|
|
170
|
+
action: string
|
|
171
|
+
resources: readonly string[]
|
|
172
|
+
effect: "allow" | "deny" | "ask"
|
|
173
|
+
message?: string
|
|
174
|
+
}) => {
|
|
175
|
+
const seat = seatOf(event.agent)
|
|
176
|
+
const context = await runtime.policy()
|
|
177
|
+
if (event.action === "edit" || event.action === "read") {
|
|
178
|
+
const decisions = event.resources.map((resource) =>
|
|
179
|
+
event.action === "edit"
|
|
180
|
+
? evaluateWrite(context, event.agent, resource)
|
|
181
|
+
: evaluateRead(context, event.agent, resource),
|
|
182
|
+
)
|
|
183
|
+
const denied = decisions.find((decision) => decision.effect === "deny")
|
|
184
|
+
if (denied && "reason" in denied) {
|
|
185
|
+
event.effect = "deny"
|
|
186
|
+
event.message = denied.reason
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
const asked = decisions.find((decision) => decision.effect === "ask")
|
|
190
|
+
if (asked && "reason" in asked && event.effect === "allow") {
|
|
191
|
+
event.effect = "ask"
|
|
192
|
+
event.message = asked.reason
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
if (seat && event.effect === "ask") {
|
|
196
|
+
// Autonomous seats never wait on a human: our policy decides.
|
|
197
|
+
event.effect = event.action === "edit" ? "allow" : "deny"
|
|
198
|
+
if (event.effect === "deny")
|
|
199
|
+
event.message = "Autonomous factory seats may not read files the host guards (e.g. .env)."
|
|
200
|
+
}
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
if (seat && event.action === "external_directory") {
|
|
204
|
+
// Sonar S5443 flags the /tmp literal; this is an access check, not temp-file creation.
|
|
205
|
+
const tmp = event.resources.every((resource) => resource === "/tmp/*" || resource.startsWith("/tmp/")) // NOSONAR
|
|
206
|
+
event.effect = tmp ? "allow" : "deny"
|
|
207
|
+
if (!tmp) event.message = "Factory seats stay inside the project (and /tmp)."
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
if (seat && event.effect === "ask") {
|
|
211
|
+
event.effect = "deny"
|
|
212
|
+
event.message = `Autonomous factory seat "${event.agent}" cannot wait for a human approval of "${event.action}".`
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const shellEnv = (event: { env: Record<string, string | undefined> }) => {
|
|
217
|
+
for (const key of Object.keys(event.env)) if (/^OPENCODE_(SERVER_)?PASSWORD$/.test(key)) delete event.env[key]
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return { before, after, evaluate, shellEnv }
|
|
221
|
+
}
|
package/src/pr.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Release PR opener backed by the GitHub CLI. It pushes only the run's
|
|
2
|
+
// integration branch (never the base branch) and opens a draft PR for a human
|
|
3
|
+
// to review and merge.
|
|
4
|
+
import type { PrOpener } from "@heretek-ai/es-core"
|
|
5
|
+
import { git, run } from "@heretek-ai/es-core"
|
|
6
|
+
|
|
7
|
+
export const ghPrOpener: PrOpener = {
|
|
8
|
+
id: "gh",
|
|
9
|
+
async available(root) {
|
|
10
|
+
const gh = await run(["gh", "auth", "status"], {
|
|
11
|
+
cwd: root,
|
|
12
|
+
timeoutMs: 15_000,
|
|
13
|
+
passEnv: ["GH_TOKEN", "GITHUB_TOKEN", "GH_HOST"],
|
|
14
|
+
}).catch(() => undefined)
|
|
15
|
+
if (gh?.code !== 0) return false
|
|
16
|
+
const remote = await git(root, ["remote", "get-url", "origin"], { allowFail: true })
|
|
17
|
+
return remote.code === 0
|
|
18
|
+
},
|
|
19
|
+
async open({ root, branch, base, title, body }) {
|
|
20
|
+
if (branch === base || !branch.startsWith("factory/")) throw new Error(`refusing to push "${branch}"`)
|
|
21
|
+
await git(root, ["push", "--set-upstream", "origin", `${branch}:${branch}`])
|
|
22
|
+
const created = await run(
|
|
23
|
+
["gh", "pr", "create", "--draft", "--head", branch, "--base", base, "--title", title, "--body", body],
|
|
24
|
+
{
|
|
25
|
+
cwd: root,
|
|
26
|
+
timeoutMs: 60_000,
|
|
27
|
+
passEnv: ["GH_TOKEN", "GITHUB_TOKEN", "GH_HOST"],
|
|
28
|
+
},
|
|
29
|
+
)
|
|
30
|
+
const url = created.stdout
|
|
31
|
+
.trim()
|
|
32
|
+
.split("\n")
|
|
33
|
+
.find((line) => /^https?:\/\//.test(line))
|
|
34
|
+
if (created.code !== 0 || !url) throw new Error(`gh pr create failed: ${(created.stderr || created.stdout).trim()}`)
|
|
35
|
+
return { url }
|
|
36
|
+
},
|
|
37
|
+
}
|
package/src/research.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Research integration: the configured search provider becomes the host's
|
|
2
|
+
// websearch (results cached content-addressed, so they can be cited), and the
|
|
3
|
+
// host's own webfetch results are cached too.
|
|
4
|
+
import { researchCache, selectProvider } from "@heretek-ai/es-core"
|
|
5
|
+
import type { Runtime } from "./runtime.ts"
|
|
6
|
+
|
|
7
|
+
export function registerWebsearch(
|
|
8
|
+
editor: { add(definition: any): void; default: { get(): unknown; set(id: string): void } },
|
|
9
|
+
runtime: Runtime,
|
|
10
|
+
) {
|
|
11
|
+
const provider = selectProvider(runtime.options.searchProvider)
|
|
12
|
+
if (!provider) return
|
|
13
|
+
const cache = researchCache(runtime.root)
|
|
14
|
+
editor.add({
|
|
15
|
+
id: "epistemic-swarm",
|
|
16
|
+
name: `Epistemic Swarm (${provider.id}, cached)`,
|
|
17
|
+
execute: async (input: { query: string; limit?: number }, context: { signal: AbortSignal }) => {
|
|
18
|
+
const results = await provider.search(input.query, { limit: input.limit ?? 8, signal: context.signal })
|
|
19
|
+
const out = []
|
|
20
|
+
for (const result of results) {
|
|
21
|
+
const cached = await cache.put({
|
|
22
|
+
url: result.url,
|
|
23
|
+
title: result.title,
|
|
24
|
+
text: result.content ?? result.snippet,
|
|
25
|
+
provider: provider.id,
|
|
26
|
+
query: input.query,
|
|
27
|
+
})
|
|
28
|
+
out.push({
|
|
29
|
+
url: result.url,
|
|
30
|
+
title: result.title,
|
|
31
|
+
content: `[cached sha256:${cached.meta.sha256}]\n${result.content ?? result.snippet}`,
|
|
32
|
+
time: {},
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
return out
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
// Additive: become the default only when nothing else is selected.
|
|
39
|
+
if (editor.default.get() === undefined) editor.default.set("epistemic-swarm")
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Cache host webfetch results so they can be cited as [VERIFIED: sha256:… "quote"]. */
|
|
43
|
+
export function createWebfetchCache(runtime: Runtime) {
|
|
44
|
+
const cache = researchCache(runtime.root)
|
|
45
|
+
return async (event: { tool: string; status: string; input: unknown; result?: any }) => {
|
|
46
|
+
if (event.tool !== "webfetch" || event.status !== "completed") return
|
|
47
|
+
const url = (event.input as { url?: string } | undefined)?.url
|
|
48
|
+
const content = event.result?.content
|
|
49
|
+
const text =
|
|
50
|
+
typeof content === "string"
|
|
51
|
+
? content
|
|
52
|
+
: Array.isArray(content)
|
|
53
|
+
? content.map((part: any) => part?.text ?? "").join("\n")
|
|
54
|
+
: ""
|
|
55
|
+
if (!url || text.trim().length < 40) return
|
|
56
|
+
const cached = await cache.put({ url, text, provider: "webfetch" })
|
|
57
|
+
const note = `[cached as sha256:${cached.meta.sha256}; cite with [VERIFIED: sha256:${cached.meta.sha256} "exact words"]]`
|
|
58
|
+
event.result = {
|
|
59
|
+
...event.result,
|
|
60
|
+
content: typeof content === "string" ? `${note}\n${content}` : [{ type: "text", text: note }, ...(content ?? [])],
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
package/src/rpc-def.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// RPC contract between the server plugin and the TUI. These methods are the
|
|
2
|
+
// TUI half of the human-only channel: no agent tool reaches them, and the TUI
|
|
3
|
+
// only calls them after a human confirms a dialog showing what is approved.
|
|
4
|
+
import { Rpc } from "@opencode/plugin/rpc"
|
|
5
|
+
|
|
6
|
+
const obj = (properties: Record<string, unknown>, required: string[] = []) => ({
|
|
7
|
+
type: "object" as const,
|
|
8
|
+
properties,
|
|
9
|
+
required,
|
|
10
|
+
additionalProperties: false,
|
|
11
|
+
})
|
|
12
|
+
const str = { type: "string" }
|
|
13
|
+
const strArray = { type: "array", items: str }
|
|
14
|
+
|
|
15
|
+
const Preview = obj({ ok: { type: "boolean" }, title: str, lines: strArray, problems: strArray, token: str }, [
|
|
16
|
+
"ok",
|
|
17
|
+
"title",
|
|
18
|
+
"lines",
|
|
19
|
+
"problems",
|
|
20
|
+
])
|
|
21
|
+
|
|
22
|
+
export const EsRpc = Rpc.define({
|
|
23
|
+
id: "epistemic-swarm",
|
|
24
|
+
methods: {
|
|
25
|
+
status: {
|
|
26
|
+
input: obj({}),
|
|
27
|
+
output: obj({ summary: str, stage: str, pending: strArray }, ["summary", "stage", "pending"]),
|
|
28
|
+
},
|
|
29
|
+
previewApproval: {
|
|
30
|
+
input: obj({ stage: { type: "string", enum: ["frontier", "spec"] } }, ["stage"]),
|
|
31
|
+
output: Preview,
|
|
32
|
+
},
|
|
33
|
+
approve: {
|
|
34
|
+
input: obj({ stage: { type: "string", enum: ["frontier", "spec"] }, user: str, token: str }, [
|
|
35
|
+
"stage",
|
|
36
|
+
"user",
|
|
37
|
+
"token",
|
|
38
|
+
]),
|
|
39
|
+
output: obj({ message: str }, ["message"]),
|
|
40
|
+
errors: { refused: obj({ reason: str }, ["reason"]) },
|
|
41
|
+
},
|
|
42
|
+
previewTrust: { input: obj({}), output: Preview },
|
|
43
|
+
trust: {
|
|
44
|
+
input: obj({ user: str, token: str }, ["user", "token"]),
|
|
45
|
+
output: obj({ message: str }, ["message"]),
|
|
46
|
+
errors: { refused: obj({ reason: str }, ["reason"]) },
|
|
47
|
+
},
|
|
48
|
+
previewResume: { input: obj({}), output: Preview },
|
|
49
|
+
previewLspInstall: { input: obj({ id: str }, ["id"]), output: Preview },
|
|
50
|
+
lspInstall: {
|
|
51
|
+
input: obj({ id: str, user: str, token: str }, ["id", "user", "token"]),
|
|
52
|
+
output: obj({ message: str }, ["message"]),
|
|
53
|
+
errors: { refused: obj({ reason: str }, ["reason"]) },
|
|
54
|
+
},
|
|
55
|
+
resume: {
|
|
56
|
+
input: obj(
|
|
57
|
+
{
|
|
58
|
+
user: str,
|
|
59
|
+
token: str,
|
|
60
|
+
acceptControlDrift: { type: "boolean" },
|
|
61
|
+
raiseCeilingUSD: { type: "number" },
|
|
62
|
+
extendRuntime: { type: "boolean" },
|
|
63
|
+
},
|
|
64
|
+
["user", "token"],
|
|
65
|
+
),
|
|
66
|
+
output: obj({ message: str }, ["message"]),
|
|
67
|
+
errors: { refused: obj({ reason: str }, ["reason"]) },
|
|
68
|
+
},
|
|
69
|
+
factoryState: {
|
|
70
|
+
input: obj({}),
|
|
71
|
+
output: obj(
|
|
72
|
+
{
|
|
73
|
+
stage: str,
|
|
74
|
+
runId: str,
|
|
75
|
+
halt: str,
|
|
76
|
+
release: str,
|
|
77
|
+
activePhase: str,
|
|
78
|
+
spend: obj({ usd: { type: "number" }, estimated: { type: "boolean" }, ceilingUSD: { type: "number" } }, [
|
|
79
|
+
"usd",
|
|
80
|
+
"estimated",
|
|
81
|
+
]),
|
|
82
|
+
phases: { type: "array" },
|
|
83
|
+
pending: strArray,
|
|
84
|
+
},
|
|
85
|
+
["stage", "spend", "phases", "pending"],
|
|
86
|
+
),
|
|
87
|
+
},
|
|
88
|
+
lspState: {
|
|
89
|
+
input: obj({}),
|
|
90
|
+
output: obj(
|
|
91
|
+
{
|
|
92
|
+
enabled: { type: "boolean" },
|
|
93
|
+
servers: { type: "array" },
|
|
94
|
+
diagnostics: strArray,
|
|
95
|
+
},
|
|
96
|
+
["enabled", "servers", "diagnostics"],
|
|
97
|
+
),
|
|
98
|
+
},
|
|
99
|
+
hooksState: {
|
|
100
|
+
input: obj({}),
|
|
101
|
+
output: obj(
|
|
102
|
+
{
|
|
103
|
+
handlers: { type: "number" },
|
|
104
|
+
projectHandlers: { type: "number" },
|
|
105
|
+
trusted: { type: "boolean" },
|
|
106
|
+
projectLines: strArray,
|
|
107
|
+
diagnostics: strArray,
|
|
108
|
+
recent: strArray,
|
|
109
|
+
loss: { type: "array" },
|
|
110
|
+
},
|
|
111
|
+
["handlers", "projectHandlers", "trusted", "projectLines", "diagnostics", "recent", "loss"],
|
|
112
|
+
),
|
|
113
|
+
},
|
|
114
|
+
brainstormState: {
|
|
115
|
+
input: obj({}),
|
|
116
|
+
output: obj(
|
|
117
|
+
{
|
|
118
|
+
active: { type: "boolean" },
|
|
119
|
+
brief: str,
|
|
120
|
+
lenses: strArray,
|
|
121
|
+
ideas: { type: "number" },
|
|
122
|
+
duplicates: { type: "number" },
|
|
123
|
+
scored: { type: "number" },
|
|
124
|
+
coverage: { type: "object" },
|
|
125
|
+
shortlist: { type: "array" },
|
|
126
|
+
gaps: strArray,
|
|
127
|
+
complete: { type: "boolean" },
|
|
128
|
+
},
|
|
129
|
+
["active"],
|
|
130
|
+
),
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
events: {
|
|
134
|
+
changed: { schema: obj({ stage: str, summary: str }, ["stage", "summary"]) },
|
|
135
|
+
},
|
|
136
|
+
})
|