gitnexus 1.6.13-rc.33 → 1.6.13-rc.35
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 +30 -2
- package/dist/cli/editor-targets.d.ts +1 -1
- package/dist/cli/editor-targets.js +12 -0
- package/dist/cli/i18n/en.d.ts +1 -1
- package/dist/cli/i18n/en.js +1 -1
- package/dist/cli/i18n/resources.d.ts +1 -1
- package/dist/cli/i18n/zh-CN.js +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/setup.js +57 -0
- package/dist/mcp/local/query-process-attaches.d.ts +2 -0
- package/dist/mcp/local/query-process-attaches.js +15 -1
- package/dist/mcp/server.js +1 -1
- package/dist/mcp/tools.js +3 -3
- package/hooks/claude/hook-db-lock-probe.cjs +74 -43
- package/hooks/claude/hook-lock.cjs +130 -6
- package/hooks/claude/registry-query.cjs +18 -23
- package/package.json +1 -1
- package/scripts/cross-platform-tests.ts +1 -0
- package/scripts/sync-plugin-manifests.mjs +13 -0
- package/web/assets/{abnfDiagram-PAR5IXJI-nRjsvWp-.js → abnfDiagram-PAR5IXJI-CM9OPETj.js} +1 -1
- package/web/assets/{agent-Bv5z78CW.js → agent-C8bWDcKv.js} +2 -2
- package/web/assets/{architectureDiagram-2PNWDENW-56fl8SSH.js → architectureDiagram-2PNWDENW-CFGPVOFL.js} +1 -1
- package/web/assets/{chunk-32MCUW2P-xLjc0mcF.js → chunk-32MCUW2P-BAKWSoJ_.js} +1 -1
- package/web/assets/{chunk-CLS4B6BI-DPDLP81t.js → chunk-CLS4B6BI-C6Eyf-Zq.js} +1 -1
- package/web/assets/{chunk-NF366YI6-D2OzsNXY.js → chunk-NF366YI6-BjQUpZZg.js} +1 -1
- package/web/assets/{chunk-SLDQ463B-DUcQswS-.js → chunk-SLDQ463B-DAs7sJcm.js} +1 -1
- package/web/assets/{classDiagram-FGAMPOII-q43g3Dpn.js → classDiagram-FGAMPOII-ipJcwT2g.js} +1 -1
- package/web/assets/{classDiagram-v2-AI5RJEK4-q43g3Dpn.js → classDiagram-v2-AI5RJEK4-ipJcwT2g.js} +1 -1
- package/web/assets/{cynefinDiagram-2OOWUY4M-Cfoko1Ws.js → cynefinDiagram-2OOWUY4M-BfCkzyqC.js} +1 -1
- package/web/assets/{dagre-MPVFI544-CTob3D2r.js → dagre-MPVFI544-CMeiFj2H.js} +1 -1
- package/web/assets/{diagram-AE4NWXFW-Dt2xfADa.js → diagram-AE4NWXFW-B7JUvrjH.js} +1 -1
- package/web/assets/{diagram-BUJID5UG-D-RUCl07.js → diagram-BUJID5UG-KMWjNniB.js} +1 -1
- package/web/assets/{diagram-C44CZ5NI-B9gPH4Gl.js → diagram-C44CZ5NI-Bx93uHLb.js} +1 -1
- package/web/assets/{diagram-EYOLEXXS-wR__cI23.js → diagram-EYOLEXXS-Dsja58oR.js} +1 -1
- package/web/assets/{ebnfDiagram-FTPZ3PV6-uw79FGz3.js → ebnfDiagram-FTPZ3PV6-Iwxb9NAS.js} +1 -1
- package/web/assets/{erDiagram-H3ERR26V-ZP_VIa5d.js → erDiagram-H3ERR26V-CJU6qpcb.js} +1 -1
- package/web/assets/{flowDiagram-YHGXBVSY-DEsmG3vH.js → flowDiagram-YHGXBVSY-Dz6dhw6j.js} +1 -1
- package/web/assets/{index-DofRiYRj.js → index-dXsywuPQ.js} +7 -7
- package/web/assets/{infoDiagram-LV547AFH-D1JPzJlf.js → infoDiagram-LV547AFH-P074dR6u.js} +1 -1
- package/web/assets/{ishikawaDiagram-PUJQGNDS-DxWZMRPX.js → ishikawaDiagram-PUJQGNDS-CZlaIkzm.js} +1 -1
- package/web/assets/{kanban-definition-JSQERAZB-S1lKJN_M.js → kanban-definition-JSQERAZB-BLX38hbF.js} +1 -1
- package/web/assets/{mindmap-definition-VBJCLQLM-JwX0Gc3J.js → mindmap-definition-VBJCLQLM-Bnu-7Z_9.js} +1 -1
- package/web/assets/{node.browser-C9G7Ehhe.js → node.browser-DIc1knUO.js} +1 -1
- package/web/assets/{pegDiagram-7WILOQTJ-DJWiAJJM.js → pegDiagram-7WILOQTJ-SH-3F770.js} +1 -1
- package/web/assets/{pieDiagram-EFA7CBSB-3i7IEboS.js → pieDiagram-EFA7CBSB-BGEXuMLa.js} +1 -1
- package/web/assets/{railroadDiagram-5NIQVNLQ-CQGcjlaN.js → railroadDiagram-5NIQVNLQ-DZMK703w.js} +1 -1
- package/web/assets/{requirementDiagram-SXREGOKZ-DH-LMk5X.js → requirementDiagram-SXREGOKZ-DLW6bNTm.js} +1 -1
- package/web/assets/{sequenceDiagram-52ZFIFDL-CrUW-Kdw.js → sequenceDiagram-52ZFIFDL-Bu6IKvYQ.js} +1 -1
- package/web/assets/{stateDiagram-3QAKPIVI-BuvX8wUJ.js → stateDiagram-3QAKPIVI-BrLxhUMB.js} +1 -1
- package/web/assets/{stateDiagram-v2-HN2P4RDZ-N7DodpOh.js → stateDiagram-v2-HN2P4RDZ-yZRLNv94.js} +1 -1
- package/web/assets/{swimlanes-QLVP42QD-DpvGI6UP.js → swimlanes-QLVP42QD-BN3CFF8X.js} +1 -1
- package/web/assets/{swimlanesDiagram-JKPDBFCN-Rd-o-as4.js → swimlanesDiagram-JKPDBFCN-BQ7NwLky.js} +1 -1
- package/web/assets/{timeline-definition-GS45XAXW-B9Rw1d9t.js → timeline-definition-GS45XAXW-DMlAtIe0.js} +1 -1
- package/web/assets/{vennDiagram-QIQU2ZRJ-CGo08RCI.js → vennDiagram-QIQU2ZRJ-DWmjtOEV.js} +1 -1
- package/web/assets/{wardleyDiagram-U6YNM66N-Dk7T5hBn.js → wardleyDiagram-U6YNM66N-BRdmEKKB.js} +1 -1
- package/web/assets/{xychartDiagram-XJAZEDNM-BJr5UGSi.js → xychartDiagram-XJAZEDNM-roHne1SF.js} +1 -1
- package/web/index.html +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Graph-powered code intelligence for AI agents.** Index any codebase into a knowledge graph, then query it via MCP or CLI.
|
|
4
4
|
|
|
5
|
-
Works with **Cursor**, **Claude Code**, **Antigravity** (Google), **Codex**, **Windsurf**, **Cline**, **OpenCode**, **CodeBuddy** (Tencent), **Qoder** (Alibaba), and any MCP-compatible tool.
|
|
5
|
+
Works with **Cursor**, **Claude Code**, **Antigravity** (Google), **Codex**, **Factory** (Droid), **Windsurf**, **Cline**, **OpenCode**, **CodeBuddy** (Tencent), **Qoder** (Alibaba), and any MCP-compatible tool.
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/gitnexus)
|
|
8
8
|
[](https://polyformproject.org/licenses/noncommercial/1.0.0/)
|
|
@@ -44,12 +44,13 @@ To configure MCP for your editor, run `npx gitnexus setup` once — or set it up
|
|
|
44
44
|
| **Cursor** | Yes | Yes | Yes (postToolUse, [manual install](../gitnexus-cursor-integration/README.md#hook-install)) | **Full** |
|
|
45
45
|
| **Antigravity** (Google) | Yes | Yes | Yes (AfterTool, [Gemini CLI hooks schema](https://geminicli.com/docs/hooks/reference/)) | **Full** |
|
|
46
46
|
| **Codex** | Yes | Yes | Yes (PreToolUse + PostToolUse, [Codex hooks](https://developers.openai.com/codex/hooks)) | **Full** |
|
|
47
|
+
| **Factory** (Droid) | Yes | Yes | Yes (PostToolUse, [plugin](../gitnexus-factory-plugin/)) | **Full** |
|
|
47
48
|
| **OpenCode** | Yes | Yes | — | MCP + Skills |
|
|
48
49
|
| **CodeBuddy** (Tencent) | Yes | Yes | — | MCP + Skills |
|
|
49
50
|
| **Qoder** (Alibaba) | Yes | Yes | — | MCP + Skills |
|
|
50
51
|
| **Windsurf** | Yes | — | — | MCP |
|
|
51
52
|
|
|
52
|
-
> **
|
|
53
|
+
> **Full** means MCP tools + agent skills + hooks that enrich searches with graph context. **Claude Code** and **Codex** go deepest: their PreToolUse hooks enrich the search before it runs, and their PostToolUse hooks also detect a stale index after commits and prompt the agent to reindex. **Cursor**, **Antigravity**, and **Factory** augment from a post-tool hook only, so they enrich the result rather than the query and do not carry the stale-index hint.
|
|
53
54
|
|
|
54
55
|
### Community Integrations
|
|
55
56
|
|
|
@@ -88,6 +89,33 @@ codex plugin marketplace add abhigyanpatwari/GitNexus
|
|
|
88
89
|
|
|
89
90
|
> **Codex notes:** SessionStart is intentionally not registered — Codex reads [AGENTS.md natively](https://developers.openai.com/codex/guides/agents-md), which already carries the GitNexus context block. Newly installed hooks need a one-time approval in Codex via `/hooks` before they run. Pick **one** install route (`gitnexus setup -c codex` **or** the plugin): plugin hooks load alongside `~/.codex/hooks.json`, so installing both can fire duplicate hooks per tool call.
|
|
90
91
|
|
|
92
|
+
### Factory (Droid) (full support — MCP + skills + hooks)
|
|
93
|
+
|
|
94
|
+
`gitnexus setup -c droid` writes the MCP server to `~/.factory/mcp.json` and installs skills to
|
|
95
|
+
`~/.factory/skills/`. To configure MCP by hand instead, add to `~/.factory/mcp.json`
|
|
96
|
+
([user scope](https://docs.factory.ai/cli/configuration/mcp) — applies to all projects):
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"mcpServers": {
|
|
101
|
+
"gitnexus": {
|
|
102
|
+
"command": "npx",
|
|
103
|
+
"args": ["-y", "gitnexus@latest", "mcp"]
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
For the PostToolUse search-augment hook, install the bundled
|
|
110
|
+
[`gitnexus-factory-plugin/`](../gitnexus-factory-plugin/) with `droid plugin install gitnexus@<marketplace>`
|
|
111
|
+
from a marketplace that includes this repo, or point Droid at it via `extraKnownMarketplaces` in
|
|
112
|
+
`.factory/settings.json`.
|
|
113
|
+
|
|
114
|
+
> **Factory notes:** Droid reads [AGENTS.md natively](https://docs.factory.ai/), which already carries the
|
|
115
|
+
> GitNexus context block, so no SessionStart hook is registered. Pick **one** install route
|
|
116
|
+
> (`gitnexus setup -c droid` **or** the plugin) — the plugin ships its own MCP entry, so installing both
|
|
117
|
+
> can register the server twice.
|
|
118
|
+
|
|
91
119
|
### Cursor / Windsurf
|
|
92
120
|
|
|
93
121
|
Add to `~/.cursor/mcp.json` (global — works for all projects):
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* implementations remain behaviourally symmetrical on top of this shared
|
|
16
16
|
* structure.
|
|
17
17
|
*/
|
|
18
|
-
export type EditorId = 'cursor' | 'claude' | 'antigravity' | 'opencode' | 'codebuddy' | 'qoder' | 'codex';
|
|
18
|
+
export type EditorId = 'cursor' | 'claude' | 'antigravity' | 'opencode' | 'codebuddy' | 'qoder' | 'codex' | 'droid';
|
|
19
19
|
/** An editor whose MCP config is a JSONC document (server keyed by name). */
|
|
20
20
|
export interface McpJsoncTarget {
|
|
21
21
|
id: EditorId;
|
|
@@ -76,6 +76,15 @@ export function getEditorTargets(home = os.homedir()) {
|
|
|
76
76
|
file: path.join(home, '.qoder.json'),
|
|
77
77
|
keyPath: ['mcpServers', 'gitnexus'],
|
|
78
78
|
},
|
|
79
|
+
{
|
|
80
|
+
id: 'droid',
|
|
81
|
+
label: 'Factory Droid',
|
|
82
|
+
// Factory's user-scope MCP config (https://docs.factory.ai/cli/configuration/mcp).
|
|
83
|
+
// Same `mcpServers` object shape as Cursor/Claude; user config takes
|
|
84
|
+
// precedence over project-level .factory/mcp.json.
|
|
85
|
+
file: path.join(home, '.factory', 'mcp.json'),
|
|
86
|
+
keyPath: ['mcpServers', 'gitnexus'],
|
|
87
|
+
},
|
|
79
88
|
];
|
|
80
89
|
const codex = {
|
|
81
90
|
id: 'codex',
|
|
@@ -98,6 +107,9 @@ export function getEditorTargets(home = os.homedir()) {
|
|
|
98
107
|
{ id: 'qoder', label: 'Qoder', dir: path.join(home, '.qoder', 'skills') },
|
|
99
108
|
// Codex reads skills from ~/.agents/skills (not ~/.codex).
|
|
100
109
|
{ id: 'codex', label: 'Codex', dir: path.join(home, '.agents', 'skills') },
|
|
110
|
+
// Factory Droid reads user-scope skills from ~/.factory/skills/{name}/SKILL.md
|
|
111
|
+
// (https://docs.factory.ai/cli/configuration/skills).
|
|
112
|
+
{ id: 'droid', label: 'Factory Droid', dir: path.join(home, '.factory', 'skills') },
|
|
101
113
|
];
|
|
102
114
|
const hooks = [
|
|
103
115
|
{
|
package/dist/cli/i18n/en.d.ts
CHANGED
|
@@ -152,7 +152,7 @@ export declare const en: {
|
|
|
152
152
|
readonly 'help.command.help.description': 'display help for command';
|
|
153
153
|
readonly 'help.option.help': 'display help for command';
|
|
154
154
|
readonly 'help.option.version': 'output the version number';
|
|
155
|
-
readonly 'help.command.setup.description': 'One-time setup: configure MCP for Cursor, Claude Code, Antigravity, OpenCode, CodeBuddy, Qoder, Codex';
|
|
155
|
+
readonly 'help.command.setup.description': 'One-time setup: configure MCP for Cursor, Claude Code, Antigravity, OpenCode, CodeBuddy, Qoder, Codex, Factory Droid';
|
|
156
156
|
readonly 'help.command.uninstall.description': 'Reverse `setup`: remove GitNexus MCP entries, skills, and hooks from all detected editors';
|
|
157
157
|
readonly 'help.command.autoSync.description': 'Control scheduled repository clone/pull and analysis from GITNEXUS_HOME/watch_config.yml';
|
|
158
158
|
readonly 'help.autoSync.details': '\nActions: init, start (default), restart, stop, status, reset\nConfiguration: GITNEXUS_HOME/watch_config.yml\nRuntime files: GITNEXUS_HOME/watch/watch.pid, watch.mutex, watch.owner.json, watch.status.json, auto-sync-state.json\nRecovery: mutexes with verified dead owners are reclaimed automatically; invalid or legacy mutexes fail closed and require manual removal after confirming no watch process is running.\nWrites: GITNEXUS_HOME/watch/project_commit_info.txt\nRemote URLs: only SSH URLs on github.com, gitlab.com, and gitee.com are allowed.\nRuns once immediately, then repeats on sync_interval_minutes.';
|
package/dist/cli/i18n/en.js
CHANGED
|
@@ -154,7 +154,7 @@ export const en = {
|
|
|
154
154
|
'help.command.help.description': 'display help for command',
|
|
155
155
|
'help.option.help': 'display help for command',
|
|
156
156
|
'help.option.version': 'output the version number',
|
|
157
|
-
'help.command.setup.description': 'One-time setup: configure MCP for Cursor, Claude Code, Antigravity, OpenCode, CodeBuddy, Qoder, Codex',
|
|
157
|
+
'help.command.setup.description': 'One-time setup: configure MCP for Cursor, Claude Code, Antigravity, OpenCode, CodeBuddy, Qoder, Codex, Factory Droid',
|
|
158
158
|
'help.command.uninstall.description': 'Reverse `setup`: remove GitNexus MCP entries, skills, and hooks from all detected editors',
|
|
159
159
|
'help.command.autoSync.description': 'Control scheduled repository clone/pull and analysis from GITNEXUS_HOME/watch_config.yml',
|
|
160
160
|
'help.autoSync.details': '\nActions: init, start (default), restart, stop, status, reset\nConfiguration: GITNEXUS_HOME/watch_config.yml\nRuntime files: GITNEXUS_HOME/watch/watch.pid, watch.mutex, watch.owner.json, watch.status.json, auto-sync-state.json\nRecovery: mutexes with verified dead owners are reclaimed automatically; invalid or legacy mutexes fail closed and require manual removal after confirming no watch process is running.\nWrites: GITNEXUS_HOME/watch/project_commit_info.txt\nRemote URLs: only SSH URLs on github.com, gitlab.com, and gitee.com are allowed.\nRuns once immediately, then repeats on sync_interval_minutes.',
|
|
@@ -153,7 +153,7 @@ export declare const cliResources: {
|
|
|
153
153
|
readonly 'help.command.help.description': 'display help for command';
|
|
154
154
|
readonly 'help.option.help': 'display help for command';
|
|
155
155
|
readonly 'help.option.version': 'output the version number';
|
|
156
|
-
readonly 'help.command.setup.description': 'One-time setup: configure MCP for Cursor, Claude Code, Antigravity, OpenCode, CodeBuddy, Qoder, Codex';
|
|
156
|
+
readonly 'help.command.setup.description': 'One-time setup: configure MCP for Cursor, Claude Code, Antigravity, OpenCode, CodeBuddy, Qoder, Codex, Factory Droid';
|
|
157
157
|
readonly 'help.command.uninstall.description': 'Reverse `setup`: remove GitNexus MCP entries, skills, and hooks from all detected editors';
|
|
158
158
|
readonly 'help.command.autoSync.description': 'Control scheduled repository clone/pull and analysis from GITNEXUS_HOME/watch_config.yml';
|
|
159
159
|
readonly 'help.autoSync.details': '\nActions: init, start (default), restart, stop, status, reset\nConfiguration: GITNEXUS_HOME/watch_config.yml\nRuntime files: GITNEXUS_HOME/watch/watch.pid, watch.mutex, watch.owner.json, watch.status.json, auto-sync-state.json\nRecovery: mutexes with verified dead owners are reclaimed automatically; invalid or legacy mutexes fail closed and require manual removal after confirming no watch process is running.\nWrites: GITNEXUS_HOME/watch/project_commit_info.txt\nRemote URLs: only SSH URLs on github.com, gitlab.com, and gitee.com are allowed.\nRuns once immediately, then repeats on sync_interval_minutes.';
|
package/dist/cli/i18n/zh-CN.js
CHANGED
|
@@ -152,7 +152,7 @@ export const zhCN = {
|
|
|
152
152
|
'help.command.help.description': '显示命令帮助',
|
|
153
153
|
'help.option.help': '显示命令帮助',
|
|
154
154
|
'help.option.version': '输出版本号',
|
|
155
|
-
'help.command.setup.description': '一次性设置:为 Cursor、Claude Code、Antigravity、OpenCode、CodeBuddy、Qoder、Codex 配置 MCP',
|
|
155
|
+
'help.command.setup.description': '一次性设置:为 Cursor、Claude Code、Antigravity、OpenCode、CodeBuddy、Qoder、Codex、Factory Droid 配置 MCP',
|
|
156
156
|
'help.command.uninstall.description': '撤销 `setup`:从所有检测到的编辑器中移除 GitNexus 的 MCP 配置、技能和钩子',
|
|
157
157
|
'help.command.autoSync.description': '控制基于 GITNEXUS_HOME/watch_config.yml 的定时 clone/pull 和分析',
|
|
158
158
|
'help.autoSync.details': '\n操作:init、start(默认)、restart、stop、status、reset\n配置:GITNEXUS_HOME/watch_config.yml\n运行时文件:GITNEXUS_HOME/watch/watch.pid、watch.mutex、watch.owner.json、watch.status.json、auto-sync-state.json\n恢复:已验证 owner 退出的 mutex 会自动回收;无效或旧版 mutex 会安全拒绝,确认没有 watch 进程运行后再手动删除。\n写入:GITNEXUS_HOME/watch/project_commit_info.txt\n远程地址:仅允许 github.com、gitlab.com 和 gitee.com 上的 SSH 地址。\n启动后立即运行一次,之后按 sync_interval_minutes 重复。',
|
package/dist/cli/index.js
CHANGED
|
@@ -17,7 +17,7 @@ function collectCodingAgents(value, previous) {
|
|
|
17
17
|
program.name('gitnexus').description('GitNexus local CLI and MCP server').version(packageVersion());
|
|
18
18
|
program
|
|
19
19
|
.command('setup')
|
|
20
|
-
.description('One-time setup: configure MCP for Cursor, Claude Code, Antigravity, OpenCode, CodeBuddy, Qoder, Codex')
|
|
20
|
+
.description('One-time setup: configure MCP for Cursor, Claude Code, Antigravity, OpenCode, CodeBuddy, Qoder, Codex, Factory Droid')
|
|
21
21
|
.option('-c, --coding-agent <agents>', 'Configure only these coding agents (comma-separated or repeatable)', collectCodingAgents)
|
|
22
22
|
.action(createLazyAction(() => import('./setup.js'), 'setupCommand'));
|
|
23
23
|
program
|
package/dist/cli/setup.js
CHANGED
|
@@ -70,6 +70,7 @@ const CODING_AGENT_IDS = {
|
|
|
70
70
|
codebuddy: 'codebuddy',
|
|
71
71
|
qoder: 'qoder',
|
|
72
72
|
codex: 'codex',
|
|
73
|
+
droid: 'droid',
|
|
73
74
|
};
|
|
74
75
|
const SUPPORTED_CODING_AGENTS = Object.values(CODING_AGENT_IDS);
|
|
75
76
|
function selectedCodingAgents(values) {
|
|
@@ -824,6 +825,55 @@ async function installQoderSkills(result) {
|
|
|
824
825
|
result.errors.push(`Qoder skills: ${err instanceof Error ? err.message : String(err)}`);
|
|
825
826
|
}
|
|
826
827
|
}
|
|
828
|
+
/**
|
|
829
|
+
* Configure the GitNexus MCP server for Factory Droid.
|
|
830
|
+
*
|
|
831
|
+
* Factory stores user-scope MCP config in ~/.factory/mcp.json using the same
|
|
832
|
+
* `{ mcpServers: { <name>: {...} } }` JSONC shape as Cursor/Claude
|
|
833
|
+
* (https://docs.factory.ai/cli/configuration/mcp), so we reuse mergeJsoncFile
|
|
834
|
+
* rather than shelling out to `droid mcp add` — that keeps setup working even
|
|
835
|
+
* when the `droid` binary isn't on PATH.
|
|
836
|
+
*/
|
|
837
|
+
async function setupDroid(result) {
|
|
838
|
+
const factoryDir = path.join(os.homedir(), '.factory');
|
|
839
|
+
if (!(await dirExists(factoryDir))) {
|
|
840
|
+
result.skipped.push('Factory Droid (not installed)');
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
const { file: mcpPath, keyPath } = mcpTarget('droid');
|
|
844
|
+
try {
|
|
845
|
+
const ok = await mergeJsoncFile(mcpPath, keyPath, getMcpEntry());
|
|
846
|
+
if (ok) {
|
|
847
|
+
result.configured.push('Factory Droid');
|
|
848
|
+
}
|
|
849
|
+
else {
|
|
850
|
+
result.errors.push('Factory Droid: mcp.json is corrupt — skipping to preserve existing content');
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
catch (err) {
|
|
854
|
+
result.errors.push(`Factory Droid: ${err.message}`);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* Install global Factory Droid skills to ~/.factory/skills/{name}/SKILL.md
|
|
859
|
+
* (https://docs.factory.ai/cli/configuration/skills — same SKILL.md layout as
|
|
860
|
+
* Claude Code).
|
|
861
|
+
*/
|
|
862
|
+
async function installDroidSkills(result) {
|
|
863
|
+
const factoryDir = path.join(os.homedir(), '.factory');
|
|
864
|
+
if (!(await dirExists(factoryDir)))
|
|
865
|
+
return;
|
|
866
|
+
const skillsDir = skillTarget('droid').dir;
|
|
867
|
+
try {
|
|
868
|
+
const installed = await installSkillsTo(skillsDir);
|
|
869
|
+
if (installed.length > 0) {
|
|
870
|
+
result.configured.push(`Factory Droid skills (${installed.length} skills → ~/.factory/skills/)`);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
catch (err) {
|
|
874
|
+
result.errors.push(`Factory Droid skills: ${err.message}`);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
827
877
|
/**
|
|
828
878
|
* Build a TOML section for Codex MCP config (~/.codex/config.toml).
|
|
829
879
|
*/
|
|
@@ -1107,6 +1157,8 @@ export const setupCommand = async (options) => {
|
|
|
1107
1157
|
await setupQoder(result);
|
|
1108
1158
|
if (selected.has('codex'))
|
|
1109
1159
|
await setupCodex(result);
|
|
1160
|
+
if (selected.has('droid'))
|
|
1161
|
+
await setupDroid(result);
|
|
1110
1162
|
// Install global skills for platforms that support them
|
|
1111
1163
|
if (selected.has('claude')) {
|
|
1112
1164
|
await installClaudeCodeSkills(result);
|
|
@@ -1128,6 +1180,11 @@ export const setupCommand = async (options) => {
|
|
|
1128
1180
|
await installCodexSkills(result);
|
|
1129
1181
|
await installClaudeSchemaHooks(result, 'codex');
|
|
1130
1182
|
}
|
|
1183
|
+
// MCP + skills only. Factory hooks (Execute matcher, PostToolUse) ship in the
|
|
1184
|
+
// standalone gitnexus-factory-plugin instead of the setup path — add a droid
|
|
1185
|
+
// hook target + adapter here if setup-installed hooks are wanted.
|
|
1186
|
+
if (selected.has('droid'))
|
|
1187
|
+
await installDroidSkills(result);
|
|
1131
1188
|
// Print results
|
|
1132
1189
|
if (result.configured.length > 0) {
|
|
1133
1190
|
console.log(' Configured:');
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* Aggregation already pushes a hub onto every owning process. This helper
|
|
6
6
|
* is the last shaping step: slice each process to `max_symbols`, keep one
|
|
7
7
|
* row per `(id, process_id)`, then set `symbol_count` from those rows.
|
|
8
|
+
* `content` stays only on the first row for each symbol id so a hub in
|
|
9
|
+
* many flows does not repeat its source text.
|
|
8
10
|
*/
|
|
9
11
|
export type QueryProcessAttach = {
|
|
10
12
|
id: string;
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* Aggregation already pushes a hub onto every owning process. This helper
|
|
6
6
|
* is the last shaping step: slice each process to `max_symbols`, keep one
|
|
7
7
|
* row per `(id, process_id)`, then set `symbol_count` from those rows.
|
|
8
|
+
* `content` stays only on the first row for each symbol id so a hub in
|
|
9
|
+
* many flows does not repeat its source text.
|
|
8
10
|
*/
|
|
9
11
|
/**
|
|
10
12
|
* Pair key for attach rows. Both `id` and `process_id` routinely contain
|
|
@@ -16,6 +18,7 @@ function attachPairKey(id, processId) {
|
|
|
16
18
|
export function shapeQueryProcessAttaches(rankedProcesses, options) {
|
|
17
19
|
const { maxSymbolsPerProcess, chainByProcessId } = options;
|
|
18
20
|
const seen = new Set();
|
|
21
|
+
const contentSeen = new Set();
|
|
19
22
|
const process_symbols = [];
|
|
20
23
|
const countByProcess = new Map();
|
|
21
24
|
for (const p of rankedProcesses) {
|
|
@@ -24,7 +27,18 @@ export function shapeQueryProcessAttaches(rankedProcesses, options) {
|
|
|
24
27
|
if (seen.has(key))
|
|
25
28
|
continue;
|
|
26
29
|
seen.add(key);
|
|
27
|
-
|
|
30
|
+
let row = s;
|
|
31
|
+
if (s.content !== undefined) {
|
|
32
|
+
if (contentSeen.has(s.id)) {
|
|
33
|
+
const { content: _content, ...rest } = s;
|
|
34
|
+
row = rest;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
contentSeen.add(s.id);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (p.entryPointId && s.id === p.entryPointId)
|
|
41
|
+
row = { ...row, is_entry_point: true };
|
|
28
42
|
process_symbols.push(row);
|
|
29
43
|
countByProcess.set(s.process_id, (countByProcess.get(s.process_id) ?? 0) + 1);
|
|
30
44
|
}
|
package/dist/mcp/server.js
CHANGED
|
@@ -38,7 +38,7 @@ function getNextStepHint(toolName, args) {
|
|
|
38
38
|
case 'list_repos':
|
|
39
39
|
return `\n\n---\n**Next:** READ gitnexus://repo/{name}/context for any repo above to get its overview and check staleness. If pagination.hasMore is true, call list_repos again with offset set to pagination.nextOffset to fetch the rest.`;
|
|
40
40
|
case 'query':
|
|
41
|
-
return `\n\n---\n**Next:** To understand a specific symbol in depth, use context({name: "<symbol_name>"${repoParam}}) to see categorized refs and process participation
|
|
41
|
+
return `\n\n---\n**Next:** To understand a specific symbol in depth, use context({name: "<symbol_name>"${repoParam}}) to see categorized refs and process participation.${args?.include_content === true ? ` For source of a process_symbols row without content, use context({uid: "<id>", include_content: true${repoParam}}).` : ''}`;
|
|
42
42
|
case 'context':
|
|
43
43
|
return `\n\n---\n**Next:** If planning changes, use impact({target: "${args?.name || '<name>'}", direction: "upstream"${repoParam}}) to check blast radius. To see execution flows, READ gitnexus://repo/${repoPath}/processes.`;
|
|
44
44
|
case 'impact':
|
package/dist/mcp/tools.js
CHANGED
|
@@ -103,11 +103,11 @@ specify the "repo" parameter explicitly.`,
|
|
|
103
103
|
Returns ranked processes plus a flat process_symbols list. Join processes[].id to process_symbols[].process_id.
|
|
104
104
|
|
|
105
105
|
WHEN TO USE: Understanding how code works together. Use this when you need execution flows and relationships, not just file matches. Complements grep/IDE search.
|
|
106
|
-
AFTER THIS: Use context() on a specific symbol for 360-degree view (callers, callees, categorized refs).
|
|
106
|
+
AFTER THIS: Use context() on a specific symbol for 360-degree view (callers, callees, categorized refs). With include_content, context() also returns that symbol's source.
|
|
107
107
|
|
|
108
108
|
Returns results grouped by process (execution flow):
|
|
109
109
|
- processes: ranked execution flows with relevance priority. When a process has an HTTP endpoint, each item includes route and method string aliases plus routes: [{ url, method? }] (same shape as context). When chain_depth > 0, each item also includes chain — layered upstream callers + downstream callees from the process entry symbol (same BFS as context({chain_depth})).
|
|
110
|
-
- process_symbols: search-hit symbols in those flows with file locations and module (functional area). On the single-repo envelope { processes, process_symbols, definitions }: One row per (id, process_id) — the same symbol id may appear under more than one process. Join a process to its rows by process_id; symbol_count is the number of those rows. When the process entry is among those hits, it is marked is_entry_point: true. A repo of "@<group>" returns { group, query, results, per_repo } and does not include process_symbols. results[].symbol_count is the member's post-slice attach count; when service is set, it counts only attaches under that prefix. To get process_symbols for one member, query again with repo "@<group>/<memberPath>" (member path from group.yaml, or results[]._repo).
|
|
110
|
+
- process_symbols: search-hit symbols in those flows with file locations and module (functional area). On the single-repo envelope { processes, process_symbols, definitions }: One row per (id, process_id) — the same symbol id may appear under more than one process. Join a process to its rows by process_id; symbol_count is the number of those rows. When the process entry is among those hits, it is marked is_entry_point: true. With include_content, content appears only on the first row for each symbol id across the whole process_symbols array, not per process; later rows for that id omit it, even under a different process_id. To get content for a row without it, find the earlier row with the same id, or call context({uid: "<id>", include_content: true}). A repo of "@<group>" returns { group, query, results, per_repo } and does not include process_symbols. results[].symbol_count is the member's post-slice attach count; when service is set, it counts only attaches under that prefix. To get process_symbols for one member, query again with repo "@<group>/<memberPath>" (member path from group.yaml, or results[]._repo).
|
|
111
111
|
- definitions: standalone types/interfaces not in any process. Keyword hits on Route URLs (route_fts) are bridged to their handler via HANDLES_ROUTE (handlerSymbolId, routes) when the edge exists; use route_map({route}) for the full HTTP surface.
|
|
112
112
|
|
|
113
113
|
Hybrid ranking: BM25 keyword + semantic vector search, ranked by Reciprocal Rank Fusion.
|
|
@@ -153,7 +153,7 @@ ${HOT_READ_STALENESS_NOTE}`,
|
|
|
153
153
|
},
|
|
154
154
|
include_content: {
|
|
155
155
|
type: 'boolean',
|
|
156
|
-
description: 'Include source text retained for matching symbols (default: false). The response reports contentAvailability; indexes built with content retention "none" explicitly report unavailable content.',
|
|
156
|
+
description: 'Include source text retained for matching symbols (default: false). The response reports contentAvailability; indexes built with content retention "none" explicitly report unavailable content. Content is sent once per symbol id, on its first process_symbols row; context({uid: "<id>", include_content: true}) returns it for any row.',
|
|
157
157
|
default: false,
|
|
158
158
|
},
|
|
159
159
|
chain_depth: {
|
|
@@ -80,9 +80,11 @@ function debugLog(msg) {
|
|
|
80
80
|
|
|
81
81
|
function resolveHookBinary(tool) {
|
|
82
82
|
const envKey = tool === 'lsof' ? 'GITNEXUS_HOOK_LSOF_PATH' : 'GITNEXUS_HOOK_PS_PATH';
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
// Trim once, exactly as hasMissingHookBinaryOverride does, so a padded but
|
|
84
|
+
// valid override (" /tmp/lsof ") is both accepted there and used here.
|
|
85
|
+
const fromEnv = process.env[envKey] ? String(process.env[envKey]).trim() : '';
|
|
86
|
+
if (fromEnv && fs.existsSync(fromEnv)) {
|
|
87
|
+
return fromEnv;
|
|
86
88
|
}
|
|
87
89
|
const candidates =
|
|
88
90
|
tool === 'lsof'
|
|
@@ -177,7 +179,13 @@ function resolveUnixGuardTimeout() {
|
|
|
177
179
|
const trimmed = fromEnv ? String(fromEnv).trim() : '';
|
|
178
180
|
if (trimmed === 'disabled') return unixGuardTimeoutCache;
|
|
179
181
|
const candidates = [];
|
|
180
|
-
|
|
182
|
+
// Resolve the override against THIS process's cwd — the directory the
|
|
183
|
+
// existsSync check and the self-test run in — so the cached/returned path is
|
|
184
|
+
// always absolute. The adapters spawn the wrapper with a different `cwd`
|
|
185
|
+
// (the tool request's), where a relative value would resolve elsewhere
|
|
186
|
+
// (ENOENT), and a slashless name would switch to a PATH lookup.
|
|
187
|
+
const override = trimmed ? path.resolve(trimmed) : '';
|
|
188
|
+
if (override && fs.existsSync(override)) candidates.push(override);
|
|
181
189
|
for (const builtin of [
|
|
182
190
|
'/usr/bin/timeout',
|
|
183
191
|
'/bin/timeout',
|
|
@@ -317,15 +325,30 @@ function getProcRoot() {
|
|
|
317
325
|
// `node <abs path to .../node_modules/gitnexus/dist/cli/index.js> mcp` line
|
|
318
326
|
// (the `mcp`/`serve` mode token lives at the very tail, so the cap must be large
|
|
319
327
|
// enough to reach it — see PROC_CMDLINE_FLOOR escalation below). Overridable for
|
|
320
|
-
// tests
|
|
328
|
+
// tests via GITNEXUS_HOOK_PROC_CMDLINE_MAX: an integer in
|
|
329
|
+
// [PROC_CMDLINE_FLOOR, PROC_CMDLINE_CEIL] is used as-is; a larger integer is
|
|
330
|
+
// CLAMPED to PROC_CMDLINE_CEIL; anything else (below the floor, fractional,
|
|
331
|
+
// non-numeric, Infinity) falls back to the 16 KiB default.
|
|
321
332
|
const PROC_CMDLINE_FLOOR = 4096;
|
|
333
|
+
// Upper bound for a single cmdline read chunk, and the absolute ceiling of the
|
|
334
|
+
// escalation path in readLinuxCmdline (same 256 KiB — no single read may exceed
|
|
335
|
+
// what the whole escalation is allowed to collect). Without it, an oversized
|
|
336
|
+
// override (e.g. 2**40 — past buffer.constants.MAX_LENGTH on older Node lines
|
|
337
|
+
// and unallocatable in practice on any) made Buffer.allocUnsafe throw; readLinuxCmdline's catch turned that into '' (a
|
|
338
|
+
// NON-candidate), so a real server owner was silently missed (fail-OPEN, the
|
|
339
|
+
// #1492 race). Oversized values are clamped rather than defaulted: the operator
|
|
340
|
+
// asked for MORE bytes, and the ceiling is the most the read will ever collect
|
|
341
|
+
// anyway, so clamping honours the intent while keeping allocation bounded.
|
|
342
|
+
const PROC_CMDLINE_CEIL = 262144;
|
|
322
343
|
function getCmdlineMaxBytes() {
|
|
323
344
|
const raw = process.env.GITNEXUS_HOOK_PROC_CMDLINE_MAX;
|
|
324
345
|
// Number() (not parseInt) so "8e3" reads as 8000, not 8 (parseInt stops at
|
|
325
346
|
// 'e'). The `raw && String(raw).trim()` guard keeps empty/whitespace on the
|
|
326
347
|
// default; trailing garbage ("8abc") now -> NaN -> default (stricter).
|
|
327
348
|
const n = raw && String(raw).trim() ? Number(String(raw).trim()) : NaN;
|
|
328
|
-
|
|
349
|
+
// Number.isInteger rejects NaN, +/-Infinity and fractions (a fractional
|
|
350
|
+
// Buffer/readSync length is not a byte count).
|
|
351
|
+
if (Number.isInteger(n) && n >= PROC_CMDLINE_FLOOR) return Math.min(n, PROC_CMDLINE_CEIL);
|
|
329
352
|
return 16384;
|
|
330
353
|
}
|
|
331
354
|
|
|
@@ -381,19 +404,24 @@ const CMDLINE_TIMEOUT = Symbol('gitnexus.cmdline.timeout');
|
|
|
381
404
|
|
|
382
405
|
// Bounded /proc/<pid>/cmdline read for Phase 1. openSync+readSync (not
|
|
383
406
|
// readFileSync) so a D-state holder cannot stall the hook on a huge or
|
|
384
|
-
// never-EOF argv: we read
|
|
385
|
-
//
|
|
407
|
+
// never-EOF argv: we read in `cap`-sized chunks and stop as soon as the text
|
|
408
|
+
// holds both server tokens, at EOF, at PROC_CMDLINE_CEIL, or when the scan
|
|
409
|
+
// budget runs out (see below). cmdline separates argv with NULs; convert to
|
|
410
|
+
// spaces for isGitNexusServerCommand.
|
|
386
411
|
//
|
|
387
412
|
// Owner-miss guard for the 4 KB cap: the `gitnexus` token usually sits in the
|
|
388
413
|
// first path component while the `mcp`/`serve` mode token is the LAST argv, so
|
|
389
414
|
// a naive 4 KB read could clip the mode token off a server launched with a very
|
|
390
415
|
// long interpreter path and silently miss a real owner. We mitigate two ways:
|
|
391
|
-
// (a) the default cap (16 KiB) already clears realistic lines
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
//
|
|
396
|
-
//
|
|
416
|
+
// (a) the default cap (16 KiB) already clears realistic lines, so almost every
|
|
417
|
+
// process is decided by the first read hitting EOF; (b) a read that fills the
|
|
418
|
+
// cap stops early ONLY once it holds BOTH tokens (decided owner). Holding one
|
|
419
|
+
// token, or neither, decides nothing: interpreter flags can put a mode-looking
|
|
420
|
+
// word first (`node --require mcp .../gitnexus/... serve`) or push the gitnexus
|
|
421
|
+
// path past the first chunk. So we keep reading in cap-sized chunks until both
|
|
422
|
+
// tokens appear, the file ends, or the hard ceiling is reached. Only processes
|
|
423
|
+
// that passed the Phase 0 comm prefilter AND have a cmdline longer than the cap
|
|
424
|
+
// ever escalate, and each escalation step is budget-gated (below).
|
|
397
425
|
//
|
|
398
426
|
// Budget (F3): the escalation loop above is the one place a SINGLE pathological
|
|
399
427
|
// candidate could read up to HARD_CEIL (256 KiB) before the next scan-level
|
|
@@ -411,7 +439,7 @@ function readLinuxCmdline(procRoot, pidStr, cap, outOfBudget) {
|
|
|
411
439
|
return '';
|
|
412
440
|
}
|
|
413
441
|
try {
|
|
414
|
-
const HARD_CEIL =
|
|
442
|
+
const HARD_CEIL = PROC_CMDLINE_CEIL; // 256 KiB absolute ceiling for the escalation path
|
|
415
443
|
let collected = Buffer.alloc(0);
|
|
416
444
|
let offset = 0;
|
|
417
445
|
let chunkCap = cap;
|
|
@@ -425,16 +453,12 @@ function readLinuxCmdline(procRoot, pidStr, cap, outOfBudget) {
|
|
|
425
453
|
collected = Buffer.concat([collected, buf.subarray(0, bytes)]);
|
|
426
454
|
offset += bytes;
|
|
427
455
|
const text = collected.toString('utf8').replace(/\0+/g, ' ');
|
|
428
|
-
// Stop early when
|
|
429
|
-
// token and a mode token
|
|
430
|
-
//
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
/node_modules[/\\]gitnexus[/\\]/.test(text);
|
|
434
|
-
const hasMode = /(?:^|\s)(mcp|serve)(?:\s|$)/.test(text);
|
|
435
|
-
if (hasMode) break; // decided (positive); isGitNexusServerCommand re-checks below
|
|
456
|
+
// Stop early only when the partial read is DECIDED: both the gitnexus
|
|
457
|
+
// token and a mode token are present (isGitNexusServerCommand is exactly
|
|
458
|
+
// that conjunction). A partial read missing either token is undecided —
|
|
459
|
+
// the missing one may lie past the chunk boundary — so it keeps reading.
|
|
460
|
+
if (isGitNexusServerCommand(text)) break; // decided (positive)
|
|
436
461
|
if (bytes < chunkCap) break; // EOF: full cmdline read, definitive
|
|
437
|
-
if (!hasGitNexus) break; // not a candidate; do not escalate the read
|
|
438
462
|
if (offset >= HARD_CEIL) break; // bounded escalation only
|
|
439
463
|
// Budget gate the escalation: a single huge-argv candidate must not burn
|
|
440
464
|
// the whole scan deadline before we re-check. Return the timeout sentinel
|
|
@@ -578,17 +602,24 @@ function linuxProcScanFindGitNexusServer(dbPathAbs, myPid) {
|
|
|
578
602
|
);
|
|
579
603
|
return 'timeout';
|
|
580
604
|
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
605
|
+
if (code === 'ENOTDIR') {
|
|
606
|
+
// The fd path is not a directory at all, so this is structurally not a
|
|
607
|
+
// real /proc/<pid>/fd — not a plausible live owner. Move on.
|
|
608
|
+
debugLog(
|
|
609
|
+
`fd dir not a directory for candidate pid ${pidStr} (ENOTDIR); ` +
|
|
610
|
+
`treating candidate as non-owner -> continue`,
|
|
611
|
+
);
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
// Any other error (EMFILE, ENFILE, ENOMEM, EINTR, no code, …) says nothing
|
|
615
|
+
// about whether this already-identified server candidate holds the lbug.
|
|
616
|
+
// Only ENOENT/ENOTDIR above establish non-ownership; everything else is
|
|
617
|
+
// inconclusive and fails closed via 'timeout', same as EACCES/EIO.
|
|
587
618
|
debugLog(
|
|
588
|
-
`fd dir
|
|
589
|
-
`(${code || 'unknown'});
|
|
619
|
+
`fd dir read failed for candidate pid ${pidStr} ` +
|
|
620
|
+
`(${code || 'unknown'}); probe inconclusive -> fail-closed (timeout)`,
|
|
590
621
|
);
|
|
591
|
-
|
|
622
|
+
return 'timeout';
|
|
592
623
|
}
|
|
593
624
|
for (const fd of fds) {
|
|
594
625
|
if (outOfBudget()) return 'timeout';
|
|
@@ -707,16 +738,12 @@ module.exports = {
|
|
|
707
738
|
// name is pinned by a source-contract test.
|
|
708
739
|
linuxProcScanFindGitNexusServer,
|
|
709
740
|
// #2163 follow-up: the hook adapters wrap the augment CLI in the same
|
|
710
|
-
// guard. Returns a self-tested wrapper path
|
|
711
|
-
//
|
|
712
|
-
//
|
|
713
|
-
//
|
|
714
|
-
//
|
|
715
|
-
//
|
|
716
|
-
// adapters spawn the CLI with a `cwd` option (chdir-before-exec), so such
|
|
717
|
-
// a value can pass here yet ENOENT at the augment call site — set the
|
|
718
|
-
// override to an absolute path. Returns null when the wrapper is
|
|
719
|
-
// disabled/unavailable. Never call on win32 (see its JSDoc).
|
|
741
|
+
// guard. Returns a self-tested, always-ABSOLUTE wrapper path: the built-in
|
|
742
|
+
// candidates are absolute, and a GITNEXUS_HOOK_TIMEOUT_PATH override is
|
|
743
|
+
// path.resolve()d against this process's cwd before its existsSync check
|
|
744
|
+
// and self-test, so the adapters can spawn it under any `cwd` option.
|
|
745
|
+
// Returns null when the wrapper is disabled/unavailable. Never call on
|
|
746
|
+
// win32 (see its JSDoc).
|
|
720
747
|
resolveUnixGuardTimeout,
|
|
721
748
|
// Exported for white-box unit tests of the numeric-env parsing (#2183 review):
|
|
722
749
|
// Number()-not-parseInt so "16e3" reads as 16000, plus the empty/whitespace
|
|
@@ -725,4 +752,8 @@ module.exports = {
|
|
|
725
752
|
// otherwise only observable indirectly through scan timing/escalation.
|
|
726
753
|
getCmdlineMaxBytes,
|
|
727
754
|
resolveLinuxProcBudgetMs,
|
|
755
|
+
// Exported for white-box tests pinning that the override check and the
|
|
756
|
+
// override lookup agree on whitespace-padded GITNEXUS_HOOK_{LSOF,PS}_PATH.
|
|
757
|
+
resolveHookBinary,
|
|
758
|
+
hasMissingHookBinaryOverride,
|
|
728
759
|
};
|