ctxloom-pro 1.3.1 → 1.5.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 +2 -2
- package/apps/dashboard/dist/server/index.js +260 -60
- package/dist/{chunk-TIYTPWYN.js → chunk-J2NLNQ4I.js} +1851 -143
- package/dist/index.js +44 -8
- package/dist/{src-KTFHRVTO.js → src-PVBWVQMM.js} +54 -2
- package/package.json +1 -1
|
@@ -6,7 +6,8 @@ import {
|
|
|
6
6
|
generateEmbedding
|
|
7
7
|
} from "./chunk-UVR65QBJ.js";
|
|
8
8
|
import {
|
|
9
|
-
diskSink
|
|
9
|
+
diskSink,
|
|
10
|
+
readEvents
|
|
10
11
|
} from "./chunk-5I6CJITG.js";
|
|
11
12
|
import {
|
|
12
13
|
logger
|
|
@@ -3460,8 +3461,8 @@ var CoChangeIndex = class _CoChangeIndex {
|
|
|
3460
3461
|
if (event.isBulk || event.isMerge) return;
|
|
3461
3462
|
const paths = event.files.map((f) => f.path);
|
|
3462
3463
|
if (paths.length === 0) return;
|
|
3463
|
-
for (const
|
|
3464
|
-
this.nodeCounts.set(
|
|
3464
|
+
for (const path37 of paths) {
|
|
3465
|
+
this.nodeCounts.set(path37, (this.nodeCounts.get(path37) ?? 0) + 1);
|
|
3465
3466
|
}
|
|
3466
3467
|
for (let i = 0; i < paths.length; i++) {
|
|
3467
3468
|
for (let j = i + 1; j < paths.length; j++) {
|
|
@@ -3608,8 +3609,8 @@ var ChurnIndex = class _ChurnIndex {
|
|
|
3608
3609
|
*/
|
|
3609
3610
|
snapshot() {
|
|
3610
3611
|
const nodes = {};
|
|
3611
|
-
for (const [
|
|
3612
|
-
nodes[
|
|
3612
|
+
for (const [path37, raw] of this.nodes) {
|
|
3613
|
+
nodes[path37] = {
|
|
3613
3614
|
commits: raw.commits,
|
|
3614
3615
|
churnLines: raw.churnLines,
|
|
3615
3616
|
bugCommits: raw.bugCommits,
|
|
@@ -3624,8 +3625,8 @@ var ChurnIndex = class _ChurnIndex {
|
|
|
3624
3625
|
*/
|
|
3625
3626
|
static load(s) {
|
|
3626
3627
|
const idx = new _ChurnIndex();
|
|
3627
|
-
for (const [
|
|
3628
|
-
idx.nodes.set(
|
|
3628
|
+
for (const [path37, raw] of Object.entries(s.nodes)) {
|
|
3629
|
+
idx.nodes.set(path37, {
|
|
3629
3630
|
commits: raw.commits,
|
|
3630
3631
|
churnLines: raw.churnLines,
|
|
3631
3632
|
bugCommits: raw.bugCommits,
|
|
@@ -3638,8 +3639,8 @@ var ChurnIndex = class _ChurnIndex {
|
|
|
3638
3639
|
// -------------------------------------------------------------------------
|
|
3639
3640
|
// Private helpers
|
|
3640
3641
|
// -------------------------------------------------------------------------
|
|
3641
|
-
getOrCreate(
|
|
3642
|
-
const existing = this.nodes.get(
|
|
3642
|
+
getOrCreate(path37) {
|
|
3643
|
+
const existing = this.nodes.get(path37);
|
|
3643
3644
|
if (existing !== void 0) return existing;
|
|
3644
3645
|
const fresh = {
|
|
3645
3646
|
commits: 0,
|
|
@@ -3648,7 +3649,7 @@ var ChurnIndex = class _ChurnIndex {
|
|
|
3648
3649
|
authorCounts: {},
|
|
3649
3650
|
lastTouch: 0
|
|
3650
3651
|
};
|
|
3651
|
-
this.nodes.set(
|
|
3652
|
+
this.nodes.set(path37, fresh);
|
|
3652
3653
|
return fresh;
|
|
3653
3654
|
}
|
|
3654
3655
|
};
|
|
@@ -3729,12 +3730,12 @@ var OwnershipIndex = class _OwnershipIndex {
|
|
|
3729
3730
|
*/
|
|
3730
3731
|
snapshot() {
|
|
3731
3732
|
const nodes = {};
|
|
3732
|
-
for (const [
|
|
3733
|
+
for (const [path37, raw] of this.nodes) {
|
|
3733
3734
|
const authorWeights = {};
|
|
3734
3735
|
for (const [email, entry] of Object.entries(raw.authorWeights)) {
|
|
3735
3736
|
authorWeights[email] = { ...entry };
|
|
3736
3737
|
}
|
|
3737
|
-
nodes[
|
|
3738
|
+
nodes[path37] = { authorWeights, lastTouch: raw.lastTouch };
|
|
3738
3739
|
}
|
|
3739
3740
|
return { version: 1, nodes };
|
|
3740
3741
|
}
|
|
@@ -3743,23 +3744,23 @@ var OwnershipIndex = class _OwnershipIndex {
|
|
|
3743
3744
|
*/
|
|
3744
3745
|
static load(s) {
|
|
3745
3746
|
const idx = new _OwnershipIndex();
|
|
3746
|
-
for (const [
|
|
3747
|
+
for (const [path37, raw] of Object.entries(s.nodes)) {
|
|
3747
3748
|
const authorWeights = {};
|
|
3748
3749
|
for (const [email, entry] of Object.entries(raw.authorWeights)) {
|
|
3749
3750
|
authorWeights[email] = { ...entry };
|
|
3750
3751
|
}
|
|
3751
|
-
idx.nodes.set(
|
|
3752
|
+
idx.nodes.set(path37, { authorWeights, lastTouch: raw.lastTouch });
|
|
3752
3753
|
}
|
|
3753
3754
|
return idx;
|
|
3754
3755
|
}
|
|
3755
3756
|
// -------------------------------------------------------------------------
|
|
3756
3757
|
// Private helpers
|
|
3757
3758
|
// -------------------------------------------------------------------------
|
|
3758
|
-
getOrCreate(
|
|
3759
|
-
const existing = this.nodes.get(
|
|
3759
|
+
getOrCreate(path37) {
|
|
3760
|
+
const existing = this.nodes.get(path37);
|
|
3760
3761
|
if (existing !== void 0) return existing;
|
|
3761
3762
|
const fresh = { authorWeights: {}, lastTouch: 0 };
|
|
3762
|
-
this.nodes.set(
|
|
3763
|
+
this.nodes.set(path37, fresh);
|
|
3763
3764
|
return fresh;
|
|
3764
3765
|
}
|
|
3765
3766
|
};
|
|
@@ -4565,7 +4566,7 @@ function renderStatusXml(input) {
|
|
|
4565
4566
|
return lines.join("\n");
|
|
4566
4567
|
}
|
|
4567
4568
|
function registerStatusTool(registry, ctx) {
|
|
4568
|
-
const
|
|
4569
|
+
const Schema31 = z2.object({ project_root: ProjectRootField });
|
|
4569
4570
|
registry.register(
|
|
4570
4571
|
"ctx_status",
|
|
4571
4572
|
{
|
|
@@ -4579,7 +4580,7 @@ function registerStatusTool(registry, ctx) {
|
|
|
4579
4580
|
}
|
|
4580
4581
|
},
|
|
4581
4582
|
async (args) => {
|
|
4582
|
-
const { project_root } =
|
|
4583
|
+
const { project_root } = Schema31.parse(args ?? {});
|
|
4583
4584
|
void project_root;
|
|
4584
4585
|
return renderStatusXml({
|
|
4585
4586
|
defaultRoot: ctx.noDefaultMode ? null : ctx.projectRoot,
|
|
@@ -4590,29 +4591,440 @@ function registerStatusTool(registry, ctx) {
|
|
|
4590
4591
|
);
|
|
4591
4592
|
}
|
|
4592
4593
|
|
|
4593
|
-
// packages/core/src/
|
|
4594
|
-
var
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4594
|
+
// packages/core/src/budget/learnedSuggestions.ts
|
|
4595
|
+
var DEFAULT_WINDOW_DAYS2 = 14;
|
|
4596
|
+
var SESSION_GAP_MS = 9e4;
|
|
4597
|
+
var MIN_SAMPLES_PER_PAIR = 3;
|
|
4598
|
+
var TOP_K = 3;
|
|
4599
|
+
var CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
4600
|
+
var _cache = null;
|
|
4601
|
+
function __resetLearnedSuggestionsCacheForTests() {
|
|
4602
|
+
_cache = null;
|
|
4603
|
+
}
|
|
4604
|
+
function clampTokens(n) {
|
|
4605
|
+
if (!Number.isFinite(n)) return 0;
|
|
4606
|
+
return Math.max(0, Math.min(1e5, Math.round(n)));
|
|
4607
|
+
}
|
|
4608
|
+
function learnSuggestionsFromTelemetry(opts = {}) {
|
|
4609
|
+
const windowDays = opts.windowDays ?? DEFAULT_WINDOW_DAYS2;
|
|
4610
|
+
const sessionGapMs = opts.sessionGapMs ?? SESSION_GAP_MS;
|
|
4611
|
+
const minSamples = opts.minSamples ?? MIN_SAMPLES_PER_PAIR;
|
|
4612
|
+
const allowlist = opts.registeredTools;
|
|
4613
|
+
const events = opts.events ?? safeReadEvents(windowDays);
|
|
4614
|
+
if (events.length === 0) return {};
|
|
4615
|
+
const sorted = events.map((e) => ({ ts: Date.parse(e.ts), tool: e.tool })).filter((e) => Number.isFinite(e.ts) && typeof e.tool === "string" && e.tool.length > 0).sort((a, b) => a.ts - b.ts);
|
|
4616
|
+
const transitions = /* @__PURE__ */ new Map();
|
|
4617
|
+
const tokenSums = /* @__PURE__ */ new Map();
|
|
4618
|
+
let prevTool = null;
|
|
4619
|
+
let prevTs = -Infinity;
|
|
4620
|
+
for (const e of sorted) {
|
|
4621
|
+
if (prevTool && e.tool !== prevTool && e.ts - prevTs <= sessionGapMs) {
|
|
4622
|
+
let row = transitions.get(prevTool);
|
|
4623
|
+
if (!row) {
|
|
4624
|
+
row = /* @__PURE__ */ new Map();
|
|
4625
|
+
transitions.set(prevTool, row);
|
|
4626
|
+
}
|
|
4627
|
+
row.set(e.tool, (row.get(e.tool) ?? 0) + 1);
|
|
4628
|
+
}
|
|
4629
|
+
prevTool = e.tool;
|
|
4630
|
+
prevTs = e.ts;
|
|
4631
|
+
}
|
|
4632
|
+
for (const raw of events) {
|
|
4633
|
+
const tok = raw.original_tokens;
|
|
4634
|
+
if (typeof tok === "number" && Number.isFinite(tok)) {
|
|
4635
|
+
const acc = tokenSums.get(raw.tool) ?? { sum: 0, n: 0 };
|
|
4636
|
+
acc.sum += tok;
|
|
4637
|
+
acc.n += 1;
|
|
4638
|
+
tokenSums.set(raw.tool, acc);
|
|
4639
|
+
}
|
|
4598
4640
|
}
|
|
4599
|
-
|
|
4600
|
-
|
|
4641
|
+
const out = {};
|
|
4642
|
+
for (const [from, row] of transitions) {
|
|
4643
|
+
if (allowlist && !allowlist.has(from)) continue;
|
|
4644
|
+
const candidates = [];
|
|
4645
|
+
for (const [to, count] of row) {
|
|
4646
|
+
if (count < minSamples) continue;
|
|
4647
|
+
if (allowlist && !allowlist.has(to)) continue;
|
|
4648
|
+
const tokenAcc = tokenSums.get(to);
|
|
4649
|
+
const avgTokens = tokenAcc && tokenAcc.n > 0 ? tokenAcc.sum / tokenAcc.n : 0;
|
|
4650
|
+
candidates.push({
|
|
4651
|
+
tool: to,
|
|
4652
|
+
why: `Learned from telemetry: ${count} agents followed ${from} with ${to}.`,
|
|
4653
|
+
estimated_tokens: clampTokens(avgTokens)
|
|
4654
|
+
});
|
|
4655
|
+
}
|
|
4656
|
+
if (candidates.length === 0) continue;
|
|
4657
|
+
candidates.sort((a, b) => {
|
|
4658
|
+
const matchA = a.why.match(/(\d+) agents/);
|
|
4659
|
+
const matchB = b.why.match(/(\d+) agents/);
|
|
4660
|
+
const ca = matchA ? parseInt(matchA[1], 10) : 0;
|
|
4661
|
+
const cb = matchB ? parseInt(matchB[1], 10) : 0;
|
|
4662
|
+
return cb - ca;
|
|
4663
|
+
});
|
|
4664
|
+
out[from] = candidates.slice(0, TOP_K);
|
|
4601
4665
|
}
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4666
|
+
return out;
|
|
4667
|
+
}
|
|
4668
|
+
function safeReadEvents(windowDays) {
|
|
4669
|
+
try {
|
|
4670
|
+
const until = /* @__PURE__ */ new Date();
|
|
4671
|
+
const since = new Date(until.getTime() - windowDays * 24 * 60 * 60 * 1e3);
|
|
4672
|
+
return readEvents({ since, until });
|
|
4673
|
+
} catch {
|
|
4674
|
+
return [];
|
|
4608
4675
|
}
|
|
4609
|
-
|
|
4610
|
-
|
|
4676
|
+
}
|
|
4677
|
+
function getLearnedRules(opts = {}) {
|
|
4678
|
+
if (_cache && _cache.expiresAt > Date.now()) {
|
|
4679
|
+
return filterRulesByAllowlist(_cache.rules, opts.registeredTools);
|
|
4611
4680
|
}
|
|
4612
|
-
|
|
4681
|
+
const unfilteredRules = learnSuggestionsFromTelemetry({
|
|
4682
|
+
...opts,
|
|
4683
|
+
registeredTools: void 0
|
|
4684
|
+
});
|
|
4685
|
+
_cache = { rules: unfilteredRules, expiresAt: Date.now() + CACHE_TTL_MS };
|
|
4686
|
+
return filterRulesByAllowlist(unfilteredRules, opts.registeredTools);
|
|
4687
|
+
}
|
|
4688
|
+
function filterRulesByAllowlist(rules, allowlist) {
|
|
4689
|
+
if (!allowlist) return rules;
|
|
4690
|
+
const out = {};
|
|
4691
|
+
for (const [from, suggestions] of Object.entries(rules)) {
|
|
4692
|
+
if (!allowlist.has(from)) continue;
|
|
4693
|
+
const kept = suggestions.filter((s) => allowlist.has(s.tool));
|
|
4694
|
+
if (kept.length > 0) out[from] = kept;
|
|
4695
|
+
}
|
|
4696
|
+
return out;
|
|
4697
|
+
}
|
|
4613
4698
|
|
|
4614
|
-
// packages/core/src/
|
|
4615
|
-
|
|
4699
|
+
// packages/core/src/budget/nextToolSuggestions.ts
|
|
4700
|
+
var STATIC_RULES = {
|
|
4701
|
+
// ─── Source-returning / file-shaped tools ────────────────────────
|
|
4702
|
+
ctx_get_file: [
|
|
4703
|
+
{
|
|
4704
|
+
tool: "ctx_get_call_graph",
|
|
4705
|
+
why: "Check who depends on this file before modifying.",
|
|
4706
|
+
estimated_tokens: 800
|
|
4707
|
+
},
|
|
4708
|
+
{
|
|
4709
|
+
tool: "ctx_get_definition",
|
|
4710
|
+
why: "Cheaper view if you need a specific symbol, not the whole file.",
|
|
4711
|
+
estimated_tokens: 2e3
|
|
4712
|
+
},
|
|
4713
|
+
{
|
|
4714
|
+
tool: "ctx_blast_radius",
|
|
4715
|
+
why: "Transitive impact analysis before a write.",
|
|
4716
|
+
estimated_tokens: 1500
|
|
4717
|
+
}
|
|
4718
|
+
],
|
|
4719
|
+
ctx_get_definition: [
|
|
4720
|
+
{
|
|
4721
|
+
tool: "ctx_get_call_graph",
|
|
4722
|
+
why: "Who calls this symbol? Almost always your next step.",
|
|
4723
|
+
estimated_tokens: 800
|
|
4724
|
+
},
|
|
4725
|
+
{
|
|
4726
|
+
tool: "ctx_blast_radius",
|
|
4727
|
+
why: "What would break if this signature changes?",
|
|
4728
|
+
estimated_tokens: 1500
|
|
4729
|
+
}
|
|
4730
|
+
],
|
|
4731
|
+
ctx_get_context_packet: [
|
|
4732
|
+
{
|
|
4733
|
+
tool: "ctx_get_call_graph",
|
|
4734
|
+
why: "Surface external callers not visible inside the packet.",
|
|
4735
|
+
estimated_tokens: 800
|
|
4736
|
+
},
|
|
4737
|
+
{
|
|
4738
|
+
tool: "ctx_get_affected_flows",
|
|
4739
|
+
why: "Execution-flow coverage of the packet files.",
|
|
4740
|
+
estimated_tokens: 2e3
|
|
4741
|
+
}
|
|
4742
|
+
],
|
|
4743
|
+
ctx_search: [
|
|
4744
|
+
{
|
|
4745
|
+
tool: "ctx_get_definition",
|
|
4746
|
+
why: "Pull the canonical definition of a top result.",
|
|
4747
|
+
estimated_tokens: 2e3
|
|
4748
|
+
},
|
|
4749
|
+
{
|
|
4750
|
+
tool: "ctx_similar_files",
|
|
4751
|
+
why: "Find related files outside the keyword/vector hit set.",
|
|
4752
|
+
estimated_tokens: 1e3
|
|
4753
|
+
}
|
|
4754
|
+
],
|
|
4755
|
+
ctx_full_text_search: [
|
|
4756
|
+
{
|
|
4757
|
+
tool: "ctx_get_file",
|
|
4758
|
+
why: "Inspect a specific match in context.",
|
|
4759
|
+
estimated_tokens: 8e3
|
|
4760
|
+
},
|
|
4761
|
+
{
|
|
4762
|
+
tool: "ctx_get_call_graph",
|
|
4763
|
+
why: "Caller graph for matched symbols.",
|
|
4764
|
+
estimated_tokens: 800
|
|
4765
|
+
}
|
|
4766
|
+
],
|
|
4767
|
+
ctx_similar_files: [
|
|
4768
|
+
{
|
|
4769
|
+
tool: "ctx_get_context_packet",
|
|
4770
|
+
why: "Bundle the cluster into a single packet for review.",
|
|
4771
|
+
estimated_tokens: 6e3
|
|
4772
|
+
}
|
|
4773
|
+
],
|
|
4774
|
+
// ─── Graph / structural queries ──────────────────────────────────
|
|
4775
|
+
// ctx_get_call_graph is the canonical "find callers" tool — pass
|
|
4776
|
+
// direction: 'callers' or 'callees' in args. The follow-ups below
|
|
4777
|
+
// assume the caller is investigating impact / coverage on the
|
|
4778
|
+
// returned caller set.
|
|
4779
|
+
ctx_get_call_graph: [
|
|
4780
|
+
{
|
|
4781
|
+
tool: "ctx_blast_radius",
|
|
4782
|
+
why: "Transitive dependents \u2014 callers of the callers you just found.",
|
|
4783
|
+
estimated_tokens: 1500
|
|
4784
|
+
},
|
|
4785
|
+
{
|
|
4786
|
+
tool: "ctx_get_affected_flows",
|
|
4787
|
+
why: "Which execution paths break if any caller is removed?",
|
|
4788
|
+
estimated_tokens: 2e3
|
|
4789
|
+
},
|
|
4790
|
+
{
|
|
4791
|
+
tool: "ctx_execution_flow",
|
|
4792
|
+
why: "Linearize the caller set into ordered execution sequences.",
|
|
4793
|
+
estimated_tokens: 4e3
|
|
4794
|
+
}
|
|
4795
|
+
],
|
|
4796
|
+
ctx_blast_radius: [
|
|
4797
|
+
{
|
|
4798
|
+
tool: "ctx_get_affected_flows",
|
|
4799
|
+
why: "Execution-flow impact on the affected files.",
|
|
4800
|
+
estimated_tokens: 2e3
|
|
4801
|
+
},
|
|
4802
|
+
{ tool: "ctx_knowledge_gaps", why: "Identify test-coverage gaps on affected files.", estimated_tokens: 1200 }
|
|
4803
|
+
],
|
|
4804
|
+
ctx_get_affected_flows: [
|
|
4805
|
+
{
|
|
4806
|
+
tool: "ctx_execution_flow",
|
|
4807
|
+
why: "Drill into a specific affected flow.",
|
|
4808
|
+
estimated_tokens: 4e3
|
|
4809
|
+
},
|
|
4810
|
+
{
|
|
4811
|
+
tool: "ctx_blast_radius",
|
|
4812
|
+
why: "Reverse direction \u2014 what affects this flow?",
|
|
4813
|
+
estimated_tokens: 1500
|
|
4814
|
+
}
|
|
4815
|
+
],
|
|
4816
|
+
ctx_execution_flow: [
|
|
4817
|
+
{
|
|
4818
|
+
tool: "ctx_get_call_graph",
|
|
4819
|
+
why: "External callers of the flow entry-point.",
|
|
4820
|
+
estimated_tokens: 800
|
|
4821
|
+
}
|
|
4822
|
+
],
|
|
4823
|
+
// ─── Architecture / overview tools ───────────────────────────────
|
|
4824
|
+
ctx_architecture_overview: [
|
|
4825
|
+
{
|
|
4826
|
+
tool: "ctx_community_list",
|
|
4827
|
+
why: "Drill into a specific community.",
|
|
4828
|
+
estimated_tokens: 1e3
|
|
4829
|
+
},
|
|
4830
|
+
{
|
|
4831
|
+
tool: "ctx_hub_nodes",
|
|
4832
|
+
why: "Top fan-in/out nodes deserving deeper inspection.",
|
|
4833
|
+
estimated_tokens: 1200
|
|
4834
|
+
},
|
|
4835
|
+
{
|
|
4836
|
+
tool: "ctx_bridge_nodes",
|
|
4837
|
+
why: "Cross-community bridges (high architectural leverage).",
|
|
4838
|
+
estimated_tokens: 1e3
|
|
4839
|
+
}
|
|
4840
|
+
],
|
|
4841
|
+
ctx_community_list: [
|
|
4842
|
+
{
|
|
4843
|
+
tool: "ctx_get_context_packet",
|
|
4844
|
+
why: "Bundle a community into a single review packet.",
|
|
4845
|
+
estimated_tokens: 6e3
|
|
4846
|
+
}
|
|
4847
|
+
],
|
|
4848
|
+
ctx_hub_nodes: [
|
|
4849
|
+
{
|
|
4850
|
+
tool: "ctx_get_call_graph",
|
|
4851
|
+
why: "Who depends on the top hub?",
|
|
4852
|
+
estimated_tokens: 800
|
|
4853
|
+
},
|
|
4854
|
+
{
|
|
4855
|
+
tool: "ctx_blast_radius",
|
|
4856
|
+
why: "Hub change-impact analysis.",
|
|
4857
|
+
estimated_tokens: 1500
|
|
4858
|
+
}
|
|
4859
|
+
],
|
|
4860
|
+
ctx_bridge_nodes: [
|
|
4861
|
+
{
|
|
4862
|
+
tool: "ctx_get_call_graph",
|
|
4863
|
+
why: "Callers across the bridge.",
|
|
4864
|
+
estimated_tokens: 800
|
|
4865
|
+
}
|
|
4866
|
+
],
|
|
4867
|
+
ctx_surprising_connections: [
|
|
4868
|
+
{
|
|
4869
|
+
tool: "ctx_blast_radius",
|
|
4870
|
+
why: "Impact analysis on a surprising-connection target.",
|
|
4871
|
+
estimated_tokens: 1500
|
|
4872
|
+
}
|
|
4873
|
+
],
|
|
4874
|
+
// ─── Review / diff tools ─────────────────────────────────────────
|
|
4875
|
+
ctx_detect_changes: [
|
|
4876
|
+
{
|
|
4877
|
+
tool: "ctx_get_file",
|
|
4878
|
+
why: "Inspect a specific risky file.",
|
|
4879
|
+
estimated_tokens: 8e3
|
|
4880
|
+
},
|
|
4881
|
+
{
|
|
4882
|
+
tool: "ctx_get_affected_flows",
|
|
4883
|
+
why: "Which execution paths the change touches.",
|
|
4884
|
+
estimated_tokens: 2e3
|
|
4885
|
+
},
|
|
4886
|
+
{
|
|
4887
|
+
tool: "ctx_git_diff_review",
|
|
4888
|
+
why: "Full diff packet for the changeset.",
|
|
4889
|
+
estimated_tokens: 8e3
|
|
4890
|
+
}
|
|
4891
|
+
],
|
|
4892
|
+
ctx_git_diff_review: [
|
|
4893
|
+
{
|
|
4894
|
+
tool: "ctx_risk_overlay",
|
|
4895
|
+
why: "Score the changed files by historical churn + coupling.",
|
|
4896
|
+
estimated_tokens: 1500
|
|
4897
|
+
},
|
|
4898
|
+
{
|
|
4899
|
+
tool: "ctx_get_call_graph",
|
|
4900
|
+
why: "Caller-side impact of the changes.",
|
|
4901
|
+
estimated_tokens: 800
|
|
4902
|
+
}
|
|
4903
|
+
],
|
|
4904
|
+
ctx_risk_overlay: [
|
|
4905
|
+
{
|
|
4906
|
+
tool: "ctx_get_file",
|
|
4907
|
+
why: "Inspect the highest-risk file in detail.",
|
|
4908
|
+
estimated_tokens: 8e3
|
|
4909
|
+
}
|
|
4910
|
+
],
|
|
4911
|
+
ctx_git_coupling: [
|
|
4912
|
+
{
|
|
4913
|
+
tool: "ctx_blast_radius",
|
|
4914
|
+
why: "Static impact analysis to complement co-change signal.",
|
|
4915
|
+
estimated_tokens: 1500
|
|
4916
|
+
}
|
|
4917
|
+
],
|
|
4918
|
+
// ─── Refactor tools ──────────────────────────────────────────────
|
|
4919
|
+
ctx_refactor_preview: [
|
|
4920
|
+
{
|
|
4921
|
+
tool: "ctx_apply_refactor",
|
|
4922
|
+
why: "Commit the preview after you reviewed the rename.",
|
|
4923
|
+
estimated_tokens: 2e3
|
|
4924
|
+
}
|
|
4925
|
+
],
|
|
4926
|
+
ctx_apply_refactor: [
|
|
4927
|
+
{
|
|
4928
|
+
tool: "ctx_detect_changes",
|
|
4929
|
+
why: "Verify the refactor produced the expected risk profile.",
|
|
4930
|
+
estimated_tokens: 1500
|
|
4931
|
+
}
|
|
4932
|
+
],
|
|
4933
|
+
// ─── Knowledge / coverage tools ──────────────────────────────────
|
|
4934
|
+
ctx_knowledge_gaps: [
|
|
4935
|
+
{ tool: "ctx_knowledge_gaps", why: "Identify test-coverage gaps on affected files.", estimated_tokens: 1200 },
|
|
4936
|
+
{
|
|
4937
|
+
tool: "ctx_get_call_graph",
|
|
4938
|
+
why: "Caller frequency on untested symbols (impact ranking).",
|
|
4939
|
+
estimated_tokens: 800
|
|
4940
|
+
}
|
|
4941
|
+
],
|
|
4942
|
+
ctx_find_large_functions: [
|
|
4943
|
+
{
|
|
4944
|
+
tool: "ctx_get_definition",
|
|
4945
|
+
why: "Inspect the largest function.",
|
|
4946
|
+
estimated_tokens: 2e3
|
|
4947
|
+
}
|
|
4948
|
+
],
|
|
4949
|
+
// ─── Metadata / status ───────────────────────────────────────────
|
|
4950
|
+
ctx_get_minimal_context: [
|
|
4951
|
+
// intentionally empty — the suggested_first_tool field IS the
|
|
4952
|
+
// next-step suggestion. Adding rules here would be redundant.
|
|
4953
|
+
],
|
|
4954
|
+
ctx_status: [],
|
|
4955
|
+
ctx_get_rules: [
|
|
4956
|
+
{
|
|
4957
|
+
tool: "ctx_rules_check",
|
|
4958
|
+
why: "Validate code against rules.",
|
|
4959
|
+
estimated_tokens: 1200
|
|
4960
|
+
}
|
|
4961
|
+
],
|
|
4962
|
+
ctx_rules_check: [],
|
|
4963
|
+
ctx_get_workflow: [],
|
|
4964
|
+
ctx_suggested_questions: [],
|
|
4965
|
+
// ─── Wiki / export ───────────────────────────────────────────────
|
|
4966
|
+
ctx_wiki_generate: [
|
|
4967
|
+
{
|
|
4968
|
+
tool: "ctx_architecture_overview",
|
|
4969
|
+
why: "Confirm the wiki structure against the live overview.",
|
|
4970
|
+
estimated_tokens: 2e3
|
|
4971
|
+
}
|
|
4972
|
+
],
|
|
4973
|
+
ctx_graph_export: [],
|
|
4974
|
+
ctx_graph_snapshot: [
|
|
4975
|
+
{
|
|
4976
|
+
tool: "ctx_graph_diff",
|
|
4977
|
+
why: "Compare against a later snapshot.",
|
|
4978
|
+
estimated_tokens: 2e3
|
|
4979
|
+
}
|
|
4980
|
+
],
|
|
4981
|
+
ctx_graph_diff: [
|
|
4982
|
+
{
|
|
4983
|
+
tool: "ctx_detect_changes",
|
|
4984
|
+
why: "Risk-scored view of the graph delta.",
|
|
4985
|
+
estimated_tokens: 1500
|
|
4986
|
+
}
|
|
4987
|
+
],
|
|
4988
|
+
// ─── Cross-repo ──────────────────────────────────────────────────
|
|
4989
|
+
ctx_cross_repo_search: [
|
|
4990
|
+
{
|
|
4991
|
+
tool: "ctx_get_file",
|
|
4992
|
+
why: "Inspect a hit in a specific repo.",
|
|
4993
|
+
estimated_tokens: 8e3
|
|
4994
|
+
}
|
|
4995
|
+
]
|
|
4996
|
+
// ─── Query primitive ─────────────────────────────────────────────
|
|
4997
|
+
// ─── Affected flows (sibling to get_affected_flows but worth pinning) ─
|
|
4998
|
+
// ─── Definition aliases / structurals not listed above are deliberate
|
|
4999
|
+
// — the test enforces the full registered-tool list is covered.
|
|
5000
|
+
};
|
|
5001
|
+
var TOKEN_ESTIMATE_MIN = 0;
|
|
5002
|
+
var TOKEN_ESTIMATE_MAX = 1e5;
|
|
5003
|
+
function clampEstimate(n) {
|
|
5004
|
+
if (!Number.isFinite(n)) return 0;
|
|
5005
|
+
return Math.max(TOKEN_ESTIMATE_MIN, Math.min(TOKEN_ESTIMATE_MAX, Math.round(n)));
|
|
5006
|
+
}
|
|
5007
|
+
function suggestNext(fromTool, registeredTools) {
|
|
5008
|
+
if (process.env.CTXLOOM_LEARNED_SUGGESTIONS === "1") {
|
|
5009
|
+
const learned = getLearnedRules({ registeredTools })[fromTool];
|
|
5010
|
+
if (learned && learned.length > 0) {
|
|
5011
|
+
return learned.slice(0, 3).map((s) => ({
|
|
5012
|
+
tool: s.tool,
|
|
5013
|
+
args: s.args,
|
|
5014
|
+
why: s.why,
|
|
5015
|
+
estimated_tokens: clampEstimate(s.estimated_tokens)
|
|
5016
|
+
}));
|
|
5017
|
+
}
|
|
5018
|
+
}
|
|
5019
|
+
const raw = STATIC_RULES[fromTool] ?? [];
|
|
5020
|
+
const filtered = registeredTools ? raw.filter((s) => registeredTools.has(s.tool)) : raw;
|
|
5021
|
+
return filtered.slice(0, 3).map((s) => ({
|
|
5022
|
+
tool: s.tool,
|
|
5023
|
+
args: s.args,
|
|
5024
|
+
why: s.why,
|
|
5025
|
+
estimated_tokens: clampEstimate(s.estimated_tokens)
|
|
5026
|
+
}));
|
|
5027
|
+
}
|
|
4616
5028
|
|
|
4617
5029
|
// packages/core/src/budget/budget.ts
|
|
4618
5030
|
var defaultTokenEstimator = (text) => Math.ceil(text.length / 4);
|
|
@@ -4649,16 +5061,21 @@ async function enforceBudget(opts) {
|
|
|
4649
5061
|
const { full, args, toolName, defaultMaxTokens, skeletonProducer } = opts;
|
|
4650
5062
|
const estimate = opts.estimator ?? defaultTokenEstimator;
|
|
4651
5063
|
const sink = opts.sink ?? opts.ctx?.telemetrySink ?? diskSink;
|
|
5064
|
+
const nextToolSuggestions = suggestNext(toolName);
|
|
5065
|
+
const withSuggestions = (base) => {
|
|
5066
|
+
if (nextToolSuggestions.length === 0) return base;
|
|
5067
|
+
return { ...base, next_tool_suggestions: nextToolSuggestions };
|
|
5068
|
+
};
|
|
4652
5069
|
const originalTokens = estimate(full);
|
|
4653
5070
|
if (isBudgetDisabled()) {
|
|
4654
5071
|
return {
|
|
4655
5072
|
text: full,
|
|
4656
|
-
meta: {
|
|
5073
|
+
meta: withSuggestions({
|
|
4657
5074
|
format: "full",
|
|
4658
5075
|
original_tokens_est: originalTokens,
|
|
4659
5076
|
returned_tokens_est: originalTokens,
|
|
4660
5077
|
fallback_reason: null
|
|
4661
|
-
}
|
|
5078
|
+
})
|
|
4662
5079
|
};
|
|
4663
5080
|
}
|
|
4664
5081
|
if (args.response_format === "skeleton" && skeletonProducer) {
|
|
@@ -4667,45 +5084,45 @@ async function enforceBudget(opts) {
|
|
|
4667
5084
|
const skTokens = estimate(skeleton2);
|
|
4668
5085
|
return {
|
|
4669
5086
|
text: skeleton2,
|
|
4670
|
-
meta: {
|
|
5087
|
+
meta: withSuggestions({
|
|
4671
5088
|
format: "skeleton",
|
|
4672
5089
|
original_tokens_est: originalTokens,
|
|
4673
5090
|
returned_tokens_est: skTokens,
|
|
4674
5091
|
fallback_reason: null
|
|
4675
|
-
}
|
|
5092
|
+
})
|
|
4676
5093
|
};
|
|
4677
5094
|
}
|
|
4678
5095
|
return {
|
|
4679
5096
|
text: full,
|
|
4680
|
-
meta: {
|
|
5097
|
+
meta: withSuggestions({
|
|
4681
5098
|
format: "full",
|
|
4682
5099
|
original_tokens_est: originalTokens,
|
|
4683
5100
|
returned_tokens_est: originalTokens,
|
|
4684
5101
|
fallback_reason: "skeleton_failed"
|
|
4685
|
-
}
|
|
5102
|
+
})
|
|
4686
5103
|
};
|
|
4687
5104
|
}
|
|
4688
5105
|
const budget = args.max_response_tokens ?? defaultMaxTokens;
|
|
4689
5106
|
if (budget === void 0) {
|
|
4690
5107
|
return {
|
|
4691
5108
|
text: full,
|
|
4692
|
-
meta: {
|
|
5109
|
+
meta: withSuggestions({
|
|
4693
5110
|
format: "full",
|
|
4694
5111
|
original_tokens_est: originalTokens,
|
|
4695
5112
|
returned_tokens_est: originalTokens,
|
|
4696
5113
|
fallback_reason: null
|
|
4697
|
-
}
|
|
5114
|
+
})
|
|
4698
5115
|
};
|
|
4699
5116
|
}
|
|
4700
5117
|
if (originalTokens <= budget) {
|
|
4701
5118
|
return {
|
|
4702
5119
|
text: full,
|
|
4703
|
-
meta: {
|
|
5120
|
+
meta: withSuggestions({
|
|
4704
5121
|
format: "full",
|
|
4705
5122
|
original_tokens_est: originalTokens,
|
|
4706
5123
|
returned_tokens_est: originalTokens,
|
|
4707
5124
|
fallback_reason: null
|
|
4708
|
-
}
|
|
5125
|
+
})
|
|
4709
5126
|
};
|
|
4710
5127
|
}
|
|
4711
5128
|
emitTelemetry({
|
|
@@ -4736,12 +5153,12 @@ async function enforceBudget(opts) {
|
|
|
4736
5153
|
}, sink);
|
|
4737
5154
|
return {
|
|
4738
5155
|
text: sliced2,
|
|
4739
|
-
meta: {
|
|
5156
|
+
meta: withSuggestions({
|
|
4740
5157
|
format: "truncated",
|
|
4741
5158
|
original_tokens_est: originalTokens,
|
|
4742
5159
|
returned_tokens_est: slicedTokens,
|
|
4743
5160
|
fallback_reason: "budget_exceeded"
|
|
4744
|
-
}
|
|
5161
|
+
})
|
|
4745
5162
|
};
|
|
4746
5163
|
}
|
|
4747
5164
|
const skeleton = skeletonProducer ? await safeSkeleton(skeletonProducer, toolName) : null;
|
|
@@ -4756,12 +5173,12 @@ async function enforceBudget(opts) {
|
|
|
4756
5173
|
}, sink);
|
|
4757
5174
|
return {
|
|
4758
5175
|
text: skeleton,
|
|
4759
|
-
meta: {
|
|
5176
|
+
meta: withSuggestions({
|
|
4760
5177
|
format: "skeleton",
|
|
4761
5178
|
original_tokens_est: originalTokens,
|
|
4762
5179
|
returned_tokens_est: skTokens,
|
|
4763
5180
|
fallback_reason: "budget_exceeded"
|
|
4764
|
-
}
|
|
5181
|
+
})
|
|
4765
5182
|
};
|
|
4766
5183
|
}
|
|
4767
5184
|
const slicedSk = skeleton.slice(0, budget * 4);
|
|
@@ -4773,12 +5190,12 @@ async function enforceBudget(opts) {
|
|
|
4773
5190
|
}, sink);
|
|
4774
5191
|
return {
|
|
4775
5192
|
text: slicedSk,
|
|
4776
|
-
meta: {
|
|
5193
|
+
meta: withSuggestions({
|
|
4777
5194
|
format: "truncated",
|
|
4778
5195
|
original_tokens_est: originalTokens,
|
|
4779
5196
|
returned_tokens_est: estimate(slicedSk),
|
|
4780
5197
|
fallback_reason: "budget_exceeded"
|
|
4781
|
-
}
|
|
5198
|
+
})
|
|
4782
5199
|
};
|
|
4783
5200
|
}
|
|
4784
5201
|
const sliced = full.slice(0, budget * 4);
|
|
@@ -4790,12 +5207,12 @@ async function enforceBudget(opts) {
|
|
|
4790
5207
|
}, sink);
|
|
4791
5208
|
return {
|
|
4792
5209
|
text: sliced,
|
|
4793
|
-
meta: {
|
|
5210
|
+
meta: withSuggestions({
|
|
4794
5211
|
format: "truncated",
|
|
4795
5212
|
original_tokens_est: originalTokens,
|
|
4796
5213
|
returned_tokens_est: estimate(sliced),
|
|
4797
5214
|
fallback_reason: "skeleton_failed"
|
|
4798
|
-
}
|
|
5215
|
+
})
|
|
4799
5216
|
};
|
|
4800
5217
|
}
|
|
4801
5218
|
async function safeSkeleton(producer, toolName) {
|
|
@@ -4817,7 +5234,159 @@ function wrapResponse(result) {
|
|
|
4817
5234
|
return JSON.stringify(envelope);
|
|
4818
5235
|
}
|
|
4819
5236
|
|
|
5237
|
+
// packages/core/src/budget/taskBudget.ts
|
|
5238
|
+
var DEFAULT_MAX_CALLS = 8;
|
|
5239
|
+
var DEFAULT_RESET_GAP_MS = 9e4;
|
|
5240
|
+
var ENV_VAR = "CTXLOOM_TASK_TOOL_BUDGET";
|
|
5241
|
+
function parseEnvBudget() {
|
|
5242
|
+
const raw = process.env[ENV_VAR];
|
|
5243
|
+
if (!raw) return null;
|
|
5244
|
+
const n = parseInt(raw, 10);
|
|
5245
|
+
if (!Number.isFinite(n) || n <= 0) return null;
|
|
5246
|
+
return n;
|
|
5247
|
+
}
|
|
5248
|
+
var TaskBudgetTracker = class {
|
|
5249
|
+
state = /* @__PURE__ */ new Map();
|
|
5250
|
+
maxCalls;
|
|
5251
|
+
resetGapMs;
|
|
5252
|
+
constructor(opts = {}) {
|
|
5253
|
+
this.maxCalls = opts.maxCalls ?? parseEnvBudget() ?? DEFAULT_MAX_CALLS;
|
|
5254
|
+
this.resetGapMs = opts.resetGapMs ?? DEFAULT_RESET_GAP_MS;
|
|
5255
|
+
}
|
|
5256
|
+
/**
|
|
5257
|
+
* Record a tool call against the budget. Returns the enforcement
|
|
5258
|
+
* decision the dispatch layer should act on.
|
|
5259
|
+
*
|
|
5260
|
+
* @param sessionId — opaque session identifier. Currently a
|
|
5261
|
+
* single global key ('process'); reserved for future multi-
|
|
5262
|
+
* session enforcement.
|
|
5263
|
+
* @param now — milliseconds since epoch. Test hook; defaults to
|
|
5264
|
+
* `Date.now()`.
|
|
5265
|
+
*/
|
|
5266
|
+
recordCall(sessionId = "process", now = Date.now()) {
|
|
5267
|
+
if (isBudgetDisabled()) {
|
|
5268
|
+
return {
|
|
5269
|
+
overBudget: false,
|
|
5270
|
+
callCount: 0,
|
|
5271
|
+
maxCalls: this.maxCalls,
|
|
5272
|
+
firstBreach: false
|
|
5273
|
+
};
|
|
5274
|
+
}
|
|
5275
|
+
const existing = this.state.get(sessionId);
|
|
5276
|
+
if (existing && now - existing.lastCallTs > this.resetGapMs) {
|
|
5277
|
+
const fresh = { count: 1, lastCallTs: now, breachEmitted: false };
|
|
5278
|
+
this.state.set(sessionId, fresh);
|
|
5279
|
+
return {
|
|
5280
|
+
overBudget: false,
|
|
5281
|
+
callCount: 1,
|
|
5282
|
+
maxCalls: this.maxCalls,
|
|
5283
|
+
firstBreach: false
|
|
5284
|
+
};
|
|
5285
|
+
}
|
|
5286
|
+
const next = (existing?.count ?? 0) + 1;
|
|
5287
|
+
const wasBreached = existing?.breachEmitted ?? false;
|
|
5288
|
+
const overBudget = next > this.maxCalls;
|
|
5289
|
+
const firstBreach = overBudget && !wasBreached;
|
|
5290
|
+
this.state.set(sessionId, {
|
|
5291
|
+
count: next,
|
|
5292
|
+
lastCallTs: now,
|
|
5293
|
+
breachEmitted: wasBreached || firstBreach
|
|
5294
|
+
});
|
|
5295
|
+
return { overBudget, callCount: next, maxCalls: this.maxCalls, firstBreach };
|
|
5296
|
+
}
|
|
5297
|
+
/**
|
|
5298
|
+
* Test-only: drop all state. Lets tests run in isolation without
|
|
5299
|
+
* depending on order.
|
|
5300
|
+
*
|
|
5301
|
+
* @internal
|
|
5302
|
+
*/
|
|
5303
|
+
reset() {
|
|
5304
|
+
this.state.clear();
|
|
5305
|
+
}
|
|
5306
|
+
/**
|
|
5307
|
+
* Test/diagnostic — current call count for the default session.
|
|
5308
|
+
* @internal
|
|
5309
|
+
*/
|
|
5310
|
+
__getCount(sessionId = "process") {
|
|
5311
|
+
return this.state.get(sessionId)?.count ?? 0;
|
|
5312
|
+
}
|
|
5313
|
+
};
|
|
5314
|
+
var _singleton = null;
|
|
5315
|
+
function getTaskBudgetTracker() {
|
|
5316
|
+
if (!_singleton) _singleton = new TaskBudgetTracker();
|
|
5317
|
+
return _singleton;
|
|
5318
|
+
}
|
|
5319
|
+
function __resetTaskBudgetTrackerForTests() {
|
|
5320
|
+
_singleton = null;
|
|
5321
|
+
}
|
|
5322
|
+
var OVER_BUDGET_ARG_OVERRIDES = Object.freeze({
|
|
5323
|
+
// Budget-surface tools (the 12 source-returning ones).
|
|
5324
|
+
max_response_tokens: 200,
|
|
5325
|
+
response_format: "skeleton",
|
|
5326
|
+
on_budget_exceeded: "skeleton",
|
|
5327
|
+
// Tools with detail_level (hub_nodes, bridge_nodes, etc).
|
|
5328
|
+
detail_level: "minimal"
|
|
5329
|
+
});
|
|
5330
|
+
function applyOverBudgetOverrides(args) {
|
|
5331
|
+
if (!args || typeof args !== "object") {
|
|
5332
|
+
return { ...OVER_BUDGET_ARG_OVERRIDES };
|
|
5333
|
+
}
|
|
5334
|
+
return { ...args, ...OVER_BUDGET_ARG_OVERRIDES };
|
|
5335
|
+
}
|
|
5336
|
+
function emitTaskBudgetBreached(toolName, callCount, maxCalls) {
|
|
5337
|
+
emitTelemetry({
|
|
5338
|
+
event: "mcp.task_budget.exceeded",
|
|
5339
|
+
tool: toolName,
|
|
5340
|
+
calls: callCount,
|
|
5341
|
+
budget: maxCalls
|
|
5342
|
+
});
|
|
5343
|
+
}
|
|
5344
|
+
|
|
5345
|
+
// packages/core/src/tools/registry.ts
|
|
5346
|
+
var TASK_BUDGET_EXEMPT = /* @__PURE__ */ new Set([
|
|
5347
|
+
"ctx_get_minimal_context",
|
|
5348
|
+
"ctx_status",
|
|
5349
|
+
"ctx_get_workflow",
|
|
5350
|
+
"ctx_get_rules",
|
|
5351
|
+
"ctx_suggested_questions"
|
|
5352
|
+
]);
|
|
5353
|
+
var ToolRegistry = class {
|
|
5354
|
+
tools = /* @__PURE__ */ new Map();
|
|
5355
|
+
register(name, schema4, handler) {
|
|
5356
|
+
this.tools.set(name, { schema: schema4, handler });
|
|
5357
|
+
}
|
|
5358
|
+
list() {
|
|
5359
|
+
return Array.from(this.tools.values()).map((t) => t.schema);
|
|
5360
|
+
}
|
|
5361
|
+
async dispatch(name, args) {
|
|
5362
|
+
const def = this.tools.get(name);
|
|
5363
|
+
if (!def) throw new Error(`Unknown tool: ${name}`);
|
|
5364
|
+
const projectRoot = args && typeof args === "object" && "project_root" in args ? args.project_root : void 0;
|
|
5365
|
+
logger.debug("tool.dispatch", { tool: name, project_root: projectRoot });
|
|
5366
|
+
let dispatchArgs = args;
|
|
5367
|
+
if (!TASK_BUDGET_EXEMPT.has(name)) {
|
|
5368
|
+
const decision = getTaskBudgetTracker().recordCall();
|
|
5369
|
+
if (decision.overBudget) {
|
|
5370
|
+
dispatchArgs = applyOverBudgetOverrides(args);
|
|
5371
|
+
if (decision.firstBreach) {
|
|
5372
|
+
logger.warn("task tool budget exceeded \u2014 auto-throttling responses to skeleton/minimal", {
|
|
5373
|
+
tool: name,
|
|
5374
|
+
calls: decision.callCount,
|
|
5375
|
+
budget: decision.maxCalls
|
|
5376
|
+
});
|
|
5377
|
+
emitTaskBudgetBreached(name, decision.callCount, decision.maxCalls);
|
|
5378
|
+
}
|
|
5379
|
+
}
|
|
5380
|
+
}
|
|
5381
|
+
return def.handler(dispatchArgs);
|
|
5382
|
+
}
|
|
5383
|
+
has(name) {
|
|
5384
|
+
return this.tools.has(name);
|
|
5385
|
+
}
|
|
5386
|
+
};
|
|
5387
|
+
|
|
4820
5388
|
// packages/core/src/tools/search.ts
|
|
5389
|
+
import { z as z3 } from "zod";
|
|
4821
5390
|
var DEFAULT_MAX_RESPONSE_TOKENS = 4e3;
|
|
4822
5391
|
var Schema = z3.object({
|
|
4823
5392
|
query: z3.string().describe("Search query \u2014 natural language or code fragment"),
|
|
@@ -6454,7 +7023,7 @@ function registerGitDiffReviewTool(registry, ctx) {
|
|
|
6454
7023
|
skeleton: include_skeletons && i < skeletonLimit ? await trySkeletonize(ctx, file, project_root) : ""
|
|
6455
7024
|
}))
|
|
6456
7025
|
);
|
|
6457
|
-
const
|
|
7026
|
+
const render2 = (withSkeletons, withTransitive) => {
|
|
6458
7027
|
const out = [`<git_diff_review changed_files="${files.length}" depth="${depth}">`];
|
|
6459
7028
|
out.push(` <changed_files count="${files.length}">`);
|
|
6460
7029
|
for (const cd of changedFileData) {
|
|
@@ -6500,8 +7069,8 @@ function registerGitDiffReviewTool(registry, ctx) {
|
|
|
6500
7069
|
out.push("</git_diff_review>");
|
|
6501
7070
|
return out.join("\n");
|
|
6502
7071
|
};
|
|
6503
|
-
const full =
|
|
6504
|
-
return maybeBudget(full, async () =>
|
|
7072
|
+
const full = render2(include_skeletons, true);
|
|
7073
|
+
return maybeBudget(full, async () => render2(false, false));
|
|
6505
7074
|
}
|
|
6506
7075
|
);
|
|
6507
7076
|
}
|
|
@@ -6603,7 +7172,7 @@ function registerRefactorPreviewTool(registry, ctx) {
|
|
|
6603
7172
|
totalOccurrences += occurrences.length;
|
|
6604
7173
|
}
|
|
6605
7174
|
}
|
|
6606
|
-
const
|
|
7175
|
+
const render2 = (includeChanges) => {
|
|
6607
7176
|
const xmlLines = [
|
|
6608
7177
|
`<refactor_preview symbol="${escapeXML17(symbol)}" new_name="${escapeXML17(new_name)}" total_files="${fileChanges.length}" total_occurrences="${totalOccurrences}">`
|
|
6609
7178
|
];
|
|
@@ -6633,7 +7202,7 @@ function registerRefactorPreviewTool(registry, ctx) {
|
|
|
6633
7202
|
xmlLines.push("</refactor_preview>");
|
|
6634
7203
|
return xmlLines.join("\n");
|
|
6635
7204
|
};
|
|
6636
|
-
const full =
|
|
7205
|
+
const full = render2(true);
|
|
6637
7206
|
if (!hasBudgetArgs(args)) return full;
|
|
6638
7207
|
const result = await enforceBudget({
|
|
6639
7208
|
ctx,
|
|
@@ -6641,7 +7210,7 @@ function registerRefactorPreviewTool(registry, ctx) {
|
|
|
6641
7210
|
args: readBudgetArgs(args),
|
|
6642
7211
|
toolName: "ctx_refactor_preview",
|
|
6643
7212
|
defaultMaxTokens: DEFAULT_MAX_RESPONSE_TOKENS7,
|
|
6644
|
-
skeletonProducer: async () =>
|
|
7213
|
+
skeletonProducer: async () => render2(false)
|
|
6645
7214
|
});
|
|
6646
7215
|
return wrapResponse(result);
|
|
6647
7216
|
}
|
|
@@ -7004,7 +7573,7 @@ function registerCrossRepoSearchTool(registry, ctx, registryFilePath) {
|
|
|
7004
7573
|
);
|
|
7005
7574
|
}
|
|
7006
7575
|
xmlLines.push(" </repos>");
|
|
7007
|
-
const
|
|
7576
|
+
const render2 = (includeContent) => {
|
|
7008
7577
|
const out = [...xmlLines];
|
|
7009
7578
|
out.push(` <results count="${topResults.length}">`);
|
|
7010
7579
|
for (const r of topResults) {
|
|
@@ -7020,7 +7589,7 @@ function registerCrossRepoSearchTool(registry, ctx, registryFilePath) {
|
|
|
7020
7589
|
out.push("</cross_repo_search>");
|
|
7021
7590
|
return out.join("\n");
|
|
7022
7591
|
};
|
|
7023
|
-
const full =
|
|
7592
|
+
const full = render2(true);
|
|
7024
7593
|
if (!hasBudgetArgs(args)) return full;
|
|
7025
7594
|
const result = await enforceBudget({
|
|
7026
7595
|
ctx,
|
|
@@ -7028,7 +7597,7 @@ function registerCrossRepoSearchTool(registry, ctx, registryFilePath) {
|
|
|
7028
7597
|
args: readBudgetArgs(args),
|
|
7029
7598
|
toolName: "ctx_cross_repo_search",
|
|
7030
7599
|
defaultMaxTokens: DEFAULT_MAX_RESPONSE_TOKENS9,
|
|
7031
|
-
skeletonProducer: async () =>
|
|
7600
|
+
skeletonProducer: async () => render2(false)
|
|
7032
7601
|
});
|
|
7033
7602
|
return wrapResponse(result);
|
|
7034
7603
|
}
|
|
@@ -7415,7 +7984,7 @@ function registerFullTextSearchTool(registry, ctx) {
|
|
|
7415
7984
|
} catch {
|
|
7416
7985
|
}
|
|
7417
7986
|
}
|
|
7418
|
-
const
|
|
7987
|
+
const render2 = (includeSnippets) => {
|
|
7419
7988
|
const xml = [
|
|
7420
7989
|
`<full_text_search query="${escapeXML22(query)}" mode="${mode}" case_sensitive="${case_sensitive}" count="${merged.length}">`
|
|
7421
7990
|
];
|
|
@@ -7434,8 +8003,8 @@ function registerFullTextSearchTool(registry, ctx) {
|
|
|
7434
8003
|
return xml.join("\n");
|
|
7435
8004
|
};
|
|
7436
8005
|
return maybeBudget(
|
|
7437
|
-
|
|
7438
|
-
async () =>
|
|
8006
|
+
render2(true),
|
|
8007
|
+
async () => render2(false)
|
|
7439
8008
|
);
|
|
7440
8009
|
}
|
|
7441
8010
|
);
|
|
@@ -8468,66 +9037,340 @@ function registerGetAffectedFlowsTool(registry, ctx) {
|
|
|
8468
9037
|
);
|
|
8469
9038
|
}
|
|
8470
9039
|
|
|
8471
|
-
// packages/core/src/tools/
|
|
8472
|
-
|
|
8473
|
-
|
|
8474
|
-
|
|
8475
|
-
|
|
8476
|
-
|
|
8477
|
-
|
|
8478
|
-
|
|
8479
|
-
|
|
8480
|
-
|
|
8481
|
-
|
|
8482
|
-
|
|
8483
|
-
|
|
8484
|
-
|
|
8485
|
-
|
|
8486
|
-
|
|
8487
|
-
|
|
8488
|
-
|
|
8489
|
-
|
|
8490
|
-
|
|
8491
|
-
|
|
8492
|
-
|
|
8493
|
-
|
|
8494
|
-
|
|
8495
|
-
|
|
8496
|
-
|
|
8497
|
-
|
|
8498
|
-
|
|
8499
|
-
|
|
8500
|
-
|
|
8501
|
-
|
|
8502
|
-
|
|
8503
|
-
|
|
8504
|
-
|
|
8505
|
-
|
|
8506
|
-
|
|
8507
|
-
|
|
8508
|
-
|
|
8509
|
-
|
|
8510
|
-
|
|
8511
|
-
|
|
8512
|
-
|
|
8513
|
-
|
|
8514
|
-
|
|
8515
|
-
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
|
|
8520
|
-
|
|
8521
|
-
|
|
8522
|
-
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
|
|
8526
|
-
|
|
8527
|
-
|
|
8528
|
-
|
|
8529
|
-
|
|
8530
|
-
|
|
9040
|
+
// packages/core/src/tools/minimal-context.ts
|
|
9041
|
+
import { execSync } from "child_process";
|
|
9042
|
+
import { z as z36 } from "zod";
|
|
9043
|
+
var Schema30 = z36.object({
|
|
9044
|
+
task: z36.string().max(200).optional().describe(
|
|
9045
|
+
"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."
|
|
9046
|
+
),
|
|
9047
|
+
project_root: ProjectRootField,
|
|
9048
|
+
max_response_tokens: z36.number().int().positive().optional(),
|
|
9049
|
+
on_budget_exceeded: z36.enum(["skeleton", "truncate", "error"]).optional(),
|
|
9050
|
+
response_format: z36.enum(["full", "skeleton", "auto"]).optional()
|
|
9051
|
+
});
|
|
9052
|
+
var DEFAULT_MAX_RESPONSE_TOKENS13 = 250;
|
|
9053
|
+
function routeFirstTool(task, hasDirtyChanges) {
|
|
9054
|
+
const t = (task ?? "").toLowerCase();
|
|
9055
|
+
if (/\b(rename|refactor|move\s+\w+|extract)\b/.test(t)) {
|
|
9056
|
+
return {
|
|
9057
|
+
tool: "ctx_get_call_graph",
|
|
9058
|
+
why: "Renames + refactors need every caller surfaced before the edit. Start here.",
|
|
9059
|
+
estimated_tokens: 800
|
|
9060
|
+
};
|
|
9061
|
+
}
|
|
9062
|
+
if (/\b(blast|impact|breaks?|affects?)\b/.test(t)) {
|
|
9063
|
+
return {
|
|
9064
|
+
tool: "ctx_blast_radius",
|
|
9065
|
+
why: "Blast-radius analysis gives transitive dependents; start here for impact questions.",
|
|
9066
|
+
estimated_tokens: 1500
|
|
9067
|
+
};
|
|
9068
|
+
}
|
|
9069
|
+
if (/\b(architect|overview|explore|onboard|tour)\b/.test(t)) {
|
|
9070
|
+
return {
|
|
9071
|
+
tool: "ctx_architecture_overview",
|
|
9072
|
+
why: "Top-down map of communities + hub nodes; the natural starting point for exploration.",
|
|
9073
|
+
estimated_tokens: 2e3
|
|
9074
|
+
};
|
|
9075
|
+
}
|
|
9076
|
+
if (/\b(test|coverage|tested)\b/.test(t)) {
|
|
9077
|
+
return {
|
|
9078
|
+
tool: "ctx_knowledge_gaps",
|
|
9079
|
+
why: "Knowledge-gap report highlights files lacking test coverage.",
|
|
9080
|
+
estimated_tokens: 1200
|
|
9081
|
+
};
|
|
9082
|
+
}
|
|
9083
|
+
if (/\b(review|audit|check|diff)\b/.test(t)) {
|
|
9084
|
+
return {
|
|
9085
|
+
tool: "ctx_detect_changes",
|
|
9086
|
+
why: "Risk-scored change analysis is the canonical start for reviews.",
|
|
9087
|
+
estimated_tokens: 1500
|
|
9088
|
+
};
|
|
9089
|
+
}
|
|
9090
|
+
if (hasDirtyChanges) {
|
|
9091
|
+
return {
|
|
9092
|
+
tool: "ctx_detect_changes",
|
|
9093
|
+
why: "Working tree has uncommitted changes \u2014 review-mode is the most-likely intent.",
|
|
9094
|
+
estimated_tokens: 1500
|
|
9095
|
+
};
|
|
9096
|
+
}
|
|
9097
|
+
return {
|
|
9098
|
+
tool: "ctx_architecture_overview",
|
|
9099
|
+
why: "Clean working tree + no task hint \u2014 orientation is the safe default.",
|
|
9100
|
+
estimated_tokens: 2e3
|
|
9101
|
+
};
|
|
9102
|
+
}
|
|
9103
|
+
function classifyStaleness(lastBuildIso) {
|
|
9104
|
+
if (!lastBuildIso) return "unbuilt";
|
|
9105
|
+
const ms = Date.now() - new Date(lastBuildIso).getTime();
|
|
9106
|
+
if (!Number.isFinite(ms) || ms < 0) return "unbuilt";
|
|
9107
|
+
if (ms < 5 * 60 * 1e3) return "fresh";
|
|
9108
|
+
if (ms < 60 * 60 * 1e3) return "stale_minutes";
|
|
9109
|
+
return "stale_hours";
|
|
9110
|
+
}
|
|
9111
|
+
function readRecentChanges(projectRoot) {
|
|
9112
|
+
try {
|
|
9113
|
+
const stdout = execSync("git status --porcelain", {
|
|
9114
|
+
cwd: projectRoot,
|
|
9115
|
+
timeout: 2e3,
|
|
9116
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
9117
|
+
encoding: "utf-8"
|
|
9118
|
+
});
|
|
9119
|
+
const lines = stdout.split("\n").filter((l) => l.trim() !== "");
|
|
9120
|
+
return lines.slice(0, 20).map((line) => {
|
|
9121
|
+
const x = line[0];
|
|
9122
|
+
const y = line[1];
|
|
9123
|
+
const path37 = line.slice(3).trim();
|
|
9124
|
+
let status = "?";
|
|
9125
|
+
const xy = x === " " ? y : x;
|
|
9126
|
+
if (xy === "M" || xy === "A" || xy === "D" || xy === "R") status = xy;
|
|
9127
|
+
return { file: path37, status };
|
|
9128
|
+
});
|
|
9129
|
+
} catch {
|
|
9130
|
+
return [];
|
|
9131
|
+
}
|
|
9132
|
+
}
|
|
9133
|
+
function computeTopHubs(graph) {
|
|
9134
|
+
const files = graph.allFiles();
|
|
9135
|
+
const scored = files.map((file) => {
|
|
9136
|
+
const inDeg = graph.getImporters(file).length;
|
|
9137
|
+
const outDeg = graph.getImports(file).length;
|
|
9138
|
+
return { file, inDeg, outDeg, total: inDeg + outDeg };
|
|
9139
|
+
}).filter((s) => s.total >= 2).sort((a, b) => b.total - a.total).slice(0, 5);
|
|
9140
|
+
return scored.map((s) => ({
|
|
9141
|
+
name: s.file,
|
|
9142
|
+
reason: s.inDeg > s.outDeg ? "fan_in" : s.outDeg > s.inDeg ? "fan_out" : "bridge"
|
|
9143
|
+
}));
|
|
9144
|
+
}
|
|
9145
|
+
var CACHE_TTL_MS2 = 1e4;
|
|
9146
|
+
var responseCache = /* @__PURE__ */ new Map();
|
|
9147
|
+
function cacheKey(projectRoot, task) {
|
|
9148
|
+
return `${projectRoot}|${task ?? ""}`;
|
|
9149
|
+
}
|
|
9150
|
+
function cacheGet(key) {
|
|
9151
|
+
const e = responseCache.get(key);
|
|
9152
|
+
if (!e) return null;
|
|
9153
|
+
if (e.expiresAt < Date.now()) {
|
|
9154
|
+
responseCache.delete(key);
|
|
9155
|
+
return null;
|
|
9156
|
+
}
|
|
9157
|
+
return e.body;
|
|
9158
|
+
}
|
|
9159
|
+
function cachePut(key, body) {
|
|
9160
|
+
responseCache.set(key, { expiresAt: Date.now() + CACHE_TTL_MS2, body });
|
|
9161
|
+
}
|
|
9162
|
+
function sanitizeTask(raw) {
|
|
9163
|
+
if (raw == null) return void 0;
|
|
9164
|
+
const stripped = raw.replace(/[\x00-\x1f\x7f]/g, "").slice(0, 200);
|
|
9165
|
+
return stripped === "" ? void 0 : stripped;
|
|
9166
|
+
}
|
|
9167
|
+
function escapeXML26(text) {
|
|
9168
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
9169
|
+
}
|
|
9170
|
+
function render(input) {
|
|
9171
|
+
const lines = ["<minimal_context>"];
|
|
9172
|
+
lines.push(
|
|
9173
|
+
` <graph ready="${input.graphReady}" nodes="${input.nodes}" edges="${input.edges}" last_build="${escapeXML26(input.lastBuildIso ?? "never")}" staleness="${input.staleness}" />`
|
|
9174
|
+
);
|
|
9175
|
+
if (input.languages.length > 0) {
|
|
9176
|
+
lines.push(` <languages>${input.languages.map(escapeXML26).join(", ")}</languages>`);
|
|
9177
|
+
}
|
|
9178
|
+
if (input.recentChanges.length > 0) {
|
|
9179
|
+
lines.push(` <recent_changes count="${input.recentChanges.length}">`);
|
|
9180
|
+
for (const c of input.recentChanges) {
|
|
9181
|
+
lines.push(` <change status="${c.status}" file="${escapeXML26(c.file)}" />`);
|
|
9182
|
+
}
|
|
9183
|
+
lines.push(" </recent_changes>");
|
|
9184
|
+
} else {
|
|
9185
|
+
lines.push(' <recent_changes count="0" />');
|
|
9186
|
+
}
|
|
9187
|
+
if (input.topHubs.length > 0) {
|
|
9188
|
+
lines.push(` <top_hubs count="${input.topHubs.length}">`);
|
|
9189
|
+
for (const h of input.topHubs) {
|
|
9190
|
+
lines.push(` <hub reason="${h.reason}" name="${escapeXML26(h.name)}" />`);
|
|
9191
|
+
}
|
|
9192
|
+
lines.push(" </top_hubs>");
|
|
9193
|
+
}
|
|
9194
|
+
lines.push(" <suggested_first_tool>");
|
|
9195
|
+
lines.push(` <tool>${escapeXML26(input.suggested.tool)}</tool>`);
|
|
9196
|
+
lines.push(` <why>${escapeXML26(input.suggested.why)}</why>`);
|
|
9197
|
+
lines.push(` <estimated_tokens>${input.suggested.estimated_tokens}</estimated_tokens>`);
|
|
9198
|
+
if (input.suggested.args && Object.keys(input.suggested.args).length > 0) {
|
|
9199
|
+
lines.push(` <args>${escapeXML26(JSON.stringify(input.suggested.args))}</args>`);
|
|
9200
|
+
}
|
|
9201
|
+
lines.push(" </suggested_first_tool>");
|
|
9202
|
+
lines.push("</minimal_context>");
|
|
9203
|
+
return lines.join("\n");
|
|
9204
|
+
}
|
|
9205
|
+
function renderSkeleton(input) {
|
|
9206
|
+
const lines = ['<minimal_context format="skeleton">'];
|
|
9207
|
+
lines.push(
|
|
9208
|
+
` <graph ready="${input.graphReady}" nodes="${input.nodes}" edges="${input.edges}" staleness="${input.staleness}" />`
|
|
9209
|
+
);
|
|
9210
|
+
lines.push(` <recent_changes count="${input.recentChanges.length}" />`);
|
|
9211
|
+
lines.push(` <top_hubs count="${input.topHubs.length}" />`);
|
|
9212
|
+
lines.push(" <suggested_first_tool>");
|
|
9213
|
+
lines.push(` <tool>${escapeXML26(input.suggested.tool)}</tool>`);
|
|
9214
|
+
lines.push(` <why>${escapeXML26(input.suggested.why)}</why>`);
|
|
9215
|
+
lines.push(` <estimated_tokens>${input.suggested.estimated_tokens}</estimated_tokens>`);
|
|
9216
|
+
lines.push(" </suggested_first_tool>");
|
|
9217
|
+
lines.push("</minimal_context>");
|
|
9218
|
+
return lines.join("\n");
|
|
9219
|
+
}
|
|
9220
|
+
function registerMinimalContextTool(registry, ctx) {
|
|
9221
|
+
registry.register(
|
|
9222
|
+
"ctx_get_minimal_context",
|
|
9223
|
+
{
|
|
9224
|
+
name: "ctx_get_minimal_context",
|
|
9225
|
+
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.',
|
|
9226
|
+
inputSchema: {
|
|
9227
|
+
type: "object",
|
|
9228
|
+
properties: {
|
|
9229
|
+
task: {
|
|
9230
|
+
type: "string",
|
|
9231
|
+
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."
|
|
9232
|
+
},
|
|
9233
|
+
project_root: PROJECT_ROOT_JSON_SCHEMA,
|
|
9234
|
+
max_response_tokens: {
|
|
9235
|
+
type: "number",
|
|
9236
|
+
description: "Optional response token budget. Default 250."
|
|
9237
|
+
},
|
|
9238
|
+
on_budget_exceeded: {
|
|
9239
|
+
type: "string",
|
|
9240
|
+
enum: ["skeleton", "truncate", "error"]
|
|
9241
|
+
},
|
|
9242
|
+
response_format: {
|
|
9243
|
+
type: "string",
|
|
9244
|
+
enum: ["full", "skeleton", "auto"]
|
|
9245
|
+
}
|
|
9246
|
+
}
|
|
9247
|
+
}
|
|
9248
|
+
},
|
|
9249
|
+
async (args) => {
|
|
9250
|
+
const parsed = Schema30.parse(args);
|
|
9251
|
+
const task = sanitizeTask(parsed.task);
|
|
9252
|
+
const projectRoot = parsed.project_root ?? ctx.projectRoot;
|
|
9253
|
+
const key = cacheKey(projectRoot, task);
|
|
9254
|
+
const cached = cacheGet(key);
|
|
9255
|
+
if (cached) {
|
|
9256
|
+
return cached;
|
|
9257
|
+
}
|
|
9258
|
+
const graphReady = ctx.isGraphInitialized();
|
|
9259
|
+
let nodes = 0;
|
|
9260
|
+
let edges = 0;
|
|
9261
|
+
let lastBuildIso = null;
|
|
9262
|
+
const languages = [];
|
|
9263
|
+
let topHubs = [];
|
|
9264
|
+
if (graphReady) {
|
|
9265
|
+
try {
|
|
9266
|
+
const graph = await ctx.getGraph(projectRoot);
|
|
9267
|
+
nodes = graph.allFiles().length;
|
|
9268
|
+
edges = graph.allFiles().reduce((acc, f) => acc + graph.getImports(f).length, 0);
|
|
9269
|
+
const extensions = /* @__PURE__ */ new Set();
|
|
9270
|
+
for (const f of graph.allFiles()) {
|
|
9271
|
+
const ext = f.split(".").pop();
|
|
9272
|
+
if (ext && ext.length <= 4) extensions.add(ext);
|
|
9273
|
+
}
|
|
9274
|
+
languages.push(...Array.from(extensions).sort());
|
|
9275
|
+
topHubs = computeTopHubs(graph);
|
|
9276
|
+
} catch {
|
|
9277
|
+
}
|
|
9278
|
+
}
|
|
9279
|
+
const recentChanges = readRecentChanges(projectRoot);
|
|
9280
|
+
const suggested = routeFirstTool(task, recentChanges.length > 0);
|
|
9281
|
+
const staleness = classifyStaleness(lastBuildIso);
|
|
9282
|
+
const renderInput = {
|
|
9283
|
+
graphReady,
|
|
9284
|
+
nodes,
|
|
9285
|
+
edges,
|
|
9286
|
+
lastBuildIso,
|
|
9287
|
+
staleness,
|
|
9288
|
+
languages,
|
|
9289
|
+
recentChanges,
|
|
9290
|
+
topHubs,
|
|
9291
|
+
suggested
|
|
9292
|
+
};
|
|
9293
|
+
const full = render(renderInput);
|
|
9294
|
+
if (!hasBudgetArgs(parsed)) {
|
|
9295
|
+
cachePut(key, full);
|
|
9296
|
+
return full;
|
|
9297
|
+
}
|
|
9298
|
+
const result = await enforceBudget({
|
|
9299
|
+
ctx,
|
|
9300
|
+
full,
|
|
9301
|
+
args: readBudgetArgs(parsed),
|
|
9302
|
+
toolName: "ctx_get_minimal_context",
|
|
9303
|
+
defaultMaxTokens: DEFAULT_MAX_RESPONSE_TOKENS13,
|
|
9304
|
+
skeletonProducer: async () => renderSkeleton(renderInput)
|
|
9305
|
+
});
|
|
9306
|
+
const body = wrapResponse(result);
|
|
9307
|
+
cachePut(key, body);
|
|
9308
|
+
return body;
|
|
9309
|
+
}
|
|
9310
|
+
);
|
|
9311
|
+
}
|
|
9312
|
+
|
|
9313
|
+
// packages/core/src/tools/index.ts
|
|
9314
|
+
function createToolRegistry(ctx) {
|
|
9315
|
+
const registry = new ToolRegistry();
|
|
9316
|
+
registerSearchTool(registry, ctx);
|
|
9317
|
+
registerFileTool(registry, ctx);
|
|
9318
|
+
registerContextPacketTool(registry, ctx);
|
|
9319
|
+
registerCallGraphTool(registry, ctx);
|
|
9320
|
+
registerDefinitionTool(registry, ctx);
|
|
9321
|
+
registerRulesTool(registry, ctx);
|
|
9322
|
+
registerRulesCheckTool(registry, ctx);
|
|
9323
|
+
registerSimilarFilesTool(registry, ctx);
|
|
9324
|
+
registerStatusTool(registry, ctx);
|
|
9325
|
+
registerBlastRadiusTool(registry, ctx);
|
|
9326
|
+
registerHubNodesTool(registry, ctx);
|
|
9327
|
+
registerBridgeNodesTool(registry, ctx);
|
|
9328
|
+
registerCommunityListTool(registry, ctx);
|
|
9329
|
+
registerArchitectureOverviewTool(registry, ctx);
|
|
9330
|
+
registerKnowledgeGapsTool(registry, ctx);
|
|
9331
|
+
registerSurprisingConnectionsTool(registry, ctx);
|
|
9332
|
+
registerWikiGenerateTool(registry, ctx);
|
|
9333
|
+
registerGraphExportTool(registry, ctx);
|
|
9334
|
+
registerGitDiffReviewTool(registry, ctx);
|
|
9335
|
+
registerRefactorPreviewTool(registry, ctx);
|
|
9336
|
+
registerExecutionFlowTool(registry, ctx);
|
|
9337
|
+
registerCrossRepoSearchTool(registry, ctx);
|
|
9338
|
+
registerApplyRefactorTool(registry, ctx);
|
|
9339
|
+
registerDetectChangesTool(registry, ctx);
|
|
9340
|
+
registerFullTextSearchTool(registry, ctx);
|
|
9341
|
+
registerSuggestedQuestionsTool(registry, ctx);
|
|
9342
|
+
registerGetWorkflowTool(registry, ctx);
|
|
9343
|
+
registerGraphSnapshotTool(registry, ctx);
|
|
9344
|
+
registerGraphDiffTool(registry, ctx);
|
|
9345
|
+
registerFindLargeFunctionsTool(registry, ctx);
|
|
9346
|
+
registerGitCouplingTool(registry, ctx);
|
|
9347
|
+
registerRiskOverlayTool(registry, ctx);
|
|
9348
|
+
registerGetAffectedFlowsTool(registry, ctx);
|
|
9349
|
+
registerMinimalContextTool(registry, ctx);
|
|
9350
|
+
return registry;
|
|
9351
|
+
}
|
|
9352
|
+
|
|
9353
|
+
// packages/core/src/tools/ruleManager.ts
|
|
9354
|
+
import fs22 from "fs";
|
|
9355
|
+
import path24 from "path";
|
|
9356
|
+
var RULE_FILES = [
|
|
9357
|
+
".cursorrules",
|
|
9358
|
+
"CLAUDE.md",
|
|
9359
|
+
"CONTEXT.md",
|
|
9360
|
+
".ctxloomrc",
|
|
9361
|
+
".cursor/rules",
|
|
9362
|
+
".claude/CLAUDE.md"
|
|
9363
|
+
];
|
|
9364
|
+
var RuleManager = class {
|
|
9365
|
+
projectRoot;
|
|
9366
|
+
pathValidator;
|
|
9367
|
+
cachedRules = null;
|
|
9368
|
+
constructor(projectRoot, pathValidator) {
|
|
9369
|
+
this.projectRoot = projectRoot;
|
|
9370
|
+
this.pathValidator = pathValidator;
|
|
9371
|
+
}
|
|
9372
|
+
/**
|
|
9373
|
+
* Scan for all rule files in the project root.
|
|
8531
9374
|
*/
|
|
8532
9375
|
async loadRules() {
|
|
8533
9376
|
if (this.cachedRules) return this.cachedRules;
|
|
@@ -9136,20 +9979,20 @@ import { readFileSync, writeFileSync, unlinkSync, mkdirSync, chmodSync, existsSy
|
|
|
9136
9979
|
import path29 from "path";
|
|
9137
9980
|
|
|
9138
9981
|
// packages/core/src/license/types.ts
|
|
9139
|
-
import { z as
|
|
9982
|
+
import { z as z37 } from "zod";
|
|
9140
9983
|
var FINGERPRINT_RE = /^sha256:[0-9a-f]{64}$/;
|
|
9141
|
-
var LicenseFileSchema =
|
|
9142
|
-
schemaVersion:
|
|
9143
|
-
key:
|
|
9144
|
-
tier:
|
|
9145
|
-
status:
|
|
9146
|
-
fingerprint:
|
|
9147
|
-
seats:
|
|
9148
|
-
issuedAt:
|
|
9149
|
-
expiresAt:
|
|
9150
|
-
lastValidatedAt:
|
|
9151
|
-
licenseId:
|
|
9152
|
-
instanceId:
|
|
9984
|
+
var LicenseFileSchema = z37.object({
|
|
9985
|
+
schemaVersion: z37.literal(1),
|
|
9986
|
+
key: z37.string().min(1),
|
|
9987
|
+
tier: z37.enum(["pro", "team", "enterprise", "trial"]),
|
|
9988
|
+
status: z37.enum(["active", "trialing", "expired"]),
|
|
9989
|
+
fingerprint: z37.string().regex(FINGERPRINT_RE),
|
|
9990
|
+
seats: z37.number().int().positive(),
|
|
9991
|
+
issuedAt: z37.string().datetime(),
|
|
9992
|
+
expiresAt: z37.string().datetime(),
|
|
9993
|
+
lastValidatedAt: z37.string().datetime(),
|
|
9994
|
+
licenseId: z37.string().min(1),
|
|
9995
|
+
instanceId: z37.string().min(1)
|
|
9153
9996
|
});
|
|
9154
9997
|
|
|
9155
9998
|
// packages/core/src/license/LicenseStore.ts
|
|
@@ -9362,7 +10205,7 @@ var TELEMETRY_DISABLED = TELEMETRY_LEVEL === "off";
|
|
|
9362
10205
|
function getTelemetryLevel() {
|
|
9363
10206
|
return TELEMETRY_LEVEL;
|
|
9364
10207
|
}
|
|
9365
|
-
var CTXLOOM_VERSION = "1.
|
|
10208
|
+
var CTXLOOM_VERSION = "1.5.0".length > 0 ? "1.5.0" : "dev";
|
|
9366
10209
|
var POSTHOG_HOST = "https://eu.i.posthog.com";
|
|
9367
10210
|
var POSTHOG_KEY = process.env["POSTHOG_API_KEY"] ?? (true ? "phc_CiDkmFLcZ2K6uCpcoSUQLmFrnnUvsyXGhSxopX5TVKE6" : "");
|
|
9368
10211
|
var SENTRY_DSN = process.env["SENTRY_DSN"] ?? (true ? "https://81c94a0f04a8e242dee493ac1e17f733@o4508531702497280.ingest.de.sentry.io/4511256875368528" : "");
|
|
@@ -9986,6 +10829,845 @@ var EmittedOnceTracker = class {
|
|
|
9986
10829
|
}
|
|
9987
10830
|
};
|
|
9988
10831
|
|
|
10832
|
+
// packages/core/src/install/installer.ts
|
|
10833
|
+
import fs28 from "fs";
|
|
10834
|
+
import path36 from "path";
|
|
10835
|
+
|
|
10836
|
+
// packages/core/src/install/templates.ts
|
|
10837
|
+
var RULES_BLOCK_NAME = "CTXLOOM-RULES";
|
|
10838
|
+
var RULES_BLOCK_CONTENT = `## MCP Tools: ctxloom
|
|
10839
|
+
|
|
10840
|
+
**IMPORTANT: This project has a knowledge graph. ALWAYS use the
|
|
10841
|
+
ctxloom MCP tools BEFORE Grep/Glob/Read to explore the codebase.**
|
|
10842
|
+
The graph is faster, cheaper (fewer tokens), and gives you
|
|
10843
|
+
structural context (callers, dependents, test coverage) that file
|
|
10844
|
+
scanning cannot.
|
|
10845
|
+
|
|
10846
|
+
### Start every workflow with \`ctx_get_minimal_context\`
|
|
10847
|
+
|
|
10848
|
+
The first MCP call into ctxloom should always be
|
|
10849
|
+
\`ctx_get_minimal_context(task="<what you're about to do>")\`. It
|
|
10850
|
+
returns ~150 tokens of orientation plus a task-aware
|
|
10851
|
+
\`suggested_first_tool\` you should call next instead of guessing.
|
|
10852
|
+
|
|
10853
|
+
### When to use graph tools FIRST
|
|
10854
|
+
|
|
10855
|
+
- **Code review**: \`ctx_detect_changes\` + \`ctx_get_review_context\`
|
|
10856
|
+
instead of reading whole files
|
|
10857
|
+
- **Understanding impact**: \`ctx_blast_radius\` + \`ctx_get_affected_flows\`
|
|
10858
|
+
instead of manually tracing imports
|
|
10859
|
+
- **Refactor planning**: \`ctx_get_call_graph(direction: 'callers')\`
|
|
10860
|
+
+ \`ctx_refactor_preview\` before any rename
|
|
10861
|
+
- **Architecture questions**: \`ctx_architecture_overview\`,
|
|
10862
|
+
\`ctx_community_list\`, \`ctx_hub_nodes\`
|
|
10863
|
+
- **Finding code**: \`ctx_search\` or \`ctx_full_text_search\` instead
|
|
10864
|
+
of \`Grep\`
|
|
10865
|
+
|
|
10866
|
+
Fall back to Grep/Glob/Read **only** when the graph doesn't cover
|
|
10867
|
+
what you need.
|
|
10868
|
+
|
|
10869
|
+
### Follow the \`next_tool_suggestions\` in every response
|
|
10870
|
+
|
|
10871
|
+
Every budget-wrapped ctxloom response includes
|
|
10872
|
+
\`meta.next_tool_suggestions\` \u2014 author-curated follow-ups with
|
|
10873
|
+
\`why\` reasoning and \`estimated_tokens\` per entry. Pick from
|
|
10874
|
+
those instead of guessing.
|
|
10875
|
+
|
|
10876
|
+
### Token-budget protocol
|
|
10877
|
+
|
|
10878
|
+
- Target: \u22648 tool calls per task, \u22642000 total tokens of graph context
|
|
10879
|
+
- Pass \`max_response_tokens\` on calls that might return large
|
|
10880
|
+
responses; the budget surface returns skeletons instead of dumping
|
|
10881
|
+
- Use \`response_format: 'skeleton'\` when you know you only need
|
|
10882
|
+
signatures, not bodies
|
|
10883
|
+
|
|
10884
|
+
### Key tools at a glance
|
|
10885
|
+
|
|
10886
|
+
| Tool | Use when |
|
|
10887
|
+
|------|----------|
|
|
10888
|
+
| \`ctx_get_minimal_context\` | START HERE \u2014 orientation anchor |
|
|
10889
|
+
| \`ctx_detect_changes\` | Reviewing code changes; risk-scored |
|
|
10890
|
+
| \`ctx_get_review_context\` | Token-efficient review snippets |
|
|
10891
|
+
| \`ctx_blast_radius\` | Blast radius of a change |
|
|
10892
|
+
| \`ctx_get_affected_flows\` | Execution paths impacted |
|
|
10893
|
+
| \`ctx_get_call_graph\` | Callers / callees of a symbol |
|
|
10894
|
+
| \`ctx_search\` / \`ctx_full_text_search\` | Find code |
|
|
10895
|
+
| \`ctx_architecture_overview\` | High-level codebase map |
|
|
10896
|
+
| \`ctx_refactor_preview\` / \`ctx_apply_refactor\` | Plan a rename |
|
|
10897
|
+
|
|
10898
|
+
### Hooks keep the graph fresh
|
|
10899
|
+
|
|
10900
|
+
\`ctxloom init\` installed a PostToolUse hook on \`Write|Edit\` that
|
|
10901
|
+
runs \`ctxloom update --incremental --quiet\` \u2014 so the graph is
|
|
10902
|
+
always up to date when you query it. No "did the index update yet?"
|
|
10903
|
+
guessing.`;
|
|
10904
|
+
var SESSION_START_HEADER = `#!/usr/bin/env bash
|
|
10905
|
+
# ctxloom \u2014 agent-harness session-start hook
|
|
10906
|
+
# Generated by \`ctxloom init\`. Re-run \`ctxloom init\` to update.
|
|
10907
|
+
# Manual edits will be overwritten on the next install.
|
|
10908
|
+
|
|
10909
|
+
set -e
|
|
10910
|
+
`;
|
|
10911
|
+
var SESSION_START_BODY = `DB=".ctxloom/graph.db"
|
|
10912
|
+
|
|
10913
|
+
if [ -f "$DB" ]; then
|
|
10914
|
+
# \`ctxloom status --json\` is cached + sub-100ms \u2014 keeps the hook
|
|
10915
|
+
# under its 2s timeout even on cold disk.
|
|
10916
|
+
STATS=$(ctxloom status --json 2>/dev/null || echo '{"nodes":0,"edges":0}')
|
|
10917
|
+
NODES=$(echo "$STATS" | grep -oE '"nodes":\\s*[0-9]+' | grep -oE '[0-9]+' || echo "?")
|
|
10918
|
+
EDGES=$(echo "$STATS" | grep -oE '"edges":\\s*[0-9]+' | grep -oE '[0-9]+' || echo "?")
|
|
10919
|
+
|
|
10920
|
+
cat <<EOF
|
|
10921
|
+
[ctxloom] Knowledge graph ready (\${NODES} nodes, \${EDGES} edges).
|
|
10922
|
+
|
|
10923
|
+
Start every workflow with \\\`ctx_get_minimal_context(task="...")\\\`.
|
|
10924
|
+
It returns ~150 tokens of orientation + a task-aware
|
|
10925
|
+
suggested_first_tool. Follow the meta.next_tool_suggestions on
|
|
10926
|
+
every response.
|
|
10927
|
+
|
|
10928
|
+
Prefer ctxloom MCP tools over Grep/Glob/Read:
|
|
10929
|
+
- ctx_detect_changes for code review
|
|
10930
|
+
- ctx_blast_radius / ctx_get_call_graph before refactoring
|
|
10931
|
+
- ctx_architecture_overview for orientation
|
|
10932
|
+
EOF
|
|
10933
|
+
else
|
|
10934
|
+
cat <<EOF
|
|
10935
|
+
[ctxloom] No knowledge graph found here.
|
|
10936
|
+
Run: ctxloom build
|
|
10937
|
+
|
|
10938
|
+
Then restart this session to enable graph-powered queries.
|
|
10939
|
+
EOF
|
|
10940
|
+
fi
|
|
10941
|
+
`;
|
|
10942
|
+
var SESSION_START_FULL = SESSION_START_HEADER + "\n" + SESSION_START_BODY;
|
|
10943
|
+
var CTXLOOM_HOOK_ENTRIES = {
|
|
10944
|
+
SessionStart: {
|
|
10945
|
+
matcher: "",
|
|
10946
|
+
hooks: [{ type: "command", command: ".claude/hooks/session-start.sh", timeout: 2 }]
|
|
10947
|
+
},
|
|
10948
|
+
PostToolUse: {
|
|
10949
|
+
matcher: "Write|Edit",
|
|
10950
|
+
hooks: [
|
|
10951
|
+
{
|
|
10952
|
+
type: "command",
|
|
10953
|
+
command: "ctxloom update --incremental --quiet",
|
|
10954
|
+
timeout: 30
|
|
10955
|
+
}
|
|
10956
|
+
]
|
|
10957
|
+
}
|
|
10958
|
+
};
|
|
10959
|
+
function plainBody() {
|
|
10960
|
+
return RULES_BLOCK_CONTENT.replace(/^##\s+/gm, "").replace(/^###\s+/gm, "").replace(/^####\s+/gm, "").replace(/\*\*(.*?)\*\*/g, "$1").replace(/`([^`]+)`/g, "$1");
|
|
10961
|
+
}
|
|
10962
|
+
var CURSOR_HEADER = `# ctxloom \u2014 Cursor agent rules
|
|
10963
|
+
# Generated by \`ctxloom init --host=cursor\`. Re-run to update.
|
|
10964
|
+
|
|
10965
|
+
`;
|
|
10966
|
+
var AIDER_HEADER = `# Project Conventions
|
|
10967
|
+
|
|
10968
|
+
Generated by \`ctxloom init --host=aider\`. Re-run to update.
|
|
10969
|
+
|
|
10970
|
+
`;
|
|
10971
|
+
var COPILOT_HEADER = `# ctxloom \u2014 GitHub Copilot instructions
|
|
10972
|
+
|
|
10973
|
+
Generated by \`ctxloom init --host=copilot\`. Re-run to update.
|
|
10974
|
+
|
|
10975
|
+
`;
|
|
10976
|
+
var WINDSURF_HEADER = `# ctxloom \u2014 Windsurf agent rules
|
|
10977
|
+
# Generated by \`ctxloom init --host=windsurf\`. Re-run to update.
|
|
10978
|
+
|
|
10979
|
+
`;
|
|
10980
|
+
var HOST_ADAPTERS = [
|
|
10981
|
+
// Note: claude / agents / gemini are NOT defined as HostAdapters in
|
|
10982
|
+
// this list — they predate the matrix and live in their own writeRulesBlock
|
|
10983
|
+
// path with HMAC-wrapped Markdown blocks. Future refactor may unify.
|
|
10984
|
+
{
|
|
10985
|
+
id: "cursor",
|
|
10986
|
+
path: ".cursorrules",
|
|
10987
|
+
defaultEnabled: false,
|
|
10988
|
+
render: () => CURSOR_HEADER + plainBody() + "\n",
|
|
10989
|
+
isCanonical(current) {
|
|
10990
|
+
return current === this.render();
|
|
10991
|
+
}
|
|
10992
|
+
},
|
|
10993
|
+
{
|
|
10994
|
+
id: "aider",
|
|
10995
|
+
path: "CONVENTIONS.md",
|
|
10996
|
+
defaultEnabled: false,
|
|
10997
|
+
render: () => AIDER_HEADER + RULES_BLOCK_CONTENT + "\n",
|
|
10998
|
+
isCanonical(current) {
|
|
10999
|
+
return current === this.render();
|
|
11000
|
+
}
|
|
11001
|
+
},
|
|
11002
|
+
{
|
|
11003
|
+
id: "copilot",
|
|
11004
|
+
path: ".github/copilot-instructions.md",
|
|
11005
|
+
defaultEnabled: false,
|
|
11006
|
+
render: () => COPILOT_HEADER + RULES_BLOCK_CONTENT + "\n",
|
|
11007
|
+
isCanonical(current) {
|
|
11008
|
+
return current === this.render();
|
|
11009
|
+
}
|
|
11010
|
+
},
|
|
11011
|
+
{
|
|
11012
|
+
id: "windsurf",
|
|
11013
|
+
path: ".windsurfrules",
|
|
11014
|
+
defaultEnabled: false,
|
|
11015
|
+
render: () => WINDSURF_HEADER + plainBody() + "\n",
|
|
11016
|
+
isCanonical(current) {
|
|
11017
|
+
return current === this.render();
|
|
11018
|
+
}
|
|
11019
|
+
}
|
|
11020
|
+
];
|
|
11021
|
+
function getHostAdapter(id) {
|
|
11022
|
+
return HOST_ADAPTERS.find((h) => h.id === id);
|
|
11023
|
+
}
|
|
11024
|
+
var SUPPORTED_HOST_IDS = HOST_ADAPTERS.map((h) => h.id);
|
|
11025
|
+
|
|
11026
|
+
// packages/core/src/install/hmacBlock.ts
|
|
11027
|
+
import crypto6 from "crypto";
|
|
11028
|
+
var DEFAULT_HMAC_KEY = "ctxloom-agent-harness-v1-published";
|
|
11029
|
+
function resolveHmacKey() {
|
|
11030
|
+
return process.env.CTXLOOM_INSTALL_KEY ?? DEFAULT_HMAC_KEY;
|
|
11031
|
+
}
|
|
11032
|
+
function computeBlockHmac(content, key = resolveHmacKey()) {
|
|
11033
|
+
return crypto6.createHmac("sha256", key).update(content, "utf-8").digest("hex");
|
|
11034
|
+
}
|
|
11035
|
+
function wrapBlock(name, content) {
|
|
11036
|
+
const hmac = computeBlockHmac(content);
|
|
11037
|
+
return [
|
|
11038
|
+
`<!-- BEGIN ${name} v:1 hmac:sha256:${hmac} -->`,
|
|
11039
|
+
content,
|
|
11040
|
+
`<!-- END ${name} -->`
|
|
11041
|
+
].join("\n");
|
|
11042
|
+
}
|
|
11043
|
+
var START_RE_TEMPLATE = (name) => new RegExp(`<!-- BEGIN ${escapeRegex(name)} v:(\\d+) hmac:sha256:([0-9a-f]{64}) -->`);
|
|
11044
|
+
var END_RE_TEMPLATE = (name) => new RegExp(`<!-- END ${escapeRegex(name)} -->`);
|
|
11045
|
+
function escapeRegex(s) {
|
|
11046
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
11047
|
+
}
|
|
11048
|
+
function extractBlock(fileContent, name) {
|
|
11049
|
+
const startRe = START_RE_TEMPLATE(name);
|
|
11050
|
+
const endRe = END_RE_TEMPLATE(name);
|
|
11051
|
+
const startMatch = startRe.exec(fileContent);
|
|
11052
|
+
if (!startMatch) return null;
|
|
11053
|
+
const startIdx = startMatch.index;
|
|
11054
|
+
const afterStart = startIdx + startMatch[0].length;
|
|
11055
|
+
endRe.lastIndex = afterStart;
|
|
11056
|
+
const endMatch = endRe.exec(fileContent.slice(afterStart));
|
|
11057
|
+
if (!endMatch) return null;
|
|
11058
|
+
const endIdx = afterStart + endMatch.index + endMatch[0].length;
|
|
11059
|
+
let inner = fileContent.slice(afterStart, afterStart + endMatch.index);
|
|
11060
|
+
if (inner.startsWith("\n")) inner = inner.slice(1);
|
|
11061
|
+
if (inner.endsWith("\n")) inner = inner.slice(0, -1);
|
|
11062
|
+
return {
|
|
11063
|
+
content: inner,
|
|
11064
|
+
declaredHmac: startMatch[2],
|
|
11065
|
+
version: Number(startMatch[1]),
|
|
11066
|
+
start: startIdx,
|
|
11067
|
+
end: endIdx
|
|
11068
|
+
};
|
|
11069
|
+
}
|
|
11070
|
+
function verifyBlock(block) {
|
|
11071
|
+
return computeBlockHmac(block.content) === block.declaredHmac;
|
|
11072
|
+
}
|
|
11073
|
+
function upsertBlock(fileContent, name, newContent) {
|
|
11074
|
+
const wrapped = wrapBlock(name, newContent);
|
|
11075
|
+
const existing = extractBlock(fileContent, name);
|
|
11076
|
+
if (existing) {
|
|
11077
|
+
return fileContent.slice(0, existing.start) + wrapped + fileContent.slice(existing.end);
|
|
11078
|
+
}
|
|
11079
|
+
const sep = fileContent.length === 0 || fileContent.endsWith("\n\n") ? "" : fileContent.endsWith("\n") ? "\n" : "\n\n";
|
|
11080
|
+
return fileContent + sep + wrapped + "\n";
|
|
11081
|
+
}
|
|
11082
|
+
|
|
11083
|
+
// packages/core/src/install/skillTemplates.ts
|
|
11084
|
+
var EXPLORE_CONTENT = `---
|
|
11085
|
+
name: ctxloom-explore
|
|
11086
|
+
description: Orient yourself to an unfamiliar codebase using ctxloom's structural graph. Architecture overview + communities + top hubs in \u22645 tool calls.
|
|
11087
|
+
---
|
|
11088
|
+
|
|
11089
|
+
# Explore Codebase
|
|
11090
|
+
|
|
11091
|
+
Use this when you need to understand a codebase you haven't worked in
|
|
11092
|
+
before, or when re-orienting after time away.
|
|
11093
|
+
|
|
11094
|
+
## Steps
|
|
11095
|
+
|
|
11096
|
+
1. **Orientation anchor**: call \`ctx_get_minimal_context(task="explore this codebase")\`.
|
|
11097
|
+
The response includes graph stats, top hubs, and a
|
|
11098
|
+
\`suggested_first_tool\` \u2014 follow it (likely
|
|
11099
|
+
\`ctx_architecture_overview\`).
|
|
11100
|
+
|
|
11101
|
+
2. **Architecture overview**: call \`ctx_architecture_overview(max_response_tokens=2000)\`.
|
|
11102
|
+
Returns the community structure + hub nodes + cross-community bridges.
|
|
11103
|
+
|
|
11104
|
+
3. **Drill into the biggest communities**: from the overview's
|
|
11105
|
+
\`meta.next_tool_suggestions\`, call \`ctx_community_list\` for the
|
|
11106
|
+
top 1\u20132 communities. Skip communities labeled "tests" or
|
|
11107
|
+
"config" \u2014 they're usually peripheral.
|
|
11108
|
+
|
|
11109
|
+
4. **Investigate the architectural bridges**: call \`ctx_bridge_nodes\`.
|
|
11110
|
+
Bridge nodes are high-leverage \u2014 changing one affects multiple
|
|
11111
|
+
communities. Read these first to understand the codebase's
|
|
11112
|
+
coupling story.
|
|
11113
|
+
|
|
11114
|
+
5. **Tour the hubs**: call \`ctx_hub_nodes(limit=5, detail_level="minimal")\`.
|
|
11115
|
+
Top 5 most-depended-upon files. These are usually the heart of
|
|
11116
|
+
the codebase.
|
|
11117
|
+
|
|
11118
|
+
## Budget
|
|
11119
|
+
|
|
11120
|
+
- \u22645 ctxloom tool calls
|
|
11121
|
+
- \u22642000 tokens total response budget
|
|
11122
|
+
- Don't \`ctx_get_file\` anything during exploration \u2014 signatures are
|
|
11123
|
+
enough. Drop to file reads only if a specific symbol needs
|
|
11124
|
+
inspection (and only via \`ctx_get_definition\`, not raw read).
|
|
11125
|
+
|
|
11126
|
+
## Output
|
|
11127
|
+
|
|
11128
|
+
Summarize for the user:
|
|
11129
|
+
- Main communities (3\u20135 named clusters)
|
|
11130
|
+
- Top hubs by fan-in (the load-bearing files)
|
|
11131
|
+
- Top bridges (the architectural seams)
|
|
11132
|
+
- Recommended deep-dive starting points
|
|
11133
|
+
`;
|
|
11134
|
+
var BLAST_CONTENT = `---
|
|
11135
|
+
name: ctxloom-blast
|
|
11136
|
+
description: Compute blast radius + affected execution flows for a symbol or file before changing it. Pinpoints what will break.
|
|
11137
|
+
argument-hint: "<symbol-name | file-path>"
|
|
11138
|
+
---
|
|
11139
|
+
|
|
11140
|
+
# Blast Radius
|
|
11141
|
+
|
|
11142
|
+
Use this before any change to a public function, type, or file
|
|
11143
|
+
where you're not sure who depends on it.
|
|
11144
|
+
|
|
11145
|
+
## Inputs
|
|
11146
|
+
|
|
11147
|
+
- \`$ARGUMENTS\` \u2014 the symbol name (e.g. \`emitTelemetry\`) or file
|
|
11148
|
+
path (e.g. \`src/server.ts\`) you're about to modify.
|
|
11149
|
+
|
|
11150
|
+
## Steps
|
|
11151
|
+
|
|
11152
|
+
1. **Orientation**: call \`ctx_get_minimal_context(task="blast radius for $ARGUMENTS")\`.
|
|
11153
|
+
|
|
11154
|
+
2. **Blast radius**: call \`ctx_blast_radius(target="$ARGUMENTS", max_response_tokens=1500)\`.
|
|
11155
|
+
Returns transitive dependents \u2014 every file (and indirectly,
|
|
11156
|
+
every flow) that would be affected by a breaking change.
|
|
11157
|
+
|
|
11158
|
+
3. **Caller graph**: call \`ctx_get_call_graph(symbol="$ARGUMENTS", direction="callers", depth=2)\`.
|
|
11159
|
+
Direct + grandparent callers. Pair with the blast radius
|
|
11160
|
+
to distinguish "many transitive deps" from "load-bearing
|
|
11161
|
+
direct API."
|
|
11162
|
+
|
|
11163
|
+
4. **Affected execution flows**: call \`ctx_get_affected_flows(target="$ARGUMENTS")\`.
|
|
11164
|
+
Maps the change to ordered execution sequences \u2014 useful for
|
|
11165
|
+
debugging "what user-facing path breaks?"
|
|
11166
|
+
|
|
11167
|
+
5. **Test coverage check**: call \`ctx_knowledge_gaps(scope="$ARGUMENTS")\`.
|
|
11168
|
+
Highlights affected files lacking test coverage \u2014 those are the
|
|
11169
|
+
real risk surface.
|
|
11170
|
+
|
|
11171
|
+
## Budget
|
|
11172
|
+
|
|
11173
|
+
- \u22645 ctxloom tool calls
|
|
11174
|
+
- \u22642000 tokens total
|
|
11175
|
+
|
|
11176
|
+
## Output
|
|
11177
|
+
|
|
11178
|
+
Report to the user:
|
|
11179
|
+
- Total transitive dependents (number + top 5 by depth)
|
|
11180
|
+
- Direct callers (the API consumers)
|
|
11181
|
+
- Affected execution flows (named user-facing paths)
|
|
11182
|
+
- Coverage gaps on the affected files (the risk surface)
|
|
11183
|
+
- Recommendation: "safe to change" / "review carefully" / "needs migration plan"
|
|
11184
|
+
`;
|
|
11185
|
+
var REFACTOR_CONTENT = `---
|
|
11186
|
+
name: ctxloom-refactor-safely
|
|
11187
|
+
description: Plan and execute a rename or signature change with full caller-aware safety. Preview before applying.
|
|
11188
|
+
argument-hint: "<old-name> <new-name>"
|
|
11189
|
+
---
|
|
11190
|
+
|
|
11191
|
+
# Refactor Safely
|
|
11192
|
+
|
|
11193
|
+
Use this for renames, signature changes, or function moves. The
|
|
11194
|
+
skill enforces preview-before-apply.
|
|
11195
|
+
|
|
11196
|
+
## Inputs
|
|
11197
|
+
|
|
11198
|
+
- \`$1\` \u2014 current symbol name (e.g. \`emitTelemetry\`)
|
|
11199
|
+
- \`$2\` \u2014 target name (e.g. \`emitTelemetryEvent\`)
|
|
11200
|
+
|
|
11201
|
+
## Steps
|
|
11202
|
+
|
|
11203
|
+
1. **Orientation**: call \`ctx_get_minimal_context(task="refactor $1 to $2")\`.
|
|
11204
|
+
|
|
11205
|
+
2. **Surface every caller**: call \`ctx_get_call_graph(symbol="$1", direction="callers", depth=1)\`.
|
|
11206
|
+
The exhaustive caller list. Without this, a rename can break
|
|
11207
|
+
files you didn't notice depended on the symbol.
|
|
11208
|
+
|
|
11209
|
+
3. **Blast radius**: call \`ctx_blast_radius(target="$1")\`.
|
|
11210
|
+
Transitive impact \u2014 useful to gauge whether this should be a
|
|
11211
|
+
single PR or split with a deprecation period.
|
|
11212
|
+
|
|
11213
|
+
4. **Generate the refactor preview**: call \`ctx_refactor_preview(symbol="$1", new_name="$2")\`.
|
|
11214
|
+
Returns a diff preview WITHOUT writing anything. Inspect it.
|
|
11215
|
+
|
|
11216
|
+
5. **Confirm with the user**: show the preview summary
|
|
11217
|
+
(N files changed, M call sites updated). **DO NOT proceed to
|
|
11218
|
+
step 6 without explicit user confirmation.**
|
|
11219
|
+
|
|
11220
|
+
6. **Apply the refactor**: call \`ctx_apply_refactor(symbol="$1", new_name="$2")\`.
|
|
11221
|
+
This writes the changes to disk. Irreversible without a git
|
|
11222
|
+
reset.
|
|
11223
|
+
|
|
11224
|
+
7. **Verify**: call \`ctx_detect_changes\` and confirm the diff
|
|
11225
|
+
matches what the preview said. Surface any unexpected changes.
|
|
11226
|
+
|
|
11227
|
+
## Safety rails
|
|
11228
|
+
|
|
11229
|
+
- ALWAYS run step 4 (preview) before step 6 (apply)
|
|
11230
|
+
- ALWAYS ask the user for confirmation between preview and apply
|
|
11231
|
+
- If preview shows >50 files affected, recommend splitting into
|
|
11232
|
+
a deprecation-style migration instead of a single rename
|
|
11233
|
+
- If the symbol is exported from a public API package, refuse to
|
|
11234
|
+
proceed and recommend the user open a tracked migration plan
|
|
11235
|
+
|
|
11236
|
+
## Budget
|
|
11237
|
+
|
|
11238
|
+
- \u22647 ctxloom tool calls
|
|
11239
|
+
- \u22643000 tokens total (preview output can be larger than other skills)
|
|
11240
|
+
`;
|
|
11241
|
+
var COVERAGE_GAP_CONTENT = `---
|
|
11242
|
+
name: ctxloom-coverage-gap
|
|
11243
|
+
description: Identify code that lacks test coverage, prioritized by caller frequency and risk.
|
|
11244
|
+
---
|
|
11245
|
+
|
|
11246
|
+
# Coverage Gap Analysis
|
|
11247
|
+
|
|
11248
|
+
Use this to find untested code that genuinely matters \u2014 the
|
|
11249
|
+
intersection of "no tests" + "many callers" + "high risk score."
|
|
11250
|
+
|
|
11251
|
+
## Steps
|
|
11252
|
+
|
|
11253
|
+
1. **Orientation**: call \`ctx_get_minimal_context(task="check test coverage")\`.
|
|
11254
|
+
|
|
11255
|
+
2. **Knowledge gaps**: call \`ctx_knowledge_gaps(max_response_tokens=1200)\`.
|
|
11256
|
+
Lists every file lacking a \`tests_for\` graph edge. Raw list
|
|
11257
|
+
without prioritization.
|
|
11258
|
+
|
|
11259
|
+
3. **Score by impact**: for each gap, call
|
|
11260
|
+
\`ctx_get_call_graph(symbol=<gap_symbol>, direction="callers")\`
|
|
11261
|
+
to count callers. High caller-count + no tests = high priority.
|
|
11262
|
+
|
|
11263
|
+
4. **Cross-reference with churn**: call
|
|
11264
|
+
\`ctx_git_coupling(file=<gap_file>)\` for the top 5 gaps.
|
|
11265
|
+
Files churning often without tests are the urgent ones.
|
|
11266
|
+
|
|
11267
|
+
5. **Risk overlay**: call \`ctx_risk_overlay(scope=<top_gap_files>)\`.
|
|
11268
|
+
Combines churn + coupling into a single risk score.
|
|
11269
|
+
|
|
11270
|
+
## Output
|
|
11271
|
+
|
|
11272
|
+
Tabular report:
|
|
11273
|
+
|
|
11274
|
+
\`\`\`
|
|
11275
|
+
| File / Symbol | Callers | Churn | Risk | Recommendation |
|
|
11276
|
+
|---|---|---|---|---|
|
|
11277
|
+
| ... | ... | ... | ... | ... |
|
|
11278
|
+
\`\`\`
|
|
11279
|
+
|
|
11280
|
+
Recommendations should distinguish:
|
|
11281
|
+
- "Add tests now" (high caller count + high churn)
|
|
11282
|
+
- "Add tests during next change" (high caller count, low churn)
|
|
11283
|
+
- "Acceptable gap" (low caller count, low churn)
|
|
11284
|
+
|
|
11285
|
+
## Budget
|
|
11286
|
+
|
|
11287
|
+
- \u22646 ctxloom tool calls
|
|
11288
|
+
- \u22642500 tokens total
|
|
11289
|
+
`;
|
|
11290
|
+
var REVIEW_PR_CONTENT = `---
|
|
11291
|
+
name: ctxloom-review-pr
|
|
11292
|
+
description: Multi-tier code review of a PR using ctxloom's structural graph. Risk-scored, blast-radius-aware, coverage-conscious.
|
|
11293
|
+
argument-hint: "<PR number | branch name>"
|
|
11294
|
+
---
|
|
11295
|
+
|
|
11296
|
+
# Review PR
|
|
11297
|
+
|
|
11298
|
+
Comprehensive PR review using ctxloom's graph. Mirrors the
|
|
11299
|
+
multi-agent review the ctxloom-bot posts automatically \u2014 useful
|
|
11300
|
+
when reviewing manually or when the bot isn't wired up.
|
|
11301
|
+
|
|
11302
|
+
## Inputs
|
|
11303
|
+
|
|
11304
|
+
- \`$ARGUMENTS\` \u2014 PR number (e.g. \`142\`) or branch name (e.g. \`feat/foo\`).
|
|
11305
|
+
Defaults to the current branch if unset.
|
|
11306
|
+
|
|
11307
|
+
## Steps
|
|
11308
|
+
|
|
11309
|
+
1. **Orientation**: call \`ctx_get_minimal_context(task="review PR $ARGUMENTS")\`.
|
|
11310
|
+
|
|
11311
|
+
2. **Detect changes**: call \`ctx_detect_changes(base="main")\`.
|
|
11312
|
+
Risk-scored per-file analysis. Take the top 5 highest-risk files.
|
|
11313
|
+
|
|
11314
|
+
3. **Pull source for the risky files**: call \`ctx_git_diff_review(base="main", max_response_tokens=4000)\`.
|
|
11315
|
+
Token-efficient diff packet covering the changed files.
|
|
11316
|
+
|
|
11317
|
+
4. **Blast radius per high-risk file**: for the top 3 risky files,
|
|
11318
|
+
call \`ctx_blast_radius(target=<file>)\`. Surfaces files that the
|
|
11319
|
+
change indirectly affects but don't appear in the diff.
|
|
11320
|
+
|
|
11321
|
+
5. **Affected flows**: call \`ctx_get_affected_flows(base="main")\`.
|
|
11322
|
+
Which execution paths the PR touches. Use to identify what
|
|
11323
|
+
integration tests should pass.
|
|
11324
|
+
|
|
11325
|
+
6. **Coverage check**: call \`ctx_knowledge_gaps(scope=<changed_files>)\`.
|
|
11326
|
+
Surface changed files lacking tests.
|
|
11327
|
+
|
|
11328
|
+
7. **Generate the review**: structured output with:
|
|
11329
|
+
- Risk summary (low/medium/high overall)
|
|
11330
|
+
- File-by-file findings (severity-ranked)
|
|
11331
|
+
- Coverage gaps that need addressing
|
|
11332
|
+
- Blast-radius observations the diff doesn't show
|
|
11333
|
+
|
|
11334
|
+
## Tier discipline
|
|
11335
|
+
|
|
11336
|
+
This skill is the agent-driven equivalent of the bot's
|
|
11337
|
+
multi-specialist review. Use the same tier ladder:
|
|
11338
|
+
|
|
11339
|
+
- **T0 (structural)**: use the tools listed above \u2014 they're
|
|
11340
|
+
pre-fetched and cheap
|
|
11341
|
+
- **T1 (skeleton)**: \`ctx_get_definition\` for individual symbols
|
|
11342
|
+
- **T2 (full body)**: \`ctx_get_file\` only for files where the
|
|
11343
|
+
skeleton view is insufficient
|
|
11344
|
+
- **T3 (raw read)**: avoid; if the graph can't answer the question,
|
|
11345
|
+
prefer \`ctx_git_diff_review\` (token-efficient diff packet) over
|
|
11346
|
+
raw \`Read\`
|
|
11347
|
+
|
|
11348
|
+
## Budget
|
|
11349
|
+
|
|
11350
|
+
- \u22648 ctxloom tool calls
|
|
11351
|
+
- \u22645000 tokens total (review needs more headroom than other skills)
|
|
11352
|
+
|
|
11353
|
+
## Output format
|
|
11354
|
+
|
|
11355
|
+
\`\`\`
|
|
11356
|
+
## PR Review: <title>
|
|
11357
|
+
|
|
11358
|
+
### Summary
|
|
11359
|
+
<1\u20133 sentence overview>
|
|
11360
|
+
|
|
11361
|
+
### Risk Assessment
|
|
11362
|
+
- Overall: Low / Medium / High
|
|
11363
|
+
- Blast radius: X files, Y flows impacted
|
|
11364
|
+
- Coverage: N changed symbols covered / M total
|
|
11365
|
+
|
|
11366
|
+
### Findings
|
|
11367
|
+
|
|
11368
|
+
#### <file_path>
|
|
11369
|
+
- **Severity**: ...
|
|
11370
|
+
- **Issue**: ...
|
|
11371
|
+
- **Suggested fix**: ...
|
|
11372
|
+
|
|
11373
|
+
### Coverage Gaps
|
|
11374
|
+
|
|
11375
|
+
<table>
|
|
11376
|
+
|
|
11377
|
+
### Suggested follow-ups
|
|
11378
|
+
|
|
11379
|
+
<list>
|
|
11380
|
+
\`\`\`
|
|
11381
|
+
`;
|
|
11382
|
+
var BUDGET_STATS_CONTENT = `---
|
|
11383
|
+
name: ctxloom-budget-stats
|
|
11384
|
+
description: Inspect ctxloom's per-tool budget telemetry \u2014 fallback distribution + original-token p50/p75/p95 \u2014 to tune defaults from real usage.
|
|
11385
|
+
---
|
|
11386
|
+
|
|
11387
|
+
# Budget Stats
|
|
11388
|
+
|
|
11389
|
+
Wrapper around \`ctxloom budget-stats\` for inline use inside a
|
|
11390
|
+
Claude Code session. Useful when:
|
|
11391
|
+
|
|
11392
|
+
- Tuning per-tool \`DEFAULT_MAX_RESPONSE_TOKENS\` from real usage
|
|
11393
|
+
(the Phase B follow-up)
|
|
11394
|
+
- Diagnosing why a tool keeps falling back to skeleton mode
|
|
11395
|
+
- Understanding which tools dominate the user's token budget
|
|
11396
|
+
|
|
11397
|
+
## Steps
|
|
11398
|
+
|
|
11399
|
+
1. **Orientation**: call \`ctx_get_minimal_context(task="inspect budget telemetry")\`.
|
|
11400
|
+
Cheap (~150 tokens). Confirms the graph is wired up \u2014 if it's
|
|
11401
|
+
not, the user's MCP server probably can't emit budget events
|
|
11402
|
+
either, and the stats will be empty.
|
|
11403
|
+
|
|
11404
|
+
3. **Window selection**: ask the user how far back to look
|
|
11405
|
+
(default: 14d). Accept \`1d\`, \`7d\`, \`14d\`, \`30d\`.
|
|
11406
|
+
|
|
11407
|
+
4. **Optional tool filter**: ask if they want stats for a specific
|
|
11408
|
+
tool (e.g. \`ctx_get_file\`) or all tools.
|
|
11409
|
+
|
|
11410
|
+
5. **Run the CLI**: \`Bash\`-tool execute:
|
|
11411
|
+
\`ctxloom budget-stats --window=<N>d [--tool=<name>]\`
|
|
11412
|
+
|
|
11413
|
+
6. **Parse + summarize the output**:
|
|
11414
|
+
- Top 3 tools by breach count \u2192 these are the candidates for
|
|
11415
|
+
budget tuning
|
|
11416
|
+
- For each top tool, the p75 column is the suggested next
|
|
11417
|
+
\`DEFAULT_MAX_RESPONSE_TOKENS\` value (rationale: 75% of
|
|
11418
|
+
real-world calls fit under p75; the rest fall back gracefully
|
|
11419
|
+
to skeletons)
|
|
11420
|
+
|
|
11421
|
+
7. **Suggest concrete edits**: for each top tool, point to the
|
|
11422
|
+
source file (\`packages/core/src/tools/<tool>.ts\`) and the
|
|
11423
|
+
current constant. Don't apply edits without user confirmation.
|
|
11424
|
+
|
|
11425
|
+
## Budget
|
|
11426
|
+
|
|
11427
|
+
- \u22642 ctxloom tool calls (this skill is mostly Bash + parsing)
|
|
11428
|
+
- \u22641500 tokens response total
|
|
11429
|
+
|
|
11430
|
+
## Output
|
|
11431
|
+
|
|
11432
|
+
\`\`\`
|
|
11433
|
+
## Budget stats \u2014 <window>
|
|
11434
|
+
|
|
11435
|
+
### Top tools by breach count
|
|
11436
|
+
1. <tool>: N breaches, skeleton%, p75=<tokens>
|
|
11437
|
+
2. ...
|
|
11438
|
+
|
|
11439
|
+
### Suggested DEFAULT_MAX_RESPONSE_TOKENS tuning
|
|
11440
|
+
- packages/core/src/tools/<tool>.ts: <current> \u2192 <suggested-p75>
|
|
11441
|
+
(rationale: ...)
|
|
11442
|
+
\`\`\`
|
|
11443
|
+
`;
|
|
11444
|
+
var CTXLOOM_SKILLS = [
|
|
11445
|
+
{ name: "ctxloom-explore", content: EXPLORE_CONTENT },
|
|
11446
|
+
{ name: "ctxloom-blast", content: BLAST_CONTENT },
|
|
11447
|
+
{ name: "ctxloom-refactor-safely", content: REFACTOR_CONTENT },
|
|
11448
|
+
{ name: "ctxloom-coverage-gap", content: COVERAGE_GAP_CONTENT },
|
|
11449
|
+
{ name: "ctxloom-review-pr", content: REVIEW_PR_CONTENT },
|
|
11450
|
+
{ name: "ctxloom-budget-stats", content: BUDGET_STATS_CONTENT }
|
|
11451
|
+
];
|
|
11452
|
+
function skillFilePath(name) {
|
|
11453
|
+
return `.claude/skills/${name}/SKILL.md`;
|
|
11454
|
+
}
|
|
11455
|
+
|
|
11456
|
+
// packages/core/src/install/installer.ts
|
|
11457
|
+
function installHarness(opts = {}) {
|
|
11458
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
11459
|
+
const projectRoot = path36.resolve(cwd);
|
|
11460
|
+
const stat = fs28.statSync(projectRoot);
|
|
11461
|
+
if (!stat.isDirectory()) {
|
|
11462
|
+
throw new Error(`installHarness: ${projectRoot} is not a directory`);
|
|
11463
|
+
}
|
|
11464
|
+
const dryRun = opts.dryRun === true;
|
|
11465
|
+
const force = opts.force === true;
|
|
11466
|
+
const warnings = [];
|
|
11467
|
+
const claudeMd = writeRulesBlock(projectRoot, "CLAUDE.md", { dryRun, force, warnings });
|
|
11468
|
+
const agentsMd = writeRulesBlock(projectRoot, "AGENTS.md", { dryRun, force, warnings });
|
|
11469
|
+
const geminiMd = writeRulesBlock(projectRoot, "GEMINI.md", { dryRun, force, warnings });
|
|
11470
|
+
const hooksJson = writeHooksJson(projectRoot, { dryRun, warnings });
|
|
11471
|
+
const sessionStartSh = writeSessionStartScript(projectRoot, { dryRun });
|
|
11472
|
+
const skills = CTXLOOM_SKILLS.map((s) => writeSkill(projectRoot, s, { dryRun }));
|
|
11473
|
+
const extraHosts = resolveExtraHosts(opts.extraHosts ?? [], warnings).map(
|
|
11474
|
+
(adapter) => Object.assign(writeHostAdapter(projectRoot, adapter, { dryRun }), { hostId: adapter.id })
|
|
11475
|
+
);
|
|
11476
|
+
return {
|
|
11477
|
+
projectRoot,
|
|
11478
|
+
claudeMd,
|
|
11479
|
+
agentsMd,
|
|
11480
|
+
geminiMd,
|
|
11481
|
+
hooksJson,
|
|
11482
|
+
sessionStartSh,
|
|
11483
|
+
skills,
|
|
11484
|
+
extraHosts,
|
|
11485
|
+
warnings
|
|
11486
|
+
};
|
|
11487
|
+
}
|
|
11488
|
+
function resolveExtraHosts(ids, warnings) {
|
|
11489
|
+
const requested = /* @__PURE__ */ new Set();
|
|
11490
|
+
for (const raw of ids) {
|
|
11491
|
+
const id = raw.trim().toLowerCase();
|
|
11492
|
+
if (id === "") continue;
|
|
11493
|
+
if (id === "all") {
|
|
11494
|
+
for (const a of HOST_ADAPTERS) requested.add(a.id);
|
|
11495
|
+
continue;
|
|
11496
|
+
}
|
|
11497
|
+
if (!getHostAdapter(id)) {
|
|
11498
|
+
warnings.push(
|
|
11499
|
+
`Unknown --host id "${id}" \u2014 ignored. Supported: ${HOST_ADAPTERS.map((a) => a.id).join(", ")}, or "all".`
|
|
11500
|
+
);
|
|
11501
|
+
continue;
|
|
11502
|
+
}
|
|
11503
|
+
requested.add(id);
|
|
11504
|
+
}
|
|
11505
|
+
return HOST_ADAPTERS.filter((a) => requested.has(a.id));
|
|
11506
|
+
}
|
|
11507
|
+
function safeJoin(root, name) {
|
|
11508
|
+
const target = path36.resolve(root, name);
|
|
11509
|
+
const rootResolved = path36.resolve(root);
|
|
11510
|
+
if (!target.startsWith(rootResolved + path36.sep) && target !== rootResolved) {
|
|
11511
|
+
throw new Error(`installHarness: refusing to write outside project root: ${target}`);
|
|
11512
|
+
}
|
|
11513
|
+
return target;
|
|
11514
|
+
}
|
|
11515
|
+
function writeRulesBlock(projectRoot, filename, opts) {
|
|
11516
|
+
const filePath = safeJoin(projectRoot, filename);
|
|
11517
|
+
const existed = fs28.existsSync(filePath);
|
|
11518
|
+
const existing = existed ? fs28.readFileSync(filePath, "utf-8") : "";
|
|
11519
|
+
if (existed) {
|
|
11520
|
+
const block = extractBlock(existing, RULES_BLOCK_NAME);
|
|
11521
|
+
if (block) {
|
|
11522
|
+
const intact = verifyBlock(block);
|
|
11523
|
+
if (intact && block.content === RULES_BLOCK_CONTENT) {
|
|
11524
|
+
return { path: filePath, created: false, updated: false, alreadyCorrect: true, dryRun: opts.dryRun };
|
|
11525
|
+
}
|
|
11526
|
+
if (!intact && !opts.force) {
|
|
11527
|
+
opts.warnings.push(
|
|
11528
|
+
`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.`
|
|
11529
|
+
);
|
|
11530
|
+
return { path: filePath, created: false, updated: false, alreadyCorrect: false, dryRun: opts.dryRun };
|
|
11531
|
+
}
|
|
11532
|
+
}
|
|
11533
|
+
}
|
|
11534
|
+
const next = upsertBlock(existing, RULES_BLOCK_NAME, RULES_BLOCK_CONTENT);
|
|
11535
|
+
if (!opts.dryRun) {
|
|
11536
|
+
fs28.writeFileSync(filePath, next, "utf-8");
|
|
11537
|
+
}
|
|
11538
|
+
return {
|
|
11539
|
+
path: filePath,
|
|
11540
|
+
created: !existed,
|
|
11541
|
+
updated: existed,
|
|
11542
|
+
alreadyCorrect: false,
|
|
11543
|
+
dryRun: opts.dryRun
|
|
11544
|
+
};
|
|
11545
|
+
}
|
|
11546
|
+
function writeHooksJson(projectRoot, opts) {
|
|
11547
|
+
const dir = safeJoin(projectRoot, ".claude");
|
|
11548
|
+
const filePath = safeJoin(projectRoot, ".claude/hooks.json");
|
|
11549
|
+
const existed = fs28.existsSync(filePath);
|
|
11550
|
+
let current = {};
|
|
11551
|
+
if (existed) {
|
|
11552
|
+
try {
|
|
11553
|
+
const text = fs28.readFileSync(filePath, "utf-8");
|
|
11554
|
+
current = JSON.parse(text);
|
|
11555
|
+
} catch (err) {
|
|
11556
|
+
opts.warnings.push(
|
|
11557
|
+
`Could not parse existing ${path36.relative(projectRoot, filePath)}; treating as empty. (${err instanceof Error ? err.message : String(err)})`
|
|
11558
|
+
);
|
|
11559
|
+
current = {};
|
|
11560
|
+
}
|
|
11561
|
+
}
|
|
11562
|
+
const merged = { ...current };
|
|
11563
|
+
for (const event of ["SessionStart", "PostToolUse"]) {
|
|
11564
|
+
const incoming = CTXLOOM_HOOK_ENTRIES[event];
|
|
11565
|
+
const existingArr = Array.isArray(merged[event]) ? merged[event] : [];
|
|
11566
|
+
const filtered = existingArr.filter(
|
|
11567
|
+
(entry) => !isCtxloomEntry(entry, incoming.matcher)
|
|
11568
|
+
);
|
|
11569
|
+
merged[event] = [...filtered, incoming];
|
|
11570
|
+
}
|
|
11571
|
+
const nextJson = JSON.stringify(merged, null, 2) + "\n";
|
|
11572
|
+
let alreadyCorrect = false;
|
|
11573
|
+
if (existed) {
|
|
11574
|
+
const currentText = fs28.readFileSync(filePath, "utf-8");
|
|
11575
|
+
if (currentText === nextJson) alreadyCorrect = true;
|
|
11576
|
+
}
|
|
11577
|
+
if (!opts.dryRun && !alreadyCorrect) {
|
|
11578
|
+
fs28.mkdirSync(dir, { recursive: true });
|
|
11579
|
+
fs28.writeFileSync(filePath, nextJson, "utf-8");
|
|
11580
|
+
}
|
|
11581
|
+
return {
|
|
11582
|
+
path: filePath,
|
|
11583
|
+
created: !existed,
|
|
11584
|
+
updated: existed && !alreadyCorrect,
|
|
11585
|
+
alreadyCorrect,
|
|
11586
|
+
dryRun: opts.dryRun
|
|
11587
|
+
};
|
|
11588
|
+
}
|
|
11589
|
+
function isCtxloomEntry(entry, expectedMatcher) {
|
|
11590
|
+
if (entry.matcher !== expectedMatcher) return false;
|
|
11591
|
+
if (!Array.isArray(entry.hooks)) return false;
|
|
11592
|
+
return entry.hooks.some((h) => {
|
|
11593
|
+
if (!h || typeof h !== "object") return false;
|
|
11594
|
+
const cmd = h.command;
|
|
11595
|
+
if (typeof cmd !== "string") return false;
|
|
11596
|
+
return cmd.includes("ctxloom") || cmd.includes(".claude/hooks/session-start.sh");
|
|
11597
|
+
});
|
|
11598
|
+
}
|
|
11599
|
+
function writeHostAdapter(projectRoot, adapter, opts) {
|
|
11600
|
+
const filePath = safeJoin(projectRoot, adapter.path);
|
|
11601
|
+
const dir = path36.dirname(filePath);
|
|
11602
|
+
const existed = fs28.existsSync(filePath);
|
|
11603
|
+
const rendered = adapter.render();
|
|
11604
|
+
let alreadyCorrect = false;
|
|
11605
|
+
if (existed) {
|
|
11606
|
+
const current = fs28.readFileSync(filePath, "utf-8");
|
|
11607
|
+
if (adapter.isCanonical(current)) {
|
|
11608
|
+
alreadyCorrect = true;
|
|
11609
|
+
}
|
|
11610
|
+
}
|
|
11611
|
+
if (!opts.dryRun && !alreadyCorrect) {
|
|
11612
|
+
fs28.mkdirSync(dir, { recursive: true });
|
|
11613
|
+
fs28.writeFileSync(filePath, rendered, "utf-8");
|
|
11614
|
+
}
|
|
11615
|
+
return {
|
|
11616
|
+
path: filePath,
|
|
11617
|
+
created: !existed,
|
|
11618
|
+
updated: existed && !alreadyCorrect,
|
|
11619
|
+
alreadyCorrect,
|
|
11620
|
+
dryRun: opts.dryRun
|
|
11621
|
+
};
|
|
11622
|
+
}
|
|
11623
|
+
function writeSkill(projectRoot, skill, opts) {
|
|
11624
|
+
const dir = safeJoin(projectRoot, `.claude/skills/${skill.name}`);
|
|
11625
|
+
const filePath = safeJoin(projectRoot, skillFilePath(skill.name));
|
|
11626
|
+
const existed = fs28.existsSync(filePath);
|
|
11627
|
+
let alreadyCorrect = false;
|
|
11628
|
+
if (existed) {
|
|
11629
|
+
if (fs28.readFileSync(filePath, "utf-8") === skill.content) {
|
|
11630
|
+
alreadyCorrect = true;
|
|
11631
|
+
}
|
|
11632
|
+
}
|
|
11633
|
+
if (!opts.dryRun && !alreadyCorrect) {
|
|
11634
|
+
fs28.mkdirSync(dir, { recursive: true });
|
|
11635
|
+
fs28.writeFileSync(filePath, skill.content, "utf-8");
|
|
11636
|
+
}
|
|
11637
|
+
return {
|
|
11638
|
+
path: filePath,
|
|
11639
|
+
created: !existed,
|
|
11640
|
+
updated: existed && !alreadyCorrect,
|
|
11641
|
+
alreadyCorrect,
|
|
11642
|
+
dryRun: opts.dryRun
|
|
11643
|
+
};
|
|
11644
|
+
}
|
|
11645
|
+
function writeSessionStartScript(projectRoot, opts) {
|
|
11646
|
+
const dir = safeJoin(projectRoot, ".claude/hooks");
|
|
11647
|
+
const filePath = safeJoin(projectRoot, ".claude/hooks/session-start.sh");
|
|
11648
|
+
const existed = fs28.existsSync(filePath);
|
|
11649
|
+
let alreadyCorrect = false;
|
|
11650
|
+
if (existed) {
|
|
11651
|
+
const current = fs28.readFileSync(filePath, "utf-8");
|
|
11652
|
+
if (current === SESSION_START_FULL) alreadyCorrect = true;
|
|
11653
|
+
}
|
|
11654
|
+
if (!opts.dryRun && !alreadyCorrect) {
|
|
11655
|
+
fs28.mkdirSync(dir, { recursive: true });
|
|
11656
|
+
fs28.writeFileSync(filePath, SESSION_START_FULL, "utf-8");
|
|
11657
|
+
try {
|
|
11658
|
+
fs28.chmodSync(filePath, 493);
|
|
11659
|
+
} catch {
|
|
11660
|
+
}
|
|
11661
|
+
}
|
|
11662
|
+
return {
|
|
11663
|
+
path: filePath,
|
|
11664
|
+
created: !existed,
|
|
11665
|
+
updated: existed && !alreadyCorrect,
|
|
11666
|
+
alreadyCorrect,
|
|
11667
|
+
dryRun: opts.dryRun
|
|
11668
|
+
};
|
|
11669
|
+
}
|
|
11670
|
+
|
|
9989
11671
|
export {
|
|
9990
11672
|
GRAMMAR_MANIFEST,
|
|
9991
11673
|
findGrammar,
|
|
@@ -10023,6 +11705,15 @@ export {
|
|
|
10023
11705
|
loadFileRiskHistory,
|
|
10024
11706
|
Skeletonizer,
|
|
10025
11707
|
renderStatusXml,
|
|
11708
|
+
__resetLearnedSuggestionsCacheForTests,
|
|
11709
|
+
learnSuggestionsFromTelemetry,
|
|
11710
|
+
getLearnedRules,
|
|
11711
|
+
TaskBudgetTracker,
|
|
11712
|
+
getTaskBudgetTracker,
|
|
11713
|
+
__resetTaskBudgetTrackerForTests,
|
|
11714
|
+
OVER_BUDGET_ARG_OVERRIDES,
|
|
11715
|
+
applyOverBudgetOverrides,
|
|
11716
|
+
emitTaskBudgetBreached,
|
|
10026
11717
|
ToolRegistry,
|
|
10027
11718
|
detectChanges,
|
|
10028
11719
|
getImpactRadius,
|
|
@@ -10090,6 +11781,23 @@ export {
|
|
|
10090
11781
|
noParseableSourcesWarning,
|
|
10091
11782
|
wrapWithIndexingEnvelope,
|
|
10092
11783
|
FirstTouchTracker,
|
|
10093
|
-
EmittedOnceTracker
|
|
11784
|
+
EmittedOnceTracker,
|
|
11785
|
+
RULES_BLOCK_NAME,
|
|
11786
|
+
RULES_BLOCK_CONTENT,
|
|
11787
|
+
SESSION_START_FULL,
|
|
11788
|
+
CTXLOOM_HOOK_ENTRIES,
|
|
11789
|
+
HOST_ADAPTERS,
|
|
11790
|
+
getHostAdapter,
|
|
11791
|
+
SUPPORTED_HOST_IDS,
|
|
11792
|
+
DEFAULT_HMAC_KEY,
|
|
11793
|
+
resolveHmacKey,
|
|
11794
|
+
computeBlockHmac,
|
|
11795
|
+
wrapBlock,
|
|
11796
|
+
extractBlock,
|
|
11797
|
+
verifyBlock,
|
|
11798
|
+
upsertBlock,
|
|
11799
|
+
CTXLOOM_SKILLS,
|
|
11800
|
+
skillFilePath,
|
|
11801
|
+
installHarness
|
|
10094
11802
|
};
|
|
10095
|
-
//# sourceMappingURL=chunk-
|
|
11803
|
+
//# sourceMappingURL=chunk-J2NLNQ4I.js.map
|