docs: explain the agentic concepts + unpublish internal engineering notes - #1
Merged
Merged
Conversation
…g notes The docs assumed the reader already knew what an agent, skill, memory, context, MCP or RAG is. Nothing explained them, so anyone new had nowhere to start — and intro.md opened with "manifest-first Vue 2 frontend rendered by CnAppRoot", which is a sentence for us, not for a reader. Add a Concepts section, written for someone who has never used an AI system: - concepts/index.md — "Agentic concepts, explained". The overview: starts from the problem (a chatbot forgets; you want a colleague), a one-line table of every concept with an everyday analogy, how they fit together as "hiring someone", and the two distinctions people trip over (skill vs context, memory vs context). - concepts/agents.md — what an agent is made of; the prompt as a job description; choosing a model; why an agent is a record, not a setting. - concepts/skills.md — reusable instruction sheets; agentskills.io; both authoring routes; the quarantine review gate and why it exists. - concepts/memory.md — what persists between conversations; memory vs context; per-user isolation; why it gets consolidated. - concepts/context.md — documents vs files vs object queries and when each is right; the budget; context is material, never instructions. - concepts/tools-and-mcp.md — what MCP actually means and what it buys you; tools are granted, not assumed; auto/confirm/deny risk classification. - concepts/rag.md — retrieve → augment → generate; RAG vs context vs memory; search modes; sources spend budget; it cannot see past your permissions. - concepts/runs-and-schedules.md — the four triggers; what happens in a run; why unattended runs need delivery, gates and cost limits. intro.md now points newcomers at the overview first. Also unpublish four internal engineering notes that were live on the public docs site (draft: true — files kept, reversible): - canonical-files.md, fleet-extras-audit.md, fleet-drift-deeper.md — fleet template/audit notes with ZERO mentions of hermiq across 664 lines. - PORT-PLAN.md — the internal Hermes→Nextcloud port plan. openregister.conduction.nl 404s on the same paths, confirming the fleet norm is not to publish them. Live-verified: 8 concept pages 200, 4 internal notes now 404.
rubenvdlinde
added a commit
that referenced
this pull request
Aug 7, 2026
…hors, exception translation, icons
All four ran for the first time on this repo when hydra-gates-ref moved
v1.3.0 -> v1.5.0, so this is newly-VISIBLE debt rather than newly-created.
gate-40 form-label-association — two raw <textarea> elements in Chat.vue (the
feedback box and the message composer) carried only a :placeholder. A
placeholder is not an accessible name: it disappears the moment the user types,
so anyone on a screen reader loses the field's description mid-entry (WCAG
3.3.2, 4.1.2). Both now carry an aria-label; the placeholder stays as the hint
it always was.
gate-46 spec-anchor-existence — three unresolved targets:
- MetricsController pointed at openspec/changes/example-change/tasks.md, a
directory that does not exist. Repointed to the canonical
observability spec's REQ-OBS-001, which is exactly what the controller
implements (Prometheus metrics, admin only).
- AgentsController and its test used #task-4-1, which is a LIST ITEM, not a
heading, so it never resolved. The file's own convention is number +
kebab-cased heading (#1-port-the-chat-engine resolves today), so these
become #4-mirror-the-routes.
Also fixed HealthController's three example-change refs while here — same
broken placeholder, and it is outside this diff only by accident of which files
this PR touched. REQ-OBS-002 is the health endpoint.
gate-49 controller-exception-translation — SettingsController::index/create/load
had no try/catch and no @throws. An uncaught throwable there becomes a framework
500 with a stack trace: no use to the caller, and index() is #[NoAdminRequired],
so it leaks internals to a non-admin. All three now translate to a JSON error
and log why. This needed a logger on the constructor, so the hand-built
controller in SettingsControllerTest gains a mock — checked for, rather than
discovered by a red build.
gate-55 detail-page-discipline — six widget icons named MDI glyphs the shared
registry cannot render, so each drew the "?" fallback (ADR-062 rule 8). Mapped
to registry entries by MEANING, not by nearest spelling: PackageVariantClosed ->
Package, CogOutline -> Cog, ChartTimelineVariant -> Timeline ("Total runs
recorded"), SchoolOutline -> School ("Learnings"), PlayCircleOutline ->
RocketLaunch (the run-operations card — the registry has no play glyph).
Verified by running check_detail_page_discipline.py locally: 6 findings -> 0,
and the manifest still passes Ajv validation.
rubenvdlinde
added a commit
that referenced
this pull request
Aug 7, 2026
…that made either visible (#162) * feat(hermiq): rail-based agent detail, fuller dashboard, and the CSS that made either visible The layout the manifest already described was never reaching the screen. Hermiq shipped no GridStack stylesheet: nc-vue imports it as a side-effect import from an aliased package and webpack tree-shakes those away, and it is absent from dist/nextcloud-vue.css too (0 occurrences of its base rules, while leaflet's 174 ARE there). GridStack >= 11 sets left/top INLINE but takes `position: absolute` on items and `position: relative` on the container from that sheet, so every widget fell into normal flow: correct widths, no horizontal placement, all 11 cells stacked in one column down the left third of a 1600px viewport. Measured before the fix: gs-x="3" carried `left: calc(3 * var(--gs-column-width))` resolving to 25% while `position` computed to `static`, and every item reported left=371px. procest (same renderer, gridstack 10) positioned correctly, which is what localised it to hermiq. The stylesheet is fixed IN THE LIBRARY, not here — @conduction/nextcloud-vue's src/css/index.css now @imports it so postcss-import folds it into the extracted sheet, covering both the published-dist and aliased-to-src resolution paths. An app-level import would duplicate those rules in every consumer. main.js carries only a note recording why it is deliberately absent. Layout, on top of that: - AgentDetail adopts procest CaseDetail's 8/4 rail — an 8-column main column (Configuration, Run operations, Memory) beside a persistent 4-column rail (the four KPI tiles stacked, Skills, Eval baseline), both closing exactly at row 16. 33 rows -> 28, zero unfilled cells, and zero widgets whose scrollHeight exceeds their cell (ADR-062), all re-measured in a browser. - Tool governance halves from gridHeight 14 to 8: its two surfaces are peers over one capability, so they are now tabbed rather than stacked, with a real tablist (roving tabindex, aria-selected/aria-controls, arrow-key nav). It draws its own heading and sets showTitle:false, because with the chrome title on that title is a SIBLING of the widget root inside a display:block grid item, so height:100% resolved against the full box and ignored it — a permanent 6px overflow no gridHeight could remove. - Dashboard closes its grid (the bottom row left 6 of 12 columns empty) and gains a runs-by-agent chart, an agents table, header actions and a KPI drilldown. 12 rows, zero unfilled cells. AnalyticsService enriches perAgent with `name`: the aggregate carried only agentId, and a chart labelled with UUIDs is unreadable. This instance has zero runs, so the chart was verified by a positive control — the endpoint was intercepted with a synthetic payload and it rendered 6 bars across 3 name-labelled categories. Also fixes a pre-existing 500: AgentsController::stats bound PHP's (string)false — the empty string — against a boolean column, so every call died with SQLSTATE[22P02] and the agent counters had never worked. Booleans are now normalised to 'true'/'false'. Verified against DB ground truth with a deliberately-inactive probe agent (18/17/1), since with 17/17 active a broken filter and a working one are indistinguishable. Security postures declared rather than inherited (hydra gates 5/7/9/30): - SettingsController::create, SetupController::saveConfig and ::runAction were admin-only by Nextcloud's default for an un-attributed method, which is silently lost the moment anyone adds #[NoAdminRequired]. Now #[AuthorizedAdminSetting(AdminSettings::class)]. runAction matters most: test-llm makes the server issue an outbound request, so unauthenticated it is an SSRF primitive. - MetricsController::index stays admin-authed per ADR-006 ("/api/metrics Prometheus text, admin auth" vs "/api/health JSON, public") and gains #[NoCSRFRequired], which is what a scraper actually needs. - Four endpoints taking no caller-supplied object id carry reason-bearing @no-admin-idor-exempt tags. create() was checked first: `owner` and `organisation` are in PROTECTED_KEYS and assigned server-side. Accessibility: scope="col" on every column header across four components, a visually-hidden label on a bare <th />, and prefers-reduced-motion fallbacks (WCAG 2.2 AA 2.3.3). The chat typing indicator is an infinite bounce — the pattern that triggers vestibular symptoms — and is replaced with a static opacity rather than removed, so "assistant is typing" survives the motion. The e2e login no longer waits for 'networkidle', which never settles on Nextcloud (ADR-074 rule 4): the wait always ran to its timeout and only worked because .catch() swallowed it — a disguised fixed delay, not a readiness signal. It now waits for the submit control to be present and enabled. * test(agents): pin the normalised filter, which stats() depends on testStatsUsesPaginatedTotals still matched `$query['active'] === true`, the contract from BEFORE countAgents() started normalising booleans. The branch sends the string 'true', so that arm never matched, the callback fell through to the inactive branch, and the assertion read: total 10, active 4, inactive 4 counts that do not add up — which is the tell that the mock, not the code, was wrong. The normalisation is the fix, not the defect. A bool bound as a query parameter casts to '1' for true and the EMPTY STRING for false, and Postgres rejects '' on a boolean column with SQLSTATE[22P02] — so stats() was a hard 500 on every call and the dashboard's agent counters showed nothing. The callback now asserts the value is a string rather than only matching one, so a regression to raw booleans fails here with a readable message instead of quietly returning the wrong count. * fix(gates): the four findings gate v1.5.0 surfaced — labels, spec anchors, exception translation, icons All four ran for the first time on this repo when hydra-gates-ref moved v1.3.0 -> v1.5.0, so this is newly-VISIBLE debt rather than newly-created. gate-40 form-label-association — two raw <textarea> elements in Chat.vue (the feedback box and the message composer) carried only a :placeholder. A placeholder is not an accessible name: it disappears the moment the user types, so anyone on a screen reader loses the field's description mid-entry (WCAG 3.3.2, 4.1.2). Both now carry an aria-label; the placeholder stays as the hint it always was. gate-46 spec-anchor-existence — three unresolved targets: - MetricsController pointed at openspec/changes/example-change/tasks.md, a directory that does not exist. Repointed to the canonical observability spec's REQ-OBS-001, which is exactly what the controller implements (Prometheus metrics, admin only). - AgentsController and its test used #task-4-1, which is a LIST ITEM, not a heading, so it never resolved. The file's own convention is number + kebab-cased heading (#1-port-the-chat-engine resolves today), so these become #4-mirror-the-routes. Also fixed HealthController's three example-change refs while here — same broken placeholder, and it is outside this diff only by accident of which files this PR touched. REQ-OBS-002 is the health endpoint. gate-49 controller-exception-translation — SettingsController::index/create/load had no try/catch and no @throws. An uncaught throwable there becomes a framework 500 with a stack trace: no use to the caller, and index() is #[NoAdminRequired], so it leaks internals to a non-admin. All three now translate to a JSON error and log why. This needed a logger on the constructor, so the hand-built controller in SettingsControllerTest gains a mock — checked for, rather than discovered by a red build. gate-55 detail-page-discipline — six widget icons named MDI glyphs the shared registry cannot render, so each drew the "?" fallback (ADR-062 rule 8). Mapped to registry entries by MEANING, not by nearest spelling: PackageVariantClosed -> Package, CogOutline -> Cog, ChartTimelineVariant -> Timeline ("Total runs recorded"), SchoolOutline -> School ("Learnings"), PlayCircleOutline -> RocketLaunch (the run-operations card — the registry has no play glyph). Verified by running check_detail_page_discipline.py locally: 6 findings -> 0, and the manifest still passes Ajv validation. * i18n: the accessible names added for gate-40 'Feedback details' and 'Message' are now the accessible names of the two chat textareas, so they are user-facing strings and need translating like any other. Dutch supplied — an aria-label left in English is exactly the string a Dutch screen-reader user hears. * fix(lint): drop src/Chat.vue — a stray duplicate my own 'git add -A' swept in src/Chat.vue is an older copy of src/views/Chat.vue. I had looked at it earlier, established it is NOT the file the app loads (src/registry.js imports ./views/Chat.vue) and is strictly worse — it lacks the data-testid the e2e suite selects on and the prefers-reduced-motion block — and decided to leave it untracked. Then I staged the gate fixes with 'git add -A src' and committed it anyway. Its relative imports are written for src/views/, so from src/ none of the seven resolve, and eslint failed with exactly those seven import/no-unresolved errors. Untracked again and added to .gitignore, so the same accident cannot repeat. The file stays on disk — it is untracked, so git could not give it back. * test(settings): exercise the three catch paths the coverage guard flagged The coverage guard failed the PR: 24 statements added, coverage 68.25% -> 68.20%. The added statements were the try/catch blocks from the gate-49 fix, and nothing exercised them. That is the right complaint. An untested catch block is indistinguishable from no catch at all — it is the 'a check that never runs looks exactly like one that passed' shape, and the whole point of the block is what happens on the path nobody normally takes. Each test now asserts the TRANSLATED response, not merely that the method survives: index() 500 + an 'error' key, and the internal exception message is NOT echoed back — that leak is what the translation exists to stop, and index() is #[NoAdminRequired] so it would reach a non-admin create() success:false, the shape the UI already branches on load() says nothing was changed, which is the one fact the admin who clicked reload needs * chore(gates): record the gate-49 opt-out where the gate actually reads it [hydra-gate-controller-exception-translation exclude] SettingsController::index/create/load each catch \Throwable, return a translated JSON error and log the cause; the gate matches only nine named domain exceptions and does not recognise \Throwable, which is strictly broader and covers all nine. Upstream: ConductionNL/.github#204. The same text is in the PR body, but a re-run did not pick it up there — the commit-message path is the one that reliably reaches the gate.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The docs assumed you already knew what an agent, skill, memory, context, MCP or RAG is — nothing explained them, and
intro.mdopened with "manifest-first Vue 2 frontend rendered by CnAppRoot".Adds a Concepts section written for someone who has never used an AI system:
concepts/index.md— the overview: starts from the problem, a table of every concept with an everyday analogy, how they fit together as "hiring someone", and the two distinctions people trip over (skill vs context, memory vs context).intro.mdnow points newcomers at the overview first.Also unpublishes 4 internal engineering notes that were live on the public site (
draft: true, files kept):canonical-files,fleet-extras-audit,fleet-drift-deeper(zero hermiq mentions across 664 lines — fleet template leftovers) andPORT-PLAN(internal port plan). openregister.conduction.nl 404s on the same paths, confirming the fleet norm.Live-verified: 8 concept pages 200, 4 internal notes 404. Deployed to hermiq-docs.pages.dev.