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
5 changes: 5 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ tasks:
cmds:
- echo "Mock generation will be added when interfaces are defined"

test:
desc: "Run the full Go test suite. Uses an extended timeout because the repository tests run against real Firestore (the per-package default of 10m is not enough); the *_Firestore tests run in parallel to keep wall-clock down. Run as `zenv task test` so TEST_FIRESTORE_PROJECT_ID is loaded."
cmds:
- go test ./... -timeout 30m

test:e2e:
desc: "Run E2E tests with Playwright (includes backend startup and teardown)"
cmds:
Expand Down
13 changes: 10 additions & 3 deletions docs/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,19 @@ configured per Workspace under `[[action.status]]`. See

### Source
An external origin of information (e.g. a Notion page, a GitHub resource, a
Slack message) that the agent tools can read to extract Knowledge. See
Slack message) that the agent tools can read while investigating a Case. See
[Integrations](integrations.md).

### Knowledge
A piece of information extracted from a Source and associated with a Case. The
AI assist agent and agent tools produce and consume Knowledge.
A **workspace-wide shared knowledge entry**: organization-specific information
that does not exist in the LLM's general knowledge (operating rules, internal
proper nouns, past judgements, threat intel, …), captured so it can be reused on
future Case processing. A Knowledge entry has a title, a single Markdown `claim`
body, and one or more free-form `tags`; it is **not** tied to a Case and carries
no custom fields. Both humans (via the WebUI **Knowledge** section) and AI agents
(via the `knowledge__*` tools) read and write it. Entries are retrieved by
semantic search (embedding-based, with a substring fallback) and by tag filter.
See [User Guide → Knowledge](user_guide.md#knowledge).

### Agent Session
A persistent AI conversation tied to a Slack thread on a Case. History and
Expand Down
7 changes: 7 additions & 0 deletions docs/eval.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,13 @@ from the eval job tool set — action creation is the primary observable.)
real API (require the matching credentials). Every tool call — sim or live — is
recorded so checks can verify tool usage and dumps can show the trajectory.

The catalog enumerates only the **external-service** tools the harness can
simulate or declare. In-process, always-present tools that operate on the local
repository — `core__*` (actions), `memo__*` (memos), and `knowledge__*`
(workspace knowledge) — are not listed in the catalog and are not
scenario-configurable; the eval agent is wired with them directly (knowledge
write is withheld for private cases, as in production).

## Diagnostic dumps

Scenarios with a failing check (or all scenarios with `--dump-all`) are dumped
Expand Down
55 changes: 55 additions & 0 deletions docs/user_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,55 @@ All mutations go through GraphQL (WebUI) or agent tools (LLM).
Domain models, repository backends, use cases, and Firestore layout for
Action Steps are documented in [develop/architecture.md](develop/architecture.md).

## Knowledge

The **Knowledge** section (sidebar → Knowledge) is a workspace-wide, shared
knowledge base for capturing organization-specific information that the LLM does
not know on its own — operating rules, internal proper nouns, past judgements,
threat intel, and so on — so it can be reused on future cases. See
[Concepts → Knowledge](concepts.md#knowledge) for the definition.

A Knowledge entry has three parts:

- **Title** — a one-line heading (required).
- **Claim** — a single **Markdown** body. Headings, lists, code blocks, and
links are rendered in the viewer. Up to ~8,000 characters.
- **Tags** — one or more free-form labels (**at least one is required**). Tags
drive filtering and are suggested from those already used in the workspace.

### Browsing and searching

The list shows each entry as a card (title, a plain-text preview of the claim,
its tags, author, and last-updated time). You can:

- **Filter by tag** — pick one or more tags; only entries carrying *all* of them
are shown.
- **Search** — type in the search box for semantic search (results ranked by
relevance, not just exact keyword match). When the deployment has no embedding
client configured, search falls back to substring matching.

### Editing

Open an entry to view the rendered claim; switch to **Edit** to change the raw
Markdown (a character counter enforces the length limit), edit the title, and
add/remove tags. **Create** and **Delete** are available from the list and
detail views. Deletion is permanent and asks for confirmation.

### Who can write

Workspace members can read and write all Knowledge through the WebUI. AI agents
can also read it always and **write it during case processing — except while
working a private case**, because shared knowledge is visible to the whole
workspace and a private case's contents must not leak into it. See
[the agent tool table](#available-agent-tools).

### Embedding (semantic search)

Semantic search reuses the existing embedding client configured via
`--embedding-gemini-project-id` / `--embedding-gemini-location` /
`--embedding-model` (see [CLI Reference](cli.md)). No Knowledge-specific flags or
environment variables are introduced, and no new Firestore index is required.

## Chat with the AI in a Slack thread

The agent that responds to `@mention` in Slack threads treats each thread as
Expand Down Expand Up @@ -610,10 +659,16 @@ own configuration; missing config silently disables that namespace's tools
| Namespace | Tools | Gate |
| --- | --- | --- |
| `core__*` | Action read/create/update/archive | Always on |
| `knowledge__*` | Workspace knowledge: `search`/`get`/`list_tags` (read), `create`/`update` (write) | Read always on; **write tools are withheld while processing a private Case** (shared knowledge is workspace-visible, so a private Case's contents must not leak into it) |
| `slack__*` | Workspace search (read-only), bulk message fetch | Slack bot token; search additionally requires the User OAuth token with `search:read` |
| `notion__*` | Page/database search, page Markdown fetch | `--notion-api-token` |
| `github__*` | Issue/PR search, single Issue/PR fetch, file content, commit history | All three `--github-app-*` flags |

The `knowledge__*` tools share the workspace-wide [Knowledge](#knowledge) base
with the WebUI. Semantic search uses the embedding client (the same one
configured by `--embedding-*`); when no embedding client is configured the agent
still works and search falls back to substring matching.

The mention flow uses the **read-only** Slack tool set (no `post_message` —
the trace UI handles outbound messages). The `assist` flow uses the full
Slack tool set including `slack__post_message`.
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import SourceList from './pages/SourceList'
import SourceDetail from './pages/SourceDetail'
import ImportNew from './pages/ImportNew'
import ImportDetail from './pages/ImportDetail'
import KnowledgeList from './pages/KnowledgeList'
import KnowledgeDetail from './pages/KnowledgeDetail'
import WorkspaceSelector from './pages/WorkspaceSelector'
import WorkspaceGuard from './components/WorkspaceGuard'
import { AuthGuard } from './components/auth/auth-guard'
Expand Down Expand Up @@ -46,6 +48,8 @@ function App() {
<Route path="drafts/:id" element={<DraftDetailRedirect />} />
<Route path="sources" element={<SourceList />} />
<Route path="sources/:id" element={<SourceDetail />} />
<Route path="knowledge" element={<KnowledgeList />} />
<Route path="knowledge/:id" element={<KnowledgeDetail />} />
{/* /imports has no list page by design — sessions are addressable
only by ID. A bare /imports navigation lands here from the
breadcrumb Layout renders, so redirect it to the new-import
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/Icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const IconRefresh = (p: P) => <Icon {...p} d="M3 12a9 9 0 1 0 9-9M3 4v5h5
export const IconFilter = (p: P) => <Icon {...p} d="M3 6h18M6 12h12M10 18h4" />
export const IconSparkle = (p: P) => <Icon {...p} d="M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M5.6 18.4l2.1-2.1M16.3 7.7l2.1-2.1" />
export const IconEdit = (p: P) => <Icon {...p} d="M4 20h4l10-10-4-4L4 16v4zM13.5 6.5l4 4" />
export const IconKnowledge = (p: P) => <Icon {...p} d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20M4 19.5A2.5 2.5 0 0 0 6.5 22H20V2H6.5A2.5 2.5 0 0 0 4 4.5v15z" />

// IconRobot — a friendly "bot" head used as the Agent surface mark.
// Built as a multi-shape SVG (head rect + antenna + ear stubs + eyes
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import { useTranslation } from '../i18n'
import { GET_CASES } from '../graphql/case'
import { GET_ACTIONS } from '../graphql/action'
import { GET_SOURCES } from '../graphql/source'
import { GET_KNOWLEDGES } from '../graphql/knowledge'
import { useCaseStatuses } from '../hooks/useCaseStatuses'
import { Avatar } from './Primitives'
import {
IconCases,
IconActions,
IconSources,
IconKnowledge,
IconSettings,
IconUser,
} from './Icons'
Expand All @@ -20,6 +22,7 @@ interface NavCount {
cases: number | null
actions: number | null
sources: number | null
knowledge: number | null
}

function useNavCounts(workspaceId: string | undefined): NavCount {
Expand All @@ -35,10 +38,15 @@ function useNavCounts(workspaceId: string | undefined): NavCount {
variables: { workspaceId },
skip: !workspaceId,
})
const { data: knowledges } = useQuery(GET_KNOWLEDGES, {
variables: { workspaceId },
skip: !workspaceId,
})
return {
cases: cases?.cases?.length ?? null,
actions: actions?.actions?.length ?? null,
sources: sources?.sources?.length ?? null,
knowledge: knowledges?.knowledges?.length ?? null,
}
}

Expand All @@ -62,6 +70,7 @@ export default function Sidebar() {
count: isThreadMode ? counts.cases : counts.actions,
},
{ id: 'sources', label: t('navSources'), Icon: IconSources, to: `${wsPrefix}/sources`, count: counts.sources },
{ id: 'knowledge', label: t('navKnowledge'), Icon: IconKnowledge, to: `${wsPrefix}/knowledge`, count: counts.knowledge },
]

return (
Expand Down
83 changes: 83 additions & 0 deletions frontend/src/components/knowledge/KnowledgeMarkdownView.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
.placeholder {
color: var(--fg-soft);
font-style: italic;
font-size: var(--t-sm);
}

.body {
font-size: var(--t-sm);
line-height: 1.7;
color: var(--fg);
}

.body p {
margin: 0 0 0.75rem;
}

.body p:last-child {
margin-bottom: 0;
}

.body h1,
.body h2,
.body h3,
.body h4,
.body h5,
.body h6 {
font-weight: 600;
margin: 1rem 0 0.375rem;
line-height: 1.3;
}

.body h1 { font-size: var(--t-xl); }
.body h2 { font-size: var(--t-lg); }
.body h3 { font-size: var(--t-base); }

.body ul,
.body ol {
margin: 0 0 0.75rem;
padding-left: 1.375rem;
}

.body li {
margin-bottom: 0.25rem;
}

.body code {
font-family: var(--font-mono, monospace);
font-size: 0.875em;
background: var(--bg-sunken);
border-radius: 3px;
padding: 0.1em 0.3em;
}

.body pre {
background: var(--bg-sunken);
border-radius: 6px;
padding: var(--spacing-sm) var(--spacing-md);
overflow-x: auto;
margin: 0 0 0.75rem;
}

.body pre code {
background: none;
padding: 0;
}

.body blockquote {
border-left: 3px solid var(--line-strong);
margin: 0 0 0.75rem;
padding: 0.25rem 0 0.25rem var(--spacing-md);
color: var(--fg-muted);
}

.body a {
color: var(--accent);
text-decoration: underline;
}

.body hr {
border: none;
border-top: 1px solid var(--line);
margin: 1rem 0;
}
22 changes: 22 additions & 0 deletions frontend/src/components/knowledge/KnowledgeMarkdownView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import ReactMarkdown, { type Components } from 'react-markdown'
import styles from './KnowledgeMarkdownView.module.css'

const components: Components = {
a: ({ node: _node, ...rest }) => <a {...rest} target="_blank" rel="noopener noreferrer" />,
}

interface Props {
source: string
placeholder?: string
}

export default function KnowledgeMarkdownView({ source, placeholder = '—' }: Props) {
if (!source.trim()) {
return <div className={styles.placeholder}>{placeholder}</div>
}
return (
<div className={styles.body}>
<ReactMarkdown components={components}>{source}</ReactMarkdown>
</div>
)
}
85 changes: 85 additions & 0 deletions frontend/src/components/knowledge/TagInput.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
.root {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-xs);
align-items: center;
min-height: 2.25rem;
padding: var(--spacing-xs) var(--spacing-sm);
border: 1px solid var(--line);
border-radius: 6px;
background: var(--bg-sunken);
cursor: text;
position: relative;
}

.root:focus-within {
outline: 2px solid var(--accent);
outline-offset: -1px;
}

.error {
border-color: var(--danger);
}

.removeBtn {
display: inline-flex;
align-items: center;
justify-content: center;
background: none;
border: none;
padding: 0 0 0 var(--spacing-xs);
cursor: pointer;
color: inherit;
opacity: 0.6;
line-height: 1;
}

.removeBtn:hover {
opacity: 1;
}

.inputWrap {
position: relative;
flex: 1;
min-width: 8rem;
}

.input {
width: 100%;
background: none;
border: none;
outline: none;
font-size: var(--t-sm);
color: var(--fg);
padding: 0;
}

.input::placeholder {
color: var(--fg-soft);
}

.suggestions {
position: absolute;
top: calc(100% + 4px);
left: 0;
right: 0;
background: var(--bg-elev);
border: 1px solid var(--line);
border-radius: 6px;
box-shadow: var(--shadow);
list-style: none;
margin: 0;
padding: var(--spacing-xs) 0;
z-index: 100;
}

.suggestion {
padding: var(--spacing-xs) var(--spacing-sm);
cursor: pointer;
font-size: var(--t-sm);
color: var(--fg);
}

.suggestion:hover {
background: var(--bg-subtle);
}
Loading
Loading