ctxloom-pro 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/apps/dashboard/dist/server/index.js +169 -102
- package/dist/budgetStats-TURA232F.js +116 -0
- package/dist/chunk-5I6CJITG.js +99 -0
- package/dist/{chunk-Q2KTZNNU.js → chunk-7GZVGIQL.js} +1429 -71
- package/dist/eventCollector-QSRBVUDF.js +18 -0
- package/dist/index.js +56 -9
- package/dist/{src-HNXOOOWF.js → src-CH2OSHKF.js} +31 -2
- package/package.json +1 -1
|
@@ -5,6 +5,9 @@ import {
|
|
|
5
5
|
collectFiles,
|
|
6
6
|
generateEmbedding
|
|
7
7
|
} from "./chunk-UVR65QBJ.js";
|
|
8
|
+
import {
|
|
9
|
+
diskSink
|
|
10
|
+
} from "./chunk-5I6CJITG.js";
|
|
8
11
|
import {
|
|
9
12
|
logger
|
|
10
13
|
} from "./chunk-TYDMSHV7.js";
|
|
@@ -3457,8 +3460,8 @@ var CoChangeIndex = class _CoChangeIndex {
|
|
|
3457
3460
|
if (event.isBulk || event.isMerge) return;
|
|
3458
3461
|
const paths = event.files.map((f) => f.path);
|
|
3459
3462
|
if (paths.length === 0) return;
|
|
3460
|
-
for (const
|
|
3461
|
-
this.nodeCounts.set(
|
|
3463
|
+
for (const path37 of paths) {
|
|
3464
|
+
this.nodeCounts.set(path37, (this.nodeCounts.get(path37) ?? 0) + 1);
|
|
3462
3465
|
}
|
|
3463
3466
|
for (let i = 0; i < paths.length; i++) {
|
|
3464
3467
|
for (let j = i + 1; j < paths.length; j++) {
|
|
@@ -3605,8 +3608,8 @@ var ChurnIndex = class _ChurnIndex {
|
|
|
3605
3608
|
*/
|
|
3606
3609
|
snapshot() {
|
|
3607
3610
|
const nodes = {};
|
|
3608
|
-
for (const [
|
|
3609
|
-
nodes[
|
|
3611
|
+
for (const [path37, raw] of this.nodes) {
|
|
3612
|
+
nodes[path37] = {
|
|
3610
3613
|
commits: raw.commits,
|
|
3611
3614
|
churnLines: raw.churnLines,
|
|
3612
3615
|
bugCommits: raw.bugCommits,
|
|
@@ -3621,8 +3624,8 @@ var ChurnIndex = class _ChurnIndex {
|
|
|
3621
3624
|
*/
|
|
3622
3625
|
static load(s) {
|
|
3623
3626
|
const idx = new _ChurnIndex();
|
|
3624
|
-
for (const [
|
|
3625
|
-
idx.nodes.set(
|
|
3627
|
+
for (const [path37, raw] of Object.entries(s.nodes)) {
|
|
3628
|
+
idx.nodes.set(path37, {
|
|
3626
3629
|
commits: raw.commits,
|
|
3627
3630
|
churnLines: raw.churnLines,
|
|
3628
3631
|
bugCommits: raw.bugCommits,
|
|
@@ -3635,8 +3638,8 @@ var ChurnIndex = class _ChurnIndex {
|
|
|
3635
3638
|
// -------------------------------------------------------------------------
|
|
3636
3639
|
// Private helpers
|
|
3637
3640
|
// -------------------------------------------------------------------------
|
|
3638
|
-
getOrCreate(
|
|
3639
|
-
const existing = this.nodes.get(
|
|
3641
|
+
getOrCreate(path37) {
|
|
3642
|
+
const existing = this.nodes.get(path37);
|
|
3640
3643
|
if (existing !== void 0) return existing;
|
|
3641
3644
|
const fresh = {
|
|
3642
3645
|
commits: 0,
|
|
@@ -3645,7 +3648,7 @@ var ChurnIndex = class _ChurnIndex {
|
|
|
3645
3648
|
authorCounts: {},
|
|
3646
3649
|
lastTouch: 0
|
|
3647
3650
|
};
|
|
3648
|
-
this.nodes.set(
|
|
3651
|
+
this.nodes.set(path37, fresh);
|
|
3649
3652
|
return fresh;
|
|
3650
3653
|
}
|
|
3651
3654
|
};
|
|
@@ -3726,12 +3729,12 @@ var OwnershipIndex = class _OwnershipIndex {
|
|
|
3726
3729
|
*/
|
|
3727
3730
|
snapshot() {
|
|
3728
3731
|
const nodes = {};
|
|
3729
|
-
for (const [
|
|
3732
|
+
for (const [path37, raw] of this.nodes) {
|
|
3730
3733
|
const authorWeights = {};
|
|
3731
3734
|
for (const [email, entry] of Object.entries(raw.authorWeights)) {
|
|
3732
3735
|
authorWeights[email] = { ...entry };
|
|
3733
3736
|
}
|
|
3734
|
-
nodes[
|
|
3737
|
+
nodes[path37] = { authorWeights, lastTouch: raw.lastTouch };
|
|
3735
3738
|
}
|
|
3736
3739
|
return { version: 1, nodes };
|
|
3737
3740
|
}
|
|
@@ -3740,23 +3743,23 @@ var OwnershipIndex = class _OwnershipIndex {
|
|
|
3740
3743
|
*/
|
|
3741
3744
|
static load(s) {
|
|
3742
3745
|
const idx = new _OwnershipIndex();
|
|
3743
|
-
for (const [
|
|
3746
|
+
for (const [path37, raw] of Object.entries(s.nodes)) {
|
|
3744
3747
|
const authorWeights = {};
|
|
3745
3748
|
for (const [email, entry] of Object.entries(raw.authorWeights)) {
|
|
3746
3749
|
authorWeights[email] = { ...entry };
|
|
3747
3750
|
}
|
|
3748
|
-
idx.nodes.set(
|
|
3751
|
+
idx.nodes.set(path37, { authorWeights, lastTouch: raw.lastTouch });
|
|
3749
3752
|
}
|
|
3750
3753
|
return idx;
|
|
3751
3754
|
}
|
|
3752
3755
|
// -------------------------------------------------------------------------
|
|
3753
3756
|
// Private helpers
|
|
3754
3757
|
// -------------------------------------------------------------------------
|
|
3755
|
-
getOrCreate(
|
|
3756
|
-
const existing = this.nodes.get(
|
|
3758
|
+
getOrCreate(path37) {
|
|
3759
|
+
const existing = this.nodes.get(path37);
|
|
3757
3760
|
if (existing !== void 0) return existing;
|
|
3758
3761
|
const fresh = { authorWeights: {}, lastTouch: 0 };
|
|
3759
|
-
this.nodes.set(
|
|
3762
|
+
this.nodes.set(path37, fresh);
|
|
3760
3763
|
return fresh;
|
|
3761
3764
|
}
|
|
3762
3765
|
};
|
|
@@ -4562,7 +4565,7 @@ function renderStatusXml(input) {
|
|
|
4562
4565
|
return lines.join("\n");
|
|
4563
4566
|
}
|
|
4564
4567
|
function registerStatusTool(registry, ctx) {
|
|
4565
|
-
const
|
|
4568
|
+
const Schema31 = z2.object({ project_root: ProjectRootField });
|
|
4566
4569
|
registry.register(
|
|
4567
4570
|
"ctx_status",
|
|
4568
4571
|
{
|
|
@@ -4576,7 +4579,7 @@ function registerStatusTool(registry, ctx) {
|
|
|
4576
4579
|
}
|
|
4577
4580
|
},
|
|
4578
4581
|
async (args) => {
|
|
4579
|
-
const { project_root } =
|
|
4582
|
+
const { project_root } = Schema31.parse(args ?? {});
|
|
4580
4583
|
void project_root;
|
|
4581
4584
|
return renderStatusXml({
|
|
4582
4585
|
defaultRoot: ctx.noDefaultMode ? null : ctx.projectRoot,
|
|
@@ -4611,6 +4614,325 @@ var ToolRegistry = class {
|
|
|
4611
4614
|
// packages/core/src/tools/search.ts
|
|
4612
4615
|
import { z as z3 } from "zod";
|
|
4613
4616
|
|
|
4617
|
+
// packages/core/src/budget/nextToolSuggestions.ts
|
|
4618
|
+
var STATIC_RULES = {
|
|
4619
|
+
// ─── Source-returning / file-shaped tools ────────────────────────
|
|
4620
|
+
ctx_get_file: [
|
|
4621
|
+
{
|
|
4622
|
+
tool: "ctx_get_call_graph",
|
|
4623
|
+
why: "Check who depends on this file before modifying.",
|
|
4624
|
+
estimated_tokens: 800
|
|
4625
|
+
},
|
|
4626
|
+
{
|
|
4627
|
+
tool: "ctx_get_definition",
|
|
4628
|
+
why: "Cheaper view if you need a specific symbol, not the whole file.",
|
|
4629
|
+
estimated_tokens: 2e3
|
|
4630
|
+
},
|
|
4631
|
+
{
|
|
4632
|
+
tool: "ctx_blast_radius",
|
|
4633
|
+
why: "Transitive impact analysis before a write.",
|
|
4634
|
+
estimated_tokens: 1500
|
|
4635
|
+
}
|
|
4636
|
+
],
|
|
4637
|
+
ctx_get_definition: [
|
|
4638
|
+
{
|
|
4639
|
+
tool: "ctx_get_call_graph",
|
|
4640
|
+
why: "Who calls this symbol? Almost always your next step.",
|
|
4641
|
+
estimated_tokens: 800
|
|
4642
|
+
},
|
|
4643
|
+
{
|
|
4644
|
+
tool: "ctx_blast_radius",
|
|
4645
|
+
why: "What would break if this signature changes?",
|
|
4646
|
+
estimated_tokens: 1500
|
|
4647
|
+
}
|
|
4648
|
+
],
|
|
4649
|
+
ctx_get_context_packet: [
|
|
4650
|
+
{
|
|
4651
|
+
tool: "ctx_get_call_graph",
|
|
4652
|
+
why: "Surface external callers not visible inside the packet.",
|
|
4653
|
+
estimated_tokens: 800
|
|
4654
|
+
},
|
|
4655
|
+
{
|
|
4656
|
+
tool: "ctx_get_affected_flows",
|
|
4657
|
+
why: "Execution-flow coverage of the packet files.",
|
|
4658
|
+
estimated_tokens: 2e3
|
|
4659
|
+
}
|
|
4660
|
+
],
|
|
4661
|
+
ctx_search: [
|
|
4662
|
+
{
|
|
4663
|
+
tool: "ctx_get_definition",
|
|
4664
|
+
why: "Pull the canonical definition of a top result.",
|
|
4665
|
+
estimated_tokens: 2e3
|
|
4666
|
+
},
|
|
4667
|
+
{
|
|
4668
|
+
tool: "ctx_similar_files",
|
|
4669
|
+
why: "Find related files outside the keyword/vector hit set.",
|
|
4670
|
+
estimated_tokens: 1e3
|
|
4671
|
+
}
|
|
4672
|
+
],
|
|
4673
|
+
ctx_full_text_search: [
|
|
4674
|
+
{
|
|
4675
|
+
tool: "ctx_get_file",
|
|
4676
|
+
why: "Inspect a specific match in context.",
|
|
4677
|
+
estimated_tokens: 8e3
|
|
4678
|
+
},
|
|
4679
|
+
{
|
|
4680
|
+
tool: "ctx_get_call_graph",
|
|
4681
|
+
why: "Caller graph for matched symbols.",
|
|
4682
|
+
estimated_tokens: 800
|
|
4683
|
+
}
|
|
4684
|
+
],
|
|
4685
|
+
ctx_similar_files: [
|
|
4686
|
+
{
|
|
4687
|
+
tool: "ctx_get_context_packet",
|
|
4688
|
+
why: "Bundle the cluster into a single packet for review.",
|
|
4689
|
+
estimated_tokens: 6e3
|
|
4690
|
+
}
|
|
4691
|
+
],
|
|
4692
|
+
// ─── Graph / structural queries ──────────────────────────────────
|
|
4693
|
+
// ctx_get_call_graph is the canonical "find callers" tool — pass
|
|
4694
|
+
// direction: 'callers' or 'callees' in args. The follow-ups below
|
|
4695
|
+
// assume the caller is investigating impact / coverage on the
|
|
4696
|
+
// returned caller set.
|
|
4697
|
+
ctx_get_call_graph: [
|
|
4698
|
+
{
|
|
4699
|
+
tool: "ctx_blast_radius",
|
|
4700
|
+
why: "Transitive dependents \u2014 callers of the callers you just found.",
|
|
4701
|
+
estimated_tokens: 1500
|
|
4702
|
+
},
|
|
4703
|
+
{
|
|
4704
|
+
tool: "ctx_get_affected_flows",
|
|
4705
|
+
why: "Which execution paths break if any caller is removed?",
|
|
4706
|
+
estimated_tokens: 2e3
|
|
4707
|
+
},
|
|
4708
|
+
{
|
|
4709
|
+
tool: "ctx_execution_flow",
|
|
4710
|
+
why: "Linearize the caller set into ordered execution sequences.",
|
|
4711
|
+
estimated_tokens: 4e3
|
|
4712
|
+
}
|
|
4713
|
+
],
|
|
4714
|
+
ctx_blast_radius: [
|
|
4715
|
+
{
|
|
4716
|
+
tool: "ctx_get_affected_flows",
|
|
4717
|
+
why: "Execution-flow impact on the affected files.",
|
|
4718
|
+
estimated_tokens: 2e3
|
|
4719
|
+
},
|
|
4720
|
+
{ tool: "ctx_knowledge_gaps", why: "Identify test-coverage gaps on affected files.", estimated_tokens: 1200 }
|
|
4721
|
+
],
|
|
4722
|
+
ctx_get_affected_flows: [
|
|
4723
|
+
{
|
|
4724
|
+
tool: "ctx_execution_flow",
|
|
4725
|
+
why: "Drill into a specific affected flow.",
|
|
4726
|
+
estimated_tokens: 4e3
|
|
4727
|
+
},
|
|
4728
|
+
{
|
|
4729
|
+
tool: "ctx_blast_radius",
|
|
4730
|
+
why: "Reverse direction \u2014 what affects this flow?",
|
|
4731
|
+
estimated_tokens: 1500
|
|
4732
|
+
}
|
|
4733
|
+
],
|
|
4734
|
+
ctx_execution_flow: [
|
|
4735
|
+
{
|
|
4736
|
+
tool: "ctx_get_call_graph",
|
|
4737
|
+
why: "External callers of the flow entry-point.",
|
|
4738
|
+
estimated_tokens: 800
|
|
4739
|
+
}
|
|
4740
|
+
],
|
|
4741
|
+
// ─── Architecture / overview tools ───────────────────────────────
|
|
4742
|
+
ctx_architecture_overview: [
|
|
4743
|
+
{
|
|
4744
|
+
tool: "ctx_community_list",
|
|
4745
|
+
why: "Drill into a specific community.",
|
|
4746
|
+
estimated_tokens: 1e3
|
|
4747
|
+
},
|
|
4748
|
+
{
|
|
4749
|
+
tool: "ctx_hub_nodes",
|
|
4750
|
+
why: "Top fan-in/out nodes deserving deeper inspection.",
|
|
4751
|
+
estimated_tokens: 1200
|
|
4752
|
+
},
|
|
4753
|
+
{
|
|
4754
|
+
tool: "ctx_bridge_nodes",
|
|
4755
|
+
why: "Cross-community bridges (high architectural leverage).",
|
|
4756
|
+
estimated_tokens: 1e3
|
|
4757
|
+
}
|
|
4758
|
+
],
|
|
4759
|
+
ctx_community_list: [
|
|
4760
|
+
{
|
|
4761
|
+
tool: "ctx_get_context_packet",
|
|
4762
|
+
why: "Bundle a community into a single review packet.",
|
|
4763
|
+
estimated_tokens: 6e3
|
|
4764
|
+
}
|
|
4765
|
+
],
|
|
4766
|
+
ctx_hub_nodes: [
|
|
4767
|
+
{
|
|
4768
|
+
tool: "ctx_get_call_graph",
|
|
4769
|
+
why: "Who depends on the top hub?",
|
|
4770
|
+
estimated_tokens: 800
|
|
4771
|
+
},
|
|
4772
|
+
{
|
|
4773
|
+
tool: "ctx_blast_radius",
|
|
4774
|
+
why: "Hub change-impact analysis.",
|
|
4775
|
+
estimated_tokens: 1500
|
|
4776
|
+
}
|
|
4777
|
+
],
|
|
4778
|
+
ctx_bridge_nodes: [
|
|
4779
|
+
{
|
|
4780
|
+
tool: "ctx_get_call_graph",
|
|
4781
|
+
why: "Callers across the bridge.",
|
|
4782
|
+
estimated_tokens: 800
|
|
4783
|
+
}
|
|
4784
|
+
],
|
|
4785
|
+
ctx_surprising_connections: [
|
|
4786
|
+
{
|
|
4787
|
+
tool: "ctx_blast_radius",
|
|
4788
|
+
why: "Impact analysis on a surprising-connection target.",
|
|
4789
|
+
estimated_tokens: 1500
|
|
4790
|
+
}
|
|
4791
|
+
],
|
|
4792
|
+
// ─── Review / diff tools ─────────────────────────────────────────
|
|
4793
|
+
ctx_detect_changes: [
|
|
4794
|
+
{
|
|
4795
|
+
tool: "ctx_get_file",
|
|
4796
|
+
why: "Inspect a specific risky file.",
|
|
4797
|
+
estimated_tokens: 8e3
|
|
4798
|
+
},
|
|
4799
|
+
{
|
|
4800
|
+
tool: "ctx_get_affected_flows",
|
|
4801
|
+
why: "Which execution paths the change touches.",
|
|
4802
|
+
estimated_tokens: 2e3
|
|
4803
|
+
},
|
|
4804
|
+
{
|
|
4805
|
+
tool: "ctx_git_diff_review",
|
|
4806
|
+
why: "Full diff packet for the changeset.",
|
|
4807
|
+
estimated_tokens: 8e3
|
|
4808
|
+
}
|
|
4809
|
+
],
|
|
4810
|
+
ctx_git_diff_review: [
|
|
4811
|
+
{
|
|
4812
|
+
tool: "ctx_risk_overlay",
|
|
4813
|
+
why: "Score the changed files by historical churn + coupling.",
|
|
4814
|
+
estimated_tokens: 1500
|
|
4815
|
+
},
|
|
4816
|
+
{
|
|
4817
|
+
tool: "ctx_get_call_graph",
|
|
4818
|
+
why: "Caller-side impact of the changes.",
|
|
4819
|
+
estimated_tokens: 800
|
|
4820
|
+
}
|
|
4821
|
+
],
|
|
4822
|
+
ctx_risk_overlay: [
|
|
4823
|
+
{
|
|
4824
|
+
tool: "ctx_get_file",
|
|
4825
|
+
why: "Inspect the highest-risk file in detail.",
|
|
4826
|
+
estimated_tokens: 8e3
|
|
4827
|
+
}
|
|
4828
|
+
],
|
|
4829
|
+
ctx_git_coupling: [
|
|
4830
|
+
{
|
|
4831
|
+
tool: "ctx_blast_radius",
|
|
4832
|
+
why: "Static impact analysis to complement co-change signal.",
|
|
4833
|
+
estimated_tokens: 1500
|
|
4834
|
+
}
|
|
4835
|
+
],
|
|
4836
|
+
// ─── Refactor tools ──────────────────────────────────────────────
|
|
4837
|
+
ctx_refactor_preview: [
|
|
4838
|
+
{
|
|
4839
|
+
tool: "ctx_apply_refactor",
|
|
4840
|
+
why: "Commit the preview after you reviewed the rename.",
|
|
4841
|
+
estimated_tokens: 2e3
|
|
4842
|
+
}
|
|
4843
|
+
],
|
|
4844
|
+
ctx_apply_refactor: [
|
|
4845
|
+
{
|
|
4846
|
+
tool: "ctx_detect_changes",
|
|
4847
|
+
why: "Verify the refactor produced the expected risk profile.",
|
|
4848
|
+
estimated_tokens: 1500
|
|
4849
|
+
}
|
|
4850
|
+
],
|
|
4851
|
+
// ─── Knowledge / coverage tools ──────────────────────────────────
|
|
4852
|
+
ctx_knowledge_gaps: [
|
|
4853
|
+
{ tool: "ctx_knowledge_gaps", why: "Identify test-coverage gaps on affected files.", estimated_tokens: 1200 },
|
|
4854
|
+
{
|
|
4855
|
+
tool: "ctx_get_call_graph",
|
|
4856
|
+
why: "Caller frequency on untested symbols (impact ranking).",
|
|
4857
|
+
estimated_tokens: 800
|
|
4858
|
+
}
|
|
4859
|
+
],
|
|
4860
|
+
ctx_find_large_functions: [
|
|
4861
|
+
{
|
|
4862
|
+
tool: "ctx_get_definition",
|
|
4863
|
+
why: "Inspect the largest function.",
|
|
4864
|
+
estimated_tokens: 2e3
|
|
4865
|
+
}
|
|
4866
|
+
],
|
|
4867
|
+
// ─── Metadata / status ───────────────────────────────────────────
|
|
4868
|
+
ctx_get_minimal_context: [
|
|
4869
|
+
// intentionally empty — the suggested_first_tool field IS the
|
|
4870
|
+
// next-step suggestion. Adding rules here would be redundant.
|
|
4871
|
+
],
|
|
4872
|
+
ctx_status: [],
|
|
4873
|
+
ctx_get_rules: [
|
|
4874
|
+
{
|
|
4875
|
+
tool: "ctx_rules_check",
|
|
4876
|
+
why: "Validate code against rules.",
|
|
4877
|
+
estimated_tokens: 1200
|
|
4878
|
+
}
|
|
4879
|
+
],
|
|
4880
|
+
ctx_rules_check: [],
|
|
4881
|
+
ctx_get_workflow: [],
|
|
4882
|
+
ctx_suggested_questions: [],
|
|
4883
|
+
// ─── Wiki / export ───────────────────────────────────────────────
|
|
4884
|
+
ctx_wiki_generate: [
|
|
4885
|
+
{
|
|
4886
|
+
tool: "ctx_architecture_overview",
|
|
4887
|
+
why: "Confirm the wiki structure against the live overview.",
|
|
4888
|
+
estimated_tokens: 2e3
|
|
4889
|
+
}
|
|
4890
|
+
],
|
|
4891
|
+
ctx_graph_export: [],
|
|
4892
|
+
ctx_graph_snapshot: [
|
|
4893
|
+
{
|
|
4894
|
+
tool: "ctx_graph_diff",
|
|
4895
|
+
why: "Compare against a later snapshot.",
|
|
4896
|
+
estimated_tokens: 2e3
|
|
4897
|
+
}
|
|
4898
|
+
],
|
|
4899
|
+
ctx_graph_diff: [
|
|
4900
|
+
{
|
|
4901
|
+
tool: "ctx_detect_changes",
|
|
4902
|
+
why: "Risk-scored view of the graph delta.",
|
|
4903
|
+
estimated_tokens: 1500
|
|
4904
|
+
}
|
|
4905
|
+
],
|
|
4906
|
+
// ─── Cross-repo ──────────────────────────────────────────────────
|
|
4907
|
+
ctx_cross_repo_search: [
|
|
4908
|
+
{
|
|
4909
|
+
tool: "ctx_get_file",
|
|
4910
|
+
why: "Inspect a hit in a specific repo.",
|
|
4911
|
+
estimated_tokens: 8e3
|
|
4912
|
+
}
|
|
4913
|
+
]
|
|
4914
|
+
// ─── Query primitive ─────────────────────────────────────────────
|
|
4915
|
+
// ─── Affected flows (sibling to get_affected_flows but worth pinning) ─
|
|
4916
|
+
// ─── Definition aliases / structurals not listed above are deliberate
|
|
4917
|
+
// — the test enforces the full registered-tool list is covered.
|
|
4918
|
+
};
|
|
4919
|
+
var TOKEN_ESTIMATE_MIN = 0;
|
|
4920
|
+
var TOKEN_ESTIMATE_MAX = 1e5;
|
|
4921
|
+
function clampEstimate(n) {
|
|
4922
|
+
if (!Number.isFinite(n)) return 0;
|
|
4923
|
+
return Math.max(TOKEN_ESTIMATE_MIN, Math.min(TOKEN_ESTIMATE_MAX, Math.round(n)));
|
|
4924
|
+
}
|
|
4925
|
+
function suggestNext(fromTool, registeredTools) {
|
|
4926
|
+
const raw = STATIC_RULES[fromTool] ?? [];
|
|
4927
|
+
const filtered = registeredTools ? raw.filter((s) => registeredTools.has(s.tool)) : raw;
|
|
4928
|
+
return filtered.slice(0, 3).map((s) => ({
|
|
4929
|
+
tool: s.tool,
|
|
4930
|
+
args: s.args,
|
|
4931
|
+
why: s.why,
|
|
4932
|
+
estimated_tokens: clampEstimate(s.estimated_tokens)
|
|
4933
|
+
}));
|
|
4934
|
+
}
|
|
4935
|
+
|
|
4614
4936
|
// packages/core/src/budget/budget.ts
|
|
4615
4937
|
var defaultTokenEstimator = (text) => Math.ceil(text.length / 4);
|
|
4616
4938
|
function hasBudgetArgs(args) {
|
|
@@ -4634,23 +4956,33 @@ function readBudgetArgs(args) {
|
|
|
4634
4956
|
function isBudgetDisabled() {
|
|
4635
4957
|
return process.env.CTXLOOM_DISABLE_BUDGET === "1";
|
|
4636
4958
|
}
|
|
4637
|
-
function emitTelemetry(event) {
|
|
4959
|
+
function emitTelemetry(event, sink = diskSink) {
|
|
4638
4960
|
if (process.env.CTXLOOM_TELEMETRY_LEVEL !== "full") return;
|
|
4639
4961
|
logger.info(event.event, event);
|
|
4962
|
+
try {
|
|
4963
|
+
sink.append(event);
|
|
4964
|
+
} catch {
|
|
4965
|
+
}
|
|
4640
4966
|
}
|
|
4641
4967
|
async function enforceBudget(opts) {
|
|
4642
4968
|
const { full, args, toolName, defaultMaxTokens, skeletonProducer } = opts;
|
|
4643
4969
|
const estimate = opts.estimator ?? defaultTokenEstimator;
|
|
4970
|
+
const sink = opts.sink ?? opts.ctx?.telemetrySink ?? diskSink;
|
|
4971
|
+
const nextToolSuggestions = suggestNext(toolName);
|
|
4972
|
+
const withSuggestions = (base) => {
|
|
4973
|
+
if (nextToolSuggestions.length === 0) return base;
|
|
4974
|
+
return { ...base, next_tool_suggestions: nextToolSuggestions };
|
|
4975
|
+
};
|
|
4644
4976
|
const originalTokens = estimate(full);
|
|
4645
4977
|
if (isBudgetDisabled()) {
|
|
4646
4978
|
return {
|
|
4647
4979
|
text: full,
|
|
4648
|
-
meta: {
|
|
4980
|
+
meta: withSuggestions({
|
|
4649
4981
|
format: "full",
|
|
4650
4982
|
original_tokens_est: originalTokens,
|
|
4651
4983
|
returned_tokens_est: originalTokens,
|
|
4652
4984
|
fallback_reason: null
|
|
4653
|
-
}
|
|
4985
|
+
})
|
|
4654
4986
|
};
|
|
4655
4987
|
}
|
|
4656
4988
|
if (args.response_format === "skeleton" && skeletonProducer) {
|
|
@@ -4659,45 +4991,45 @@ async function enforceBudget(opts) {
|
|
|
4659
4991
|
const skTokens = estimate(skeleton2);
|
|
4660
4992
|
return {
|
|
4661
4993
|
text: skeleton2,
|
|
4662
|
-
meta: {
|
|
4994
|
+
meta: withSuggestions({
|
|
4663
4995
|
format: "skeleton",
|
|
4664
4996
|
original_tokens_est: originalTokens,
|
|
4665
4997
|
returned_tokens_est: skTokens,
|
|
4666
4998
|
fallback_reason: null
|
|
4667
|
-
}
|
|
4999
|
+
})
|
|
4668
5000
|
};
|
|
4669
5001
|
}
|
|
4670
5002
|
return {
|
|
4671
5003
|
text: full,
|
|
4672
|
-
meta: {
|
|
5004
|
+
meta: withSuggestions({
|
|
4673
5005
|
format: "full",
|
|
4674
5006
|
original_tokens_est: originalTokens,
|
|
4675
5007
|
returned_tokens_est: originalTokens,
|
|
4676
5008
|
fallback_reason: "skeleton_failed"
|
|
4677
|
-
}
|
|
5009
|
+
})
|
|
4678
5010
|
};
|
|
4679
5011
|
}
|
|
4680
5012
|
const budget = args.max_response_tokens ?? defaultMaxTokens;
|
|
4681
5013
|
if (budget === void 0) {
|
|
4682
5014
|
return {
|
|
4683
5015
|
text: full,
|
|
4684
|
-
meta: {
|
|
5016
|
+
meta: withSuggestions({
|
|
4685
5017
|
format: "full",
|
|
4686
5018
|
original_tokens_est: originalTokens,
|
|
4687
5019
|
returned_tokens_est: originalTokens,
|
|
4688
5020
|
fallback_reason: null
|
|
4689
|
-
}
|
|
5021
|
+
})
|
|
4690
5022
|
};
|
|
4691
5023
|
}
|
|
4692
5024
|
if (originalTokens <= budget) {
|
|
4693
5025
|
return {
|
|
4694
5026
|
text: full,
|
|
4695
|
-
meta: {
|
|
5027
|
+
meta: withSuggestions({
|
|
4696
5028
|
format: "full",
|
|
4697
5029
|
original_tokens_est: originalTokens,
|
|
4698
5030
|
returned_tokens_est: originalTokens,
|
|
4699
5031
|
fallback_reason: null
|
|
4700
|
-
}
|
|
5032
|
+
})
|
|
4701
5033
|
};
|
|
4702
5034
|
}
|
|
4703
5035
|
emitTelemetry({
|
|
@@ -4706,7 +5038,7 @@ async function enforceBudget(opts) {
|
|
|
4706
5038
|
original_tokens: originalTokens,
|
|
4707
5039
|
budget,
|
|
4708
5040
|
ratio: originalTokens / budget
|
|
4709
|
-
});
|
|
5041
|
+
}, sink);
|
|
4710
5042
|
const mode = args.on_budget_exceeded ?? "skeleton";
|
|
4711
5043
|
if (mode === "error") {
|
|
4712
5044
|
const err = new Error(
|
|
@@ -4725,15 +5057,15 @@ async function enforceBudget(opts) {
|
|
|
4725
5057
|
tool: toolName,
|
|
4726
5058
|
fallback_reason: "budget_exceeded",
|
|
4727
5059
|
mode: "truncate"
|
|
4728
|
-
});
|
|
5060
|
+
}, sink);
|
|
4729
5061
|
return {
|
|
4730
5062
|
text: sliced2,
|
|
4731
|
-
meta: {
|
|
5063
|
+
meta: withSuggestions({
|
|
4732
5064
|
format: "truncated",
|
|
4733
5065
|
original_tokens_est: originalTokens,
|
|
4734
5066
|
returned_tokens_est: slicedTokens,
|
|
4735
5067
|
fallback_reason: "budget_exceeded"
|
|
4736
|
-
}
|
|
5068
|
+
})
|
|
4737
5069
|
};
|
|
4738
5070
|
}
|
|
4739
5071
|
const skeleton = skeletonProducer ? await safeSkeleton(skeletonProducer, toolName) : null;
|
|
@@ -4745,15 +5077,15 @@ async function enforceBudget(opts) {
|
|
|
4745
5077
|
tool: toolName,
|
|
4746
5078
|
fallback_reason: "budget_exceeded",
|
|
4747
5079
|
mode: "skeleton"
|
|
4748
|
-
});
|
|
5080
|
+
}, sink);
|
|
4749
5081
|
return {
|
|
4750
5082
|
text: skeleton,
|
|
4751
|
-
meta: {
|
|
5083
|
+
meta: withSuggestions({
|
|
4752
5084
|
format: "skeleton",
|
|
4753
5085
|
original_tokens_est: originalTokens,
|
|
4754
5086
|
returned_tokens_est: skTokens,
|
|
4755
5087
|
fallback_reason: "budget_exceeded"
|
|
4756
|
-
}
|
|
5088
|
+
})
|
|
4757
5089
|
};
|
|
4758
5090
|
}
|
|
4759
5091
|
const slicedSk = skeleton.slice(0, budget * 4);
|
|
@@ -4762,15 +5094,15 @@ async function enforceBudget(opts) {
|
|
|
4762
5094
|
tool: toolName,
|
|
4763
5095
|
fallback_reason: "budget_exceeded",
|
|
4764
5096
|
mode: "skeleton+truncate"
|
|
4765
|
-
});
|
|
5097
|
+
}, sink);
|
|
4766
5098
|
return {
|
|
4767
5099
|
text: slicedSk,
|
|
4768
|
-
meta: {
|
|
5100
|
+
meta: withSuggestions({
|
|
4769
5101
|
format: "truncated",
|
|
4770
5102
|
original_tokens_est: originalTokens,
|
|
4771
5103
|
returned_tokens_est: estimate(slicedSk),
|
|
4772
5104
|
fallback_reason: "budget_exceeded"
|
|
4773
|
-
}
|
|
5105
|
+
})
|
|
4774
5106
|
};
|
|
4775
5107
|
}
|
|
4776
5108
|
const sliced = full.slice(0, budget * 4);
|
|
@@ -4779,15 +5111,15 @@ async function enforceBudget(opts) {
|
|
|
4779
5111
|
tool: toolName,
|
|
4780
5112
|
fallback_reason: "skeleton_failed",
|
|
4781
5113
|
mode: "truncate-fallback"
|
|
4782
|
-
});
|
|
5114
|
+
}, sink);
|
|
4783
5115
|
return {
|
|
4784
5116
|
text: sliced,
|
|
4785
|
-
meta: {
|
|
5117
|
+
meta: withSuggestions({
|
|
4786
5118
|
format: "truncated",
|
|
4787
5119
|
original_tokens_est: originalTokens,
|
|
4788
5120
|
returned_tokens_est: estimate(sliced),
|
|
4789
5121
|
fallback_reason: "skeleton_failed"
|
|
4790
|
-
}
|
|
5122
|
+
})
|
|
4791
5123
|
};
|
|
4792
5124
|
}
|
|
4793
5125
|
async function safeSkeleton(producer, toolName) {
|
|
@@ -4887,6 +5219,7 @@ function registerSearchTool(registry, ctx) {
|
|
|
4887
5219
|
if (!hasBudgetArgs(args)) return full;
|
|
4888
5220
|
const skeletonProducer = async () => renderResults(parsed.query, ranked, false);
|
|
4889
5221
|
const result = await enforceBudget({
|
|
5222
|
+
ctx,
|
|
4890
5223
|
full,
|
|
4891
5224
|
args: readBudgetArgs(args),
|
|
4892
5225
|
toolName: "ctx_search",
|
|
@@ -4946,6 +5279,7 @@ function registerFileTool(registry, ctx) {
|
|
|
4946
5279
|
const absPath = validator.validate(parsed.path);
|
|
4947
5280
|
const skeletonizer = await ctx.getSkeletonizer(parsed.project_root);
|
|
4948
5281
|
const result = await enforceBudget({
|
|
5282
|
+
ctx,
|
|
4949
5283
|
full,
|
|
4950
5284
|
args: readBudgetArgs(args),
|
|
4951
5285
|
toolName: "ctx_get_file",
|
|
@@ -5054,6 +5388,7 @@ ${sk}`;
|
|
|
5054
5388
|
return renderPacket({ ...parts, primaryContent: primarySkeleton });
|
|
5055
5389
|
};
|
|
5056
5390
|
const result = await enforceBudget({
|
|
5391
|
+
ctx,
|
|
5057
5392
|
full,
|
|
5058
5393
|
args: readBudgetArgs(args),
|
|
5059
5394
|
toolName: "ctx_get_context_packet",
|
|
@@ -5217,6 +5552,7 @@ function registerDefinitionTool(registry, ctx) {
|
|
|
5217
5552
|
}
|
|
5218
5553
|
if (!hasBudgetArgs(args)) return full;
|
|
5219
5554
|
const result = await enforceBudget({
|
|
5555
|
+
ctx,
|
|
5220
5556
|
full,
|
|
5221
5557
|
args: readBudgetArgs(args),
|
|
5222
5558
|
toolName: "ctx_get_definition",
|
|
@@ -6258,6 +6594,7 @@ function registerWikiGenerateTool(registry, ctx) {
|
|
|
6258
6594
|
const full = detail_level === "minimal" ? renderMinimal() : renderStandard();
|
|
6259
6595
|
if (!hasBudgetArgs(args)) return full;
|
|
6260
6596
|
const budgetResult = await enforceBudget({
|
|
6597
|
+
ctx,
|
|
6261
6598
|
full,
|
|
6262
6599
|
args: readBudgetArgs(args),
|
|
6263
6600
|
toolName: "ctx_wiki_generate",
|
|
@@ -6405,6 +6742,7 @@ function registerGitDiffReviewTool(registry, ctx) {
|
|
|
6405
6742
|
const maybeBudget = async (full2, skeletonProducer) => {
|
|
6406
6743
|
if (!hasBudgetArgs(args)) return full2;
|
|
6407
6744
|
const result = await enforceBudget({
|
|
6745
|
+
ctx,
|
|
6408
6746
|
full: full2,
|
|
6409
6747
|
args: readBudgetArgs(args),
|
|
6410
6748
|
toolName: "ctx_git_diff_review",
|
|
@@ -6440,7 +6778,7 @@ function registerGitDiffReviewTool(registry, ctx) {
|
|
|
6440
6778
|
skeleton: include_skeletons && i < skeletonLimit ? await trySkeletonize(ctx, file, project_root) : ""
|
|
6441
6779
|
}))
|
|
6442
6780
|
);
|
|
6443
|
-
const
|
|
6781
|
+
const render2 = (withSkeletons, withTransitive) => {
|
|
6444
6782
|
const out = [`<git_diff_review changed_files="${files.length}" depth="${depth}">`];
|
|
6445
6783
|
out.push(` <changed_files count="${files.length}">`);
|
|
6446
6784
|
for (const cd of changedFileData) {
|
|
@@ -6486,8 +6824,8 @@ function registerGitDiffReviewTool(registry, ctx) {
|
|
|
6486
6824
|
out.push("</git_diff_review>");
|
|
6487
6825
|
return out.join("\n");
|
|
6488
6826
|
};
|
|
6489
|
-
const full =
|
|
6490
|
-
return maybeBudget(full, async () =>
|
|
6827
|
+
const full = render2(include_skeletons, true);
|
|
6828
|
+
return maybeBudget(full, async () => render2(false, false));
|
|
6491
6829
|
}
|
|
6492
6830
|
);
|
|
6493
6831
|
}
|
|
@@ -6589,7 +6927,7 @@ function registerRefactorPreviewTool(registry, ctx) {
|
|
|
6589
6927
|
totalOccurrences += occurrences.length;
|
|
6590
6928
|
}
|
|
6591
6929
|
}
|
|
6592
|
-
const
|
|
6930
|
+
const render2 = (includeChanges) => {
|
|
6593
6931
|
const xmlLines = [
|
|
6594
6932
|
`<refactor_preview symbol="${escapeXML17(symbol)}" new_name="${escapeXML17(new_name)}" total_files="${fileChanges.length}" total_occurrences="${totalOccurrences}">`
|
|
6595
6933
|
];
|
|
@@ -6619,14 +6957,15 @@ function registerRefactorPreviewTool(registry, ctx) {
|
|
|
6619
6957
|
xmlLines.push("</refactor_preview>");
|
|
6620
6958
|
return xmlLines.join("\n");
|
|
6621
6959
|
};
|
|
6622
|
-
const full =
|
|
6960
|
+
const full = render2(true);
|
|
6623
6961
|
if (!hasBudgetArgs(args)) return full;
|
|
6624
6962
|
const result = await enforceBudget({
|
|
6963
|
+
ctx,
|
|
6625
6964
|
full,
|
|
6626
6965
|
args: readBudgetArgs(args),
|
|
6627
6966
|
toolName: "ctx_refactor_preview",
|
|
6628
6967
|
defaultMaxTokens: DEFAULT_MAX_RESPONSE_TOKENS7,
|
|
6629
|
-
skeletonProducer: async () =>
|
|
6968
|
+
skeletonProducer: async () => render2(false)
|
|
6630
6969
|
});
|
|
6631
6970
|
return wrapResponse(result);
|
|
6632
6971
|
}
|
|
@@ -6743,6 +7082,7 @@ function registerExecutionFlowTool(registry, ctx) {
|
|
|
6743
7082
|
const maybeBudget = async (full) => {
|
|
6744
7083
|
if (!hasBudgetArgs(args)) return full;
|
|
6745
7084
|
const result = await enforceBudget({
|
|
7085
|
+
ctx,
|
|
6746
7086
|
full,
|
|
6747
7087
|
args: readBudgetArgs(args),
|
|
6748
7088
|
toolName: "ctx_execution_flow",
|
|
@@ -6899,7 +7239,7 @@ var Schema20 = z22.object({
|
|
|
6899
7239
|
function escapeXML19(text) {
|
|
6900
7240
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
6901
7241
|
}
|
|
6902
|
-
function registerCrossRepoSearchTool(registry,
|
|
7242
|
+
function registerCrossRepoSearchTool(registry, ctx, registryFilePath) {
|
|
6903
7243
|
const repoRegistryPath = registryFilePath ?? path18.join(process.env.HOME ?? process.env.USERPROFILE ?? "", ".ctxloom", "repos.json");
|
|
6904
7244
|
registry.register(
|
|
6905
7245
|
"ctx_cross_repo_search",
|
|
@@ -6988,7 +7328,7 @@ function registerCrossRepoSearchTool(registry, _ctx, registryFilePath) {
|
|
|
6988
7328
|
);
|
|
6989
7329
|
}
|
|
6990
7330
|
xmlLines.push(" </repos>");
|
|
6991
|
-
const
|
|
7331
|
+
const render2 = (includeContent) => {
|
|
6992
7332
|
const out = [...xmlLines];
|
|
6993
7333
|
out.push(` <results count="${topResults.length}">`);
|
|
6994
7334
|
for (const r of topResults) {
|
|
@@ -7004,14 +7344,15 @@ function registerCrossRepoSearchTool(registry, _ctx, registryFilePath) {
|
|
|
7004
7344
|
out.push("</cross_repo_search>");
|
|
7005
7345
|
return out.join("\n");
|
|
7006
7346
|
};
|
|
7007
|
-
const full =
|
|
7347
|
+
const full = render2(true);
|
|
7008
7348
|
if (!hasBudgetArgs(args)) return full;
|
|
7009
7349
|
const result = await enforceBudget({
|
|
7350
|
+
ctx,
|
|
7010
7351
|
full,
|
|
7011
7352
|
args: readBudgetArgs(args),
|
|
7012
7353
|
toolName: "ctx_cross_repo_search",
|
|
7013
7354
|
defaultMaxTokens: DEFAULT_MAX_RESPONSE_TOKENS9,
|
|
7014
|
-
skeletonProducer: async () =>
|
|
7355
|
+
skeletonProducer: async () => render2(false)
|
|
7015
7356
|
});
|
|
7016
7357
|
return wrapResponse(result);
|
|
7017
7358
|
}
|
|
@@ -7116,6 +7457,7 @@ function registerApplyRefactorTool(registry, ctx) {
|
|
|
7116
7457
|
const full = xml.join("\n");
|
|
7117
7458
|
if (!hasBudgetArgs(args)) return full;
|
|
7118
7459
|
const result = await enforceBudget({
|
|
7460
|
+
ctx,
|
|
7119
7461
|
full,
|
|
7120
7462
|
args: readBudgetArgs(args),
|
|
7121
7463
|
toolName: "ctx_apply_refactor",
|
|
@@ -7333,6 +7675,7 @@ function registerFullTextSearchTool(registry, ctx) {
|
|
|
7333
7675
|
const maybeBudget = async (full, skeletonProducer) => {
|
|
7334
7676
|
if (!hasBudgetArgs(args)) return full;
|
|
7335
7677
|
const result = await enforceBudget({
|
|
7678
|
+
ctx,
|
|
7336
7679
|
full,
|
|
7337
7680
|
args: readBudgetArgs(args),
|
|
7338
7681
|
toolName: "ctx_full_text_search",
|
|
@@ -7396,7 +7739,7 @@ function registerFullTextSearchTool(registry, ctx) {
|
|
|
7396
7739
|
} catch {
|
|
7397
7740
|
}
|
|
7398
7741
|
}
|
|
7399
|
-
const
|
|
7742
|
+
const render2 = (includeSnippets) => {
|
|
7400
7743
|
const xml = [
|
|
7401
7744
|
`<full_text_search query="${escapeXML22(query)}" mode="${mode}" case_sensitive="${case_sensitive}" count="${merged.length}">`
|
|
7402
7745
|
];
|
|
@@ -7415,8 +7758,8 @@ function registerFullTextSearchTool(registry, ctx) {
|
|
|
7415
7758
|
return xml.join("\n");
|
|
7416
7759
|
};
|
|
7417
7760
|
return maybeBudget(
|
|
7418
|
-
|
|
7419
|
-
async () =>
|
|
7761
|
+
render2(true),
|
|
7762
|
+
async () => render2(false)
|
|
7420
7763
|
);
|
|
7421
7764
|
}
|
|
7422
7765
|
);
|
|
@@ -7897,6 +8240,7 @@ function registerFindLargeFunctionsTool(registry, ctx) {
|
|
|
7897
8240
|
}
|
|
7898
8241
|
if (!hasBudgetArgs(args)) return full;
|
|
7899
8242
|
const result = await enforceBudget({
|
|
8243
|
+
ctx,
|
|
7900
8244
|
full,
|
|
7901
8245
|
args: readBudgetArgs(args),
|
|
7902
8246
|
toolName: "ctx_find_large_functions",
|
|
@@ -8448,6 +8792,279 @@ function registerGetAffectedFlowsTool(registry, ctx) {
|
|
|
8448
8792
|
);
|
|
8449
8793
|
}
|
|
8450
8794
|
|
|
8795
|
+
// packages/core/src/tools/minimal-context.ts
|
|
8796
|
+
import { execSync } from "child_process";
|
|
8797
|
+
import { z as z36 } from "zod";
|
|
8798
|
+
var Schema30 = z36.object({
|
|
8799
|
+
task: z36.string().max(200).optional().describe(
|
|
8800
|
+
"Free-text description of what you're about to do (e.g. 'review PR 142', 'rename emitTelemetry'). The tool routes by regex to the most-fitting suggested-first-tool. Capped at 200 chars; control characters stripped."
|
|
8801
|
+
),
|
|
8802
|
+
project_root: ProjectRootField,
|
|
8803
|
+
max_response_tokens: z36.number().int().positive().optional(),
|
|
8804
|
+
on_budget_exceeded: z36.enum(["skeleton", "truncate", "error"]).optional(),
|
|
8805
|
+
response_format: z36.enum(["full", "skeleton", "auto"]).optional()
|
|
8806
|
+
});
|
|
8807
|
+
var DEFAULT_MAX_RESPONSE_TOKENS13 = 250;
|
|
8808
|
+
function routeFirstTool(task, hasDirtyChanges) {
|
|
8809
|
+
const t = (task ?? "").toLowerCase();
|
|
8810
|
+
if (/\b(rename|refactor|move\s+\w+|extract)\b/.test(t)) {
|
|
8811
|
+
return {
|
|
8812
|
+
tool: "ctx_get_call_graph",
|
|
8813
|
+
why: "Renames + refactors need every caller surfaced before the edit. Start here.",
|
|
8814
|
+
estimated_tokens: 800
|
|
8815
|
+
};
|
|
8816
|
+
}
|
|
8817
|
+
if (/\b(blast|impact|breaks?|affects?)\b/.test(t)) {
|
|
8818
|
+
return {
|
|
8819
|
+
tool: "ctx_blast_radius",
|
|
8820
|
+
why: "Blast-radius analysis gives transitive dependents; start here for impact questions.",
|
|
8821
|
+
estimated_tokens: 1500
|
|
8822
|
+
};
|
|
8823
|
+
}
|
|
8824
|
+
if (/\b(architect|overview|explore|onboard|tour)\b/.test(t)) {
|
|
8825
|
+
return {
|
|
8826
|
+
tool: "ctx_architecture_overview",
|
|
8827
|
+
why: "Top-down map of communities + hub nodes; the natural starting point for exploration.",
|
|
8828
|
+
estimated_tokens: 2e3
|
|
8829
|
+
};
|
|
8830
|
+
}
|
|
8831
|
+
if (/\b(test|coverage|tested)\b/.test(t)) {
|
|
8832
|
+
return {
|
|
8833
|
+
tool: "ctx_knowledge_gaps",
|
|
8834
|
+
why: "Knowledge-gap report highlights files lacking test coverage.",
|
|
8835
|
+
estimated_tokens: 1200
|
|
8836
|
+
};
|
|
8837
|
+
}
|
|
8838
|
+
if (/\b(review|audit|check|diff)\b/.test(t)) {
|
|
8839
|
+
return {
|
|
8840
|
+
tool: "ctx_detect_changes",
|
|
8841
|
+
why: "Risk-scored change analysis is the canonical start for reviews.",
|
|
8842
|
+
estimated_tokens: 1500
|
|
8843
|
+
};
|
|
8844
|
+
}
|
|
8845
|
+
if (hasDirtyChanges) {
|
|
8846
|
+
return {
|
|
8847
|
+
tool: "ctx_detect_changes",
|
|
8848
|
+
why: "Working tree has uncommitted changes \u2014 review-mode is the most-likely intent.",
|
|
8849
|
+
estimated_tokens: 1500
|
|
8850
|
+
};
|
|
8851
|
+
}
|
|
8852
|
+
return {
|
|
8853
|
+
tool: "ctx_architecture_overview",
|
|
8854
|
+
why: "Clean working tree + no task hint \u2014 orientation is the safe default.",
|
|
8855
|
+
estimated_tokens: 2e3
|
|
8856
|
+
};
|
|
8857
|
+
}
|
|
8858
|
+
function classifyStaleness(lastBuildIso) {
|
|
8859
|
+
if (!lastBuildIso) return "unbuilt";
|
|
8860
|
+
const ms = Date.now() - new Date(lastBuildIso).getTime();
|
|
8861
|
+
if (!Number.isFinite(ms) || ms < 0) return "unbuilt";
|
|
8862
|
+
if (ms < 5 * 60 * 1e3) return "fresh";
|
|
8863
|
+
if (ms < 60 * 60 * 1e3) return "stale_minutes";
|
|
8864
|
+
return "stale_hours";
|
|
8865
|
+
}
|
|
8866
|
+
function readRecentChanges(projectRoot) {
|
|
8867
|
+
try {
|
|
8868
|
+
const stdout = execSync("git status --porcelain", {
|
|
8869
|
+
cwd: projectRoot,
|
|
8870
|
+
timeout: 2e3,
|
|
8871
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
8872
|
+
encoding: "utf-8"
|
|
8873
|
+
});
|
|
8874
|
+
const lines = stdout.split("\n").filter((l) => l.trim() !== "");
|
|
8875
|
+
return lines.slice(0, 20).map((line) => {
|
|
8876
|
+
const x = line[0];
|
|
8877
|
+
const y = line[1];
|
|
8878
|
+
const path37 = line.slice(3).trim();
|
|
8879
|
+
let status = "?";
|
|
8880
|
+
const xy = x === " " ? y : x;
|
|
8881
|
+
if (xy === "M" || xy === "A" || xy === "D" || xy === "R") status = xy;
|
|
8882
|
+
return { file: path37, status };
|
|
8883
|
+
});
|
|
8884
|
+
} catch {
|
|
8885
|
+
return [];
|
|
8886
|
+
}
|
|
8887
|
+
}
|
|
8888
|
+
function computeTopHubs(graph) {
|
|
8889
|
+
const files = graph.allFiles();
|
|
8890
|
+
const scored = files.map((file) => {
|
|
8891
|
+
const inDeg = graph.getImporters(file).length;
|
|
8892
|
+
const outDeg = graph.getImports(file).length;
|
|
8893
|
+
return { file, inDeg, outDeg, total: inDeg + outDeg };
|
|
8894
|
+
}).filter((s) => s.total >= 2).sort((a, b) => b.total - a.total).slice(0, 5);
|
|
8895
|
+
return scored.map((s) => ({
|
|
8896
|
+
name: s.file,
|
|
8897
|
+
reason: s.inDeg > s.outDeg ? "fan_in" : s.outDeg > s.inDeg ? "fan_out" : "bridge"
|
|
8898
|
+
}));
|
|
8899
|
+
}
|
|
8900
|
+
var CACHE_TTL_MS = 1e4;
|
|
8901
|
+
var responseCache = /* @__PURE__ */ new Map();
|
|
8902
|
+
function cacheKey(projectRoot, task) {
|
|
8903
|
+
return `${projectRoot}|${task ?? ""}`;
|
|
8904
|
+
}
|
|
8905
|
+
function cacheGet(key) {
|
|
8906
|
+
const e = responseCache.get(key);
|
|
8907
|
+
if (!e) return null;
|
|
8908
|
+
if (e.expiresAt < Date.now()) {
|
|
8909
|
+
responseCache.delete(key);
|
|
8910
|
+
return null;
|
|
8911
|
+
}
|
|
8912
|
+
return e.body;
|
|
8913
|
+
}
|
|
8914
|
+
function cachePut(key, body) {
|
|
8915
|
+
responseCache.set(key, { expiresAt: Date.now() + CACHE_TTL_MS, body });
|
|
8916
|
+
}
|
|
8917
|
+
function sanitizeTask(raw) {
|
|
8918
|
+
if (raw == null) return void 0;
|
|
8919
|
+
const stripped = raw.replace(/[\x00-\x1f\x7f]/g, "").slice(0, 200);
|
|
8920
|
+
return stripped === "" ? void 0 : stripped;
|
|
8921
|
+
}
|
|
8922
|
+
function escapeXML26(text) {
|
|
8923
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
8924
|
+
}
|
|
8925
|
+
function render(input) {
|
|
8926
|
+
const lines = ["<minimal_context>"];
|
|
8927
|
+
lines.push(
|
|
8928
|
+
` <graph ready="${input.graphReady}" nodes="${input.nodes}" edges="${input.edges}" last_build="${escapeXML26(input.lastBuildIso ?? "never")}" staleness="${input.staleness}" />`
|
|
8929
|
+
);
|
|
8930
|
+
if (input.languages.length > 0) {
|
|
8931
|
+
lines.push(` <languages>${input.languages.map(escapeXML26).join(", ")}</languages>`);
|
|
8932
|
+
}
|
|
8933
|
+
if (input.recentChanges.length > 0) {
|
|
8934
|
+
lines.push(` <recent_changes count="${input.recentChanges.length}">`);
|
|
8935
|
+
for (const c of input.recentChanges) {
|
|
8936
|
+
lines.push(` <change status="${c.status}" file="${escapeXML26(c.file)}" />`);
|
|
8937
|
+
}
|
|
8938
|
+
lines.push(" </recent_changes>");
|
|
8939
|
+
} else {
|
|
8940
|
+
lines.push(' <recent_changes count="0" />');
|
|
8941
|
+
}
|
|
8942
|
+
if (input.topHubs.length > 0) {
|
|
8943
|
+
lines.push(` <top_hubs count="${input.topHubs.length}">`);
|
|
8944
|
+
for (const h of input.topHubs) {
|
|
8945
|
+
lines.push(` <hub reason="${h.reason}" name="${escapeXML26(h.name)}" />`);
|
|
8946
|
+
}
|
|
8947
|
+
lines.push(" </top_hubs>");
|
|
8948
|
+
}
|
|
8949
|
+
lines.push(" <suggested_first_tool>");
|
|
8950
|
+
lines.push(` <tool>${escapeXML26(input.suggested.tool)}</tool>`);
|
|
8951
|
+
lines.push(` <why>${escapeXML26(input.suggested.why)}</why>`);
|
|
8952
|
+
lines.push(` <estimated_tokens>${input.suggested.estimated_tokens}</estimated_tokens>`);
|
|
8953
|
+
if (input.suggested.args && Object.keys(input.suggested.args).length > 0) {
|
|
8954
|
+
lines.push(` <args>${escapeXML26(JSON.stringify(input.suggested.args))}</args>`);
|
|
8955
|
+
}
|
|
8956
|
+
lines.push(" </suggested_first_tool>");
|
|
8957
|
+
lines.push("</minimal_context>");
|
|
8958
|
+
return lines.join("\n");
|
|
8959
|
+
}
|
|
8960
|
+
function renderSkeleton(input) {
|
|
8961
|
+
const lines = ['<minimal_context format="skeleton">'];
|
|
8962
|
+
lines.push(
|
|
8963
|
+
` <graph ready="${input.graphReady}" nodes="${input.nodes}" edges="${input.edges}" staleness="${input.staleness}" />`
|
|
8964
|
+
);
|
|
8965
|
+
lines.push(` <recent_changes count="${input.recentChanges.length}" />`);
|
|
8966
|
+
lines.push(` <top_hubs count="${input.topHubs.length}" />`);
|
|
8967
|
+
lines.push(" <suggested_first_tool>");
|
|
8968
|
+
lines.push(` <tool>${escapeXML26(input.suggested.tool)}</tool>`);
|
|
8969
|
+
lines.push(` <why>${escapeXML26(input.suggested.why)}</why>`);
|
|
8970
|
+
lines.push(` <estimated_tokens>${input.suggested.estimated_tokens}</estimated_tokens>`);
|
|
8971
|
+
lines.push(" </suggested_first_tool>");
|
|
8972
|
+
lines.push("</minimal_context>");
|
|
8973
|
+
return lines.join("\n");
|
|
8974
|
+
}
|
|
8975
|
+
function registerMinimalContextTool(registry, ctx) {
|
|
8976
|
+
registry.register(
|
|
8977
|
+
"ctx_get_minimal_context",
|
|
8978
|
+
{
|
|
8979
|
+
name: "ctx_get_minimal_context",
|
|
8980
|
+
description: 'Orientation anchor \u2014 call this FIRST in any workflow. Returns ~150 tokens covering graph readiness, recent working-tree changes, top hub nodes, and a task-aware suggested-first-tool. Cached for 10s. Pass `task` as a free-text intent description ("review PR 142", "rename X", "check coverage") and the tool routes to the most-fitting follow-up. The agent should call the suggested tool next rather than guessing.',
|
|
8981
|
+
inputSchema: {
|
|
8982
|
+
type: "object",
|
|
8983
|
+
properties: {
|
|
8984
|
+
task: {
|
|
8985
|
+
type: "string",
|
|
8986
|
+
description: "Free-text intent (max 200 chars). Routes the suggested-first-tool by regex. Keywords: review/audit, rename/refactor, blast/impact, architect/explore, test/coverage."
|
|
8987
|
+
},
|
|
8988
|
+
project_root: PROJECT_ROOT_JSON_SCHEMA,
|
|
8989
|
+
max_response_tokens: {
|
|
8990
|
+
type: "number",
|
|
8991
|
+
description: "Optional response token budget. Default 250."
|
|
8992
|
+
},
|
|
8993
|
+
on_budget_exceeded: {
|
|
8994
|
+
type: "string",
|
|
8995
|
+
enum: ["skeleton", "truncate", "error"]
|
|
8996
|
+
},
|
|
8997
|
+
response_format: {
|
|
8998
|
+
type: "string",
|
|
8999
|
+
enum: ["full", "skeleton", "auto"]
|
|
9000
|
+
}
|
|
9001
|
+
}
|
|
9002
|
+
}
|
|
9003
|
+
},
|
|
9004
|
+
async (args) => {
|
|
9005
|
+
const parsed = Schema30.parse(args);
|
|
9006
|
+
const task = sanitizeTask(parsed.task);
|
|
9007
|
+
const projectRoot = parsed.project_root ?? ctx.projectRoot;
|
|
9008
|
+
const key = cacheKey(projectRoot, task);
|
|
9009
|
+
const cached = cacheGet(key);
|
|
9010
|
+
if (cached) {
|
|
9011
|
+
return cached;
|
|
9012
|
+
}
|
|
9013
|
+
const graphReady = ctx.isGraphInitialized();
|
|
9014
|
+
let nodes = 0;
|
|
9015
|
+
let edges = 0;
|
|
9016
|
+
let lastBuildIso = null;
|
|
9017
|
+
const languages = [];
|
|
9018
|
+
let topHubs = [];
|
|
9019
|
+
if (graphReady) {
|
|
9020
|
+
try {
|
|
9021
|
+
const graph = await ctx.getGraph(projectRoot);
|
|
9022
|
+
nodes = graph.allFiles().length;
|
|
9023
|
+
edges = graph.allFiles().reduce((acc, f) => acc + graph.getImports(f).length, 0);
|
|
9024
|
+
const extensions = /* @__PURE__ */ new Set();
|
|
9025
|
+
for (const f of graph.allFiles()) {
|
|
9026
|
+
const ext = f.split(".").pop();
|
|
9027
|
+
if (ext && ext.length <= 4) extensions.add(ext);
|
|
9028
|
+
}
|
|
9029
|
+
languages.push(...Array.from(extensions).sort());
|
|
9030
|
+
topHubs = computeTopHubs(graph);
|
|
9031
|
+
} catch {
|
|
9032
|
+
}
|
|
9033
|
+
}
|
|
9034
|
+
const recentChanges = readRecentChanges(projectRoot);
|
|
9035
|
+
const suggested = routeFirstTool(task, recentChanges.length > 0);
|
|
9036
|
+
const staleness = classifyStaleness(lastBuildIso);
|
|
9037
|
+
const renderInput = {
|
|
9038
|
+
graphReady,
|
|
9039
|
+
nodes,
|
|
9040
|
+
edges,
|
|
9041
|
+
lastBuildIso,
|
|
9042
|
+
staleness,
|
|
9043
|
+
languages,
|
|
9044
|
+
recentChanges,
|
|
9045
|
+
topHubs,
|
|
9046
|
+
suggested
|
|
9047
|
+
};
|
|
9048
|
+
const full = render(renderInput);
|
|
9049
|
+
if (!hasBudgetArgs(parsed)) {
|
|
9050
|
+
cachePut(key, full);
|
|
9051
|
+
return full;
|
|
9052
|
+
}
|
|
9053
|
+
const result = await enforceBudget({
|
|
9054
|
+
ctx,
|
|
9055
|
+
full,
|
|
9056
|
+
args: readBudgetArgs(parsed),
|
|
9057
|
+
toolName: "ctx_get_minimal_context",
|
|
9058
|
+
defaultMaxTokens: DEFAULT_MAX_RESPONSE_TOKENS13,
|
|
9059
|
+
skeletonProducer: async () => renderSkeleton(renderInput)
|
|
9060
|
+
});
|
|
9061
|
+
const body = wrapResponse(result);
|
|
9062
|
+
cachePut(key, body);
|
|
9063
|
+
return body;
|
|
9064
|
+
}
|
|
9065
|
+
);
|
|
9066
|
+
}
|
|
9067
|
+
|
|
8451
9068
|
// packages/core/src/tools/index.ts
|
|
8452
9069
|
function createToolRegistry(ctx) {
|
|
8453
9070
|
const registry = new ToolRegistry();
|
|
@@ -8484,6 +9101,7 @@ function createToolRegistry(ctx) {
|
|
|
8484
9101
|
registerGitCouplingTool(registry, ctx);
|
|
8485
9102
|
registerRiskOverlayTool(registry, ctx);
|
|
8486
9103
|
registerGetAffectedFlowsTool(registry, ctx);
|
|
9104
|
+
registerMinimalContextTool(registry, ctx);
|
|
8487
9105
|
return registry;
|
|
8488
9106
|
}
|
|
8489
9107
|
|
|
@@ -9116,20 +9734,20 @@ import { readFileSync, writeFileSync, unlinkSync, mkdirSync, chmodSync, existsSy
|
|
|
9116
9734
|
import path29 from "path";
|
|
9117
9735
|
|
|
9118
9736
|
// packages/core/src/license/types.ts
|
|
9119
|
-
import { z as
|
|
9737
|
+
import { z as z37 } from "zod";
|
|
9120
9738
|
var FINGERPRINT_RE = /^sha256:[0-9a-f]{64}$/;
|
|
9121
|
-
var LicenseFileSchema =
|
|
9122
|
-
schemaVersion:
|
|
9123
|
-
key:
|
|
9124
|
-
tier:
|
|
9125
|
-
status:
|
|
9126
|
-
fingerprint:
|
|
9127
|
-
seats:
|
|
9128
|
-
issuedAt:
|
|
9129
|
-
expiresAt:
|
|
9130
|
-
lastValidatedAt:
|
|
9131
|
-
licenseId:
|
|
9132
|
-
instanceId:
|
|
9739
|
+
var LicenseFileSchema = z37.object({
|
|
9740
|
+
schemaVersion: z37.literal(1),
|
|
9741
|
+
key: z37.string().min(1),
|
|
9742
|
+
tier: z37.enum(["pro", "team", "enterprise", "trial"]),
|
|
9743
|
+
status: z37.enum(["active", "trialing", "expired"]),
|
|
9744
|
+
fingerprint: z37.string().regex(FINGERPRINT_RE),
|
|
9745
|
+
seats: z37.number().int().positive(),
|
|
9746
|
+
issuedAt: z37.string().datetime(),
|
|
9747
|
+
expiresAt: z37.string().datetime(),
|
|
9748
|
+
lastValidatedAt: z37.string().datetime(),
|
|
9749
|
+
licenseId: z37.string().min(1),
|
|
9750
|
+
instanceId: z37.string().min(1)
|
|
9133
9751
|
});
|
|
9134
9752
|
|
|
9135
9753
|
// packages/core/src/license/LicenseStore.ts
|
|
@@ -9342,7 +9960,7 @@ var TELEMETRY_DISABLED = TELEMETRY_LEVEL === "off";
|
|
|
9342
9960
|
function getTelemetryLevel() {
|
|
9343
9961
|
return TELEMETRY_LEVEL;
|
|
9344
9962
|
}
|
|
9345
|
-
var CTXLOOM_VERSION = "1.
|
|
9963
|
+
var CTXLOOM_VERSION = "1.4.0".length > 0 ? "1.4.0" : "dev";
|
|
9346
9964
|
var POSTHOG_HOST = "https://eu.i.posthog.com";
|
|
9347
9965
|
var POSTHOG_KEY = process.env["POSTHOG_API_KEY"] ?? (true ? "phc_CiDkmFLcZ2K6uCpcoSUQLmFrnnUvsyXGhSxopX5TVKE6" : "");
|
|
9348
9966
|
var SENTRY_DSN = process.env["SENTRY_DSN"] ?? (true ? "https://81c94a0f04a8e242dee493ac1e17f733@o4508531702497280.ingest.de.sentry.io/4511256875368528" : "");
|
|
@@ -9966,6 +10584,732 @@ var EmittedOnceTracker = class {
|
|
|
9966
10584
|
}
|
|
9967
10585
|
};
|
|
9968
10586
|
|
|
10587
|
+
// packages/core/src/install/installer.ts
|
|
10588
|
+
import fs28 from "fs";
|
|
10589
|
+
import path36 from "path";
|
|
10590
|
+
|
|
10591
|
+
// packages/core/src/install/templates.ts
|
|
10592
|
+
var RULES_BLOCK_NAME = "CTXLOOM-RULES";
|
|
10593
|
+
var RULES_BLOCK_CONTENT = `## MCP Tools: ctxloom
|
|
10594
|
+
|
|
10595
|
+
**IMPORTANT: This project has a knowledge graph. ALWAYS use the
|
|
10596
|
+
ctxloom MCP tools BEFORE Grep/Glob/Read to explore the codebase.**
|
|
10597
|
+
The graph is faster, cheaper (fewer tokens), and gives you
|
|
10598
|
+
structural context (callers, dependents, test coverage) that file
|
|
10599
|
+
scanning cannot.
|
|
10600
|
+
|
|
10601
|
+
### Start every workflow with \`ctx_get_minimal_context\`
|
|
10602
|
+
|
|
10603
|
+
The first MCP call into ctxloom should always be
|
|
10604
|
+
\`ctx_get_minimal_context(task="<what you're about to do>")\`. It
|
|
10605
|
+
returns ~150 tokens of orientation plus a task-aware
|
|
10606
|
+
\`suggested_first_tool\` you should call next instead of guessing.
|
|
10607
|
+
|
|
10608
|
+
### When to use graph tools FIRST
|
|
10609
|
+
|
|
10610
|
+
- **Code review**: \`ctx_detect_changes\` + \`ctx_get_review_context\`
|
|
10611
|
+
instead of reading whole files
|
|
10612
|
+
- **Understanding impact**: \`ctx_blast_radius\` + \`ctx_get_affected_flows\`
|
|
10613
|
+
instead of manually tracing imports
|
|
10614
|
+
- **Refactor planning**: \`ctx_get_call_graph(direction: 'callers')\`
|
|
10615
|
+
+ \`ctx_refactor_preview\` before any rename
|
|
10616
|
+
- **Architecture questions**: \`ctx_architecture_overview\`,
|
|
10617
|
+
\`ctx_community_list\`, \`ctx_hub_nodes\`
|
|
10618
|
+
- **Finding code**: \`ctx_search\` or \`ctx_full_text_search\` instead
|
|
10619
|
+
of \`Grep\`
|
|
10620
|
+
|
|
10621
|
+
Fall back to Grep/Glob/Read **only** when the graph doesn't cover
|
|
10622
|
+
what you need.
|
|
10623
|
+
|
|
10624
|
+
### Follow the \`next_tool_suggestions\` in every response
|
|
10625
|
+
|
|
10626
|
+
Every budget-wrapped ctxloom response includes
|
|
10627
|
+
\`meta.next_tool_suggestions\` \u2014 author-curated follow-ups with
|
|
10628
|
+
\`why\` reasoning and \`estimated_tokens\` per entry. Pick from
|
|
10629
|
+
those instead of guessing.
|
|
10630
|
+
|
|
10631
|
+
### Token-budget protocol
|
|
10632
|
+
|
|
10633
|
+
- Target: \u22648 tool calls per task, \u22642000 total tokens of graph context
|
|
10634
|
+
- Pass \`max_response_tokens\` on calls that might return large
|
|
10635
|
+
responses; the budget surface returns skeletons instead of dumping
|
|
10636
|
+
- Use \`response_format: 'skeleton'\` when you know you only need
|
|
10637
|
+
signatures, not bodies
|
|
10638
|
+
|
|
10639
|
+
### Key tools at a glance
|
|
10640
|
+
|
|
10641
|
+
| Tool | Use when |
|
|
10642
|
+
|------|----------|
|
|
10643
|
+
| \`ctx_get_minimal_context\` | START HERE \u2014 orientation anchor |
|
|
10644
|
+
| \`ctx_detect_changes\` | Reviewing code changes; risk-scored |
|
|
10645
|
+
| \`ctx_get_review_context\` | Token-efficient review snippets |
|
|
10646
|
+
| \`ctx_blast_radius\` | Blast radius of a change |
|
|
10647
|
+
| \`ctx_get_affected_flows\` | Execution paths impacted |
|
|
10648
|
+
| \`ctx_get_call_graph\` | Callers / callees of a symbol |
|
|
10649
|
+
| \`ctx_search\` / \`ctx_full_text_search\` | Find code |
|
|
10650
|
+
| \`ctx_architecture_overview\` | High-level codebase map |
|
|
10651
|
+
| \`ctx_refactor_preview\` / \`ctx_apply_refactor\` | Plan a rename |
|
|
10652
|
+
|
|
10653
|
+
### Hooks keep the graph fresh
|
|
10654
|
+
|
|
10655
|
+
\`ctxloom init\` installed a PostToolUse hook on \`Write|Edit\` that
|
|
10656
|
+
runs \`ctxloom update --incremental --quiet\` \u2014 so the graph is
|
|
10657
|
+
always up to date when you query it. No "did the index update yet?"
|
|
10658
|
+
guessing.`;
|
|
10659
|
+
var SESSION_START_HEADER = `#!/usr/bin/env bash
|
|
10660
|
+
# ctxloom \u2014 agent-harness session-start hook
|
|
10661
|
+
# Generated by \`ctxloom init\`. Re-run \`ctxloom init\` to update.
|
|
10662
|
+
# Manual edits will be overwritten on the next install.
|
|
10663
|
+
|
|
10664
|
+
set -e
|
|
10665
|
+
`;
|
|
10666
|
+
var SESSION_START_BODY = `DB=".ctxloom/graph.db"
|
|
10667
|
+
|
|
10668
|
+
if [ -f "$DB" ]; then
|
|
10669
|
+
# \`ctxloom status --json\` is cached + sub-100ms \u2014 keeps the hook
|
|
10670
|
+
# under its 2s timeout even on cold disk.
|
|
10671
|
+
STATS=$(ctxloom status --json 2>/dev/null || echo '{"nodes":0,"edges":0}')
|
|
10672
|
+
NODES=$(echo "$STATS" | grep -oE '"nodes":\\s*[0-9]+' | grep -oE '[0-9]+' || echo "?")
|
|
10673
|
+
EDGES=$(echo "$STATS" | grep -oE '"edges":\\s*[0-9]+' | grep -oE '[0-9]+' || echo "?")
|
|
10674
|
+
|
|
10675
|
+
cat <<EOF
|
|
10676
|
+
[ctxloom] Knowledge graph ready (\${NODES} nodes, \${EDGES} edges).
|
|
10677
|
+
|
|
10678
|
+
Start every workflow with \\\`ctx_get_minimal_context(task="...")\\\`.
|
|
10679
|
+
It returns ~150 tokens of orientation + a task-aware
|
|
10680
|
+
suggested_first_tool. Follow the meta.next_tool_suggestions on
|
|
10681
|
+
every response.
|
|
10682
|
+
|
|
10683
|
+
Prefer ctxloom MCP tools over Grep/Glob/Read:
|
|
10684
|
+
- ctx_detect_changes for code review
|
|
10685
|
+
- ctx_blast_radius / ctx_get_call_graph before refactoring
|
|
10686
|
+
- ctx_architecture_overview for orientation
|
|
10687
|
+
EOF
|
|
10688
|
+
else
|
|
10689
|
+
cat <<EOF
|
|
10690
|
+
[ctxloom] No knowledge graph found here.
|
|
10691
|
+
Run: ctxloom build
|
|
10692
|
+
|
|
10693
|
+
Then restart this session to enable graph-powered queries.
|
|
10694
|
+
EOF
|
|
10695
|
+
fi
|
|
10696
|
+
`;
|
|
10697
|
+
var SESSION_START_FULL = SESSION_START_HEADER + "\n" + SESSION_START_BODY;
|
|
10698
|
+
var CTXLOOM_HOOK_ENTRIES = {
|
|
10699
|
+
SessionStart: {
|
|
10700
|
+
matcher: "",
|
|
10701
|
+
hooks: [{ type: "command", command: ".claude/hooks/session-start.sh", timeout: 2 }]
|
|
10702
|
+
},
|
|
10703
|
+
PostToolUse: {
|
|
10704
|
+
matcher: "Write|Edit",
|
|
10705
|
+
hooks: [
|
|
10706
|
+
{
|
|
10707
|
+
type: "command",
|
|
10708
|
+
command: "ctxloom update --incremental --quiet",
|
|
10709
|
+
timeout: 30
|
|
10710
|
+
}
|
|
10711
|
+
]
|
|
10712
|
+
}
|
|
10713
|
+
};
|
|
10714
|
+
|
|
10715
|
+
// packages/core/src/install/hmacBlock.ts
|
|
10716
|
+
import crypto6 from "crypto";
|
|
10717
|
+
var DEFAULT_HMAC_KEY = "ctxloom-agent-harness-v1-published";
|
|
10718
|
+
function resolveHmacKey() {
|
|
10719
|
+
return process.env.CTXLOOM_INSTALL_KEY ?? DEFAULT_HMAC_KEY;
|
|
10720
|
+
}
|
|
10721
|
+
function computeBlockHmac(content, key = resolveHmacKey()) {
|
|
10722
|
+
return crypto6.createHmac("sha256", key).update(content, "utf-8").digest("hex");
|
|
10723
|
+
}
|
|
10724
|
+
function wrapBlock(name, content) {
|
|
10725
|
+
const hmac = computeBlockHmac(content);
|
|
10726
|
+
return [
|
|
10727
|
+
`<!-- BEGIN ${name} v:1 hmac:sha256:${hmac} -->`,
|
|
10728
|
+
content,
|
|
10729
|
+
`<!-- END ${name} -->`
|
|
10730
|
+
].join("\n");
|
|
10731
|
+
}
|
|
10732
|
+
var START_RE_TEMPLATE = (name) => new RegExp(`<!-- BEGIN ${escapeRegex(name)} v:(\\d+) hmac:sha256:([0-9a-f]{64}) -->`);
|
|
10733
|
+
var END_RE_TEMPLATE = (name) => new RegExp(`<!-- END ${escapeRegex(name)} -->`);
|
|
10734
|
+
function escapeRegex(s) {
|
|
10735
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
10736
|
+
}
|
|
10737
|
+
function extractBlock(fileContent, name) {
|
|
10738
|
+
const startRe = START_RE_TEMPLATE(name);
|
|
10739
|
+
const endRe = END_RE_TEMPLATE(name);
|
|
10740
|
+
const startMatch = startRe.exec(fileContent);
|
|
10741
|
+
if (!startMatch) return null;
|
|
10742
|
+
const startIdx = startMatch.index;
|
|
10743
|
+
const afterStart = startIdx + startMatch[0].length;
|
|
10744
|
+
endRe.lastIndex = afterStart;
|
|
10745
|
+
const endMatch = endRe.exec(fileContent.slice(afterStart));
|
|
10746
|
+
if (!endMatch) return null;
|
|
10747
|
+
const endIdx = afterStart + endMatch.index + endMatch[0].length;
|
|
10748
|
+
let inner = fileContent.slice(afterStart, afterStart + endMatch.index);
|
|
10749
|
+
if (inner.startsWith("\n")) inner = inner.slice(1);
|
|
10750
|
+
if (inner.endsWith("\n")) inner = inner.slice(0, -1);
|
|
10751
|
+
return {
|
|
10752
|
+
content: inner,
|
|
10753
|
+
declaredHmac: startMatch[2],
|
|
10754
|
+
version: Number(startMatch[1]),
|
|
10755
|
+
start: startIdx,
|
|
10756
|
+
end: endIdx
|
|
10757
|
+
};
|
|
10758
|
+
}
|
|
10759
|
+
function verifyBlock(block) {
|
|
10760
|
+
return computeBlockHmac(block.content) === block.declaredHmac;
|
|
10761
|
+
}
|
|
10762
|
+
function upsertBlock(fileContent, name, newContent) {
|
|
10763
|
+
const wrapped = wrapBlock(name, newContent);
|
|
10764
|
+
const existing = extractBlock(fileContent, name);
|
|
10765
|
+
if (existing) {
|
|
10766
|
+
return fileContent.slice(0, existing.start) + wrapped + fileContent.slice(existing.end);
|
|
10767
|
+
}
|
|
10768
|
+
const sep = fileContent.length === 0 || fileContent.endsWith("\n\n") ? "" : fileContent.endsWith("\n") ? "\n" : "\n\n";
|
|
10769
|
+
return fileContent + sep + wrapped + "\n";
|
|
10770
|
+
}
|
|
10771
|
+
|
|
10772
|
+
// packages/core/src/install/skillTemplates.ts
|
|
10773
|
+
var EXPLORE_CONTENT = `---
|
|
10774
|
+
name: ctxloom-explore
|
|
10775
|
+
description: Orient yourself to an unfamiliar codebase using ctxloom's structural graph. Architecture overview + communities + top hubs in \u22645 tool calls.
|
|
10776
|
+
---
|
|
10777
|
+
|
|
10778
|
+
# Explore Codebase
|
|
10779
|
+
|
|
10780
|
+
Use this when you need to understand a codebase you haven't worked in
|
|
10781
|
+
before, or when re-orienting after time away.
|
|
10782
|
+
|
|
10783
|
+
## Steps
|
|
10784
|
+
|
|
10785
|
+
1. **Orientation anchor**: call \`ctx_get_minimal_context(task="explore this codebase")\`.
|
|
10786
|
+
The response includes graph stats, top hubs, and a
|
|
10787
|
+
\`suggested_first_tool\` \u2014 follow it (likely
|
|
10788
|
+
\`ctx_architecture_overview\`).
|
|
10789
|
+
|
|
10790
|
+
2. **Architecture overview**: call \`ctx_architecture_overview(max_response_tokens=2000)\`.
|
|
10791
|
+
Returns the community structure + hub nodes + cross-community bridges.
|
|
10792
|
+
|
|
10793
|
+
3. **Drill into the biggest communities**: from the overview's
|
|
10794
|
+
\`meta.next_tool_suggestions\`, call \`ctx_community_list\` for the
|
|
10795
|
+
top 1\u20132 communities. Skip communities labeled "tests" or
|
|
10796
|
+
"config" \u2014 they're usually peripheral.
|
|
10797
|
+
|
|
10798
|
+
4. **Investigate the architectural bridges**: call \`ctx_bridge_nodes\`.
|
|
10799
|
+
Bridge nodes are high-leverage \u2014 changing one affects multiple
|
|
10800
|
+
communities. Read these first to understand the codebase's
|
|
10801
|
+
coupling story.
|
|
10802
|
+
|
|
10803
|
+
5. **Tour the hubs**: call \`ctx_hub_nodes(limit=5, detail_level="minimal")\`.
|
|
10804
|
+
Top 5 most-depended-upon files. These are usually the heart of
|
|
10805
|
+
the codebase.
|
|
10806
|
+
|
|
10807
|
+
## Budget
|
|
10808
|
+
|
|
10809
|
+
- \u22645 ctxloom tool calls
|
|
10810
|
+
- \u22642000 tokens total response budget
|
|
10811
|
+
- Don't \`ctx_get_file\` anything during exploration \u2014 signatures are
|
|
10812
|
+
enough. Drop to file reads only if a specific symbol needs
|
|
10813
|
+
inspection (and only via \`ctx_get_definition\`, not raw read).
|
|
10814
|
+
|
|
10815
|
+
## Output
|
|
10816
|
+
|
|
10817
|
+
Summarize for the user:
|
|
10818
|
+
- Main communities (3\u20135 named clusters)
|
|
10819
|
+
- Top hubs by fan-in (the load-bearing files)
|
|
10820
|
+
- Top bridges (the architectural seams)
|
|
10821
|
+
- Recommended deep-dive starting points
|
|
10822
|
+
`;
|
|
10823
|
+
var BLAST_CONTENT = `---
|
|
10824
|
+
name: ctxloom-blast
|
|
10825
|
+
description: Compute blast radius + affected execution flows for a symbol or file before changing it. Pinpoints what will break.
|
|
10826
|
+
argument-hint: "<symbol-name | file-path>"
|
|
10827
|
+
---
|
|
10828
|
+
|
|
10829
|
+
# Blast Radius
|
|
10830
|
+
|
|
10831
|
+
Use this before any change to a public function, type, or file
|
|
10832
|
+
where you're not sure who depends on it.
|
|
10833
|
+
|
|
10834
|
+
## Inputs
|
|
10835
|
+
|
|
10836
|
+
- \`$ARGUMENTS\` \u2014 the symbol name (e.g. \`emitTelemetry\`) or file
|
|
10837
|
+
path (e.g. \`src/server.ts\`) you're about to modify.
|
|
10838
|
+
|
|
10839
|
+
## Steps
|
|
10840
|
+
|
|
10841
|
+
1. **Orientation**: call \`ctx_get_minimal_context(task="blast radius for $ARGUMENTS")\`.
|
|
10842
|
+
|
|
10843
|
+
2. **Blast radius**: call \`ctx_blast_radius(target="$ARGUMENTS", max_response_tokens=1500)\`.
|
|
10844
|
+
Returns transitive dependents \u2014 every file (and indirectly,
|
|
10845
|
+
every flow) that would be affected by a breaking change.
|
|
10846
|
+
|
|
10847
|
+
3. **Caller graph**: call \`ctx_get_call_graph(symbol="$ARGUMENTS", direction="callers", depth=2)\`.
|
|
10848
|
+
Direct + grandparent callers. Pair with the blast radius
|
|
10849
|
+
to distinguish "many transitive deps" from "load-bearing
|
|
10850
|
+
direct API."
|
|
10851
|
+
|
|
10852
|
+
4. **Affected execution flows**: call \`ctx_get_affected_flows(target="$ARGUMENTS")\`.
|
|
10853
|
+
Maps the change to ordered execution sequences \u2014 useful for
|
|
10854
|
+
debugging "what user-facing path breaks?"
|
|
10855
|
+
|
|
10856
|
+
5. **Test coverage check**: call \`ctx_knowledge_gaps(scope="$ARGUMENTS")\`.
|
|
10857
|
+
Highlights affected files lacking test coverage \u2014 those are the
|
|
10858
|
+
real risk surface.
|
|
10859
|
+
|
|
10860
|
+
## Budget
|
|
10861
|
+
|
|
10862
|
+
- \u22645 ctxloom tool calls
|
|
10863
|
+
- \u22642000 tokens total
|
|
10864
|
+
|
|
10865
|
+
## Output
|
|
10866
|
+
|
|
10867
|
+
Report to the user:
|
|
10868
|
+
- Total transitive dependents (number + top 5 by depth)
|
|
10869
|
+
- Direct callers (the API consumers)
|
|
10870
|
+
- Affected execution flows (named user-facing paths)
|
|
10871
|
+
- Coverage gaps on the affected files (the risk surface)
|
|
10872
|
+
- Recommendation: "safe to change" / "review carefully" / "needs migration plan"
|
|
10873
|
+
`;
|
|
10874
|
+
var REFACTOR_CONTENT = `---
|
|
10875
|
+
name: ctxloom-refactor-safely
|
|
10876
|
+
description: Plan and execute a rename or signature change with full caller-aware safety. Preview before applying.
|
|
10877
|
+
argument-hint: "<old-name> <new-name>"
|
|
10878
|
+
---
|
|
10879
|
+
|
|
10880
|
+
# Refactor Safely
|
|
10881
|
+
|
|
10882
|
+
Use this for renames, signature changes, or function moves. The
|
|
10883
|
+
skill enforces preview-before-apply.
|
|
10884
|
+
|
|
10885
|
+
## Inputs
|
|
10886
|
+
|
|
10887
|
+
- \`$1\` \u2014 current symbol name (e.g. \`emitTelemetry\`)
|
|
10888
|
+
- \`$2\` \u2014 target name (e.g. \`emitTelemetryEvent\`)
|
|
10889
|
+
|
|
10890
|
+
## Steps
|
|
10891
|
+
|
|
10892
|
+
1. **Orientation**: call \`ctx_get_minimal_context(task="refactor $1 to $2")\`.
|
|
10893
|
+
|
|
10894
|
+
2. **Surface every caller**: call \`ctx_get_call_graph(symbol="$1", direction="callers", depth=1)\`.
|
|
10895
|
+
The exhaustive caller list. Without this, a rename can break
|
|
10896
|
+
files you didn't notice depended on the symbol.
|
|
10897
|
+
|
|
10898
|
+
3. **Blast radius**: call \`ctx_blast_radius(target="$1")\`.
|
|
10899
|
+
Transitive impact \u2014 useful to gauge whether this should be a
|
|
10900
|
+
single PR or split with a deprecation period.
|
|
10901
|
+
|
|
10902
|
+
4. **Generate the refactor preview**: call \`ctx_refactor_preview(symbol="$1", new_name="$2")\`.
|
|
10903
|
+
Returns a diff preview WITHOUT writing anything. Inspect it.
|
|
10904
|
+
|
|
10905
|
+
5. **Confirm with the user**: show the preview summary
|
|
10906
|
+
(N files changed, M call sites updated). **DO NOT proceed to
|
|
10907
|
+
step 6 without explicit user confirmation.**
|
|
10908
|
+
|
|
10909
|
+
6. **Apply the refactor**: call \`ctx_apply_refactor(symbol="$1", new_name="$2")\`.
|
|
10910
|
+
This writes the changes to disk. Irreversible without a git
|
|
10911
|
+
reset.
|
|
10912
|
+
|
|
10913
|
+
7. **Verify**: call \`ctx_detect_changes\` and confirm the diff
|
|
10914
|
+
matches what the preview said. Surface any unexpected changes.
|
|
10915
|
+
|
|
10916
|
+
## Safety rails
|
|
10917
|
+
|
|
10918
|
+
- ALWAYS run step 4 (preview) before step 6 (apply)
|
|
10919
|
+
- ALWAYS ask the user for confirmation between preview and apply
|
|
10920
|
+
- If preview shows >50 files affected, recommend splitting into
|
|
10921
|
+
a deprecation-style migration instead of a single rename
|
|
10922
|
+
- If the symbol is exported from a public API package, refuse to
|
|
10923
|
+
proceed and recommend the user open a tracked migration plan
|
|
10924
|
+
|
|
10925
|
+
## Budget
|
|
10926
|
+
|
|
10927
|
+
- \u22647 ctxloom tool calls
|
|
10928
|
+
- \u22643000 tokens total (preview output can be larger than other skills)
|
|
10929
|
+
`;
|
|
10930
|
+
var COVERAGE_GAP_CONTENT = `---
|
|
10931
|
+
name: ctxloom-coverage-gap
|
|
10932
|
+
description: Identify code that lacks test coverage, prioritized by caller frequency and risk.
|
|
10933
|
+
---
|
|
10934
|
+
|
|
10935
|
+
# Coverage Gap Analysis
|
|
10936
|
+
|
|
10937
|
+
Use this to find untested code that genuinely matters \u2014 the
|
|
10938
|
+
intersection of "no tests" + "many callers" + "high risk score."
|
|
10939
|
+
|
|
10940
|
+
## Steps
|
|
10941
|
+
|
|
10942
|
+
1. **Orientation**: call \`ctx_get_minimal_context(task="check test coverage")\`.
|
|
10943
|
+
|
|
10944
|
+
2. **Knowledge gaps**: call \`ctx_knowledge_gaps(max_response_tokens=1200)\`.
|
|
10945
|
+
Lists every file lacking a \`tests_for\` graph edge. Raw list
|
|
10946
|
+
without prioritization.
|
|
10947
|
+
|
|
10948
|
+
3. **Score by impact**: for each gap, call
|
|
10949
|
+
\`ctx_get_call_graph(symbol=<gap_symbol>, direction="callers")\`
|
|
10950
|
+
to count callers. High caller-count + no tests = high priority.
|
|
10951
|
+
|
|
10952
|
+
4. **Cross-reference with churn**: call
|
|
10953
|
+
\`ctx_git_coupling(file=<gap_file>)\` for the top 5 gaps.
|
|
10954
|
+
Files churning often without tests are the urgent ones.
|
|
10955
|
+
|
|
10956
|
+
5. **Risk overlay**: call \`ctx_risk_overlay(scope=<top_gap_files>)\`.
|
|
10957
|
+
Combines churn + coupling into a single risk score.
|
|
10958
|
+
|
|
10959
|
+
## Output
|
|
10960
|
+
|
|
10961
|
+
Tabular report:
|
|
10962
|
+
|
|
10963
|
+
\`\`\`
|
|
10964
|
+
| File / Symbol | Callers | Churn | Risk | Recommendation |
|
|
10965
|
+
|---|---|---|---|---|
|
|
10966
|
+
| ... | ... | ... | ... | ... |
|
|
10967
|
+
\`\`\`
|
|
10968
|
+
|
|
10969
|
+
Recommendations should distinguish:
|
|
10970
|
+
- "Add tests now" (high caller count + high churn)
|
|
10971
|
+
- "Add tests during next change" (high caller count, low churn)
|
|
10972
|
+
- "Acceptable gap" (low caller count, low churn)
|
|
10973
|
+
|
|
10974
|
+
## Budget
|
|
10975
|
+
|
|
10976
|
+
- \u22646 ctxloom tool calls
|
|
10977
|
+
- \u22642500 tokens total
|
|
10978
|
+
`;
|
|
10979
|
+
var REVIEW_PR_CONTENT = `---
|
|
10980
|
+
name: ctxloom-review-pr
|
|
10981
|
+
description: Multi-tier code review of a PR using ctxloom's structural graph. Risk-scored, blast-radius-aware, coverage-conscious.
|
|
10982
|
+
argument-hint: "<PR number | branch name>"
|
|
10983
|
+
---
|
|
10984
|
+
|
|
10985
|
+
# Review PR
|
|
10986
|
+
|
|
10987
|
+
Comprehensive PR review using ctxloom's graph. Mirrors the
|
|
10988
|
+
multi-agent review the ctxloom-bot posts automatically \u2014 useful
|
|
10989
|
+
when reviewing manually or when the bot isn't wired up.
|
|
10990
|
+
|
|
10991
|
+
## Inputs
|
|
10992
|
+
|
|
10993
|
+
- \`$ARGUMENTS\` \u2014 PR number (e.g. \`142\`) or branch name (e.g. \`feat/foo\`).
|
|
10994
|
+
Defaults to the current branch if unset.
|
|
10995
|
+
|
|
10996
|
+
## Steps
|
|
10997
|
+
|
|
10998
|
+
1. **Orientation**: call \`ctx_get_minimal_context(task="review PR $ARGUMENTS")\`.
|
|
10999
|
+
|
|
11000
|
+
2. **Detect changes**: call \`ctx_detect_changes(base="main")\`.
|
|
11001
|
+
Risk-scored per-file analysis. Take the top 5 highest-risk files.
|
|
11002
|
+
|
|
11003
|
+
3. **Pull source for the risky files**: call \`ctx_git_diff_review(base="main", max_response_tokens=4000)\`.
|
|
11004
|
+
Token-efficient diff packet covering the changed files.
|
|
11005
|
+
|
|
11006
|
+
4. **Blast radius per high-risk file**: for the top 3 risky files,
|
|
11007
|
+
call \`ctx_blast_radius(target=<file>)\`. Surfaces files that the
|
|
11008
|
+
change indirectly affects but don't appear in the diff.
|
|
11009
|
+
|
|
11010
|
+
5. **Affected flows**: call \`ctx_get_affected_flows(base="main")\`.
|
|
11011
|
+
Which execution paths the PR touches. Use to identify what
|
|
11012
|
+
integration tests should pass.
|
|
11013
|
+
|
|
11014
|
+
6. **Coverage check**: call \`ctx_knowledge_gaps(scope=<changed_files>)\`.
|
|
11015
|
+
Surface changed files lacking tests.
|
|
11016
|
+
|
|
11017
|
+
7. **Generate the review**: structured output with:
|
|
11018
|
+
- Risk summary (low/medium/high overall)
|
|
11019
|
+
- File-by-file findings (severity-ranked)
|
|
11020
|
+
- Coverage gaps that need addressing
|
|
11021
|
+
- Blast-radius observations the diff doesn't show
|
|
11022
|
+
|
|
11023
|
+
## Tier discipline
|
|
11024
|
+
|
|
11025
|
+
This skill is the agent-driven equivalent of the bot's
|
|
11026
|
+
multi-specialist review. Use the same tier ladder:
|
|
11027
|
+
|
|
11028
|
+
- **T0 (structural)**: use the tools listed above \u2014 they're
|
|
11029
|
+
pre-fetched and cheap
|
|
11030
|
+
- **T1 (skeleton)**: \`ctx_get_definition\` for individual symbols
|
|
11031
|
+
- **T2 (full body)**: \`ctx_get_file\` only for files where the
|
|
11032
|
+
skeleton view is insufficient
|
|
11033
|
+
- **T3 (raw read)**: avoid; if the graph can't answer the question,
|
|
11034
|
+
prefer \`ctx_git_diff_review\` (token-efficient diff packet) over
|
|
11035
|
+
raw \`Read\`
|
|
11036
|
+
|
|
11037
|
+
## Budget
|
|
11038
|
+
|
|
11039
|
+
- \u22648 ctxloom tool calls
|
|
11040
|
+
- \u22645000 tokens total (review needs more headroom than other skills)
|
|
11041
|
+
|
|
11042
|
+
## Output format
|
|
11043
|
+
|
|
11044
|
+
\`\`\`
|
|
11045
|
+
## PR Review: <title>
|
|
11046
|
+
|
|
11047
|
+
### Summary
|
|
11048
|
+
<1\u20133 sentence overview>
|
|
11049
|
+
|
|
11050
|
+
### Risk Assessment
|
|
11051
|
+
- Overall: Low / Medium / High
|
|
11052
|
+
- Blast radius: X files, Y flows impacted
|
|
11053
|
+
- Coverage: N changed symbols covered / M total
|
|
11054
|
+
|
|
11055
|
+
### Findings
|
|
11056
|
+
|
|
11057
|
+
#### <file_path>
|
|
11058
|
+
- **Severity**: ...
|
|
11059
|
+
- **Issue**: ...
|
|
11060
|
+
- **Suggested fix**: ...
|
|
11061
|
+
|
|
11062
|
+
### Coverage Gaps
|
|
11063
|
+
|
|
11064
|
+
<table>
|
|
11065
|
+
|
|
11066
|
+
### Suggested follow-ups
|
|
11067
|
+
|
|
11068
|
+
<list>
|
|
11069
|
+
\`\`\`
|
|
11070
|
+
`;
|
|
11071
|
+
var BUDGET_STATS_CONTENT = `---
|
|
11072
|
+
name: ctxloom-budget-stats
|
|
11073
|
+
description: Inspect ctxloom's per-tool budget telemetry \u2014 fallback distribution + original-token p50/p75/p95 \u2014 to tune defaults from real usage.
|
|
11074
|
+
---
|
|
11075
|
+
|
|
11076
|
+
# Budget Stats
|
|
11077
|
+
|
|
11078
|
+
Wrapper around \`ctxloom budget-stats\` for inline use inside a
|
|
11079
|
+
Claude Code session. Useful when:
|
|
11080
|
+
|
|
11081
|
+
- Tuning per-tool \`DEFAULT_MAX_RESPONSE_TOKENS\` from real usage
|
|
11082
|
+
(the Phase B follow-up)
|
|
11083
|
+
- Diagnosing why a tool keeps falling back to skeleton mode
|
|
11084
|
+
- Understanding which tools dominate the user's token budget
|
|
11085
|
+
|
|
11086
|
+
## Steps
|
|
11087
|
+
|
|
11088
|
+
1. **Orientation**: call \`ctx_get_minimal_context(task="inspect budget telemetry")\`.
|
|
11089
|
+
Cheap (~150 tokens). Confirms the graph is wired up \u2014 if it's
|
|
11090
|
+
not, the user's MCP server probably can't emit budget events
|
|
11091
|
+
either, and the stats will be empty.
|
|
11092
|
+
|
|
11093
|
+
3. **Window selection**: ask the user how far back to look
|
|
11094
|
+
(default: 14d). Accept \`1d\`, \`7d\`, \`14d\`, \`30d\`.
|
|
11095
|
+
|
|
11096
|
+
4. **Optional tool filter**: ask if they want stats for a specific
|
|
11097
|
+
tool (e.g. \`ctx_get_file\`) or all tools.
|
|
11098
|
+
|
|
11099
|
+
5. **Run the CLI**: \`Bash\`-tool execute:
|
|
11100
|
+
\`ctxloom budget-stats --window=<N>d [--tool=<name>]\`
|
|
11101
|
+
|
|
11102
|
+
6. **Parse + summarize the output**:
|
|
11103
|
+
- Top 3 tools by breach count \u2192 these are the candidates for
|
|
11104
|
+
budget tuning
|
|
11105
|
+
- For each top tool, the p75 column is the suggested next
|
|
11106
|
+
\`DEFAULT_MAX_RESPONSE_TOKENS\` value (rationale: 75% of
|
|
11107
|
+
real-world calls fit under p75; the rest fall back gracefully
|
|
11108
|
+
to skeletons)
|
|
11109
|
+
|
|
11110
|
+
7. **Suggest concrete edits**: for each top tool, point to the
|
|
11111
|
+
source file (\`packages/core/src/tools/<tool>.ts\`) and the
|
|
11112
|
+
current constant. Don't apply edits without user confirmation.
|
|
11113
|
+
|
|
11114
|
+
## Budget
|
|
11115
|
+
|
|
11116
|
+
- \u22642 ctxloom tool calls (this skill is mostly Bash + parsing)
|
|
11117
|
+
- \u22641500 tokens response total
|
|
11118
|
+
|
|
11119
|
+
## Output
|
|
11120
|
+
|
|
11121
|
+
\`\`\`
|
|
11122
|
+
## Budget stats \u2014 <window>
|
|
11123
|
+
|
|
11124
|
+
### Top tools by breach count
|
|
11125
|
+
1. <tool>: N breaches, skeleton%, p75=<tokens>
|
|
11126
|
+
2. ...
|
|
11127
|
+
|
|
11128
|
+
### Suggested DEFAULT_MAX_RESPONSE_TOKENS tuning
|
|
11129
|
+
- packages/core/src/tools/<tool>.ts: <current> \u2192 <suggested-p75>
|
|
11130
|
+
(rationale: ...)
|
|
11131
|
+
\`\`\`
|
|
11132
|
+
`;
|
|
11133
|
+
var CTXLOOM_SKILLS = [
|
|
11134
|
+
{ name: "ctxloom-explore", content: EXPLORE_CONTENT },
|
|
11135
|
+
{ name: "ctxloom-blast", content: BLAST_CONTENT },
|
|
11136
|
+
{ name: "ctxloom-refactor-safely", content: REFACTOR_CONTENT },
|
|
11137
|
+
{ name: "ctxloom-coverage-gap", content: COVERAGE_GAP_CONTENT },
|
|
11138
|
+
{ name: "ctxloom-review-pr", content: REVIEW_PR_CONTENT },
|
|
11139
|
+
{ name: "ctxloom-budget-stats", content: BUDGET_STATS_CONTENT }
|
|
11140
|
+
];
|
|
11141
|
+
function skillFilePath(name) {
|
|
11142
|
+
return `.claude/skills/${name}/SKILL.md`;
|
|
11143
|
+
}
|
|
11144
|
+
|
|
11145
|
+
// packages/core/src/install/installer.ts
|
|
11146
|
+
function installHarness(opts = {}) {
|
|
11147
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
11148
|
+
const projectRoot = path36.resolve(cwd);
|
|
11149
|
+
const stat = fs28.statSync(projectRoot);
|
|
11150
|
+
if (!stat.isDirectory()) {
|
|
11151
|
+
throw new Error(`installHarness: ${projectRoot} is not a directory`);
|
|
11152
|
+
}
|
|
11153
|
+
const dryRun = opts.dryRun === true;
|
|
11154
|
+
const force = opts.force === true;
|
|
11155
|
+
const warnings = [];
|
|
11156
|
+
const claudeMd = writeRulesBlock(projectRoot, "CLAUDE.md", { dryRun, force, warnings });
|
|
11157
|
+
const agentsMd = writeRulesBlock(projectRoot, "AGENTS.md", { dryRun, force, warnings });
|
|
11158
|
+
const geminiMd = writeRulesBlock(projectRoot, "GEMINI.md", { dryRun, force, warnings });
|
|
11159
|
+
const hooksJson = writeHooksJson(projectRoot, { dryRun, warnings });
|
|
11160
|
+
const sessionStartSh = writeSessionStartScript(projectRoot, { dryRun });
|
|
11161
|
+
const skills = CTXLOOM_SKILLS.map((s) => writeSkill(projectRoot, s, { dryRun }));
|
|
11162
|
+
return {
|
|
11163
|
+
projectRoot,
|
|
11164
|
+
claudeMd,
|
|
11165
|
+
agentsMd,
|
|
11166
|
+
geminiMd,
|
|
11167
|
+
hooksJson,
|
|
11168
|
+
sessionStartSh,
|
|
11169
|
+
skills,
|
|
11170
|
+
warnings
|
|
11171
|
+
};
|
|
11172
|
+
}
|
|
11173
|
+
function safeJoin(root, name) {
|
|
11174
|
+
const target = path36.resolve(root, name);
|
|
11175
|
+
const rootResolved = path36.resolve(root);
|
|
11176
|
+
if (!target.startsWith(rootResolved + path36.sep) && target !== rootResolved) {
|
|
11177
|
+
throw new Error(`installHarness: refusing to write outside project root: ${target}`);
|
|
11178
|
+
}
|
|
11179
|
+
return target;
|
|
11180
|
+
}
|
|
11181
|
+
function writeRulesBlock(projectRoot, filename, opts) {
|
|
11182
|
+
const filePath = safeJoin(projectRoot, filename);
|
|
11183
|
+
const existed = fs28.existsSync(filePath);
|
|
11184
|
+
const existing = existed ? fs28.readFileSync(filePath, "utf-8") : "";
|
|
11185
|
+
if (existed) {
|
|
11186
|
+
const block = extractBlock(existing, RULES_BLOCK_NAME);
|
|
11187
|
+
if (block) {
|
|
11188
|
+
const intact = verifyBlock(block);
|
|
11189
|
+
if (intact && block.content === RULES_BLOCK_CONTENT) {
|
|
11190
|
+
return { path: filePath, created: false, updated: false, alreadyCorrect: true, dryRun: opts.dryRun };
|
|
11191
|
+
}
|
|
11192
|
+
if (!intact && !opts.force) {
|
|
11193
|
+
opts.warnings.push(
|
|
11194
|
+
`Drift detected in ${filename}: the CTXLOOM-RULES block has been hand-edited. Re-run \`ctxloom init --force\` to overwrite, or commit your changes and re-run.`
|
|
11195
|
+
);
|
|
11196
|
+
return { path: filePath, created: false, updated: false, alreadyCorrect: false, dryRun: opts.dryRun };
|
|
11197
|
+
}
|
|
11198
|
+
}
|
|
11199
|
+
}
|
|
11200
|
+
const next = upsertBlock(existing, RULES_BLOCK_NAME, RULES_BLOCK_CONTENT);
|
|
11201
|
+
if (!opts.dryRun) {
|
|
11202
|
+
fs28.writeFileSync(filePath, next, "utf-8");
|
|
11203
|
+
}
|
|
11204
|
+
return {
|
|
11205
|
+
path: filePath,
|
|
11206
|
+
created: !existed,
|
|
11207
|
+
updated: existed,
|
|
11208
|
+
alreadyCorrect: false,
|
|
11209
|
+
dryRun: opts.dryRun
|
|
11210
|
+
};
|
|
11211
|
+
}
|
|
11212
|
+
function writeHooksJson(projectRoot, opts) {
|
|
11213
|
+
const dir = safeJoin(projectRoot, ".claude");
|
|
11214
|
+
const filePath = safeJoin(projectRoot, ".claude/hooks.json");
|
|
11215
|
+
const existed = fs28.existsSync(filePath);
|
|
11216
|
+
let current = {};
|
|
11217
|
+
if (existed) {
|
|
11218
|
+
try {
|
|
11219
|
+
const text = fs28.readFileSync(filePath, "utf-8");
|
|
11220
|
+
current = JSON.parse(text);
|
|
11221
|
+
} catch (err) {
|
|
11222
|
+
opts.warnings.push(
|
|
11223
|
+
`Could not parse existing ${path36.relative(projectRoot, filePath)}; treating as empty. (${err instanceof Error ? err.message : String(err)})`
|
|
11224
|
+
);
|
|
11225
|
+
current = {};
|
|
11226
|
+
}
|
|
11227
|
+
}
|
|
11228
|
+
const merged = { ...current };
|
|
11229
|
+
for (const event of ["SessionStart", "PostToolUse"]) {
|
|
11230
|
+
const incoming = CTXLOOM_HOOK_ENTRIES[event];
|
|
11231
|
+
const existingArr = Array.isArray(merged[event]) ? merged[event] : [];
|
|
11232
|
+
const filtered = existingArr.filter(
|
|
11233
|
+
(entry) => !isCtxloomEntry(entry, incoming.matcher)
|
|
11234
|
+
);
|
|
11235
|
+
merged[event] = [...filtered, incoming];
|
|
11236
|
+
}
|
|
11237
|
+
const nextJson = JSON.stringify(merged, null, 2) + "\n";
|
|
11238
|
+
let alreadyCorrect = false;
|
|
11239
|
+
if (existed) {
|
|
11240
|
+
const currentText = fs28.readFileSync(filePath, "utf-8");
|
|
11241
|
+
if (currentText === nextJson) alreadyCorrect = true;
|
|
11242
|
+
}
|
|
11243
|
+
if (!opts.dryRun && !alreadyCorrect) {
|
|
11244
|
+
fs28.mkdirSync(dir, { recursive: true });
|
|
11245
|
+
fs28.writeFileSync(filePath, nextJson, "utf-8");
|
|
11246
|
+
}
|
|
11247
|
+
return {
|
|
11248
|
+
path: filePath,
|
|
11249
|
+
created: !existed,
|
|
11250
|
+
updated: existed && !alreadyCorrect,
|
|
11251
|
+
alreadyCorrect,
|
|
11252
|
+
dryRun: opts.dryRun
|
|
11253
|
+
};
|
|
11254
|
+
}
|
|
11255
|
+
function isCtxloomEntry(entry, expectedMatcher) {
|
|
11256
|
+
if (entry.matcher !== expectedMatcher) return false;
|
|
11257
|
+
if (!Array.isArray(entry.hooks)) return false;
|
|
11258
|
+
return entry.hooks.some((h) => {
|
|
11259
|
+
if (!h || typeof h !== "object") return false;
|
|
11260
|
+
const cmd = h.command;
|
|
11261
|
+
if (typeof cmd !== "string") return false;
|
|
11262
|
+
return cmd.includes("ctxloom") || cmd.includes(".claude/hooks/session-start.sh");
|
|
11263
|
+
});
|
|
11264
|
+
}
|
|
11265
|
+
function writeSkill(projectRoot, skill, opts) {
|
|
11266
|
+
const dir = safeJoin(projectRoot, `.claude/skills/${skill.name}`);
|
|
11267
|
+
const filePath = safeJoin(projectRoot, skillFilePath(skill.name));
|
|
11268
|
+
const existed = fs28.existsSync(filePath);
|
|
11269
|
+
let alreadyCorrect = false;
|
|
11270
|
+
if (existed) {
|
|
11271
|
+
if (fs28.readFileSync(filePath, "utf-8") === skill.content) {
|
|
11272
|
+
alreadyCorrect = true;
|
|
11273
|
+
}
|
|
11274
|
+
}
|
|
11275
|
+
if (!opts.dryRun && !alreadyCorrect) {
|
|
11276
|
+
fs28.mkdirSync(dir, { recursive: true });
|
|
11277
|
+
fs28.writeFileSync(filePath, skill.content, "utf-8");
|
|
11278
|
+
}
|
|
11279
|
+
return {
|
|
11280
|
+
path: filePath,
|
|
11281
|
+
created: !existed,
|
|
11282
|
+
updated: existed && !alreadyCorrect,
|
|
11283
|
+
alreadyCorrect,
|
|
11284
|
+
dryRun: opts.dryRun
|
|
11285
|
+
};
|
|
11286
|
+
}
|
|
11287
|
+
function writeSessionStartScript(projectRoot, opts) {
|
|
11288
|
+
const dir = safeJoin(projectRoot, ".claude/hooks");
|
|
11289
|
+
const filePath = safeJoin(projectRoot, ".claude/hooks/session-start.sh");
|
|
11290
|
+
const existed = fs28.existsSync(filePath);
|
|
11291
|
+
let alreadyCorrect = false;
|
|
11292
|
+
if (existed) {
|
|
11293
|
+
const current = fs28.readFileSync(filePath, "utf-8");
|
|
11294
|
+
if (current === SESSION_START_FULL) alreadyCorrect = true;
|
|
11295
|
+
}
|
|
11296
|
+
if (!opts.dryRun && !alreadyCorrect) {
|
|
11297
|
+
fs28.mkdirSync(dir, { recursive: true });
|
|
11298
|
+
fs28.writeFileSync(filePath, SESSION_START_FULL, "utf-8");
|
|
11299
|
+
try {
|
|
11300
|
+
fs28.chmodSync(filePath, 493);
|
|
11301
|
+
} catch {
|
|
11302
|
+
}
|
|
11303
|
+
}
|
|
11304
|
+
return {
|
|
11305
|
+
path: filePath,
|
|
11306
|
+
created: !existed,
|
|
11307
|
+
updated: existed && !alreadyCorrect,
|
|
11308
|
+
alreadyCorrect,
|
|
11309
|
+
dryRun: opts.dryRun
|
|
11310
|
+
};
|
|
11311
|
+
}
|
|
11312
|
+
|
|
9969
11313
|
export {
|
|
9970
11314
|
GRAMMAR_MANIFEST,
|
|
9971
11315
|
findGrammar,
|
|
@@ -10070,6 +11414,20 @@ export {
|
|
|
10070
11414
|
noParseableSourcesWarning,
|
|
10071
11415
|
wrapWithIndexingEnvelope,
|
|
10072
11416
|
FirstTouchTracker,
|
|
10073
|
-
EmittedOnceTracker
|
|
11417
|
+
EmittedOnceTracker,
|
|
11418
|
+
RULES_BLOCK_NAME,
|
|
11419
|
+
RULES_BLOCK_CONTENT,
|
|
11420
|
+
SESSION_START_FULL,
|
|
11421
|
+
CTXLOOM_HOOK_ENTRIES,
|
|
11422
|
+
DEFAULT_HMAC_KEY,
|
|
11423
|
+
resolveHmacKey,
|
|
11424
|
+
computeBlockHmac,
|
|
11425
|
+
wrapBlock,
|
|
11426
|
+
extractBlock,
|
|
11427
|
+
verifyBlock,
|
|
11428
|
+
upsertBlock,
|
|
11429
|
+
CTXLOOM_SKILLS,
|
|
11430
|
+
skillFilePath,
|
|
11431
|
+
installHarness
|
|
10074
11432
|
};
|
|
10075
|
-
//# sourceMappingURL=chunk-
|
|
11433
|
+
//# sourceMappingURL=chunk-7GZVGIQL.js.map
|