@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
|
@@ -1,339 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Content-addressed WebFetch cache-through gate for Claude Code hooks.
|
|
3
|
-
|
|
4
|
-
Replaces the blanket PreToolUse deny with the cache-through pattern
|
|
5
|
-
(proven by theYahia/claude-webcache):
|
|
6
|
-
|
|
7
|
-
gate (PreToolUse WebFetch): serve `.research/sources/<sha256>.md` hits
|
|
8
|
-
via deny + additionalContext (no network); allow misses/stale.
|
|
9
|
-
archive (PostToolUse WebFetch): store the live response as md + json
|
|
10
|
-
sidecar, same layout as SourceHasher (content-hash filename).
|
|
11
|
-
stats (SessionStart): one-line cache summary via additionalContext.
|
|
12
|
-
|
|
13
|
-
Fail-open everywhere: any error exits 0 with no output (allow), logging to
|
|
14
|
-
stderr (visible with --debug). This is a citation-integrity control, not a
|
|
15
|
-
security boundary.
|
|
16
|
-
|
|
17
|
-
URL canonicalization (borrowed from webcache): lowercase host, strip default
|
|
18
|
-
ports and fragments, sort query parameters — so formatting variance does not
|
|
19
|
-
cause silent misses.
|
|
20
|
-
|
|
21
|
-
TTL: sidecar `cached_at`; default 7 days (IUMBTEMS_FETCH_TTL_DAYS), 30 days
|
|
22
|
-
for stable documentation domains. Inline cap for served hits: 12000 chars
|
|
23
|
-
(IUMBTEMS_FETCH_MAX_INLINE), remainder via file pointer.
|
|
24
|
-
|
|
25
|
-
Raw bodies: archived unless `IUMBTEMS_CACHE_RAW_MARKDOWN=0` (config
|
|
26
|
-
`cache_raw_markdown: false`, exported by the runner to spawned children).
|
|
27
|
-
"""
|
|
28
|
-
|
|
29
|
-
import hashlib
|
|
30
|
-
import json
|
|
31
|
-
import os
|
|
32
|
-
import sys
|
|
33
|
-
import urllib.parse
|
|
34
|
-
from datetime import datetime, timezone
|
|
35
|
-
from pathlib import Path
|
|
36
|
-
|
|
37
|
-
DEFAULT_TTL_DAYS = 7
|
|
38
|
-
DOC_DOMAIN_TTL_DAYS = 30
|
|
39
|
-
DOC_DOMAINS = (
|
|
40
|
-
"code.claude.com",
|
|
41
|
-
"docs.anthropic.com",
|
|
42
|
-
"platform.claude.com",
|
|
43
|
-
"docs.python.org",
|
|
44
|
-
"developer.mozilla.org",
|
|
45
|
-
)
|
|
46
|
-
DEFAULT_MAX_INLINE = 12000
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def log(msg):
|
|
50
|
-
sys.stderr.write(f"[webcache] {msg}\n")
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def canonical_url(url):
|
|
54
|
-
try:
|
|
55
|
-
p = urllib.parse.urlsplit(url.strip())
|
|
56
|
-
host = (p.hostname or "").lower()
|
|
57
|
-
if not host:
|
|
58
|
-
return None
|
|
59
|
-
port = p.port
|
|
60
|
-
if (p.scheme == "http" and port == 80) or (p.scheme == "https" and port == 443):
|
|
61
|
-
port = None
|
|
62
|
-
netloc = f"{host}:{port}" if port else host
|
|
63
|
-
query = urllib.parse.urlencode(
|
|
64
|
-
sorted(urllib.parse.parse_qsl(p.query, keep_blank_values=True))
|
|
65
|
-
)
|
|
66
|
-
return urllib.parse.urlunsplit(
|
|
67
|
-
(p.scheme or "https", netloc, p.path or "/", query, "")
|
|
68
|
-
)
|
|
69
|
-
except Exception:
|
|
70
|
-
return None
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def domain_of(url):
|
|
74
|
-
try:
|
|
75
|
-
return (urllib.parse.urlsplit(url).hostname or "").lower()
|
|
76
|
-
except Exception:
|
|
77
|
-
return ""
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def ttl_days(url):
|
|
81
|
-
try:
|
|
82
|
-
override = int(os.environ.get("IUMBTEMS_FETCH_TTL_DAYS", ""))
|
|
83
|
-
return max(override, 0)
|
|
84
|
-
except ValueError:
|
|
85
|
-
pass
|
|
86
|
-
domain = domain_of(url)
|
|
87
|
-
if any(domain == d or domain.endswith("." + d) for d in DOC_DOMAINS):
|
|
88
|
-
return DOC_DOMAIN_TTL_DAYS
|
|
89
|
-
return DEFAULT_TTL_DAYS
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def max_inline():
|
|
93
|
-
try:
|
|
94
|
-
return max(int(os.environ.get("IUMBTEMS_FETCH_MAX_INLINE", "")), 1000)
|
|
95
|
-
except ValueError:
|
|
96
|
-
return DEFAULT_MAX_INLINE
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def research_dir(stdin_data):
|
|
100
|
-
base = stdin_data.get("cwd") or os.environ.get("CLAUDE_PROJECT_DIR") or os.getcwd()
|
|
101
|
-
return Path(base) / ".research"
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def find_cached(sources_dir, url):
|
|
105
|
-
"""Scan json sidecars for a matching canonical URL. Returns (md_path, meta) or None."""
|
|
106
|
-
canon = canonical_url(url)
|
|
107
|
-
if not canon or not sources_dir.is_dir():
|
|
108
|
-
return None
|
|
109
|
-
for sidecar in sources_dir.glob("*.json"):
|
|
110
|
-
try:
|
|
111
|
-
meta = json.loads(sidecar.read_text(encoding="utf-8"))
|
|
112
|
-
except (OSError, ValueError):
|
|
113
|
-
continue
|
|
114
|
-
if not isinstance(meta, dict):
|
|
115
|
-
continue
|
|
116
|
-
if canonical_url(str(meta.get("url", ""))) == canon:
|
|
117
|
-
md_path = sidecar.with_suffix(".md")
|
|
118
|
-
if md_path.exists():
|
|
119
|
-
return md_path, meta
|
|
120
|
-
return None
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def is_fresh(meta, url):
|
|
124
|
-
try:
|
|
125
|
-
cached_at = datetime.fromisoformat(str(meta.get("cached_at", "")))
|
|
126
|
-
if cached_at.tzinfo is None:
|
|
127
|
-
cached_at = cached_at.replace(tzinfo=timezone.utc)
|
|
128
|
-
age_days = (datetime.now(timezone.utc) - cached_at).total_seconds() / 86400
|
|
129
|
-
return age_days <= ttl_days(url)
|
|
130
|
-
except (ValueError, TypeError):
|
|
131
|
-
return False
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
def cmd_gate(stdin_data):
|
|
135
|
-
"""PreToolUse: serve fresh hits, allow everything else (fail-open)."""
|
|
136
|
-
tool_input = stdin_data.get("tool_input") or {}
|
|
137
|
-
url = tool_input.get("url", "")
|
|
138
|
-
if not url:
|
|
139
|
-
return None # allow
|
|
140
|
-
sources_dir = research_dir(stdin_data) / "sources"
|
|
141
|
-
try:
|
|
142
|
-
found = find_cached(sources_dir, url)
|
|
143
|
-
except Exception as e:
|
|
144
|
-
log(f"cache lookup failed for {domain_of(url)}: {e}")
|
|
145
|
-
return None # allow
|
|
146
|
-
if not found:
|
|
147
|
-
return None # miss -> allow, PostToolUse archives
|
|
148
|
-
md_path, meta = found
|
|
149
|
-
if not is_fresh(meta, url):
|
|
150
|
-
log(
|
|
151
|
-
f"stale entry for {domain_of(url)} ({meta.get('cached_at')}); allowing live fetch"
|
|
152
|
-
)
|
|
153
|
-
return None # stale -> allow, PostToolUse refreshes
|
|
154
|
-
try:
|
|
155
|
-
content = md_path.read_text(encoding="utf-8")
|
|
156
|
-
except OSError as e:
|
|
157
|
-
log(f"cache read failed: {e}")
|
|
158
|
-
return None # allow
|
|
159
|
-
cap = max_inline()
|
|
160
|
-
served = content if len(content) <= cap else content[:cap] + "\n\n[... truncated]"
|
|
161
|
-
body = (
|
|
162
|
-
f"<!-- EPISTEMIC_CACHE_HIT: {md_path.name} -->\n"
|
|
163
|
-
f"**Source URL:** {url}\n"
|
|
164
|
-
f"**Cached:** {meta.get('cached_at')} **Content SHA-256:** `{meta.get('hash')}`\n"
|
|
165
|
-
f"**Verification Tag:** `[VERIFIED: {str(meta.get('hash', ''))[:16]}]`\n\n"
|
|
166
|
-
f"{served}\n"
|
|
167
|
-
)
|
|
168
|
-
if len(content) > cap:
|
|
169
|
-
body += f"\n(Full cached text in {md_path}; cite the file for passages beyond the excerpt.)\n"
|
|
170
|
-
log(f"cache HIT for {domain_of(url)} ({len(content)} chars, {md_path.name})")
|
|
171
|
-
return {
|
|
172
|
-
"hookSpecificOutput": {
|
|
173
|
-
"hookEventName": "PreToolUse",
|
|
174
|
-
"permissionDecision": "deny",
|
|
175
|
-
"permissionDecisionReason": (
|
|
176
|
-
"Served from the epistemic content cache (.research/sources); "
|
|
177
|
-
"no network fetch needed. Cite the cached file."
|
|
178
|
-
),
|
|
179
|
-
"additionalContext": body,
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
def _text_from_value(val):
|
|
185
|
-
"""Recursively pull the first non-empty text out of a response value."""
|
|
186
|
-
if isinstance(val, str) and val.strip():
|
|
187
|
-
return val
|
|
188
|
-
if isinstance(val, dict):
|
|
189
|
-
for sub in ("text", "content", "output"):
|
|
190
|
-
got = _text_from_value(val.get(sub))
|
|
191
|
-
if got:
|
|
192
|
-
return got
|
|
193
|
-
if isinstance(val, list):
|
|
194
|
-
parts = [
|
|
195
|
-
p.get("text", "")
|
|
196
|
-
for p in val
|
|
197
|
-
if isinstance(p, dict) and isinstance(p.get("text"), str)
|
|
198
|
-
]
|
|
199
|
-
if parts:
|
|
200
|
-
return "\n".join(parts)
|
|
201
|
-
return ""
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
def extract_response_text(stdin_data):
|
|
205
|
-
"""Defensively locate the tool response text across field-name variants."""
|
|
206
|
-
for key in ("tool_response", "response", "tool_result", "result", "output"):
|
|
207
|
-
got = _text_from_value(stdin_data.get(key))
|
|
208
|
-
if got:
|
|
209
|
-
return got
|
|
210
|
-
return ""
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
def extract_url(stdin_data):
|
|
214
|
-
tool_input = stdin_data.get("tool_input") or stdin_data.get("input") or {}
|
|
215
|
-
if isinstance(tool_input, dict):
|
|
216
|
-
url = tool_input.get("url", "")
|
|
217
|
-
if url:
|
|
218
|
-
return url
|
|
219
|
-
# Fall back to a URL already recorded in a response envelope.
|
|
220
|
-
for key in ("tool_response", "response", "tool_result"):
|
|
221
|
-
val = stdin_data.get(key)
|
|
222
|
-
if isinstance(val, dict) and val.get("url"):
|
|
223
|
-
return val["url"]
|
|
224
|
-
return ""
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
def raw_markdown_enabled():
|
|
228
|
-
"""Whether raw markdown bodies may be archived.
|
|
229
|
-
|
|
230
|
-
Honored env override (config `cache_raw_markdown` -> spawned children):
|
|
231
|
-
`IUMBTEMS_CACHE_RAW_MARKDOWN=0|false|no|off` disables archiving entirely.
|
|
232
|
-
Unset (the default) preserves the historical archive-everything behavior.
|
|
233
|
-
"""
|
|
234
|
-
return os.environ.get("IUMBTEMS_CACHE_RAW_MARKDOWN", "1").strip().lower() not in (
|
|
235
|
-
"0",
|
|
236
|
-
"false",
|
|
237
|
-
"no",
|
|
238
|
-
"off",
|
|
239
|
-
)
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
def cmd_archive(stdin_data):
|
|
243
|
-
"""PostToolUse: store the live response as md + json sidecar."""
|
|
244
|
-
url = extract_url(stdin_data)
|
|
245
|
-
text = extract_response_text(stdin_data)
|
|
246
|
-
if not url or not text:
|
|
247
|
-
return None
|
|
248
|
-
if not raw_markdown_enabled():
|
|
249
|
-
log(
|
|
250
|
-
f"raw markdown caching disabled (IUMBTEMS_CACHE_RAW_MARKDOWN=0); "
|
|
251
|
-
f"not archiving {domain_of(url)}"
|
|
252
|
-
)
|
|
253
|
-
return None
|
|
254
|
-
try:
|
|
255
|
-
sources_dir = research_dir(stdin_data) / "sources"
|
|
256
|
-
sources_dir.mkdir(parents=True, exist_ok=True)
|
|
257
|
-
digest = hashlib.sha256(text.encode("utf-8")).hexdigest()
|
|
258
|
-
sources_dir.joinpath(f"{digest}.md").write_text(text, encoding="utf-8")
|
|
259
|
-
sources_dir.joinpath(f"{digest}.json").write_text(
|
|
260
|
-
json.dumps(
|
|
261
|
-
{
|
|
262
|
-
"hash": digest,
|
|
263
|
-
"url": url,
|
|
264
|
-
"title": url,
|
|
265
|
-
"tier": "WEB_DOCUMENT",
|
|
266
|
-
"cached_at": datetime.now(timezone.utc).isoformat(),
|
|
267
|
-
"byte_size": len(text.encode("utf-8")),
|
|
268
|
-
"char_count": len(text),
|
|
269
|
-
"custom_metadata": {"via": "webfetch-hook"},
|
|
270
|
-
},
|
|
271
|
-
indent=2,
|
|
272
|
-
),
|
|
273
|
-
encoding="utf-8",
|
|
274
|
-
)
|
|
275
|
-
log(f"archived {domain_of(url)} -> {digest[:16]} ({len(text)} chars)")
|
|
276
|
-
try:
|
|
277
|
-
root = Path(__file__).resolve().parents[3]
|
|
278
|
-
if str(root) not in sys.path:
|
|
279
|
-
sys.path.insert(0, str(root))
|
|
280
|
-
from runner.retrieval_log import log_event
|
|
281
|
-
|
|
282
|
-
log_event(
|
|
283
|
-
research_dir(stdin_data), "cache", url=url, hash=digest, tool="webcache"
|
|
284
|
-
)
|
|
285
|
-
except Exception:
|
|
286
|
-
pass
|
|
287
|
-
except Exception as e:
|
|
288
|
-
log(f"archive failed for {domain_of(url)}: {e}")
|
|
289
|
-
return None
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
def cmd_stats(stdin_data):
|
|
293
|
-
"""SessionStart: one-line cache summary; silent when empty."""
|
|
294
|
-
try:
|
|
295
|
-
sources_dir = research_dir(stdin_data) / "sources"
|
|
296
|
-
if not sources_dir.is_dir():
|
|
297
|
-
return None
|
|
298
|
-
md_files = list(sources_dir.glob("*.md"))
|
|
299
|
-
if not md_files:
|
|
300
|
-
return None
|
|
301
|
-
return {
|
|
302
|
-
"hookSpecificOutput": {
|
|
303
|
-
"hookEventName": "SessionStart",
|
|
304
|
-
"additionalContext": (
|
|
305
|
-
f"epistemic-cache: {len(md_files)} page(s) cached in "
|
|
306
|
-
f".research/sources — WebFetch hits are served from cache."
|
|
307
|
-
),
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
except Exception as e:
|
|
311
|
-
log(f"stats failed: {e}")
|
|
312
|
-
return None
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
def main(argv):
|
|
316
|
-
mode = argv[1] if len(argv) > 1 else "gate"
|
|
317
|
-
try:
|
|
318
|
-
raw = sys.stdin.read() if not sys.stdin.isatty() else ""
|
|
319
|
-
stdin_data = json.loads(raw) if raw.strip() else {}
|
|
320
|
-
if not isinstance(stdin_data, dict):
|
|
321
|
-
stdin_data = {}
|
|
322
|
-
except (ValueError, OSError):
|
|
323
|
-
stdin_data = {}
|
|
324
|
-
handler = {"gate": cmd_gate, "archive": cmd_archive, "stats": cmd_stats}.get(mode)
|
|
325
|
-
if handler is None:
|
|
326
|
-
log(f"unknown mode: {mode}")
|
|
327
|
-
return 1
|
|
328
|
-
try:
|
|
329
|
-
out = handler(stdin_data)
|
|
330
|
-
except Exception as e:
|
|
331
|
-
log(f"{mode} failed: {e}")
|
|
332
|
-
return 0 # fail-open
|
|
333
|
-
if out:
|
|
334
|
-
sys.stdout.write(json.dumps(out))
|
|
335
|
-
return 0
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
if __name__ == "__main__":
|
|
339
|
-
sys.exit(main(sys.argv))
|
package/skills/factory/SKILL.md
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: factory
|
|
3
|
-
description: Coding-factory Manager loop. Use when user invokes /factory or /domainexpansion, or wants grill-gated phased builds with programmer spawns and dual QA. Manager grills until frontier settled, runs brainstorm plus darkharvest swarms per gate, synthesizes .roadmap phases, spawns programmer per phase, and enforces dual-QA retry bounds. Never writes code itself; never bypasses explicit user sign-off (except inside /domainexpansion count).
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Factory — Manager Loop & Domain Expansion
|
|
7
|
-
|
|
8
|
-
## 1. Roles (OpenCode profiles in `config/opencode-snippet.json`)
|
|
9
|
-
|
|
10
|
-
- **manager** (primary): owns gates, grilling, swarm dispatch, roadmap synthesis, tiebreaks.
|
|
11
|
-
Task allowlist: `factory-*`, `programmer`, `qa-a`, `qa-b`, `brainstormer`, `darkharvester` deny `*` otherwise.
|
|
12
|
-
- **programmer** (subagent): implements exactly one phase brief. May call `iumbtems_verify_quote`; may NOT invoke swarms or other programmers.
|
|
13
|
-
- **qa-a / qa-b** (subagents): same model, DIVERGED prompts (functional-correctness vs adversarial edge-case). Read-only plus test execution; never edit.
|
|
14
|
-
- **researcher** = existing `iumbtems_brainstorm` + `iumbtems_darkharvest` swarms (no new profile).
|
|
15
|
-
|
|
16
|
-
## 1b. Driving run state (no filesystem paths)
|
|
17
|
-
|
|
18
|
-
Use the **`iumbtems_factory` MCP tool** for all run-state changes — never a
|
|
19
|
-
relative `skills/factory/scripts/factory.py` path (the toolchain lives in the
|
|
20
|
-
npm cache in consuming projects, and relative paths broke live: the manager
|
|
21
|
-
agent ran `find / -name factory.py`). The tool wraps the same helper and
|
|
22
|
-
resolves the project via `project_dir` argument, `IUMBTEMS_PROJECT_DIR`, or
|
|
23
|
-
the session cwd. It supports `init`, `phase-add`, `qa-record`, `expansion`,
|
|
24
|
-
`stop`, `gate` (`--action open|settle|approve|waive|escalate|count`), and returns `status: escalated` (exit 2) on the 3rd QA failure.
|
|
25
|
-
|
|
26
|
-
## 2. Gate protocol (max 5 swarm cycles per gate)
|
|
27
|
-
|
|
28
|
-
1. Grill until `.factory/frontier.json` settled (grilling skill).
|
|
29
|
-
2. Run brainstorm + darkharvest swarms (mock-first on fixtures).
|
|
30
|
-
3. Manager synthesizes `.roadmap/<phase>/` (GOAL.md + dossier.json).
|
|
31
|
-
4. Explicit user `approve` advances; anything else regrills (cycle counter in `.factory/state.json`).
|
|
32
|
-
|
|
33
|
-
## 3. Phase contract (`.roadmap/<phase>/`)
|
|
34
|
-
|
|
35
|
-
- `GOAL.md`: human-readable goal + acceptance criteria.
|
|
36
|
-
- `dossier.json`: `{phase, goal, evidence:[{hash, quote}], acceptance[], brief, verdict, hashes}`. Every harvest/claim entry needs a SHA-256 source hash or `file://` pointer or it is purged to NEGATIVE_KNOWLEDGE.
|
|
37
|
-
- Programmer receives the phase dossier (Manager chooses freeform vs strict brief but MUST cite phase hashes).
|
|
38
|
-
|
|
39
|
-
## 4. QA protocol (3 retries, then escalate)
|
|
40
|
-
|
|
41
|
-
- Both QA seats run per phase; disagreements go to manager tiebreak.
|
|
42
|
-
- `skills/factory/scripts/factory.py` tracks `qa_retries` per phase in `.factory/state.json`. On 3rd rejection: halt phase, return to manager with both QA reports (retry loop per plan; manager may regrill scope or escalate to user).
|
|
43
|
-
- QA verdicts: `pass | fail(reason) | conditional(note)`.
|
|
44
|
-
|
|
45
|
-
## 5. Domain expansion (`/domainexpansion <n>`)
|
|
46
|
-
|
|
47
|
-
- Bypasses per-loop gates; stops on count OR `.factory/STOP` file OR user kill, whichever first.
|
|
48
|
-
- Each loop: agents propose direction → quick swarm check → implement → QA → next.
|
|
49
|
-
- `factory.py` enforces: refuse `n < 1`, cap `n` at `--max-loops` default 10, check STOP file before every loop.
|
|
50
|
-
|
|
51
|
-
## 6. State layout (three dirs, distinct jobs)
|
|
52
|
-
|
|
53
|
-
- `.factory/`: run state (`state.json`, `frontier.json`, `STOP` kill-file). Gitignored runtime state.
|
|
54
|
-
- `.roadmap/`: output (phase dirs). Committed.
|
|
55
|
-
- `.research/`: evidence (swarm dossiers, source cache). Gitignored (existing rule).
|
|
56
|
-
|
|
57
|
-
## 7. Anti-patterns
|
|
58
|
-
|
|
59
|
-
- No code writes by manager; no swarm invocation by programmer; no edits by QA.
|
|
60
|
-
- No gate bypass outside `/domainexpansion`; no uncapped loops.
|
|
61
|
-
- No VERIFIED claims without hashes, even in expansion proposals.
|
|
@@ -1,300 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Factory run-state helper: phase dossiers, QA retry bounds, expansion loop guard.
|
|
4
|
-
|
|
5
|
-
All state lives under <project>/.factory/ (gitignored runtime state). Phase
|
|
6
|
-
output goes to <project>/.roadmap/<phase>/. Evidence stays in <project>/
|
|
7
|
-
.research/. Read-only w.r.t. repo code.
|
|
8
|
-
|
|
9
|
-
Project directory resolution: --project-dir > IUMBTEMS_PROJECT_DIR env > the
|
|
10
|
-
toolchain repo root (local-dev default). A consuming project must never leak
|
|
11
|
-
state into the IUMBTEMS checkout.
|
|
12
|
-
|
|
13
|
-
Usage:
|
|
14
|
-
python3 skills/factory/scripts/factory.py init --run <name>
|
|
15
|
-
python3 skills/factory/scripts/factory.py phase-add --run <name> --phase 01-auth --goal "..." --accept "..."
|
|
16
|
-
python3 skills/factory/scripts/factory.py qa-record --run <name> --phase 01-auth --seat qa-a --verdict fail --reason "..."
|
|
17
|
-
python3 skills/factory/scripts/factory.py expansion --run <name> --loops 10 [--max-loops 10]
|
|
18
|
-
python3 skills/factory/scripts/factory.py stop --run <name> # write STOP kill-file
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
import argparse
|
|
22
|
-
import json
|
|
23
|
-
import os
|
|
24
|
-
import sys
|
|
25
|
-
from datetime import datetime, timezone
|
|
26
|
-
from pathlib import Path
|
|
27
|
-
|
|
28
|
-
REPO_ROOT = Path(__file__).resolve().parent.parent.parent.parent
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def resolve_project_root(cli_value=None):
|
|
32
|
-
"""--project-dir > IUMBTEMS_PROJECT_DIR > repo root (local-dev default)."""
|
|
33
|
-
for candidate in (cli_value, os.environ.get("IUMBTEMS_PROJECT_DIR")):
|
|
34
|
-
if candidate and Path(candidate).is_dir():
|
|
35
|
-
return Path(candidate).resolve()
|
|
36
|
-
return REPO_ROOT
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
PROJECT_DIR = resolve_project_root()
|
|
40
|
-
FACTORY_DIR = PROJECT_DIR / ".factory"
|
|
41
|
-
ROADMAP_DIR = PROJECT_DIR / ".roadmap"
|
|
42
|
-
|
|
43
|
-
MAX_QA_RETRIES = 3
|
|
44
|
-
MAX_EXPANSION_LOOPS = 10
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def now():
|
|
48
|
-
return datetime.now(timezone.utc).isoformat()
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def run_dir(run):
|
|
52
|
-
return FACTORY_DIR / run
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def load_state(run):
|
|
56
|
-
p = run_dir(run) / "state.json"
|
|
57
|
-
if not p.exists():
|
|
58
|
-
raise SystemExit(
|
|
59
|
-
f"No factory run '{run}'. Pass --run <name> (or run "
|
|
60
|
-
f"`factory.py init --run <name>` first)."
|
|
61
|
-
)
|
|
62
|
-
return json.loads(p.read_text(encoding="utf-8"))
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def save_state(run, state):
|
|
66
|
-
p = run_dir(run) / "state.json"
|
|
67
|
-
p.write_text(json.dumps(state, indent=2), encoding="utf-8")
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def cmd_init(args):
|
|
71
|
-
d = run_dir(args.run)
|
|
72
|
-
d.mkdir(parents=True, exist_ok=True)
|
|
73
|
-
state = {
|
|
74
|
-
"run": args.run,
|
|
75
|
-
"created_at": now(),
|
|
76
|
-
"gate_cycles": 0,
|
|
77
|
-
"phases": {},
|
|
78
|
-
"expansion": {"loops_done": 0, "loops_planned": 0},
|
|
79
|
-
}
|
|
80
|
-
save_state(args.run, state)
|
|
81
|
-
print(f"✅ Factory run '{args.run}' initialised at {d}")
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def cmd_phase_add(args):
|
|
85
|
-
state = load_state(args.run)
|
|
86
|
-
if args.phase in state["phases"]:
|
|
87
|
-
raise SystemExit(f"Phase '{args.phase}' already exists.")
|
|
88
|
-
# Phase output goes to phase.json — a run-state helper must never clobber the
|
|
89
|
-
# author-written GOAL.md / dossier.json it is meant to track (#6 item 2.3).
|
|
90
|
-
# This check runs BEFORE any state mutation so a refusal leaves no trace.
|
|
91
|
-
phase_dir = ROADMAP_DIR / args.phase
|
|
92
|
-
authored = [
|
|
93
|
-
name for name in ("GOAL.md", "dossier.json") if (phase_dir / name).exists()
|
|
94
|
-
]
|
|
95
|
-
if authored and not getattr(args, "force", False):
|
|
96
|
-
raise SystemExit(
|
|
97
|
-
f"Refusing to touch author-written file(s) in {phase_dir}: "
|
|
98
|
-
f"{', '.join(authored)}. phase-add now writes phase.json only; "
|
|
99
|
-
f"re-run with --force to overwrite."
|
|
100
|
-
)
|
|
101
|
-
state["phases"][args.phase] = {
|
|
102
|
-
"goal": args.goal,
|
|
103
|
-
"acceptance": [a.strip() for a in args.accept.split(";") if a.strip()],
|
|
104
|
-
"status": "briefed",
|
|
105
|
-
"qa_retries": 0,
|
|
106
|
-
"qa_reports": [],
|
|
107
|
-
}
|
|
108
|
-
save_state(args.run, state)
|
|
109
|
-
phase_dir.mkdir(parents=True, exist_ok=True)
|
|
110
|
-
(phase_dir / "phase.json").write_text(
|
|
111
|
-
json.dumps(
|
|
112
|
-
{
|
|
113
|
-
"phase": args.phase,
|
|
114
|
-
"goal": args.goal,
|
|
115
|
-
"acceptance": state["phases"][args.phase]["acceptance"],
|
|
116
|
-
"status": "briefed",
|
|
117
|
-
},
|
|
118
|
-
indent=2,
|
|
119
|
-
),
|
|
120
|
-
encoding="utf-8",
|
|
121
|
-
)
|
|
122
|
-
print(f"✅ Phase '{args.phase}' briefed → {phase_dir / 'phase.json'}")
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
def cmd_gate(args):
|
|
126
|
-
"""First-class gate bookkeeping (open/settle/approve/waive/escalate/count)."""
|
|
127
|
-
state = load_state(args.run)
|
|
128
|
-
gates = state.setdefault("gates", {})
|
|
129
|
-
gate = gates.setdefault(
|
|
130
|
-
args.phase, {"status": "closed", "cycles": 0, "waivers": [], "history": []}
|
|
131
|
-
)
|
|
132
|
-
action = (args.action or "").lower()
|
|
133
|
-
if action == "open":
|
|
134
|
-
gate["status"] = "open"
|
|
135
|
-
gate["cycles"] = gate.get("cycles", 0) + 1
|
|
136
|
-
state["gate_cycles"] = state.get("gate_cycles", 0) + 1
|
|
137
|
-
elif action == "settle":
|
|
138
|
-
gate["status"] = "settled"
|
|
139
|
-
elif action == "approve":
|
|
140
|
-
gate["status"] = "approved"
|
|
141
|
-
elif action == "waive":
|
|
142
|
-
gate["status"] = "waived"
|
|
143
|
-
gate["waivers"].append(args.reason or "")
|
|
144
|
-
elif action == "escalate":
|
|
145
|
-
gate["status"] = "escalated"
|
|
146
|
-
gate["cycles"] = gate.get("cycles", 0) + 1
|
|
147
|
-
state["gate_cycles"] = state.get("gate_cycles", 0) + 1
|
|
148
|
-
elif action != "count":
|
|
149
|
-
raise SystemExit(
|
|
150
|
-
"gate --action must be one of: open, settle, approve, waive, escalate, count"
|
|
151
|
-
)
|
|
152
|
-
gate["history"].append({"action": action, "at": now(), "reason": args.reason or ""})
|
|
153
|
-
save_state(args.run, state)
|
|
154
|
-
print(json.dumps({"phase": args.phase, "gate": gate}, indent=2))
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
def cmd_qa_record(args):
|
|
158
|
-
state = load_state(args.run)
|
|
159
|
-
phase = state["phases"].get(args.phase)
|
|
160
|
-
if phase is None:
|
|
161
|
-
raise SystemExit(f"Unknown phase '{args.phase}'.")
|
|
162
|
-
if args.verdict not in ("pass", "fail", "conditional"):
|
|
163
|
-
raise SystemExit("verdict must be pass|fail|conditional.")
|
|
164
|
-
phase["qa_reports"].append(
|
|
165
|
-
{
|
|
166
|
-
"seat": args.seat,
|
|
167
|
-
"verdict": args.verdict,
|
|
168
|
-
"reason": args.reason or "",
|
|
169
|
-
"at": now(),
|
|
170
|
-
}
|
|
171
|
-
)
|
|
172
|
-
if args.verdict == "fail":
|
|
173
|
-
phase["qa_retries"] += 1
|
|
174
|
-
if phase["qa_retries"] >= MAX_QA_RETRIES:
|
|
175
|
-
phase["status"] = "escalated"
|
|
176
|
-
save_state(args.run, state)
|
|
177
|
-
print(
|
|
178
|
-
f"🛑 Phase '{args.phase}' ESCALATED after "
|
|
179
|
-
f"{MAX_QA_RETRIES} QA failures — back to manager."
|
|
180
|
-
)
|
|
181
|
-
return 2
|
|
182
|
-
phase["status"] = "retrying"
|
|
183
|
-
elif args.verdict == "pass":
|
|
184
|
-
phase["status"] = "signed-off"
|
|
185
|
-
else:
|
|
186
|
-
phase["status"] = "conditional"
|
|
187
|
-
save_state(args.run, state)
|
|
188
|
-
print(
|
|
189
|
-
f"✅ QA recorded: {args.phase} [{args.seat}] → {args.verdict} "
|
|
190
|
-
f"(status={phase['status']}, retries={phase['qa_retries']})"
|
|
191
|
-
)
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
def stop_requested(run):
|
|
195
|
-
return (run_dir(run) / "STOP").exists()
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
def cmd_expansion(args):
|
|
199
|
-
state = load_state(args.run)
|
|
200
|
-
n = args.loops
|
|
201
|
-
if n < 1:
|
|
202
|
-
raise SystemExit("loops must be >= 1.")
|
|
203
|
-
cap = args.max_loops or MAX_EXPANSION_LOOPS
|
|
204
|
-
n = min(n, cap)
|
|
205
|
-
state["expansion"]["loops_planned"] = n
|
|
206
|
-
save_state(args.run, state)
|
|
207
|
-
done = 0
|
|
208
|
-
for _ in range(1, n + 1):
|
|
209
|
-
if stop_requested(args.run):
|
|
210
|
-
print(f"🛑 STOP file present — halting after {done}/{n} loops.")
|
|
211
|
-
break
|
|
212
|
-
done += 1
|
|
213
|
-
print(
|
|
214
|
-
f"🔁 Expansion loop {done}/{n}: agents propose → swarm check → "
|
|
215
|
-
f"implement → QA (Manager drives each step)."
|
|
216
|
-
)
|
|
217
|
-
state = load_state(args.run)
|
|
218
|
-
state["expansion"]["loops_done"] += done
|
|
219
|
-
save_state(args.run, state)
|
|
220
|
-
print(f"✅ Expansion finished {done} loop(s).")
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
def cmd_stop(args):
|
|
224
|
-
(run_dir(args.run)).mkdir(parents=True, exist_ok=True)
|
|
225
|
-
(run_dir(args.run) / "STOP").write_text(
|
|
226
|
-
f"stop requested at {now()}\n", encoding="utf-8"
|
|
227
|
-
)
|
|
228
|
-
print(f"🛑 STOP file written for run '{args.run}'.")
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
def _add_common(parser):
|
|
232
|
-
parser.add_argument(
|
|
233
|
-
"--project-dir",
|
|
234
|
-
default=None,
|
|
235
|
-
help="Project the factory state belongs to (default: IUMBTEMS_PROJECT_DIR env, else repo root)",
|
|
236
|
-
)
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
def main():
|
|
240
|
-
global FACTORY_DIR, ROADMAP_DIR
|
|
241
|
-
ap = argparse.ArgumentParser(description="Factory run-state helper")
|
|
242
|
-
sub = ap.add_subparsers(dest="command", required=True)
|
|
243
|
-
|
|
244
|
-
p = sub.add_parser("init")
|
|
245
|
-
p.add_argument("--run", required=True)
|
|
246
|
-
_add_common(p)
|
|
247
|
-
p = sub.add_parser("phase-add")
|
|
248
|
-
p.add_argument("--run", required=True)
|
|
249
|
-
p.add_argument("--phase", required=True)
|
|
250
|
-
p.add_argument("--goal", required=True)
|
|
251
|
-
p.add_argument("--accept", default="")
|
|
252
|
-
p.add_argument(
|
|
253
|
-
"--force",
|
|
254
|
-
action="store_true",
|
|
255
|
-
help="Overwrite author-written GOAL.md/dossier.json if present",
|
|
256
|
-
)
|
|
257
|
-
_add_common(p)
|
|
258
|
-
p = sub.add_parser("qa-record")
|
|
259
|
-
p.add_argument("--run", required=True)
|
|
260
|
-
p.add_argument("--phase", required=True)
|
|
261
|
-
p.add_argument("--seat", required=True)
|
|
262
|
-
p.add_argument("--verdict", required=True)
|
|
263
|
-
p.add_argument("--reason", default="")
|
|
264
|
-
_add_common(p)
|
|
265
|
-
p = sub.add_parser("expansion")
|
|
266
|
-
p.add_argument("--run", required=True)
|
|
267
|
-
p.add_argument("--loops", type=int, required=True)
|
|
268
|
-
p.add_argument("--max-loops", type=int, default=MAX_EXPANSION_LOOPS)
|
|
269
|
-
_add_common(p)
|
|
270
|
-
p = sub.add_parser("stop")
|
|
271
|
-
p.add_argument("--run", required=True)
|
|
272
|
-
_add_common(p)
|
|
273
|
-
p = sub.add_parser("gate")
|
|
274
|
-
p.add_argument("--run", required=True)
|
|
275
|
-
p.add_argument("--phase", required=True)
|
|
276
|
-
p.add_argument(
|
|
277
|
-
"--action",
|
|
278
|
-
required=True,
|
|
279
|
-
choices=["open", "settle", "approve", "waive", "escalate", "count"],
|
|
280
|
-
)
|
|
281
|
-
p.add_argument("--reason", default="")
|
|
282
|
-
_add_common(p)
|
|
283
|
-
|
|
284
|
-
args = ap.parse_args()
|
|
285
|
-
root = resolve_project_root(args.project_dir)
|
|
286
|
-
FACTORY_DIR = root / ".factory"
|
|
287
|
-
ROADMAP_DIR = root / ".roadmap"
|
|
288
|
-
code = {
|
|
289
|
-
"init": cmd_init,
|
|
290
|
-
"phase-add": cmd_phase_add,
|
|
291
|
-
"qa-record": cmd_qa_record,
|
|
292
|
-
"expansion": cmd_expansion,
|
|
293
|
-
"stop": cmd_stop,
|
|
294
|
-
"gate": cmd_gate,
|
|
295
|
-
}[args.command](args)
|
|
296
|
-
sys.exit(code if isinstance(code, int) else 0)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
if __name__ == "__main__":
|
|
300
|
-
main()
|