Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@ Gemfile

expo-env.d.ts
# @end expo-cli
.dual-graph/
81 changes: 81 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<!-- dgc-policy-v11 -->
# Dual-Graph Context Policy

This project uses a local dual-graph MCP server for efficient context retrieval.

## MANDATORY: Always follow this order

1. **Call `graph_continue` first** — before any file exploration, grep, or code reading.

2. **If `graph_continue` returns `needs_project=true`**: call `graph_scan` with the
current project directory (`pwd`). Do NOT ask the user.

3. **If `graph_continue` returns `skip=true`**: project has fewer than 5 files.
Do NOT do broad or recursive exploration. Read only specific files if their names
are mentioned, or ask the user what to work on.

4. **Read `recommended_files`** using `graph_read` — **one call per file**.
- `graph_read` accepts a single `file` parameter (string). Call it separately for each
recommended file. Do NOT pass an array or batch multiple files into one call.
- `recommended_files` may contain `file::symbol` entries (e.g. `src/auth.ts::handleLogin`).
Pass them verbatim to `graph_read(file: "src/auth.ts::handleLogin")` — it reads only
that symbol's lines, not the full file.
- Example: if `recommended_files` is `["src/auth.ts::handleLogin", "src/db.ts"]`,
call `graph_read(file: "src/auth.ts::handleLogin")` and `graph_read(file: "src/db.ts")`
as two separate calls (they can be parallel).

5. **Check `confidence` and obey the caps strictly:**
- `confidence=high` -> Stop. Do NOT grep or explore further.
- `confidence=medium` -> If recommended files are insufficient, call `fallback_rg`
at most `max_supplementary_greps` time(s) with specific terms, then `graph_read`
at most `max_supplementary_files` additional file(s). Then stop.
- `confidence=low` -> Call `fallback_rg` at most `max_supplementary_greps` time(s),
then `graph_read` at most `max_supplementary_files` file(s). Then stop.

## Token Usage

A `token-counter` MCP is available for tracking live token usage.

- To check how many tokens a large file or text will cost **before** reading it:
`count_tokens({text: "<content>"})`
- To log actual usage after a task completes (if the user asks):
`log_usage({input_tokens: <est>, output_tokens: <est>, description: "<task>"})`
- To show the user their running session cost:
`get_session_stats()`

Live dashboard URL is printed at startup next to "Token usage".

## Rules

- Do NOT use `rg`, `grep`, or bash file exploration before calling `graph_continue`.
- Do NOT do broad/recursive exploration at any confidence level.
- `max_supplementary_greps` and `max_supplementary_files` are hard caps - never exceed them.
- Do NOT dump full chat history.
- Do NOT call `graph_retrieve` more than once per turn.
- After edits, call `graph_register_edit` with the changed files. Use `file::symbol` notation (e.g. `src/auth.ts::handleLogin`) when the edit targets a specific function, class, or hook.

## Context Store

Whenever you make a decision, identify a task, note a next step, fact, or blocker during a conversation, call `graph_add_memory`.

**To add an entry:**
```
graph_add_memory(type="decision|task|next|fact|blocker", content="one sentence max 15 words", tags=["topic"], files=["relevant/file.ts"])
```

**Do NOT write context-store.json directly** — always use `graph_add_memory`. It applies pruning and keeps the store healthy.

**Rules:**
- Only log things worth remembering across sessions (not every minor detail)
- `content` must be under 15 words
- `files` lists the files this decision/task relates to (can be empty)
- Log immediately when the item arises — not at session end

## Session End

When the user signals they are done (e.g. "bye", "done", "wrap up", "end session"), proactively update `CONTEXT.md` in the project root with:
- **Current Task**: one sentence on what was being worked on
- **Key Decisions**: bullet list, max 3 items
- **Next Steps**: bullet list, max 3 items

Keep `CONTEXT.md` under 20 lines total. Do NOT summarize the full conversation — only what's needed to resume next session.
4 changes: 4 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Resgrid Dispatch
Copyright 2026 Resgrid, LLC

This product includes software developed for the Resgrid project.
117 changes: 117 additions & 0 deletions __mocks__/@gorhom/bottom-sheet.web.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/**
* Web-safe mock for @gorhom/bottom-sheet.
* The real package depends on react-native-reanimated worklets which are unavailable on web.
* This mock respects index/-1 visibility, onChange callbacks, and ref methods
* so modals open/close correctly on web.
*/
const React = require('react');
const { View, StyleSheet, Pressable, Modal } = require('react-native');

const BottomSheet = React.forwardRef(function BottomSheet(props, ref) {
const { index = -1, children, onChange, snapPoints, backdropComponent, enablePanDownToClose } = props;
const [currentIndex, setCurrentIndex] = React.useState(index);

React.useEffect(() => {
setCurrentIndex(index);
}, [index]);

React.useImperativeHandle(ref, () => ({
expand: () => {
setCurrentIndex(0);
if (onChange) onChange(0);
},
collapse: () => {
setCurrentIndex(0);
if (onChange) onChange(0);
},
close: () => {
setCurrentIndex(-1);
if (onChange) onChange(-1);
},
snapToIndex: (i) => {
setCurrentIndex(i);
if (onChange) onChange(i);
},
snapToPosition: () => {},
forceClose: () => {
setCurrentIndex(-1);
if (onChange) onChange(-1);
},
}));

const isVisible = currentIndex >= 0;
if (!isVisible) return null;

const handleBackdropPress = () => {
if (enablePanDownToClose !== false) {
setCurrentIndex(-1);
if (onChange) onChange(-1);
}
};

return React.createElement(
Modal,
{ visible: true, transparent: true, animationType: 'slide', onRequestClose: handleBackdropPress },
React.createElement(
View,
{ style: styles.overlay },
React.createElement(Pressable, { style: styles.backdrop, onPress: handleBackdropPress }),
React.createElement(
View,
{ style: styles.sheet },
children
)
)
);
});

const BottomSheetView = function BottomSheetView(props) {
return React.createElement(View, { style: props.style }, props.children);
};

const BottomSheetScrollView = function BottomSheetScrollView(props) {
const { ScrollView } = require('react-native');
return React.createElement(ScrollView, props, props.children);
};

const BottomSheetBackdrop = function BottomSheetBackdrop() {
return null;
};

const BottomSheetModalProvider = function BottomSheetModalProvider(props) {
return props.children || null;
};

const BottomSheetModal = React.forwardRef(function BottomSheetModal(props, ref) {
return React.createElement(BottomSheet, Object.assign({}, props, { ref: ref }));
});

const styles = StyleSheet.create({
overlay: {
flex: 1,
justifyContent: 'flex-end',
},
backdrop: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0,0,0,0.4)',
},
sheet: {
backgroundColor: '#fff',
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
maxHeight: '70%',
minHeight: 200,
overflow: 'hidden',
},
});

module.exports = {
__esModule: true,
default: BottomSheet,
BottomSheet,
BottomSheetView,
BottomSheetScrollView,
BottomSheetBackdrop,
BottomSheetModalProvider,
BottomSheetModal,
};
26 changes: 20 additions & 6 deletions metro.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@
const _ = require('lodash');
const path = require('path');
const { getSentryExpoConfig } = require('@sentry/react-native/metro');
const exclusionList = require('metro-config/src/defaults/exclusionList');
//const { getDefaultConfig } = require('expo/metro-config');
//const path = require('path');
const { withNativeWind } = require('nativewind/metro');

const config = getSentryExpoConfig(__dirname, {
Expand All @@ -14,9 +11,11 @@ const config = getSentryExpoConfig(__dirname, {

// Exclude electron directory from Metro bundler for Android/iOS
// Electron files use Node.js APIs that don't exist in React Native
config.resolver.blockList = exclusionList([
/electron\/.*/,
]);
const existingBlockList = config.resolver.blockList;
const extraBlocked = [/electron\/.*/];
config.resolver.blockList = existingBlockList
? [...(Array.isArray(existingBlockList) ? existingBlockList : [existingBlockList]), ...extraBlocked]
: extraBlocked;

// 1. Watch all files within the monorepo
// 2. Let Metro know where to resolve packages and in what order
Expand Down Expand Up @@ -48,6 +47,21 @@ config.resolver.resolveRequest = (context, moduleName, platform) => {
};
}

// @gorhom/bottom-sheet depends on reanimated worklets - not available on web
if (moduleName === '@gorhom/bottom-sheet') {
return {
type: 'sourceFile',
filePath: path.resolve(__dirname, '__mocks__/@gorhom/bottom-sheet.web.js'),
};
}

// NetInfo - not needed on web, use navigator.onLine instead
if (moduleName === '@react-native-community/netinfo') {
return {
type: 'empty',
};
}

// expo-keep-awake mock for web
if (moduleName === 'expo-keep-awake') {
return {
Expand Down
Loading
Loading