fux-engine 0.2.0__tar.gz → 0.3.0__tar.gz
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.
- {fux_engine-0.2.0/fux_engine.egg-info → fux_engine-0.3.0}/PKG-INFO +1 -1
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/__init__.py +1 -1
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/assets/graph_boot.js +9 -1
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/assets/graph_template.html +2 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/build.py +3 -1
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/cli.py +9 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/clicmds.py +19 -2
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/config.py +6 -1
- fux_engine-0.3.0/fux/data/hooks/pre_commit.sh +27 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/graph.py +3 -1
- fux_engine-0.3.0/fux/graphhtml.py +20 -0
- fux_engine-0.3.0/fux/hookinstall.py +88 -0
- fux_engine-0.3.0/fux/settings.py +95 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0/fux_engine.egg-info}/PKG-INFO +1 -1
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux_engine.egg-info/SOURCES.txt +5 -0
- fux_engine-0.3.0/tests/test_graph_determinism.py +30 -0
- fux_engine-0.3.0/tests/test_graphhtml_links.py +31 -0
- fux_engine-0.3.0/tests/test_hookinstall.py +73 -0
- fux_engine-0.2.0/fux/graphhtml.py +0 -14
- fux_engine-0.2.0/fux/settings.py +0 -52
- {fux_engine-0.2.0 → fux_engine-0.3.0}/LICENSE +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/README.md +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/__main__.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/assets/fux-icon.svg +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/assets/fux-lockup.svg +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/assets/fux-mark.svg +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/astextract.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/bench.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/capture.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/check.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/cligraph.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/cliquery.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/cliutil.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/community.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/components.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/context.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/costledger.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/coverage.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/data/copilot/prompts/fux-plan.prompt.md +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/data/copilot/prompts/fux.prompt.md +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/data/global/README.md +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/data/global/rules/async-everywhere.md +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/data/global/rules/doc-per-code-change.md +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/data/global/rules/files-max-100-lines.md +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/data/global/rules/no-secrets-in-vcs.md +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/data/hooks/_common.sh +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/data/hooks/post_tool_use.sh +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/data/hooks/session_start.sh +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/data/hooks/stop.sh +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/data/hooks/user_prompt_submit.sh +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/data/packs/indian-markets-tax/pack.toml +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/data/packs/indian-markets-tax/rules/capital-gains-equity.md +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/data/packs/indian-markets-tax/rules/market-hours-nse.md +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/data/schema.json +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/data/skills/adr/SKILL.md +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/data/skills/distill/SKILL.md +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/data/skills/fetch-rules/SKILL.md +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/data/skills/fux/SKILL.md +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/data/skills/plan/SKILL.md +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/data/skills/savings/SKILL.md +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/data/skills/trace/SKILL.md +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/drift.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/embed.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/explain.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/feedback.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/fetchrules.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/findings.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/fix.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/fmwrite.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/frontmatter.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/gate.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/gitutil.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/globs.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/governance.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/graphquery.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/hookio.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/hooks.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/hybrid.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/impact.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/importer.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/index.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/initcmd.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/lint.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/loader.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/mcpserver.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/mine.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/model.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/narrative.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/pack.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/parity.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/paths.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/recall.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/report.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/savings.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/scaffold.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/scalars.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/schema.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/seal.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/serve.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/stats.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/templates/formula.md +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/templates/spec.md +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/touch.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/tour.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/uispec.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/usage.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/verify.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux/vexamples.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux_engine.egg-info/dependency_links.txt +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux_engine.egg-info/entry_points.txt +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux_engine.egg-info/requires.txt +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/fux_engine.egg-info/top_level.txt +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/pyproject.toml +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/setup.cfg +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/tests/test_ast_backend.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/tests/test_astextract.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/tests/test_bm25f_expand.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/tests/test_capture_governance.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/tests/test_centrality.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/tests/test_check_fix.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/tests/test_components.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/tests/test_costledger.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/tests/test_crossfile_calls.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/tests/test_edge_confidence.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/tests/test_embed_rerank.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/tests/test_examples.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/tests/test_feedback.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/tests/test_fetch_rules.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/tests/test_frontmatter.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/tests/test_fuzz_mine.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/tests/test_globs.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/tests/test_graphhtml.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/tests/test_hybrid.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/tests/test_impact.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/tests/test_lint_stats_gate.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/tests/test_mcp.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/tests/test_mcp_extra.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/tests/test_pack.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/tests/test_parity_import.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/tests/test_recall_build_verify.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/tests/test_recall_eval.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/tests/test_resolution.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/tests/test_savings.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/tests/test_schema_scaffold_init.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/tests/test_seal.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/tests/test_serve_sanitize.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/tests/test_uispec.py +0 -0
- {fux_engine-0.2.0 → fux_engine-0.3.0}/tests/test_verify_hardening.py +0 -0
|
@@ -459,7 +459,7 @@ function showDetail(n){ $("agentrow").style.display="flex";
|
|
|
459
459
|
`<span class="lab" style="${hue!=null?"":"color:var(--muted)"}">${n.type}${isKnow(n)?" · knowledge layer":""}</span></div>`+
|
|
460
460
|
`<div class="ins-title">${esc(n.label)}</div>`+
|
|
461
461
|
`<div class="ins-sub">community ${n.community} · degree ${deg[n.id]||0} · centrality ${(n.centrality||0).toFixed(3)}`+
|
|
462
|
-
(n.file?` · ${
|
|
462
|
+
(n.file?` · ${fileLink(n)}`:"")+`</div>`;
|
|
463
463
|
const pills=[]; if(n.layer)pills.push(["layer: "+n.layer,1]); if(n.domain)pills.push(["domain: "+n.domain,0]);
|
|
464
464
|
if(n.status)pills.push(["status: "+n.status,0]); if(isGod(n))pills.push(["⭐ hub",1]);
|
|
465
465
|
if(pills.length) s += `<div class="pills">`+pills.map(([p,a])=>`<span class="pill${a?" amb":""}">${esc(p)}</span>`).join("")+`</div>`;
|
|
@@ -472,6 +472,14 @@ function showDetail(n){ $("agentrow").style.display="flex";
|
|
|
472
472
|
$("detail").innerHTML = s; wireGo(); }
|
|
473
473
|
function clearDetail(){ $("agentrow").style.display="none";
|
|
474
474
|
$("detail").innerHTML = `<div class="ins-sub" style="margin:0">Click a node. Double-click to focus its neighbourhood.</div>`; }
|
|
475
|
+
// file:line → an <editor>://file/<abs>:<line>:<col> deep link (opens VSCode/Cursor on
|
|
476
|
+
// the exact line). Falls back to plain text when the build embedded no project ROOT.
|
|
477
|
+
function fileLink(n){
|
|
478
|
+
const label = esc(n.file) + (n.line ? ":"+n.line : "");
|
|
479
|
+
if(!ROOT) return label;
|
|
480
|
+
const href = encodeURI(EDITOR + "://file" + ROOT + "/" + n.file + ":" + (n.line||1) + ":1");
|
|
481
|
+
return `<a href="${href}" title="open in ${esc(EDITOR)} at line ${n.line||1}"`+
|
|
482
|
+
` style="color:#ffb877;text-decoration:none">${label}<span style="opacity:.6"> ↗</span></a>`; }
|
|
475
483
|
|
|
476
484
|
// ---- markdown export ----------------------------------------------------
|
|
477
485
|
function nodeMarkdown(n){ let s=`### ${n.label} (${n.type})\n`;
|
|
@@ -301,6 +301,8 @@
|
|
|
301
301
|
</div>
|
|
302
302
|
<script>
|
|
303
303
|
const DATA = __GRAPH_DATA__;
|
|
304
|
+
const ROOT = __ROOT__; // absolute project dir (for editor deep links)
|
|
305
|
+
const EDITOR = __EDITOR__; // vscode | vscode-insiders | cursor | windsurf
|
|
304
306
|
// node-type accent (used by community/heat lenses + meters); knowledge ignites amber.
|
|
305
307
|
const COLORS = { "code-file":"#3e4750","function":"#4a5560","class":"#5a4f63",
|
|
306
308
|
"rule":"#ffa44f","formula":"#ffd27f","glossary":"#ffce8a","invariant":"#ff7a5c",
|
|
@@ -17,7 +17,9 @@ def run(root: Path, full: bool = False) -> dict:
|
|
|
17
17
|
|
|
18
18
|
g = graph.build(root, rs, cfg, full=full)
|
|
19
19
|
fp.out_file("graph.json").write_text(graph.to_json(g), encoding="utf-8")
|
|
20
|
-
fp.out_file("graph.html").write_text(
|
|
20
|
+
fp.out_file("graph.html").write_text(
|
|
21
|
+
graphhtml.render(g, root=root, editor=cfg.get("graph_editor", "vscode")),
|
|
22
|
+
encoding="utf-8")
|
|
21
23
|
fp.out_file("GRAPH_REPORT.md").write_text(report.render(g), encoding="utf-8")
|
|
22
24
|
|
|
23
25
|
narr = narrative.render(rs)
|
|
@@ -141,6 +141,15 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
141
141
|
|
|
142
142
|
sub.add_parser("report", help="write GRAPH_REPORT.md (god nodes + communities)").set_defaults(fn=cligraph.cmd_report)
|
|
143
143
|
|
|
144
|
+
hk = sub.add_parser("hooks", help="install/uninstall/status Fux hooks across git + agents")
|
|
145
|
+
hk.add_argument("action", choices=["install", "uninstall", "status"], nargs="?",
|
|
146
|
+
default="install", help="default: install")
|
|
147
|
+
hk.add_argument("--all", action="store_true", help="all surfaces (the default)")
|
|
148
|
+
for _s in ("git", "claude", "codex", "copilot"):
|
|
149
|
+
hk.add_argument(f"--{_s}", action="store_true", help=f"only the {_s} surface")
|
|
150
|
+
hk.add_argument("--recall", action="store_true", help="also wire the UserPromptSubmit recall hook")
|
|
151
|
+
hk.set_defaults(fn=clicmds.cmd_hooks)
|
|
152
|
+
|
|
144
153
|
sub.add_parser("setup", help="copy bundled assets (schema, hooks, skills) to ~/.claude/fux/").set_defaults(fn=clicmds.cmd_setup)
|
|
145
154
|
|
|
146
155
|
fr = sub.add_parser("fetch-rules", help="fetch plain text from a URL / file / PDF for rule extraction")
|
|
@@ -5,8 +5,8 @@ import shutil
|
|
|
5
5
|
import subprocess
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
|
-
from fux import (build, check, config, context, fix, gate,
|
|
9
|
-
mcpserver, paths, serve)
|
|
8
|
+
from fux import (build, check, config, context, fix, gate, hookinstall, importer,
|
|
9
|
+
initcmd, mcpserver, paths, serve)
|
|
10
10
|
from fux.cliutil import root
|
|
11
11
|
from fux.findings import blocking
|
|
12
12
|
|
|
@@ -24,6 +24,23 @@ def cmd_init(args) -> int:
|
|
|
24
24
|
return 0
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
def cmd_hooks(args) -> int:
|
|
28
|
+
"""install | uninstall | status across git + claude + codex + copilot surfaces."""
|
|
29
|
+
picked = [s for s in hookinstall.SURFACES if getattr(args, s, False)]
|
|
30
|
+
surfaces = None if (getattr(args, "all", False) or not picked) else picked
|
|
31
|
+
if args.action == "status":
|
|
32
|
+
for surface, on in hookinstall.status(root()).items():
|
|
33
|
+
print(f" {'✔' if on else '·'} {surface:<8} {'wired' if on else 'not wired'}")
|
|
34
|
+
return 0
|
|
35
|
+
fn = hookinstall.uninstall if args.action == "uninstall" else hookinstall.install
|
|
36
|
+
kw = {} if args.action == "uninstall" else {"recall": getattr(args, "recall", False)}
|
|
37
|
+
verb = "removed from" if args.action == "uninstall" else "wired into"
|
|
38
|
+
print(f"✔ Fux hooks {verb}:")
|
|
39
|
+
for surface, where in fn(root(), surfaces, **kw).items():
|
|
40
|
+
print(f" {surface:<8} → {where}")
|
|
41
|
+
return 0
|
|
42
|
+
|
|
43
|
+
|
|
27
44
|
def cmd_build(args) -> int:
|
|
28
45
|
s = build.run(root(), full=getattr(args, "full", False))
|
|
29
46
|
print(f"✔ Built: {s['active']} active rules · {s['code_files']} code files · "
|
|
@@ -32,6 +32,8 @@ DEFAULTS = {
|
|
|
32
32
|
"cost_tracking": False, # opt-in: record each lookup's savings → cumulative cost.json (§12)
|
|
33
33
|
"parity_stay": [], # docs that stay/are out-of-scope for `fux parity` (§17.17)
|
|
34
34
|
"context_budget_tokens": 0, # >0 ⇒ knapsack-pack the SessionStart INDEX (§17.25)
|
|
35
|
+
"graph_editor": "vscode", # editor URI scheme for clickable graph.html node links:
|
|
36
|
+
# vscode | vscode-insiders | cursor | windsurf (§7)
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
|
|
@@ -81,5 +83,8 @@ def default_toml() -> str:
|
|
|
81
83
|
"parity_stay = []\n\n"
|
|
82
84
|
"# Token budget for the SessionStart INDEX. 0 = inject everything; >0 picks\n"
|
|
83
85
|
"# the optimal (knapsack) rule subset that fits — for very large corpora.\n"
|
|
84
|
-
"context_budget_tokens = 0\n"
|
|
86
|
+
"context_budget_tokens = 0\n\n"
|
|
87
|
+
"# Editor for clickable file:line node links in graph.html.\n"
|
|
88
|
+
"# vscode | vscode-insiders | cursor | windsurf\n"
|
|
89
|
+
'graph_editor = "vscode"\n'
|
|
85
90
|
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Fux pre-commit — rebuild the derived views ($0, AST-only) and stage them so
|
|
3
|
+
# .fux/out/ always matches the committed code IN THE SAME COMMIT. Non-blocking:
|
|
4
|
+
# a build failure warns but never aborts the commit (use `fux gate` to *block* on
|
|
5
|
+
# drift). git invokes this with cwd at the repo root. Installed by `fux hooks install`.
|
|
6
|
+
set -uo pipefail
|
|
7
|
+
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
# shellcheck source=_common.sh
|
|
9
|
+
. "$DIR/_common.sh"
|
|
10
|
+
|
|
11
|
+
# Skip during rebase/merge/cherry-pick — don't fight --continue with new changes.
|
|
12
|
+
GIT_DIR="$(git rev-parse --git-dir 2>/dev/null || echo .git)"
|
|
13
|
+
[ -d "$GIT_DIR/rebase-merge" ] && exit 0
|
|
14
|
+
[ -d "$GIT_DIR/rebase-apply" ] && exit 0
|
|
15
|
+
[ -f "$GIT_DIR/MERGE_HEAD" ] && exit 0
|
|
16
|
+
[ -f "$GIT_DIR/CHERRY_PICK_HEAD" ] && exit 0
|
|
17
|
+
|
|
18
|
+
# Only act inside a project that has a .fux/ footprint.
|
|
19
|
+
fux_run context >/dev/null 2>&1 || exit 0
|
|
20
|
+
|
|
21
|
+
echo "[fux hook] rebuilding derived views..."
|
|
22
|
+
if fux_run build >/dev/null 2>&1; then
|
|
23
|
+
git add .fux/out 2>/dev/null || true # .session-*.json is gitignored, so skipped
|
|
24
|
+
else
|
|
25
|
+
echo "[fux hook] build failed — committing without refreshed views (run \`fux build\`)."
|
|
26
|
+
fi
|
|
27
|
+
exit 0
|
|
@@ -126,7 +126,9 @@ def _xref(nodes: dict, texts: dict[str, str], covered: set[tuple[str, str]]) ->
|
|
|
126
126
|
index = _symbol_index(nodes)
|
|
127
127
|
seen, out = set(), []
|
|
128
128
|
for rel, text in texts.items():
|
|
129
|
-
|
|
129
|
+
# `call_names` returns a set — sort so reference-edge order (and thus
|
|
130
|
+
# graph.json) is reproducible across builds (no PYTHONHASHSEED churn).
|
|
131
|
+
for name in sorted(astextract.call_names(text) - astextract.CALL_KEYWORDS):
|
|
130
132
|
for tid in index.get(name, []):
|
|
131
133
|
if tid.startswith(rel + "::") or (rel, tid) in covered or (rel, tid) in seen:
|
|
132
134
|
continue
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Render the merged graph as a self-contained interactive HTML file (plan §7)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
_ASSETS = Path(__file__).parent / "assets"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def render(graph: dict, root: Path | None = None, editor: str = "vscode") -> str:
|
|
11
|
+
"""Build the offline viewer. ``root`` (absolute project dir) + ``editor`` make
|
|
12
|
+
file:line node labels clickable as ``<editor>://file/<abs>:<line>`` deep links."""
|
|
13
|
+
template = (_ASSETS / "graph_template.html").read_text(encoding="utf-8")
|
|
14
|
+
boot = (_ASSETS / "graph_boot.js").read_text(encoding="utf-8")
|
|
15
|
+
data = json.dumps(graph, ensure_ascii=False)
|
|
16
|
+
root_str = str(root.resolve()) if root is not None else ""
|
|
17
|
+
return (template.replace("__GRAPH_DATA__", data)
|
|
18
|
+
.replace("__BOOT__", boot)
|
|
19
|
+
.replace("__ROOT__", json.dumps(root_str))
|
|
20
|
+
.replace("__EDITOR__", json.dumps(editor or "vscode")))
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""`fux hooks install` — wire Fux across every agent surface from one command.
|
|
2
|
+
|
|
3
|
+
Four surfaces, all pointing at the *installed package* scripts (~/.claude/fux/hooks),
|
|
4
|
+
never a sibling dev checkout:
|
|
5
|
+
git → .git/hooks/pre-commit shim → packaged pre_commit.sh (build + stage views)
|
|
6
|
+
claude → .claude/settings.json (SessionStart/PostToolUse/Stop hooks)
|
|
7
|
+
codex → .codex/hooks.json
|
|
8
|
+
copilot → .copilot/settings.json
|
|
9
|
+
Idempotent; `uninstall` / `status` mirror it. Git is non-blocking by design.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import stat
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from fux import gitutil, paths, settings
|
|
17
|
+
|
|
18
|
+
SURFACES = ["git", "claude", "codex", "copilot"]
|
|
19
|
+
_MARK = "fux-hook"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _packaged_precommit() -> Path:
|
|
23
|
+
return paths.claude_home() / "fux" / "hooks" / "pre_commit.sh"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _shim(target: Path) -> str:
|
|
27
|
+
return (f"#!/bin/sh\n# {_MARK} — installed by `fux hooks install`. Delegates to the\n"
|
|
28
|
+
f"# packaged Fux pre-commit (build derived views + stage them).\n"
|
|
29
|
+
f'HOOK="{target}"\n'
|
|
30
|
+
'[ -x "$HOOK" ] && exec "$HOOK" "$@"\n'
|
|
31
|
+
'exit 0\n')
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _install_git(root: Path) -> str:
|
|
35
|
+
hooks = gitutil.hooks_dir(root)
|
|
36
|
+
if hooks is None:
|
|
37
|
+
return "skipped (not a git repo)"
|
|
38
|
+
hooks.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
hook = hooks / "pre-commit"
|
|
40
|
+
if hook.exists() and _MARK not in hook.read_text(encoding="utf-8", errors="ignore"):
|
|
41
|
+
backup = hook.with_suffix(".pre-fux")
|
|
42
|
+
hook.rename(backup)
|
|
43
|
+
note = f" (existing hook backed up → {backup.name})"
|
|
44
|
+
else:
|
|
45
|
+
note = ""
|
|
46
|
+
hook.write_text(_shim(_packaged_precommit()), encoding="utf-8")
|
|
47
|
+
hook.chmod(hook.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
48
|
+
return f"{hook}{note}"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def install(root: Path, surfaces: list[str] | None = None, recall: bool = False) -> dict:
|
|
52
|
+
todo = surfaces or SURFACES
|
|
53
|
+
out: dict[str, str] = {}
|
|
54
|
+
if "git" in todo:
|
|
55
|
+
out["git"] = _install_git(root)
|
|
56
|
+
for agent in ("claude", "codex", "copilot"):
|
|
57
|
+
if agent in todo:
|
|
58
|
+
out[agent] = str(settings.wire_file(root / settings.AGENT_FILES[agent], recall=recall))
|
|
59
|
+
return out
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def uninstall(root: Path, surfaces: list[str] | None = None) -> dict:
|
|
63
|
+
todo = surfaces or SURFACES
|
|
64
|
+
out: dict[str, str] = {}
|
|
65
|
+
if "git" in todo:
|
|
66
|
+
hooks = gitutil.hooks_dir(root)
|
|
67
|
+
hook = hooks / "pre-commit" if hooks else None
|
|
68
|
+
if hook and hook.exists() and _MARK in hook.read_text(encoding="utf-8", errors="ignore"):
|
|
69
|
+
hook.unlink()
|
|
70
|
+
out["git"] = "removed"
|
|
71
|
+
else:
|
|
72
|
+
out["git"] = "not installed"
|
|
73
|
+
for agent in ("claude", "codex", "copilot"):
|
|
74
|
+
if agent in todo:
|
|
75
|
+
removed = settings.unwire_file(root / settings.AGENT_FILES[agent])
|
|
76
|
+
out[agent] = "removed" if removed else "not installed"
|
|
77
|
+
return out
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def status(root: Path) -> dict:
|
|
81
|
+
out: dict[str, bool] = {}
|
|
82
|
+
hooks = gitutil.hooks_dir(root)
|
|
83
|
+
hook = hooks / "pre-commit" if hooks else None
|
|
84
|
+
out["git"] = bool(hook and hook.exists()
|
|
85
|
+
and _MARK in hook.read_text(encoding="utf-8", errors="ignore"))
|
|
86
|
+
for agent in ("claude", "codex", "copilot"):
|
|
87
|
+
out[agent] = settings.is_wired(root / settings.AGENT_FILES[agent])
|
|
88
|
+
return out
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Wire Fux hooks into an agent's JSON settings (plan §8). Idempotent.
|
|
2
|
+
|
|
3
|
+
Claude (`.claude/settings.json`), Codex (`.codex/hooks.json`), and Copilot
|
|
4
|
+
(`.copilot/settings.json`) share one event→hook shape, so one writer serves all
|
|
5
|
+
three. Prefers the installed wrapper scripts (~/.claude/fux/hooks/*.sh, which carry
|
|
6
|
+
a `python -m fux` fallback) so hooks fire even when the `fux` console script is not
|
|
7
|
+
on PATH; falls back to the bare `fux <subcommand>` form otherwise.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from fux import paths
|
|
15
|
+
|
|
16
|
+
# event → (wrapper script name, bare-command fallback, optional matcher)
|
|
17
|
+
_SPEC = {
|
|
18
|
+
"SessionStart": ("session_start.sh", "fux context", None),
|
|
19
|
+
"PostToolUse": ("post_tool_use.sh", "fux hook-touch", "Edit|Write"),
|
|
20
|
+
"Stop": ("stop.sh", "fux hook-check", None),
|
|
21
|
+
}
|
|
22
|
+
_RECALL = {"UserPromptSubmit": ("user_prompt_submit.sh", "fux hook-recall", None)}
|
|
23
|
+
|
|
24
|
+
# agent → settings file (relative to project root)
|
|
25
|
+
AGENT_FILES = {
|
|
26
|
+
"claude": Path(".claude") / "settings.json",
|
|
27
|
+
"codex": Path(".codex") / "hooks.json",
|
|
28
|
+
"copilot": Path(".copilot") / "settings.json",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _command(script: str, fallback: str) -> str:
|
|
33
|
+
wrapper = paths.claude_home() / "fux" / "hooks" / script
|
|
34
|
+
return str(wrapper) if wrapper.exists() else fallback
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _entry(script: str, fallback: str, matcher: str | None) -> dict:
|
|
38
|
+
hook = {"hooks": [{"type": "command", "command": _command(script, fallback)}]}
|
|
39
|
+
if matcher:
|
|
40
|
+
hook["matcher"] = matcher
|
|
41
|
+
return hook
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def wire_file(path: Path, recall: bool = False) -> Path:
|
|
45
|
+
"""Wire the Fux hook spec into one agent settings file. Idempotent per event."""
|
|
46
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
data = json.loads(path.read_text()) if path.exists() else {}
|
|
48
|
+
hooks = data.setdefault("hooks", {})
|
|
49
|
+
spec = {**_SPEC, **(_RECALL if recall else {})}
|
|
50
|
+
for event, (script, fallback, matcher) in spec.items():
|
|
51
|
+
existing = hooks.setdefault(event, [])
|
|
52
|
+
if not _already(existing):
|
|
53
|
+
existing.append(_entry(script, fallback, matcher))
|
|
54
|
+
path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
55
|
+
return path
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def wire(root: Path, recall: bool = False) -> Path:
|
|
59
|
+
"""Wire Claude's `.claude/settings.json` (the original entry point)."""
|
|
60
|
+
return wire_file(root / AGENT_FILES["claude"], recall=recall)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def unwire_file(path: Path) -> bool:
|
|
64
|
+
"""Remove every Fux hook entry from an agent settings file. True if changed."""
|
|
65
|
+
if not path.exists():
|
|
66
|
+
return False
|
|
67
|
+
data = json.loads(path.read_text())
|
|
68
|
+
hooks = data.get("hooks", {})
|
|
69
|
+
changed = False
|
|
70
|
+
for event in list(hooks):
|
|
71
|
+
kept = [e for e in hooks[event]
|
|
72
|
+
if not any("fux" in h.get("command", "") for h in e.get("hooks", []))]
|
|
73
|
+
if len(kept) != len(hooks[event]):
|
|
74
|
+
changed = True
|
|
75
|
+
hooks[event] = kept
|
|
76
|
+
if not hooks[event]:
|
|
77
|
+
del hooks[event]
|
|
78
|
+
if changed:
|
|
79
|
+
path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
80
|
+
return changed
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def is_wired(path: Path) -> bool:
|
|
84
|
+
"""True if any Fux hook is present in this agent settings file."""
|
|
85
|
+
if not path.exists():
|
|
86
|
+
return False
|
|
87
|
+
hooks = json.loads(path.read_text()).get("hooks", {})
|
|
88
|
+
return any("fux" in h.get("command", "")
|
|
89
|
+
for evs in hooks.values() for e in evs for h in e.get("hooks", []))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _already(existing: list) -> bool:
|
|
93
|
+
"""True if any Fux hook (either wiring form) is already present for this event."""
|
|
94
|
+
cmds = [h.get("command", "") for e in existing for h in e.get("hooks", [])]
|
|
95
|
+
return any("fux" in c for c in cmds)
|
|
@@ -35,6 +35,7 @@ fux/governance.py
|
|
|
35
35
|
fux/graph.py
|
|
36
36
|
fux/graphhtml.py
|
|
37
37
|
fux/graphquery.py
|
|
38
|
+
fux/hookinstall.py
|
|
38
39
|
fux/hookio.py
|
|
39
40
|
fux/hooks.py
|
|
40
41
|
fux/hybrid.py
|
|
@@ -82,6 +83,7 @@ fux/data/global/rules/files-max-100-lines.md
|
|
|
82
83
|
fux/data/global/rules/no-secrets-in-vcs.md
|
|
83
84
|
fux/data/hooks/_common.sh
|
|
84
85
|
fux/data/hooks/post_tool_use.sh
|
|
86
|
+
fux/data/hooks/pre_commit.sh
|
|
85
87
|
fux/data/hooks/session_start.sh
|
|
86
88
|
fux/data/hooks/stop.sh
|
|
87
89
|
fux/data/hooks/user_prompt_submit.sh
|
|
@@ -120,7 +122,10 @@ tests/test_fetch_rules.py
|
|
|
120
122
|
tests/test_frontmatter.py
|
|
121
123
|
tests/test_fuzz_mine.py
|
|
122
124
|
tests/test_globs.py
|
|
125
|
+
tests/test_graph_determinism.py
|
|
123
126
|
tests/test_graphhtml.py
|
|
127
|
+
tests/test_graphhtml_links.py
|
|
128
|
+
tests/test_hookinstall.py
|
|
124
129
|
tests/test_hybrid.py
|
|
125
130
|
tests/test_impact.py
|
|
126
131
|
tests/test_lint_stats_gate.py
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Reproducible builds: `_xref` iterates a set (`call_names`), so without an
|
|
2
|
+
explicit sort the `references` edge order — and thus graph.json — churns across
|
|
3
|
+
builds under hash randomization, making committed views noisy. The fix sorts the
|
|
4
|
+
iteration; this guards it by asserting the emitted order is canonical (sorted)."""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
from fux import build
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _refs_from(project, src_file):
|
|
13
|
+
g = json.loads((project / ".fux" / "out" / "graph.json").read_text())
|
|
14
|
+
return [e for e in g["edges"]
|
|
15
|
+
if e["type"] == "references" and e["source"] == src_file]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_reference_edges_emitted_in_sorted_target_order(project):
|
|
19
|
+
# caller.py references three symbols defined elsewhere, at module scope (so they
|
|
20
|
+
# are loose file→symbol `references`, the edges built by `_xref`).
|
|
21
|
+
(project / "src" / "defs.py").write_text(
|
|
22
|
+
"def zeta():\n return 1\n\ndef alpha():\n return 2\n\ndef mu():\n return 3\n")
|
|
23
|
+
(project / "src" / "caller.py").write_text("zeta()\nalpha()\nmu()\n")
|
|
24
|
+
build.run(project)
|
|
25
|
+
|
|
26
|
+
refs = _refs_from(project, "src/caller.py")
|
|
27
|
+
names = [e["target"].split("::", 1)[1] for e in refs]
|
|
28
|
+
assert set(names) >= {"alpha", "mu", "zeta"}, names
|
|
29
|
+
# canonical order — independent of set-iteration / PYTHONHASHSEED.
|
|
30
|
+
assert names == sorted(names), f"reference edges not in sorted order: {names}"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""graph.html embeds the project root + editor scheme so node file:line labels
|
|
2
|
+
become clickable <editor>://file/<abs>:<line> deep links."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from fux import graphhtml
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _graph():
|
|
11
|
+
return {"nodes": [{"id": "src/a.py::f", "label": "f", "type": "function",
|
|
12
|
+
"file": "src/a.py", "line": 12}],
|
|
13
|
+
"edges": [], "meta": {}}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_render_embeds_resolved_root_and_editor(tmp_path):
|
|
17
|
+
html = graphhtml.render(_graph(), root=tmp_path, editor="cursor")
|
|
18
|
+
assert f'const ROOT = "{tmp_path.resolve()}"' in html
|
|
19
|
+
assert 'const EDITOR = "cursor"' in html
|
|
20
|
+
assert "function fileLink" in html # the deep-link helper is shipped
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_render_defaults_editor_to_vscode(tmp_path):
|
|
24
|
+
html = graphhtml.render(_graph(), root=tmp_path)
|
|
25
|
+
assert 'const EDITOR = "vscode"' in html
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_render_without_root_is_inert():
|
|
29
|
+
# No root → empty ROOT string → fileLink falls back to plain text (no crash).
|
|
30
|
+
html = graphhtml.render(_graph())
|
|
31
|
+
assert 'const ROOT = ""' in html
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""`fux hooks install` wires git + claude + codex + copilot from one command,
|
|
2
|
+
always pointing at packaged scripts, idempotent, with status/uninstall mirrors."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
|
|
8
|
+
from fux import hookinstall, settings
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _git_init(root):
|
|
12
|
+
subprocess.run(["git", "init", "-q"], cwd=root, check=True)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_install_wires_all_three_agents(project):
|
|
16
|
+
out = hookinstall.install(project, ["claude", "codex", "copilot"])
|
|
17
|
+
for agent, rel in settings.AGENT_FILES.items():
|
|
18
|
+
path = project / rel
|
|
19
|
+
assert path.exists(), f"{agent} settings not written"
|
|
20
|
+
hooks = json.loads(path.read_text())["hooks"]
|
|
21
|
+
cmds = [h["command"] for evs in hooks.values() for e in evs for h in e["hooks"]]
|
|
22
|
+
assert any("fux" in c for c in cmds), f"{agent} has no fux hook"
|
|
23
|
+
assert set(out) == {"claude", "codex", "copilot"}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_install_is_idempotent(project):
|
|
27
|
+
hookinstall.install(project, ["claude"])
|
|
28
|
+
hookinstall.install(project, ["claude"])
|
|
29
|
+
hooks = json.loads((project / ".claude/settings.json").read_text())["hooks"]
|
|
30
|
+
# SessionStart wired exactly once, not duplicated on a second install.
|
|
31
|
+
assert len(hooks["SessionStart"]) == 1
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_git_install_writes_executable_precommit_shim(project):
|
|
35
|
+
_git_init(project)
|
|
36
|
+
out = hookinstall.install(project, ["git"])
|
|
37
|
+
hook = project / ".git" / "hooks" / "pre-commit"
|
|
38
|
+
assert hook.exists() and hook.stat().st_mode & 0o111, "pre-commit not executable"
|
|
39
|
+
body = hook.read_text()
|
|
40
|
+
assert "fux-hook" in body # our marker
|
|
41
|
+
assert "pre_commit.sh" in body # delegates to the packaged script
|
|
42
|
+
assert str(hook) in out["git"]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_git_install_backs_up_foreign_precommit(project):
|
|
46
|
+
_git_init(project)
|
|
47
|
+
hook = project / ".git" / "hooks" / "pre-commit"
|
|
48
|
+
hook.write_text("#!/bin/sh\necho mine\n")
|
|
49
|
+
hookinstall.install(project, ["git"])
|
|
50
|
+
assert (project / ".git" / "hooks" / "pre-commit.pre-fux").exists()
|
|
51
|
+
assert "fux-hook" in hook.read_text()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_status_and_uninstall_roundtrip(project):
|
|
55
|
+
_git_init(project)
|
|
56
|
+
hookinstall.install(project)
|
|
57
|
+
st = hookinstall.status(project)
|
|
58
|
+
assert all(st.values()), st
|
|
59
|
+
hookinstall.uninstall(project)
|
|
60
|
+
st2 = hookinstall.status(project)
|
|
61
|
+
assert not any(st2.values()), st2
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_uninstall_leaves_foreign_hooks(project):
|
|
65
|
+
settings.wire_file(project / ".claude/settings.json")
|
|
66
|
+
data = json.loads((project / ".claude/settings.json").read_text())
|
|
67
|
+
data["hooks"].setdefault("SessionStart", [])[0] # fux entry exists
|
|
68
|
+
data["hooks"]["Stop"].append({"hooks": [{"type": "command", "command": "echo other"}]})
|
|
69
|
+
(project / ".claude/settings.json").write_text(json.dumps(data))
|
|
70
|
+
hookinstall.uninstall(project, ["claude"])
|
|
71
|
+
hooks = json.loads((project / ".claude/settings.json").read_text())["hooks"]
|
|
72
|
+
cmds = [h["command"] for evs in hooks.values() for e in evs for h in e["hooks"]]
|
|
73
|
+
assert "echo other" in cmds and not any("fux" in c for c in cmds)
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
"""Render the merged graph as a self-contained interactive HTML file (plan §7)."""
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
import json
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
|
|
7
|
-
_ASSETS = Path(__file__).parent / "assets"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def render(graph: dict) -> str:
|
|
11
|
-
template = (_ASSETS / "graph_template.html").read_text(encoding="utf-8")
|
|
12
|
-
boot = (_ASSETS / "graph_boot.js").read_text(encoding="utf-8")
|
|
13
|
-
data = json.dumps(graph, ensure_ascii=False)
|
|
14
|
-
return template.replace("__GRAPH_DATA__", data).replace("__BOOT__", boot)
|
fux_engine-0.2.0/fux/settings.py
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
"""Wire Fux hooks into a project's .claude/settings.json (plan §8). Idempotent.
|
|
2
|
-
|
|
3
|
-
Prefers the installed wrapper scripts (~/.claude/fux/hooks/*.sh, which carry a
|
|
4
|
-
`python -m fux` fallback) so hooks work even when the `fux` console script is not
|
|
5
|
-
on PATH; falls back to the bare `fux <subcommand>` form otherwise.
|
|
6
|
-
"""
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
import json
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
|
|
12
|
-
from fux import paths
|
|
13
|
-
|
|
14
|
-
# event → (wrapper script name, bare-command fallback, optional matcher)
|
|
15
|
-
_SPEC = {
|
|
16
|
-
"SessionStart": ("session_start.sh", "fux context", None),
|
|
17
|
-
"PostToolUse": ("post_tool_use.sh", "fux hook-touch", "Edit|Write"),
|
|
18
|
-
"Stop": ("stop.sh", "fux hook-check", None),
|
|
19
|
-
}
|
|
20
|
-
_RECALL = {"UserPromptSubmit": ("user_prompt_submit.sh", "fux hook-recall", None)}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def _command(script: str, fallback: str) -> str:
|
|
24
|
-
wrapper = paths.claude_home() / "fux" / "hooks" / script
|
|
25
|
-
return str(wrapper) if wrapper.exists() else fallback
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def _entry(script: str, fallback: str, matcher: str | None) -> dict:
|
|
29
|
-
hook = {"hooks": [{"type": "command", "command": _command(script, fallback)}]}
|
|
30
|
-
if matcher:
|
|
31
|
-
hook["matcher"] = matcher
|
|
32
|
-
return hook
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def wire(root: Path, recall: bool = False) -> Path:
|
|
36
|
-
path = root / ".claude" / "settings.json"
|
|
37
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
38
|
-
data = json.loads(path.read_text()) if path.exists() else {}
|
|
39
|
-
hooks = data.setdefault("hooks", {})
|
|
40
|
-
spec = {**_SPEC, **(_RECALL if recall else {})}
|
|
41
|
-
for event, (script, fallback, matcher) in spec.items():
|
|
42
|
-
existing = hooks.setdefault(event, [])
|
|
43
|
-
if not _already(existing):
|
|
44
|
-
existing.append(_entry(script, fallback, matcher))
|
|
45
|
-
path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
46
|
-
return path
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def _already(existing: list) -> bool:
|
|
50
|
-
"""True if any Fux hook (either wiring form) is already present for this event."""
|
|
51
|
-
cmds = [h.get("command", "") for e in existing for h in e.get("hooks", [])]
|
|
52
|
-
return any("fux" in c for c in cmds)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fux_engine-0.2.0 → fux_engine-0.3.0}/fux/data/packs/indian-markets-tax/rules/market-hours-nse.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|