From 6e87ffbe9c039800dc941154283fe3635e8583f9 Mon Sep 17 00:00:00 2001 From: d-oit Date: Tue, 3 Nov 2026 09:25:34 +0100 Subject: [PATCH 1/3] chore(agents): centralize instruction surfaces Generate tool instructions and labeler globs from the manifest; validate the declared per-client skill-link strategies. Fixes #1050. --- .agents/instructions/claude.md | 42 +++ .agents/manifest.json | 13 +- .gemini/skills/deepsource | 1 + .gemini/skills/static-analysis-suppression | 1 + .gemini/skills/vercel-deployment | 1 + .gemini/skills/verify-before-asserting | 1 + .github/labeler.yml | 4 +- .qwen/skills/deepsource | 1 + .qwen/skills/static-analysis-suppression | 1 + .qwen/skills/vercel-deployment | 1 + .qwen/skills/verify-before-asserting | 1 + CLAUDE.md | 253 +++++++++++++++++- GEMINI.md | 253 +++++++++++++++++- QWEN.md | 253 +++++++++++++++++- agents-docs/HARNESS.md | 16 +- agents-docs/MANIFEST.md | 40 +-- ...-1065-ordered-implementation-2026-11-03.md | 101 +++++++ .../ADRs/029-manifest-driven-agent-harness.md | 39 ++- plans/INDEX.md | 7 + scripts/agent-surface.py | 120 ++++++--- scripts/agent_surface_generated.py | 228 ++++++++++++++++ scripts/setup-skills.sh | 60 +---- tests/agent-surface-generated.bats | 141 ++++++++++ tests/setup-skills.bats | 72 +++-- 24 files changed, 1496 insertions(+), 154 deletions(-) create mode 100644 .agents/instructions/claude.md create mode 120000 .gemini/skills/deepsource create mode 120000 .gemini/skills/static-analysis-suppression create mode 120000 .gemini/skills/vercel-deployment create mode 120000 .gemini/skills/verify-before-asserting create mode 120000 .qwen/skills/deepsource create mode 120000 .qwen/skills/static-analysis-suppression create mode 120000 .qwen/skills/vercel-deployment create mode 120000 .qwen/skills/verify-before-asserting create mode 100644 plans/194-open-issues-1050-1065-ordered-implementation-2026-11-03.md create mode 100755 scripts/agent_surface_generated.py create mode 100755 tests/agent-surface-generated.bats diff --git a/.agents/instructions/claude.md b/.agents/instructions/claude.md new file mode 100644 index 00000000..43fea272 --- /dev/null +++ b/.agents/instructions/claude.md @@ -0,0 +1,42 @@ +## Claude Code Features + +### Sub-Agents +Sub-agent definitions live in `.claude/agents/`. Each is a Markdown file with YAML +frontmatter (`name`, `description`, `tools`, `model`). + +Default agents in this template: +- `agent-creator` - scaffold new sub-agent definitions +- `goap-agent` - goal-oriented action planning for complex workflows +- `loop-agent` - iterative refinement loops +- `analysis-swarm` - parallel multi-perspective code analysis + +Delegate context-heavy research to sub-agents to keep the parent session focused. +See `agents-docs/SUB-AGENTS.md`. + +### Skills +Skills live canonically in `.agents/skills/`. The `.claude/skills/` folder contains +only symlinks. Run `./scripts/setup-skills.sh` once after cloning. + +Skills use progressive disclosure - `SKILL.md` is injected only when the agent +decides the skill is needed. Do not pre-load all skills at session start. +See `agents-docs/AVAILABLE_SKILLS.md`. + +### Custom Commands +Project slash commands live in `.claude/commands/`. Use them for repeatable workflows. + +### Hooks +Verification hooks run automatically on agent stop events. +- Exit `0` = silent success (nothing extra enters context) +- Exit `2` = errors surfaced to agent, forcing remediation before finishing +See `agents-docs/HOOKS.md`. + +### Headless / CI Mode +```bash +claude -p "your prompt" --output-format stream-json +``` + +### Context Management +- Use `/clear` between unrelated tasks +- Use `Glob`/`Grep` to find code instead of reading whole file trees +- Prefer sub-agents for multi-step research tasks +- See `agents-docs/CONTEXT.md` diff --git a/.agents/manifest.json b/.agents/manifest.json index 2c06ade1..bd49becf 100644 --- a/.agents/manifest.json +++ b/.agents/manifest.json @@ -1,6 +1,8 @@ { "version": "1.0.0", + "canonical_instructions": "AGENTS.md", "canonical_skills": ".agents/skills", + "skill_labeler": ".github/labeler.yml", "global_docs": [ "AGENTS.md", "README.md" @@ -12,7 +14,8 @@ "docs": [ "CLAUDE.md" ], - "symlink_strategy": "relative" + "instruction_addendum": ".agents/instructions/claude.md", + "symlink_strategy": "relative-entries" }, "gemini": { "directory": ".gemini", @@ -20,7 +23,7 @@ "docs": [ "GEMINI.md" ], - "symlink_strategy": "relative" + "symlink_strategy": "relative-entries" }, "qwen": { "directory": ".qwen", @@ -28,17 +31,17 @@ "docs": [ "QWEN.md" ], - "symlink_strategy": "relative" + "symlink_strategy": "relative-entries" }, "cursor": { "directory": ".cursor", "skills_directory": ".cursor/skills", - "symlink_strategy": "relative" + "symlink_strategy": "relative-directory" }, "windsurf": { "directory": ".windsurf", "skills_directory": ".windsurf/skills", - "symlink_strategy": "relative" + "symlink_strategy": "relative-directory" }, "opencode": { "directory": ".opencode", diff --git a/.gemini/skills/deepsource b/.gemini/skills/deepsource new file mode 120000 index 00000000..a4d9536b --- /dev/null +++ b/.gemini/skills/deepsource @@ -0,0 +1 @@ +../../.agents/skills/deepsource \ No newline at end of file diff --git a/.gemini/skills/static-analysis-suppression b/.gemini/skills/static-analysis-suppression new file mode 120000 index 00000000..a460ba4d --- /dev/null +++ b/.gemini/skills/static-analysis-suppression @@ -0,0 +1 @@ +../../.agents/skills/static-analysis-suppression \ No newline at end of file diff --git a/.gemini/skills/vercel-deployment b/.gemini/skills/vercel-deployment new file mode 120000 index 00000000..f43a08ec --- /dev/null +++ b/.gemini/skills/vercel-deployment @@ -0,0 +1 @@ +../../.agents/skills/vercel-deployment \ No newline at end of file diff --git a/.gemini/skills/verify-before-asserting b/.gemini/skills/verify-before-asserting new file mode 120000 index 00000000..9eba7a05 --- /dev/null +++ b/.gemini/skills/verify-before-asserting @@ -0,0 +1 @@ +../../.agents/skills/verify-before-asserting \ No newline at end of file diff --git a/.github/labeler.yml b/.github/labeler.yml index 52f0a1a1..fc2ffb82 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -11,15 +11,17 @@ documentation: - 'docs/**/*' # Skill changes +# BEGIN GENERATED SKILLS LABELS skills: - changed-files: - any-glob-to-any-file: - '.agents/skills/**/*' - '.claude/skills/**/*' + - '.cursor/skills/**/*' - '.gemini/skills/**/*' - '.qwen/skills/**/*' - - '.cursor/skills/**/*' - '.windsurf/skills/**/*' +# END GENERATED SKILLS LABELS # Script changes scripts: diff --git a/.qwen/skills/deepsource b/.qwen/skills/deepsource new file mode 120000 index 00000000..a4d9536b --- /dev/null +++ b/.qwen/skills/deepsource @@ -0,0 +1 @@ +../../.agents/skills/deepsource \ No newline at end of file diff --git a/.qwen/skills/static-analysis-suppression b/.qwen/skills/static-analysis-suppression new file mode 120000 index 00000000..a460ba4d --- /dev/null +++ b/.qwen/skills/static-analysis-suppression @@ -0,0 +1 @@ +../../.agents/skills/static-analysis-suppression \ No newline at end of file diff --git a/.qwen/skills/vercel-deployment b/.qwen/skills/vercel-deployment new file mode 120000 index 00000000..f43a08ec --- /dev/null +++ b/.qwen/skills/vercel-deployment @@ -0,0 +1 @@ +../../.agents/skills/vercel-deployment \ No newline at end of file diff --git a/.qwen/skills/verify-before-asserting b/.qwen/skills/verify-before-asserting new file mode 120000 index 00000000..9eba7a05 --- /dev/null +++ b/.qwen/skills/verify-before-asserting @@ -0,0 +1 @@ +../../.agents/skills/verify-before-asserting \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 73c9d86f..cf7145f6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,255 @@ -@AGENTS.md + - +# AGENTS.md + +> Single source of truth for AI coding agents working in this repository. + +## Project + +Local-first knowledge studio built with Next.js 16 / React 19 / Tailwind 4 / shadcn / Zustand. Rich text editing, knowledge graph, mind maps, client-side search, export, and an AI agent harness. Persistence via Zustand + IndexedDB, with a small localStorage metadata/recovery envelope. No required backend. + +## Hard Rules + +- Local-first only. Do not introduce a required backend. +- Zustand + IndexedDB is the persistence layer: the canonical corpus lives in IndexedDB, and localStorage carries only schema metadata, recovery snapshots and quarantine copies. Do not grow the localStorage envelope into a second corpus. +- Markdown is import/export, not canonical truth. +- Use strict TypeScript. Do not use `any`. +- No magic numbers; extract descriptive named constants. +- No hardcoded project-level settings outside config, constants, or env layers. +- Max 500 LOC per source file; refactor before extending oversized files. Exception: shadcn-generated primitives (e.g., `sidebar.tsx`) may exceed this limit — document the exception in `plans/` rather than splitting vendor scaffolding. +- Prefer small, composable modules over mixed-responsibility files. +- Reuse existing abstractions before introducing new patterns. +- Keep changes scoped; avoid unrelated refactors in the same commit. +- **Never create a GitHub release without explicit human instruction.** Releases are coordinated manually by the maintainer — the `version-propagation.yml` workflow was retired in Plan 073. Do NOT run `gh release create` unless the user explicitly asks you to cut a release. You may prepare draft release notes in `plans/` on request, but never publish a release autonomously. +- Never modify `biome.json`, any `eslint` configuration file, or lint suppressions/ignore settings unless I explicitly request that change. If such a change seems necessary, stop, explain why, and ask for approval before editing. +- Escape JSX text content — use `'` for apostrophes, `"` for quotes in JSX text nodes. Do not rely on raw punctuation in JSX strings. +- All `catch` blocks must handle errors meaningfully — log, toast, or rethrow. Never use empty `catch {}` blocks. +- Use `try/catch` with `finally` for resource cleanup (AbortController, timers, subscriptions, file handles). +- Clean up side effects in `useEffect` return functions — abort fetches, clear timers, remove listeners. +- Use `AbortController` for all `fetch` calls to prevent stale responses and memory leaks. +- Prefer `const` assertions and `satisfies` over type assertions for type narrowing. +- Use `React.memo` for pure components that receive stable props to prevent unnecessary re-renders. +- Use `useCallback` for event handlers passed to child components, `useMemo` for expensive computations. +- Never hardcode user-facing strings — extract to constants or i18n-ready string maps for future localization. +- Use `Intl.DateTimeFormat`, `Intl.NumberFormat`, and `Intl.RelativeTimeFormat` for locale-sensitive formatting. +- Validate all external input at boundaries (API responses, user input, localStorage rehydration) with Zod schemas. +- Never log secrets, API keys, or sensitive data. Use `console.error` for errors, not `console.log` in production. +- Prefer `structuredClone` over JSON parse/stringify for deep copies. +- Use `crypto.randomUUID()` for generating IDs, not `Math.random()`. +- Prefer native `AbortController` over custom cancellation patterns. +- Use `queueMicrotask` or `requestAnimationFrame` for batching DOM updates, not `setTimeout(fn, 0)`. +- Prefer `Intl.Segmenter` over manual string splitting for i18n-safe text processing. +- **Always use named exports.** Never use `export default`. Named exports enable better tree-shaking, safer refactoring (find-all-references works), and consistent import style across the codebase. **Exception**: Next.js App Router files (`src/app/page.tsx`, `src/app/layout.tsx`) require `export default` by framework convention — this is the only acceptable case. +- **Prefer `const fn = () => {}` over `function fn() {}` for module-scope helper functions (especially in test files).** DeepSource's JS-0067 rule flags top-level `function` declarations as "global scope" (a false positive for ES modules, but it fails the check). Arrow-function expressions avoid this. This applies to test helpers (`makeEntity`, `resetStore`, etc.) and non-exported internal helpers in `src/`. The `.deepsource.toml` `issue_patterns` suppression for JS-0067 is a belt-and-suspenders fallback, not the primary fix. +- **Never ignore pre-existing issues or warnings.** Every warning, lint error, or failing check must be fixed or documented as a follow-up task in `plans/`. Ignoring issues compounds technical debt and erodes trust in the pipeline. +- Test filenames should describe the behavior or subsystem under test, not coverage chasing. Rename historical files only when a consumer or naming collision requires it. +- **Treat warnings as errors in quality gates.** When running `pnpm run lint`, `pnpm run typecheck`, `pnpm run test`, or `pnpm run build`, any output containing `⚠`, `warning`, `Warning`, or non-zero exit code must be addressed immediately — do not proceed to the next task until the warning is fixed or explicitly documented as a follow-up. Build warnings (e.g., deprecation notices, lockfile conflicts) are not informational — they are defects. +- **Always run code review before merge.** After CI passes and before requesting merge, invoke the `code-review-assistant` skill to perform a structured review of all changed files. Address all P1/P2 findings. No PR merges without a completed review pass. +- **Never merge with Codacy issues.** If Codacy reports `ACTION_REQUIRED`, `FAILED`, or any new issues on a PR, the PR must not be merged until all issues are resolved or explicitly suppressed with a documented reason. Zero tolerance: no warnings, no failures, no "action required" status. + +When Codacy reports new issues on a PR, invoke the `codacy` skill to query findings via `codacy pull-request ... --output json`, identify whether each is a true or false positive using `toolInfo.name` and `resultDataId`, and suppress false positives with `--ignore-issue`. Do not guess the analysis engine from suppression comment syntax — read `toolInfo.name` from the API. + +## Repository Shape + +- `src/app` - App shell, routing, layout (App Router) +- `src/components/studio` - React components (views, UI primitives) +- `src/lib/studio` - Zustand store, types, seed data, utilities +- `src/lib/ai` - AI provider adapters · `src/lib/search` - retrieval engine +- `src/lib/export` - JSON, MD, HTML, encrypted export +- `scripts/` - repository automation · `plans/` - GOAP plans, ADRs, audits +- `agents-docs/` - harness, workflow, config, hooks, skill docs + +## Planning Workflow + +- For every non-trivial task, create or update planning artifacts in `plans/`. +- Use `plans/` for GOAP decomposition, task plans, audits, benchmarks, and implementation notes. +- Use `plans/ADRs/` for architecture decision records. +- If a task changes architecture, storage, search behavior, export behavior, agent workflow, or cross-cutting infrastructure, add or update an ADR. +- Follow the existing `plans/` naming style: numbered plans for implementation tracks, descriptive names for audits and benchmarks, and dedicated ADR files in `plans/ADRs/`. +- Do not write planning, audit, or analysis markdown in the repository root. + +## File Placement Rules + +- Never create temp, debug, scratch, analysis, or one-off script files in the repository root. +- Never leave ad-hoc `tmp-*`, `debug-*`, `analyze-*`, `notes-*`, or throwaway files in the root. +- Use `plans/` for persistent analysis and planning artifacts. +- Use `scripts/` only for reusable repository automation that belongs in version control. +- Use system temp locations such as `/tmp` or `mktemp` for ephemeral files. +- If an output is only needed during execution, keep it out of the repository. +- Root-level files must remain intentional project manifests, configs, or primary documentation only. + +## Setup + +Use `pnpm` only — never npm or yarn (the lockfile is pnpm v9 format). + +```bash +pnpm install +./scripts/setup-skills.sh +./scripts/validate-skills.sh +``` + +## Development Commands + +```bash +pnpm run dev +pnpm run build +pnpm run start +pnpm run lint +pnpm run typecheck +pnpm run test +``` + +## Quality Workflow + +```bash +pnpm run lint && pnpm run typecheck && pnpm run test && pnpm run build +``` + +- Fast loop: `./scripts/minimal_quality_gate.sh` (lint + typecheck + test), or pick a command from the `do-harness.toml` sensor map (`feedback`, `verification`, `offline`, `schema/data`, `release`; see `agents-docs/HARNESS.md`). +- Also run `pnpm run test:e2e` for UI, editor, graph, mind map, search, export, + or any critical workflow change — a unit test proves the function, not the app. +- Before commit: `./scripts/quality_gate.sh`. After CI passes: the + `code-review-assistant` skill, addressing all P1/P2 findings. +- CI repair: `./scripts/self-fix-loop.sh`. + +Warnings are errors — a deprecation notice or lockfile conflict is a defect, +not progress. Full script catalog: `agents-docs/SCRIPTS.md`. + +## Delivery Lifecycle + +A change is not done when it builds — it is done when it survives production. + +```text +production → failure → reproduce → candidate fix → evaluate + → adversarial → shadow → canary → promote | rollback +``` + +Each stage needs a falsifiable exit criterion: a test that fails for the +*stated* reason, a green gate, an attempt to break it that failed, a +comparison recorded in `plans/`. "Looks good" is not a criterion. + +Stage-by-stage exit criteria and tooling: `agents-docs/DELIVERY-LIFECYCLE.md`. +Pick depth by blast radius: docs need only Evaluate; a bug fix runs +reproduce → evaluate → adversarial; persistence, hydration, undo/redo, and +sync additionally need shadow and canary. **Data-loss bugs start at +production** — never ship a persistence change that was not driven against a +real corpus in a real browser. + +This repo is local-first with **no** flag library, **no** error telemetry, and +a single global Vercel deployment. A flag is a store field or `localStorage` +read, default-off. Shadow means both paths reachable, compared on the same +input, recorded in `plans/` — skip it for a pure refactor rather than fake one. +Never add machinery that needs a server to work offline. + +Stage mechanics, harness guardrails, and the per-stage skill map: +`agents-docs/DELIVERY-LIFECYCLE.md`. + +## Testing Expectations + +- Use Vitest for unit and integration coverage. +- Keep critical flows covered: entity CRUD, claim creation, local search/chat flows, graph interaction, and mind map editing. +- When touching database, validation, NLP/search, queueing, or export behavior, add or update tests close to the changed logic. +- Run `pnpm run test:coverage` for core data-model, search, export, or infrastructure changes. + +## UI / UX Guardrails + +- Use design tokens from `src/app/globals.css` `@theme` block (source of truth). +- Two themes: `light` and `dark` via the `.dark` class on `` (next-themes `attribute="class"`). +- Accent color: Saffron (`#9a5c2a` light, `#e5944a` dark). +- Font: Geist Sans (body) + Newsreader (serif headings). +- Build mobile-first; keep interactive targets at least 44x44px. +- Preserve responsive behavior across editor, graph, search, and mind map views. +- Never hardcode hex values in components — use tokens. + +## Git Workflow + +- Branch from up to date, never commit to `main`, use conventional commits. +- Code review (`code-review-assistant`) is mandatory after CI passes; address + all P1/P2 findings before requesting merge. +- `Codacy Static Code Analysis` is the only ruleset-required check on `main` and + has a zero-tolerance rule. A **missing** check is a transient delay, not a + defect — diagnose via `commits/{sha}/check-runs`, then nudge with an + empty-commit push. Never merge with it failing. +- Unresolved review threads block merges, **including outdated ones**. Resolve + every thread via GraphQL `resolveReviewThread`. +- `required_linear_history` is set: always `--squash`, never a merge commit. +- Never use `gh pr merge --admin` without explicit human approval. +- `BLOCKED` with all-green `gh pr checks` is usually staleness, not a real + failure — `pr-merge-state-diagnoser.yml` automates that diagnosis. + +### Standing merge authorization — do not ask + +**When a PR is fully clean, merge it. Do not ask.** The maintainer has +pre-authorized this; a redundant confirmation prompt is a defect, not caution. + +Merge when ALL hold: every check passes (including Codacy and e2e), every +review thread is resolved (outdated ones count), the `code-review-assistant` +pass is complete with no unaddressed P1/P2, and the local gate is green. Then +`gh pr merge --auto --squash --delete-branch` and report the outcome. +`--auto` lands it when CI clears, without holding a session open. + +Ask ONLY for: a suspected-genuine Codacy false positive, a declined P1/P2, a +release/migration/auth or otherwise irreversible change, a branch-protection +edit, or an explicit "hold" in the thread. Never use `--admin` to clear a gate. + +Full criteria, rationale, and the worked example: +`agents-docs/GIT-WORKFLOW.md`. + +Full procedure, staleness ladder, and the Codacy false-positive playbook: +`agents-docs/GIT-WORKFLOW.md`. Lessons: `agents-docs/LESSONS.md`. + +## Learnings (session-distilled) + +Non-obvious toolchain facts; full detail in `agents-docs/LESSONS.md` +(LESSON-001..049) and `lessons.jsonl`. + +- **gitleaks-action v3+ needs a paid licence** (pin v2.x); yamllint enforces + `line-length` 120 and a trailing newline inside `run: |` blocks. +- **DeepSource `.deepsource.toml` suppressions do not reliably fail-proof a check** + — `const fn = () => {}` for module-scope helpers; exported complexity under 6. +- **A test you have not seen fail is not evidence** (LESSON-041): re-inject the + defect, watch it fail, restore it. Codacy/DeepSource re-post stale findings as + NEW blocking threads — fix, reply, resolve, stop pushing (LESSON-034). Vitest + typecheck misses `*.test.ts` source errors (LESSON-035). +- **A check that cannot fail is worse than none** (LESSON-046, LESSON-047): + `markdownlint.toml` was inert (rules under `[tool.markdownlint]`, ignored by the + CLI; unknown ids are accepted *silently*) and `quality_gate.sh` passed its config + only as `lint_if_changed`'s cache key, so the command ran without `--config`. + `MD018`'s `--fix` rewrites wrapped prose beginning `#865` into a real `

`. +- **Vercel Hobby allows 100 deployments per 24 h, owner-scoped, and production + shares previews' budget** (LESSON-048, LESSON-049): every deployment — the live + site included — failed 2026-10-03..13 while the non-required `Vercel` check + blocked nothing. One push is one deployment; never skip `scripts/` (`pnpm run + build` writes the served precache manifest); fail **closed** on unknown state. +- **Worktrees:** `git add -A` stages a symlinked `node_modules` (`HUSKY=0` does not + skip `.githooks`); commit before any rebase or `reset --hard` (LESSON-042). Bless + a head before merging — stale-base automation arrives with `main`-reverting trees + (LESSON-043); dependency ranges can move backwards silently (LESSON-044). +- **Feature-detecting two APIs hides a missing feature** — an `AbortSignal.any` + fallback dropped a timeout; a bare `stop` binds `window.stop` (LESSON-045). +- **CI dispatch** — push and dispatch runs on `main` cancel each other; rerun the cancelled workflow (LESSON-050/051). + +## Skills + +Canonical skills live in `.agents/skills/`; refresh symlinks with +`./scripts/setup-skills.sh`. **Load only what the stage needs** — every +always-loaded skill is a tax. Prefer existing skills and `agents-docs/` +guidance over inventing a workflow, and add a skill only after a real failure +(`agents-docs/HARNESS.md`). + +Catalog: `agents-docs/AVAILABLE_SKILLS.md` (regenerate with +`./scripts/generate-skills-docs.py`); per-stage map: +`agents-docs/DELIVERY-LIFECYCLE.md`. + + + +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. + +This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. + + ## Claude Code Features diff --git a/GEMINI.md b/GEMINI.md index 8bac7d1b..554ce2dd 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -1,3 +1,252 @@ -@AGENTS.md + - +# AGENTS.md + +> Single source of truth for AI coding agents working in this repository. + +## Project + +Local-first knowledge studio built with Next.js 16 / React 19 / Tailwind 4 / shadcn / Zustand. Rich text editing, knowledge graph, mind maps, client-side search, export, and an AI agent harness. Persistence via Zustand + IndexedDB, with a small localStorage metadata/recovery envelope. No required backend. + +## Hard Rules + +- Local-first only. Do not introduce a required backend. +- Zustand + IndexedDB is the persistence layer: the canonical corpus lives in IndexedDB, and localStorage carries only schema metadata, recovery snapshots and quarantine copies. Do not grow the localStorage envelope into a second corpus. +- Markdown is import/export, not canonical truth. +- Use strict TypeScript. Do not use `any`. +- No magic numbers; extract descriptive named constants. +- No hardcoded project-level settings outside config, constants, or env layers. +- Max 500 LOC per source file; refactor before extending oversized files. Exception: shadcn-generated primitives (e.g., `sidebar.tsx`) may exceed this limit — document the exception in `plans/` rather than splitting vendor scaffolding. +- Prefer small, composable modules over mixed-responsibility files. +- Reuse existing abstractions before introducing new patterns. +- Keep changes scoped; avoid unrelated refactors in the same commit. +- **Never create a GitHub release without explicit human instruction.** Releases are coordinated manually by the maintainer — the `version-propagation.yml` workflow was retired in Plan 073. Do NOT run `gh release create` unless the user explicitly asks you to cut a release. You may prepare draft release notes in `plans/` on request, but never publish a release autonomously. +- Never modify `biome.json`, any `eslint` configuration file, or lint suppressions/ignore settings unless I explicitly request that change. If such a change seems necessary, stop, explain why, and ask for approval before editing. +- Escape JSX text content — use `'` for apostrophes, `"` for quotes in JSX text nodes. Do not rely on raw punctuation in JSX strings. +- All `catch` blocks must handle errors meaningfully — log, toast, or rethrow. Never use empty `catch {}` blocks. +- Use `try/catch` with `finally` for resource cleanup (AbortController, timers, subscriptions, file handles). +- Clean up side effects in `useEffect` return functions — abort fetches, clear timers, remove listeners. +- Use `AbortController` for all `fetch` calls to prevent stale responses and memory leaks. +- Prefer `const` assertions and `satisfies` over type assertions for type narrowing. +- Use `React.memo` for pure components that receive stable props to prevent unnecessary re-renders. +- Use `useCallback` for event handlers passed to child components, `useMemo` for expensive computations. +- Never hardcode user-facing strings — extract to constants or i18n-ready string maps for future localization. +- Use `Intl.DateTimeFormat`, `Intl.NumberFormat`, and `Intl.RelativeTimeFormat` for locale-sensitive formatting. +- Validate all external input at boundaries (API responses, user input, localStorage rehydration) with Zod schemas. +- Never log secrets, API keys, or sensitive data. Use `console.error` for errors, not `console.log` in production. +- Prefer `structuredClone` over JSON parse/stringify for deep copies. +- Use `crypto.randomUUID()` for generating IDs, not `Math.random()`. +- Prefer native `AbortController` over custom cancellation patterns. +- Use `queueMicrotask` or `requestAnimationFrame` for batching DOM updates, not `setTimeout(fn, 0)`. +- Prefer `Intl.Segmenter` over manual string splitting for i18n-safe text processing. +- **Always use named exports.** Never use `export default`. Named exports enable better tree-shaking, safer refactoring (find-all-references works), and consistent import style across the codebase. **Exception**: Next.js App Router files (`src/app/page.tsx`, `src/app/layout.tsx`) require `export default` by framework convention — this is the only acceptable case. +- **Prefer `const fn = () => {}` over `function fn() {}` for module-scope helper functions (especially in test files).** DeepSource's JS-0067 rule flags top-level `function` declarations as "global scope" (a false positive for ES modules, but it fails the check). Arrow-function expressions avoid this. This applies to test helpers (`makeEntity`, `resetStore`, etc.) and non-exported internal helpers in `src/`. The `.deepsource.toml` `issue_patterns` suppression for JS-0067 is a belt-and-suspenders fallback, not the primary fix. +- **Never ignore pre-existing issues or warnings.** Every warning, lint error, or failing check must be fixed or documented as a follow-up task in `plans/`. Ignoring issues compounds technical debt and erodes trust in the pipeline. +- Test filenames should describe the behavior or subsystem under test, not coverage chasing. Rename historical files only when a consumer or naming collision requires it. +- **Treat warnings as errors in quality gates.** When running `pnpm run lint`, `pnpm run typecheck`, `pnpm run test`, or `pnpm run build`, any output containing `⚠`, `warning`, `Warning`, or non-zero exit code must be addressed immediately — do not proceed to the next task until the warning is fixed or explicitly documented as a follow-up. Build warnings (e.g., deprecation notices, lockfile conflicts) are not informational — they are defects. +- **Always run code review before merge.** After CI passes and before requesting merge, invoke the `code-review-assistant` skill to perform a structured review of all changed files. Address all P1/P2 findings. No PR merges without a completed review pass. +- **Never merge with Codacy issues.** If Codacy reports `ACTION_REQUIRED`, `FAILED`, or any new issues on a PR, the PR must not be merged until all issues are resolved or explicitly suppressed with a documented reason. Zero tolerance: no warnings, no failures, no "action required" status. + +When Codacy reports new issues on a PR, invoke the `codacy` skill to query findings via `codacy pull-request ... --output json`, identify whether each is a true or false positive using `toolInfo.name` and `resultDataId`, and suppress false positives with `--ignore-issue`. Do not guess the analysis engine from suppression comment syntax — read `toolInfo.name` from the API. + +## Repository Shape + +- `src/app` - App shell, routing, layout (App Router) +- `src/components/studio` - React components (views, UI primitives) +- `src/lib/studio` - Zustand store, types, seed data, utilities +- `src/lib/ai` - AI provider adapters · `src/lib/search` - retrieval engine +- `src/lib/export` - JSON, MD, HTML, encrypted export +- `scripts/` - repository automation · `plans/` - GOAP plans, ADRs, audits +- `agents-docs/` - harness, workflow, config, hooks, skill docs + +## Planning Workflow + +- For every non-trivial task, create or update planning artifacts in `plans/`. +- Use `plans/` for GOAP decomposition, task plans, audits, benchmarks, and implementation notes. +- Use `plans/ADRs/` for architecture decision records. +- If a task changes architecture, storage, search behavior, export behavior, agent workflow, or cross-cutting infrastructure, add or update an ADR. +- Follow the existing `plans/` naming style: numbered plans for implementation tracks, descriptive names for audits and benchmarks, and dedicated ADR files in `plans/ADRs/`. +- Do not write planning, audit, or analysis markdown in the repository root. + +## File Placement Rules + +- Never create temp, debug, scratch, analysis, or one-off script files in the repository root. +- Never leave ad-hoc `tmp-*`, `debug-*`, `analyze-*`, `notes-*`, or throwaway files in the root. +- Use `plans/` for persistent analysis and planning artifacts. +- Use `scripts/` only for reusable repository automation that belongs in version control. +- Use system temp locations such as `/tmp` or `mktemp` for ephemeral files. +- If an output is only needed during execution, keep it out of the repository. +- Root-level files must remain intentional project manifests, configs, or primary documentation only. + +## Setup + +Use `pnpm` only — never npm or yarn (the lockfile is pnpm v9 format). + +```bash +pnpm install +./scripts/setup-skills.sh +./scripts/validate-skills.sh +``` + +## Development Commands + +```bash +pnpm run dev +pnpm run build +pnpm run start +pnpm run lint +pnpm run typecheck +pnpm run test +``` + +## Quality Workflow + +```bash +pnpm run lint && pnpm run typecheck && pnpm run test && pnpm run build +``` + +- Fast loop: `./scripts/minimal_quality_gate.sh` (lint + typecheck + test), or pick a command from the `do-harness.toml` sensor map (`feedback`, `verification`, `offline`, `schema/data`, `release`; see `agents-docs/HARNESS.md`). +- Also run `pnpm run test:e2e` for UI, editor, graph, mind map, search, export, + or any critical workflow change — a unit test proves the function, not the app. +- Before commit: `./scripts/quality_gate.sh`. After CI passes: the + `code-review-assistant` skill, addressing all P1/P2 findings. +- CI repair: `./scripts/self-fix-loop.sh`. + +Warnings are errors — a deprecation notice or lockfile conflict is a defect, +not progress. Full script catalog: `agents-docs/SCRIPTS.md`. + +## Delivery Lifecycle + +A change is not done when it builds — it is done when it survives production. + +```text +production → failure → reproduce → candidate fix → evaluate + → adversarial → shadow → canary → promote | rollback +``` + +Each stage needs a falsifiable exit criterion: a test that fails for the +*stated* reason, a green gate, an attempt to break it that failed, a +comparison recorded in `plans/`. "Looks good" is not a criterion. + +Stage-by-stage exit criteria and tooling: `agents-docs/DELIVERY-LIFECYCLE.md`. +Pick depth by blast radius: docs need only Evaluate; a bug fix runs +reproduce → evaluate → adversarial; persistence, hydration, undo/redo, and +sync additionally need shadow and canary. **Data-loss bugs start at +production** — never ship a persistence change that was not driven against a +real corpus in a real browser. + +This repo is local-first with **no** flag library, **no** error telemetry, and +a single global Vercel deployment. A flag is a store field or `localStorage` +read, default-off. Shadow means both paths reachable, compared on the same +input, recorded in `plans/` — skip it for a pure refactor rather than fake one. +Never add machinery that needs a server to work offline. + +Stage mechanics, harness guardrails, and the per-stage skill map: +`agents-docs/DELIVERY-LIFECYCLE.md`. + +## Testing Expectations + +- Use Vitest for unit and integration coverage. +- Keep critical flows covered: entity CRUD, claim creation, local search/chat flows, graph interaction, and mind map editing. +- When touching database, validation, NLP/search, queueing, or export behavior, add or update tests close to the changed logic. +- Run `pnpm run test:coverage` for core data-model, search, export, or infrastructure changes. + +## UI / UX Guardrails + +- Use design tokens from `src/app/globals.css` `@theme` block (source of truth). +- Two themes: `light` and `dark` via the `.dark` class on `` (next-themes `attribute="class"`). +- Accent color: Saffron (`#9a5c2a` light, `#e5944a` dark). +- Font: Geist Sans (body) + Newsreader (serif headings). +- Build mobile-first; keep interactive targets at least 44x44px. +- Preserve responsive behavior across editor, graph, search, and mind map views. +- Never hardcode hex values in components — use tokens. + +## Git Workflow + +- Branch from up to date, never commit to `main`, use conventional commits. +- Code review (`code-review-assistant`) is mandatory after CI passes; address + all P1/P2 findings before requesting merge. +- `Codacy Static Code Analysis` is the only ruleset-required check on `main` and + has a zero-tolerance rule. A **missing** check is a transient delay, not a + defect — diagnose via `commits/{sha}/check-runs`, then nudge with an + empty-commit push. Never merge with it failing. +- Unresolved review threads block merges, **including outdated ones**. Resolve + every thread via GraphQL `resolveReviewThread`. +- `required_linear_history` is set: always `--squash`, never a merge commit. +- Never use `gh pr merge --admin` without explicit human approval. +- `BLOCKED` with all-green `gh pr checks` is usually staleness, not a real + failure — `pr-merge-state-diagnoser.yml` automates that diagnosis. + +### Standing merge authorization — do not ask + +**When a PR is fully clean, merge it. Do not ask.** The maintainer has +pre-authorized this; a redundant confirmation prompt is a defect, not caution. + +Merge when ALL hold: every check passes (including Codacy and e2e), every +review thread is resolved (outdated ones count), the `code-review-assistant` +pass is complete with no unaddressed P1/P2, and the local gate is green. Then +`gh pr merge --auto --squash --delete-branch` and report the outcome. +`--auto` lands it when CI clears, without holding a session open. + +Ask ONLY for: a suspected-genuine Codacy false positive, a declined P1/P2, a +release/migration/auth or otherwise irreversible change, a branch-protection +edit, or an explicit "hold" in the thread. Never use `--admin` to clear a gate. + +Full criteria, rationale, and the worked example: +`agents-docs/GIT-WORKFLOW.md`. + +Full procedure, staleness ladder, and the Codacy false-positive playbook: +`agents-docs/GIT-WORKFLOW.md`. Lessons: `agents-docs/LESSONS.md`. + +## Learnings (session-distilled) + +Non-obvious toolchain facts; full detail in `agents-docs/LESSONS.md` +(LESSON-001..049) and `lessons.jsonl`. + +- **gitleaks-action v3+ needs a paid licence** (pin v2.x); yamllint enforces + `line-length` 120 and a trailing newline inside `run: |` blocks. +- **DeepSource `.deepsource.toml` suppressions do not reliably fail-proof a check** + — `const fn = () => {}` for module-scope helpers; exported complexity under 6. +- **A test you have not seen fail is not evidence** (LESSON-041): re-inject the + defect, watch it fail, restore it. Codacy/DeepSource re-post stale findings as + NEW blocking threads — fix, reply, resolve, stop pushing (LESSON-034). Vitest + typecheck misses `*.test.ts` source errors (LESSON-035). +- **A check that cannot fail is worse than none** (LESSON-046, LESSON-047): + `markdownlint.toml` was inert (rules under `[tool.markdownlint]`, ignored by the + CLI; unknown ids are accepted *silently*) and `quality_gate.sh` passed its config + only as `lint_if_changed`'s cache key, so the command ran without `--config`. + `MD018`'s `--fix` rewrites wrapped prose beginning `#865` into a real `

`. +- **Vercel Hobby allows 100 deployments per 24 h, owner-scoped, and production + shares previews' budget** (LESSON-048, LESSON-049): every deployment — the live + site included — failed 2026-10-03..13 while the non-required `Vercel` check + blocked nothing. One push is one deployment; never skip `scripts/` (`pnpm run + build` writes the served precache manifest); fail **closed** on unknown state. +- **Worktrees:** `git add -A` stages a symlinked `node_modules` (`HUSKY=0` does not + skip `.githooks`); commit before any rebase or `reset --hard` (LESSON-042). Bless + a head before merging — stale-base automation arrives with `main`-reverting trees + (LESSON-043); dependency ranges can move backwards silently (LESSON-044). +- **Feature-detecting two APIs hides a missing feature** — an `AbortSignal.any` + fallback dropped a timeout; a bare `stop` binds `window.stop` (LESSON-045). +- **CI dispatch** — push and dispatch runs on `main` cancel each other; rerun the cancelled workflow (LESSON-050/051). + +## Skills + +Canonical skills live in `.agents/skills/`; refresh symlinks with +`./scripts/setup-skills.sh`. **Load only what the stage needs** — every +always-loaded skill is a tax. Prefer existing skills and `agents-docs/` +guidance over inventing a workflow, and add a skill only after a real failure +(`agents-docs/HARNESS.md`). + +Catalog: `agents-docs/AVAILABLE_SKILLS.md` (regenerate with +`./scripts/generate-skills-docs.py`); per-stage map: +`agents-docs/DELIVERY-LIFECYCLE.md`. + + + +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. + +This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. + + diff --git a/QWEN.md b/QWEN.md index 6b49ae54..554ce2dd 100644 --- a/QWEN.md +++ b/QWEN.md @@ -1,3 +1,252 @@ -@AGENTS.md + - +# AGENTS.md + +> Single source of truth for AI coding agents working in this repository. + +## Project + +Local-first knowledge studio built with Next.js 16 / React 19 / Tailwind 4 / shadcn / Zustand. Rich text editing, knowledge graph, mind maps, client-side search, export, and an AI agent harness. Persistence via Zustand + IndexedDB, with a small localStorage metadata/recovery envelope. No required backend. + +## Hard Rules + +- Local-first only. Do not introduce a required backend. +- Zustand + IndexedDB is the persistence layer: the canonical corpus lives in IndexedDB, and localStorage carries only schema metadata, recovery snapshots and quarantine copies. Do not grow the localStorage envelope into a second corpus. +- Markdown is import/export, not canonical truth. +- Use strict TypeScript. Do not use `any`. +- No magic numbers; extract descriptive named constants. +- No hardcoded project-level settings outside config, constants, or env layers. +- Max 500 LOC per source file; refactor before extending oversized files. Exception: shadcn-generated primitives (e.g., `sidebar.tsx`) may exceed this limit — document the exception in `plans/` rather than splitting vendor scaffolding. +- Prefer small, composable modules over mixed-responsibility files. +- Reuse existing abstractions before introducing new patterns. +- Keep changes scoped; avoid unrelated refactors in the same commit. +- **Never create a GitHub release without explicit human instruction.** Releases are coordinated manually by the maintainer — the `version-propagation.yml` workflow was retired in Plan 073. Do NOT run `gh release create` unless the user explicitly asks you to cut a release. You may prepare draft release notes in `plans/` on request, but never publish a release autonomously. +- Never modify `biome.json`, any `eslint` configuration file, or lint suppressions/ignore settings unless I explicitly request that change. If such a change seems necessary, stop, explain why, and ask for approval before editing. +- Escape JSX text content — use `'` for apostrophes, `"` for quotes in JSX text nodes. Do not rely on raw punctuation in JSX strings. +- All `catch` blocks must handle errors meaningfully — log, toast, or rethrow. Never use empty `catch {}` blocks. +- Use `try/catch` with `finally` for resource cleanup (AbortController, timers, subscriptions, file handles). +- Clean up side effects in `useEffect` return functions — abort fetches, clear timers, remove listeners. +- Use `AbortController` for all `fetch` calls to prevent stale responses and memory leaks. +- Prefer `const` assertions and `satisfies` over type assertions for type narrowing. +- Use `React.memo` for pure components that receive stable props to prevent unnecessary re-renders. +- Use `useCallback` for event handlers passed to child components, `useMemo` for expensive computations. +- Never hardcode user-facing strings — extract to constants or i18n-ready string maps for future localization. +- Use `Intl.DateTimeFormat`, `Intl.NumberFormat`, and `Intl.RelativeTimeFormat` for locale-sensitive formatting. +- Validate all external input at boundaries (API responses, user input, localStorage rehydration) with Zod schemas. +- Never log secrets, API keys, or sensitive data. Use `console.error` for errors, not `console.log` in production. +- Prefer `structuredClone` over JSON parse/stringify for deep copies. +- Use `crypto.randomUUID()` for generating IDs, not `Math.random()`. +- Prefer native `AbortController` over custom cancellation patterns. +- Use `queueMicrotask` or `requestAnimationFrame` for batching DOM updates, not `setTimeout(fn, 0)`. +- Prefer `Intl.Segmenter` over manual string splitting for i18n-safe text processing. +- **Always use named exports.** Never use `export default`. Named exports enable better tree-shaking, safer refactoring (find-all-references works), and consistent import style across the codebase. **Exception**: Next.js App Router files (`src/app/page.tsx`, `src/app/layout.tsx`) require `export default` by framework convention — this is the only acceptable case. +- **Prefer `const fn = () => {}` over `function fn() {}` for module-scope helper functions (especially in test files).** DeepSource's JS-0067 rule flags top-level `function` declarations as "global scope" (a false positive for ES modules, but it fails the check). Arrow-function expressions avoid this. This applies to test helpers (`makeEntity`, `resetStore`, etc.) and non-exported internal helpers in `src/`. The `.deepsource.toml` `issue_patterns` suppression for JS-0067 is a belt-and-suspenders fallback, not the primary fix. +- **Never ignore pre-existing issues or warnings.** Every warning, lint error, or failing check must be fixed or documented as a follow-up task in `plans/`. Ignoring issues compounds technical debt and erodes trust in the pipeline. +- Test filenames should describe the behavior or subsystem under test, not coverage chasing. Rename historical files only when a consumer or naming collision requires it. +- **Treat warnings as errors in quality gates.** When running `pnpm run lint`, `pnpm run typecheck`, `pnpm run test`, or `pnpm run build`, any output containing `⚠`, `warning`, `Warning`, or non-zero exit code must be addressed immediately — do not proceed to the next task until the warning is fixed or explicitly documented as a follow-up. Build warnings (e.g., deprecation notices, lockfile conflicts) are not informational — they are defects. +- **Always run code review before merge.** After CI passes and before requesting merge, invoke the `code-review-assistant` skill to perform a structured review of all changed files. Address all P1/P2 findings. No PR merges without a completed review pass. +- **Never merge with Codacy issues.** If Codacy reports `ACTION_REQUIRED`, `FAILED`, or any new issues on a PR, the PR must not be merged until all issues are resolved or explicitly suppressed with a documented reason. Zero tolerance: no warnings, no failures, no "action required" status. + +When Codacy reports new issues on a PR, invoke the `codacy` skill to query findings via `codacy pull-request ... --output json`, identify whether each is a true or false positive using `toolInfo.name` and `resultDataId`, and suppress false positives with `--ignore-issue`. Do not guess the analysis engine from suppression comment syntax — read `toolInfo.name` from the API. + +## Repository Shape + +- `src/app` - App shell, routing, layout (App Router) +- `src/components/studio` - React components (views, UI primitives) +- `src/lib/studio` - Zustand store, types, seed data, utilities +- `src/lib/ai` - AI provider adapters · `src/lib/search` - retrieval engine +- `src/lib/export` - JSON, MD, HTML, encrypted export +- `scripts/` - repository automation · `plans/` - GOAP plans, ADRs, audits +- `agents-docs/` - harness, workflow, config, hooks, skill docs + +## Planning Workflow + +- For every non-trivial task, create or update planning artifacts in `plans/`. +- Use `plans/` for GOAP decomposition, task plans, audits, benchmarks, and implementation notes. +- Use `plans/ADRs/` for architecture decision records. +- If a task changes architecture, storage, search behavior, export behavior, agent workflow, or cross-cutting infrastructure, add or update an ADR. +- Follow the existing `plans/` naming style: numbered plans for implementation tracks, descriptive names for audits and benchmarks, and dedicated ADR files in `plans/ADRs/`. +- Do not write planning, audit, or analysis markdown in the repository root. + +## File Placement Rules + +- Never create temp, debug, scratch, analysis, or one-off script files in the repository root. +- Never leave ad-hoc `tmp-*`, `debug-*`, `analyze-*`, `notes-*`, or throwaway files in the root. +- Use `plans/` for persistent analysis and planning artifacts. +- Use `scripts/` only for reusable repository automation that belongs in version control. +- Use system temp locations such as `/tmp` or `mktemp` for ephemeral files. +- If an output is only needed during execution, keep it out of the repository. +- Root-level files must remain intentional project manifests, configs, or primary documentation only. + +## Setup + +Use `pnpm` only — never npm or yarn (the lockfile is pnpm v9 format). + +```bash +pnpm install +./scripts/setup-skills.sh +./scripts/validate-skills.sh +``` + +## Development Commands + +```bash +pnpm run dev +pnpm run build +pnpm run start +pnpm run lint +pnpm run typecheck +pnpm run test +``` + +## Quality Workflow + +```bash +pnpm run lint && pnpm run typecheck && pnpm run test && pnpm run build +``` + +- Fast loop: `./scripts/minimal_quality_gate.sh` (lint + typecheck + test), or pick a command from the `do-harness.toml` sensor map (`feedback`, `verification`, `offline`, `schema/data`, `release`; see `agents-docs/HARNESS.md`). +- Also run `pnpm run test:e2e` for UI, editor, graph, mind map, search, export, + or any critical workflow change — a unit test proves the function, not the app. +- Before commit: `./scripts/quality_gate.sh`. After CI passes: the + `code-review-assistant` skill, addressing all P1/P2 findings. +- CI repair: `./scripts/self-fix-loop.sh`. + +Warnings are errors — a deprecation notice or lockfile conflict is a defect, +not progress. Full script catalog: `agents-docs/SCRIPTS.md`. + +## Delivery Lifecycle + +A change is not done when it builds — it is done when it survives production. + +```text +production → failure → reproduce → candidate fix → evaluate + → adversarial → shadow → canary → promote | rollback +``` + +Each stage needs a falsifiable exit criterion: a test that fails for the +*stated* reason, a green gate, an attempt to break it that failed, a +comparison recorded in `plans/`. "Looks good" is not a criterion. + +Stage-by-stage exit criteria and tooling: `agents-docs/DELIVERY-LIFECYCLE.md`. +Pick depth by blast radius: docs need only Evaluate; a bug fix runs +reproduce → evaluate → adversarial; persistence, hydration, undo/redo, and +sync additionally need shadow and canary. **Data-loss bugs start at +production** — never ship a persistence change that was not driven against a +real corpus in a real browser. + +This repo is local-first with **no** flag library, **no** error telemetry, and +a single global Vercel deployment. A flag is a store field or `localStorage` +read, default-off. Shadow means both paths reachable, compared on the same +input, recorded in `plans/` — skip it for a pure refactor rather than fake one. +Never add machinery that needs a server to work offline. + +Stage mechanics, harness guardrails, and the per-stage skill map: +`agents-docs/DELIVERY-LIFECYCLE.md`. + +## Testing Expectations + +- Use Vitest for unit and integration coverage. +- Keep critical flows covered: entity CRUD, claim creation, local search/chat flows, graph interaction, and mind map editing. +- When touching database, validation, NLP/search, queueing, or export behavior, add or update tests close to the changed logic. +- Run `pnpm run test:coverage` for core data-model, search, export, or infrastructure changes. + +## UI / UX Guardrails + +- Use design tokens from `src/app/globals.css` `@theme` block (source of truth). +- Two themes: `light` and `dark` via the `.dark` class on `` (next-themes `attribute="class"`). +- Accent color: Saffron (`#9a5c2a` light, `#e5944a` dark). +- Font: Geist Sans (body) + Newsreader (serif headings). +- Build mobile-first; keep interactive targets at least 44x44px. +- Preserve responsive behavior across editor, graph, search, and mind map views. +- Never hardcode hex values in components — use tokens. + +## Git Workflow + +- Branch from up to date, never commit to `main`, use conventional commits. +- Code review (`code-review-assistant`) is mandatory after CI passes; address + all P1/P2 findings before requesting merge. +- `Codacy Static Code Analysis` is the only ruleset-required check on `main` and + has a zero-tolerance rule. A **missing** check is a transient delay, not a + defect — diagnose via `commits/{sha}/check-runs`, then nudge with an + empty-commit push. Never merge with it failing. +- Unresolved review threads block merges, **including outdated ones**. Resolve + every thread via GraphQL `resolveReviewThread`. +- `required_linear_history` is set: always `--squash`, never a merge commit. +- Never use `gh pr merge --admin` without explicit human approval. +- `BLOCKED` with all-green `gh pr checks` is usually staleness, not a real + failure — `pr-merge-state-diagnoser.yml` automates that diagnosis. + +### Standing merge authorization — do not ask + +**When a PR is fully clean, merge it. Do not ask.** The maintainer has +pre-authorized this; a redundant confirmation prompt is a defect, not caution. + +Merge when ALL hold: every check passes (including Codacy and e2e), every +review thread is resolved (outdated ones count), the `code-review-assistant` +pass is complete with no unaddressed P1/P2, and the local gate is green. Then +`gh pr merge --auto --squash --delete-branch` and report the outcome. +`--auto` lands it when CI clears, without holding a session open. + +Ask ONLY for: a suspected-genuine Codacy false positive, a declined P1/P2, a +release/migration/auth or otherwise irreversible change, a branch-protection +edit, or an explicit "hold" in the thread. Never use `--admin` to clear a gate. + +Full criteria, rationale, and the worked example: +`agents-docs/GIT-WORKFLOW.md`. + +Full procedure, staleness ladder, and the Codacy false-positive playbook: +`agents-docs/GIT-WORKFLOW.md`. Lessons: `agents-docs/LESSONS.md`. + +## Learnings (session-distilled) + +Non-obvious toolchain facts; full detail in `agents-docs/LESSONS.md` +(LESSON-001..049) and `lessons.jsonl`. + +- **gitleaks-action v3+ needs a paid licence** (pin v2.x); yamllint enforces + `line-length` 120 and a trailing newline inside `run: |` blocks. +- **DeepSource `.deepsource.toml` suppressions do not reliably fail-proof a check** + — `const fn = () => {}` for module-scope helpers; exported complexity under 6. +- **A test you have not seen fail is not evidence** (LESSON-041): re-inject the + defect, watch it fail, restore it. Codacy/DeepSource re-post stale findings as + NEW blocking threads — fix, reply, resolve, stop pushing (LESSON-034). Vitest + typecheck misses `*.test.ts` source errors (LESSON-035). +- **A check that cannot fail is worse than none** (LESSON-046, LESSON-047): + `markdownlint.toml` was inert (rules under `[tool.markdownlint]`, ignored by the + CLI; unknown ids are accepted *silently*) and `quality_gate.sh` passed its config + only as `lint_if_changed`'s cache key, so the command ran without `--config`. + `MD018`'s `--fix` rewrites wrapped prose beginning `#865` into a real `

`. +- **Vercel Hobby allows 100 deployments per 24 h, owner-scoped, and production + shares previews' budget** (LESSON-048, LESSON-049): every deployment — the live + site included — failed 2026-10-03..13 while the non-required `Vercel` check + blocked nothing. One push is one deployment; never skip `scripts/` (`pnpm run + build` writes the served precache manifest); fail **closed** on unknown state. +- **Worktrees:** `git add -A` stages a symlinked `node_modules` (`HUSKY=0` does not + skip `.githooks`); commit before any rebase or `reset --hard` (LESSON-042). Bless + a head before merging — stale-base automation arrives with `main`-reverting trees + (LESSON-043); dependency ranges can move backwards silently (LESSON-044). +- **Feature-detecting two APIs hides a missing feature** — an `AbortSignal.any` + fallback dropped a timeout; a bare `stop` binds `window.stop` (LESSON-045). +- **CI dispatch** — push and dispatch runs on `main` cancel each other; rerun the cancelled workflow (LESSON-050/051). + +## Skills + +Canonical skills live in `.agents/skills/`; refresh symlinks with +`./scripts/setup-skills.sh`. **Load only what the stage needs** — every +always-loaded skill is a tax. Prefer existing skills and `agents-docs/` +guidance over inventing a workflow, and add a skill only after a real failure +(`agents-docs/HARNESS.md`). + +Catalog: `agents-docs/AVAILABLE_SKILLS.md` (regenerate with +`./scripts/generate-skills-docs.py`); per-stage map: +`agents-docs/DELIVERY-LIFECYCLE.md`. + + + +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. + +This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. + + diff --git a/agents-docs/HARNESS.md b/agents-docs/HARNESS.md index 3b5ec68f..e2de232f 100644 --- a/agents-docs/HARNESS.md +++ b/agents-docs/HARNESS.md @@ -22,8 +22,10 @@ Throw away what does not help - more config is not always better. ## Skills (Single Canonical Source) -All skills live in `.agents/skills/`. Claude Code, Gemini CLI, and Qwen Code use symlinks -(`.claude/skills/`, `.gemini/skills/`, `.qwen/skills/`) created by `./scripts/setup-skills.sh`. +All skills live in `.agents/skills/`. Claude Code, Gemini CLI, and Qwen Code use +relative symlinks for each skill inside their skills directories. Cursor and +Windsurf use relative directory symlinks to the canonical skills directory. +`./scripts/setup-skills.sh` synchronizes both declared strategies. OpenCode reads skills directly from `.agents/skills/` - no symlinks needed. See `agents-docs/AVAILABLE_SKILLS.md`. @@ -63,12 +65,12 @@ This is a **convention, not a tool**: no script, hook, or CI job parses `do-harn | Agent | Skills Location | Sub-agents | |-------|-----------------|------------| -| Claude Code | `.claude/skills/` (symlinks) | `.claude/agents/` | -| Gemini CLI | `.gemini/skills/` (symlinks) | - | +| Claude Code | `.claude/skills/` (relative per-skill symlinks) | `.claude/agents/` | +| Gemini CLI | `.gemini/skills/` (relative per-skill symlinks) | - | | OpenCode | `.agents/skills/` (direct) | `.opencode/agents/` | -| Qwen Code | `.qwen/skills/` (symlinks) | - | -| Cursor | `.cursor/skills/` (symlink) | - | -| Windsurf | `.windsurf/skills/` (symlink) | - | +| Qwen Code | `.qwen/skills/` (relative per-skill symlinks) | - | +| Cursor | `.cursor/skills/` (relative directory symlink) | - | +| Windsurf | `.windsurf/skills/` (relative directory symlink) | - | | Jules | managed via manifest (`symlink_strategy: none`) | - | > Machine-readable source of truth: `.agents/manifest.json` — update both when adding a tool. diff --git a/agents-docs/MANIFEST.md b/agents-docs/MANIFEST.md index 17cdd7be..2034c648 100644 --- a/agents-docs/MANIFEST.md +++ b/agents-docs/MANIFEST.md @@ -12,48 +12,54 @@ The `manifest.json` file describes all AI agent tools supported by this reposito ```json { "version": "1.0.0", + "canonical_instructions": "AGENTS.md", "canonical_skills": ".agents/skills", - "global_docs": [ - "AGENTS.md", - "README.md" - ], + "skill_labeler": ".github/labeler.yml", + "global_docs": ["AGENTS.md", "README.md"], "tools": { "tool-id": { "directory": ".tool-dir", "skills_directory": ".tool-dir/skills", - "docs": [ - "TOOL.md" - ], - "symlink_strategy": "relative | none" + "symlink_strategy": "relative-directory" } } } ``` +`instruction_addendum` is optional. A declared tool doc is generated with the +full canonical instruction source followed by its tool-specific addendum, if +present. This avoids depending on client-specific import syntax. + ### Fields - `version`: Manifest schema version. +- `canonical_instructions`: The single shared instruction source (`AGENTS.md`). - `canonical_skills`: Relative path to the single source of truth for skills. +- `skill_labeler`: Labeler config whose generated skill globs follow declared skill links. - `global_docs`: Documentation files that apply to all agents. - `tools`: A map of tool configurations. - `directory`: The root directory for tool-specific configuration. - `skills_directory`: (Optional) Where skills should be symlinked. - - `docs`: (Optional) Tool-specific documentation files. - - `symlink_strategy`: `relative` or `none`. `none` skips the - skills link; `relative` creates a relative directory symlink to canonical skills. + - `docs`: (Optional) Generated root instruction files. + - `instruction_addendum`: (Optional) Source of tool-specific instructions. + - `symlink_strategy`: `none`, `relative-directory`, or `relative-entries`. + `relative-directory` symlinks the whole directory; `relative-entries` + creates one relative symlink per canonical skill. ## Management Script Use `scripts/agent-surface.py` for manifest-managed surfaces: -- `sync`: Creates missing tool directories and skill links; reports existing - path or link mismatches rather than replacing them. -- `validate`: Checks skill symlinks, canonical skill metadata, evaluations, - skill-local links, `AGENTS.md` requirements, and generated catalog freshness. +- `sync`: Creates or repairs declared skill links, generated root instruction + files, and the skill-labeler block. +- `validate`: Checks skill links, canonical skill metadata, evaluations, + skill-local links, `AGENTS.md` requirements, generated root docs/labeler globs, + and generated catalog freshness. -`scripts/setup-skills.sh` creates links from the manifest and runs +`scripts/setup-skills.sh` creates skill links and runs `scripts/generate-skills-docs.py`, which updates `agents-docs/AVAILABLE_SKILLS.md` -and `.agents/skills/README.md`. +and `.agents/skills/README.md`. The catalog generator is not an +`agent-surface.py` subcommand; its `--check` mode detects drift without writing. ## Adding a New Tool diff --git a/plans/194-open-issues-1050-1065-ordered-implementation-2026-11-03.md b/plans/194-open-issues-1050-1065-ordered-implementation-2026-11-03.md new file mode 100644 index 00000000..ca106e57 --- /dev/null +++ b/plans/194-open-issues-1050-1065-ordered-implementation-2026-11-03.md @@ -0,0 +1,101 @@ +# Plan 194 — Ordered implementation of open issues #1050–#1065 (2026-11-03) + +**Status**: Active — acceptance audit complete; implementation underway. +**Goal**: Complete every currently open issue #1050–#1065 in a dependency-safe sequence, with one focused PR per implementation unit and squash merges only after local gates, all CI, and code review pass. +**Constraints**: Preserve local-first operation, fail-closed persistence/import, strict TypeScript, current PWA/offline behavior, and no required backend. Do not close an issue on historical plan text alone; verify current code and attach the merge/verification evidence. + +## Audit snapshot + +The issue descriptions were checked against current source, plans, and open PRs. The six audits were read-only; no tests or builds were run. Existing partial work is reused rather than duplicated. + +| Issue | Current status | Material remaining acceptance work | +|---|---|---| +| #1050 | Local implementation complete; PR/CI pending | S1–S3 rechecked; generated instruction/labeler surfaces and explicit skill-link strategies are implemented. | +| #1051 | Partial, security constraints unresolved | Narrow `connect-src`, remove script `unsafe-inline`, add all-view/offline CSP E2E and update ADR 045. Current ONNX runtime demonstrably executes JavaScript `eval`, so dropping `unsafe-eval` without replacing that runtime would break semantic search. Preserve that behavior while evaluating safe alternatives; do not weaken offline support. | +| #1052 | Partial | ICE/TURN settings and encrypted credentials, connection/candidate diagnostics, Sync-integrated file fallback, TURN validation, docs and tests. Existing signaling URL configuration is not ICE configuration. | +| #1053 | Partial | 500/2k/5k/10k measurements and documented budgets; large-corpus renderer/culling and accessible fallback with evidence for the 5k target. | +| #1054 | Partial | Multi-file/folder/Obsidian Markdown import, frontmatter and two-pass link resolution, limits/fail-closed preview, round-trip and restore E2E. | +| #1055 | Partial | `[[...]]` editor tokens/export/import, alias resolution, unlinked-mention indexing, rename behavior, keyboard/SR tests. Existing ID-based `@` mentions are retained as the canonical storage link. | +| #1056 | Open | Required architecture ADR, offline read-only MCP CLI over exported bundles, provenance-preserving tools, SDK contract test and setup docs. | +| #1057 | Open | Provider-backed extraction into an isolated proposal inbox, bounded Zod validation, provenance/privacy disclosure, explicit accept/edit/reject, evaluation and undo. | +| #1058 | Open | Pure shortest-path/filter API, accessible results, named views persisted through reload/backup/sync, and AI `find_path` integration. | +| #1059 | Open | Incremental duplicate/contradiction candidate detection, dismissals persisted and synced, user-confirmed actions, labeled fixtures and metrics. | +| #1060 | Open | Separate durable per-entity revision store, quota/retention, restore UI, merge/import revision tags, optional backup inclusion, latency evidence. | +| #1061 | Open | PWA share-target intake and offline queue, safe transient capture/confirmation, URL clipper/bookmarklet and support documentation. | +| #1062 | Open | Bounded `fast-check` properties in normal tests, scheduled/on-demand Stryker with reproducible seeds and score/artifact, survivor triage and contributor guidance. | +| #1063 | Partial | Current dynamic PDF/DOCX imports are already lazy. Still need current before/after route/chunk evidence, `sharp` production-dependency decision, unused-code advisory CI and final precache verification. | +| #1064 | Open | Generated README visuals, new-profile choice, accessible/reduced-motion/i18n tour, persisted completion/replay, privacy panel and E2E. | +| #1065 | Partial | RRF, text signal badges, fallback, evaluation and chat/context integration exist. Complete the requested worker-backed/non-blocking semantic path and explicit Hybrid/default-when-ready mode, with evidence that evaluation remains honest and regressions are bounded. | + +### Issue #1050: S1–S3 recheck + +- **S1 — skill catalog:** already resolved on `main`; the canonical + `verify-before-asserting` skill appears once in the generated catalog. The + current generator reports 58 skills, and its two-run idempotency test passes. +- **S2 — command documentation:** still open in `agents-docs/MANIFEST.md` and + ADR 029, which advertised a nonexistent `generate-docs` subcommand. This PR + documents the actual `sync`/`validate` interface and the separate catalog + generator. `SKIP_LINKS` appears only as a historical/audit mention, not an + active recommendation. +- **S3 — link strategy:** the manifest’s ambiguous `relative` value did not + describe both existing layouts. Claude/Gemini/Qwen use per-skill relative + links; Cursor/Windsurf use relative directory links. This PR names and + validates both strategies explicitly. + +## Dependency and merge order + +Each numbered step is a separate main-target PR and is merged before the next dependent implementation begins. #1056 has a design-only ADR PR first and a later implementation PR; its issue remains open until the implementation PR satisfies all criteria. + +1. **#1050 — agent-surface source of truth.** Validate/generated root instruction surfaces and reconcile current docs/ADR drift. Establishes clean tooling for subsequent work. +2. **#1062 — sync/migration property and mutation harness.** Pin actual merge, tombstone, hydration and migration invariants before changing sync or persistence behavior. +3. **#1065 — finish hybrid search.** Move semantic retrieval to the worker-backed path, define mode/default behavior, and preserve measured lexical fallback; required by #1059. +4. **#1055 — Wikilinks and aliases.** Establish canonical resolution and Markdown token behavior before importing vault links. +5. **#1054 — Markdown/Obsidian import.** Reuse #1055 resolution; enforce bounds before staging any mutation. +6. **#1056 — design ADR only.** Compare offline bundle CLI, localhost bridge, and browser-native registration; select the least-privilege offline option before implementation. +7. **#1058 — graph query and saved-view model.** Deliver the pure `find_path`/filter API and durable saved-view contract. +8. **#1053 — graph scalability.** Measure and optimize the graph renderer against the finalized query/view model; do not claim 5k performance without a reproducible profile. +9. **#1056 — offline read-only MCP implementation.** Consume the exported bundle and reuse #1058 `find_path`; no default mutation tools. +10. **#1060 — persistent entity revision history.** Use a separate IndexedDB store and quota-safe path; preserve restore-as-new-revision semantics. +11. **#1052 — ICE/TURN diagnostics and file sync.** Build on #1062 test semantics and #1060 revision capture for pre-merge history; update ADR 045. +12. **#1057 — Synthesis Inbox.** Keep model output outside canonical entities/claims until a user accepts validated proposals. +13. **#1059 — claim conflict suggestions.** Reuse #1065 semantic indexing; persist dismissals and keep all claim changes user-confirmed. +14. **#1061 — quick capture.** Integrate PWA share intake with the safe import/capture path from #1054 and preserve worker/cache upgrade identity. +15. **#1064 — onboarding and README visuals.** Ship after import/capture flows exist so the first-run choices and tour lead to real destinations. +16. **#1063 — dependency/bundle hygiene.** Measure the feature-complete baseline, prune/justify dependencies and unused code only after all new callsites exist, then measure after and verify precache parity. +17. **#1051 — CSP hardening.** Last, after all supported AI/sync/model endpoints are settled. Do not remove `unsafe-eval` unless the chosen runtime is proven to work without it; do not switch to dynamic nonce rendering without proving the static/offline contract. If the runtime constraint cannot be removed safely, record the exact unmet criterion and prerequisite rather than shipping a broken semantic path. + +## Delivery gate per PR + +- Issue-specific skill loaded before editing; relevant Next.js local guide read before any Next code change. +- Acceptance behavior covered by targeted tests and a real smoke path. Storage, import, sync, hydration, backup and offline changes also get their required browser/E2E evidence. +- Update the relevant plan/ADR/docs in the same PR. Keep source files under 500 LOC and user-facing strings in the existing message catalogs. +- Run `./scripts/quality_gate.sh` (including any required E2E) with warnings treated as failures. Confirm all GitHub checks, including Codacy and e2e, pass; resolve every review thread; run `code-review-assistant` and address all P1/P2 findings. +- Merge with `gh pr merge --auto --squash --delete-branch`; never bypass branch protection. Continue only after the required PR is merged. + +## Official documentation consulted + +- Next.js 16 [CSP guide](https://nextjs.org/docs/app/guides/content-security-policy): + nonces are generated through Proxy and require dynamic rendering; static + optimization/ISR are disabled. Re-evaluate this against the existing + precache/offline contract before adopting a nonce design. +- MCP TypeScript SDK [first-server guide](https://ts.sdk.modelcontextprotocol.io/v2/get-started/first-server): + `serveStdio` owns stdio, tool inputs use Zod, and stdout is reserved for + protocol messages (diagnostics go to stderr). +- Chrome [Web Share Target guide](https://developer.chrome.com/docs/capabilities/web-apis/web-share-target): + an installed PWA is required; POST/file payloads are handled through the + service worker fetch event. +- fast-check [Vitest integration guide](https://fast-check.dev/docs/tutorials/setting-up-your-test-environment/property-based-testing-with-vitest/): + property generators integrate with Vitest and support seed-based replay. +- StrykerJS [Vitest runner guide](https://stryker-mutator.io/docs/stryker-js/vitest-runner/): + configure the dedicated runner plugin and mutation-score thresholds. + +## Progress + +- [x] Enumerate all 16 open issues and inspect existing code/plans. +- [x] Record dependency order and delivery gates. +- [x] #1050 local implementation: 31 targeted BATS tests, agent-surface + validation, and full local quality gate pass. Mutation probes made generated + drift, missing globs/docs, wrong per-skill link targets, and legacy strategy + rejection fail as expected. +- [ ] Create, review, and merge each issue PR in sequence; continue to #1062 + only after #1050 merges. diff --git a/plans/ADRs/029-manifest-driven-agent-harness.md b/plans/ADRs/029-manifest-driven-agent-harness.md index 2ce61880..86dc5ce5 100644 --- a/plans/ADRs/029-manifest-driven-agent-harness.md +++ b/plans/ADRs/029-manifest-driven-agent-harness.md @@ -39,12 +39,11 @@ surfaces absent from the manifest are not claimed as supported. ### 2. Setup is idempotent and non-destructive -The setup command creates or repairs only managed artifacts declared in the -manifest. It does not overwrite unrelated user-managed files. Re-running setup -on a valid checkout produces no diff. - -Broken or stale managed links are repaired deliberately. Unexpected conflicting -files cause an actionable failure rather than silent replacement. +The setup command regenerates only declared outputs and creates missing managed +links. Re-running setup on a valid checkout produces no diff. Existing matching +links are left intact; broken or mismatched links and conflicting files produce +an actionable failure rather than silently replacing paths that may contain +user-managed content. ### 3. Validation proves usability, not file presence @@ -64,16 +63,21 @@ Validation fails on: The validator returns non-zero on every error and provides stable, concise diagnostics suitable for CI. -### 4. One implementation owns setup, validate, and docs generation +### 4. The manifest owns generated surfaces; focused generators own output formats The agent-surface tool exposes only commands it implements: -- `sync` for managed surface creation/repair; -- `validate` for canonical and target conformance; -- `generate-docs` for derived skill catalogs, if the repository keeps them. +- `sync` creates or repairs manifest-managed skill links, generated root + instruction files, and the generated skill-labeler block; +- `validate` checks canonical and target conformance, including generated-surface + drift. -Shell wrappers remain thin entry points. Documentation is generated from the -manifest and validator output rather than independently maintained inventories. +`scripts/generate-skills-docs.py` renders the skill catalogs and is invoked by +`scripts/setup-skills.sh`; it is not an `agent-surface.py` subcommand. +Documentation must not advertise nonexistent commands such as `generate-docs`. +Root tool instructions contain the canonical `AGENTS.md` text plus only their +declared tool-specific addendum, so supported clients receive the same shared +constraints without depending on client-specific import syntax. ### 5. Skill routing boundaries are explicit @@ -82,6 +86,15 @@ descriptions. Project-specific skills inherit root architecture constraints and must not prescribe SQLite, a backend, or undefined logging/error abstractions unless an accepted project ADR introduces them. +### 6. Client-specific skill-link scopes are explicit + +Claude Code, Gemini CLI, and Qwen Code use relative symlinks for each canonical +skill inside their existing skills directories. Cursor and Windsurf use a +relative directory symlink to `.agents/skills`. The manifest names these scopes +as `relative-entries` and `relative-directory`; sync and validation implement +both explicitly. Copy synchronization is rejected because it creates a second +mutable skill tree. + ## Consequences ### Positive @@ -121,6 +134,8 @@ unless an accepted project ADR introduces them. `.jules` claims with actual manifest support. - Validate eval files where present and define which skills may omit them. - Generate or verify repository-relative command references in agent docs. +- Generate and validate root tool instructions from `AGENTS.md` and declared + addenda; generate skill-labeler globs from the manifest. - Keep root `AGENTS.md` focused on universal constraints and link to generated detail. diff --git a/plans/INDEX.md b/plans/INDEX.md index 966a5959..f62cbd27 100644 --- a/plans/INDEX.md +++ b/plans/INDEX.md @@ -9,6 +9,13 @@ flight or just landed is kept wave-by-wave below; every landed plan is one row. ## Active work +### Plan 194 — Ordered implementation of open issues #1050–#1065 (2026-11-03) + +| Wave | Goal | Status | Outcome | +| --- | --- | --- | --- | +| Audit | Inspect all 16 open issues and map dependencies | Done | Plan 194 records current gaps, prerequisite order and per-PR merge gates; implementation remains active. | +| Delivery | Complete each issue and merge in dependency order | Active | See [Plan 194](194-open-issues-1050-1065-ordered-implementation-2026-11-03.md). | + ### Plan 193 — Reconcile license, version and repo-description metadata (2026-11-02) | Wave | Goal | Status | Outcome | diff --git a/scripts/agent-surface.py b/scripts/agent-surface.py index 243da199..551a4cee 100755 --- a/scripts/agent-surface.py +++ b/scripts/agent-surface.py @@ -9,16 +9,20 @@ - Malformed eval JSON or unsupported eval schema - Broken repository-relative links in skill content - Duplicate canonical names -- Unmanaged drift in generated surfaces +- Generated instruction and labeler surfaces """ import importlib.util import json +import os import re import sys from pathlib import Path -import yaml +from agent_surface_generated import ( + sync_generated_surfaces, + validate_generated_surfaces, +) def load_manifest(repo_root: Path) -> dict: @@ -62,6 +66,8 @@ def validate_canonical_skills(repo_root: Path, manifest: dict) -> list[str]: def validate_skill_frontmatter(repo_root: Path) -> list[str]: """Validate SKILL.md files have required YAML frontmatter.""" + import yaml + errors = [] skills_dir = repo_root / ".agents" / "skills" if not skills_dir.exists(): @@ -102,6 +108,7 @@ def validate_skill_frontmatter(repo_root: Path) -> list[str]: def validate_skill_name_mismatch(repo_root: Path) -> list[str]: """Check that skill name matches its directory name.""" + import yaml errors = [] skills_dir = repo_root / ".agents" / "skills" if not skills_dir.exists(): @@ -155,6 +162,8 @@ def validate_eval_json(repo_root: Path) -> list[str]: def validate_duplicate_names(repo_root: Path) -> list[str]: """Check for duplicate canonical skill names.""" + import yaml + errors = [] skills_dir = repo_root / ".agents" / "skills" if not skills_dir.exists(): @@ -295,48 +304,97 @@ def read_agents_md_line_limit(repo_root: Path) -> int | None: -def sync_managed_surfaces(repo_root: Path, manifest: dict) -> list[str]: - """Create or repair managed skill symlinks for all declared tools.""" +def check_managed_skill_links( + repo_root: Path, manifest: dict, *, repair: bool +) -> list[str]: + """Check or repair manifest-declared relative skill links.""" errors = [] canonical_skills = manifest.get("canonical_skills", ".agents/skills") - tools = manifest.get("tools", {}) + if not isinstance(canonical_skills, str) or not canonical_skills: + return ["Manifest canonical_skills must be a non-empty path"] + canonical_path = repo_root / canonical_skills + if not canonical_path.is_dir(): + return [f"Canonical skills directory not found: {canonical_skills}"] + tools = manifest.get("tools", {}) + if not isinstance(tools, dict): + return ["Manifest tools must be an object"] for tool_name, config in tools.items(): + if not isinstance(config, dict): + errors.append(f"Manifest tool {tool_name!r} must be an object") + continue strategy = config.get("symlink_strategy", "none") if strategy == "none": continue - - tool_dir = config.get("directory", "") - skills_dir = config.get("skills_directory", "") - if not tool_dir or not skills_dir: + if strategy not in {"relative-directory", "relative-entries"}: + errors.append( + f"Unsupported symlink_strategy for {tool_name!r}: {strategy!r}" + ) continue + tool_dir = config.get("directory") + skills_dir = config.get("skills_directory") + if not isinstance(tool_dir, str) or not tool_dir: + errors.append(f"Manifest directory missing for {tool_name!r}") + continue + if not isinstance(skills_dir, str) or not skills_dir: + errors.append(f"Manifest skills_directory missing for {tool_name!r}") + continue full_tool_dir = repo_root / tool_dir full_skills_dir = repo_root / skills_dir - canonical_path = repo_root / canonical_skills - if not canonical_path.exists(): - errors.append(f"Canonical skills directory not found: {canonical_skills}") + if strategy == "relative-directory": + if full_skills_dir.is_symlink(): + target = full_skills_dir.resolve() + if not target.exists(): + errors.append(f"Broken symlink: {skills_dir} -> {full_skills_dir.readlink()}") + elif target != canonical_path.resolve(): + errors.append(f"Symlink mismatch: {skills_dir} -> {full_skills_dir.readlink()}") + elif full_skills_dir.exists(): + errors.append(f"{skills_dir} exists and is not a directory symlink") + elif repair: + full_tool_dir.mkdir(parents=True, exist_ok=True) + relative_target = os.path.relpath(canonical_path, full_tool_dir) + full_skills_dir.symlink_to(relative_target) + else: + errors.append(f"Missing skill directory symlink: {skills_dir}") continue - full_tool_dir.mkdir(parents=True, exist_ok=True) - if full_skills_dir.is_symlink(): - target = full_skills_dir.resolve() - if not target.exists(): - errors.append(f"Broken symlink: {skills_dir} -> {full_skills_dir.readlink()}") - elif target != canonical_path.resolve(): - errors.append( - f"Symlink mismatch: {skills_dir} -> {full_skills_dir.readlink()}" - ) - elif full_skills_dir.exists(): - errors.append(f"Warning: {skills_dir} exists and is not a symlink") - else: - import os - relative_target = os.path.relpath(str(canonical_path), str(full_tool_dir)) - full_skills_dir.symlink_to(relative_target) - print(f"Created symlink: {skills_dir} -> {relative_target}") + errors.append(f"{skills_dir} must be a directory containing skill symlinks") + continue + if full_skills_dir.exists() and not full_skills_dir.is_dir(): + errors.append(f"{skills_dir} exists and is not a directory") + continue + if not full_skills_dir.exists(): + if not repair: + errors.append(f"Missing skill link directory: {skills_dir}") + continue + full_skills_dir.mkdir(parents=True, exist_ok=True) + for skill in canonical_path.iterdir(): + if not skill.is_dir(): + continue + link = full_skills_dir / skill.name + if link.is_symlink(): + if not link.exists(): + errors.append(f"Broken symlink: {skills_dir}/{skill.name}") + elif link.resolve() != skill.resolve(): + errors.append(f"Symlink mismatch: {skills_dir}/{skill.name}") + elif link.exists(): + errors.append(f"Skill link path exists and is not a symlink: {skills_dir}/{skill.name}") + elif repair: + relative_target = os.path.relpath(skill, full_skills_dir) + link.symlink_to(relative_target) + else: + errors.append(f"Missing skill symlink: {skills_dir}/{skill.name}") + return errors + + +def sync_managed_surfaces(repo_root: Path, manifest: dict) -> list[str]: + """Create or repair manifest-managed skill links and generated surfaces.""" + errors = check_managed_skill_links(repo_root, manifest, repair=True) + errors.extend(sync_generated_surfaces(repo_root, manifest)) return errors @@ -390,7 +448,7 @@ def main() -> int: return 1 command = sys.argv[1] - repo_root = Path(__file__).resolve().parent.parent + repo_root = Path(os.environ.get("REPO_ROOT", Path(__file__).resolve().parent.parent)).resolve() manifest = load_manifest(repo_root) if command == "validate": @@ -401,8 +459,8 @@ def main() -> int: errors.extend(validate_skill_name_mismatch(repo_root)) errors.extend(validate_eval_json(repo_root)) errors.extend(validate_duplicate_names(repo_root)) - errors.extend(validate_broken_links(repo_root)) - errors.extend(validate_agents_md(repo_root)) + errors.extend(check_managed_skill_links(repo_root, manifest, repair=False)) + errors.extend(validate_generated_surfaces(repo_root, manifest)) errors.extend(validate_skills_catalog_freshness(repo_root)) if errors: diff --git a/scripts/agent_surface_generated.py b/scripts/agent_surface_generated.py new file mode 100755 index 00000000..19b26d7e --- /dev/null +++ b/scripts/agent_surface_generated.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +"""Render and validate manifest-managed agent instruction and labeler surfaces.""" + +from pathlib import Path + +GENERATED_DOCS_HEADER = ( + "" +) +SKILL_LABELER_BEGIN = "# BEGIN GENERATED SKILLS LABELS" +SKILL_LABELER_END = "# END GENERATED SKILLS LABELS" + + +def _repo_path(repo_root: Path, value: object, description: str) -> tuple[Path | None, str | None]: + if not isinstance(value, str) or not value: + return None, f"{description} must be a non-empty repository-relative path" + relative = Path(value) + if relative.is_absolute() or ".." in relative.parts: + return None, f"{description} must stay inside the repository: {value}" + root = repo_root.resolve() + path = root / relative + try: + path.resolve(strict=False).relative_to(root) + except ValueError: + return None, f"{description} resolves outside the repository: {value}" + return path, None + + +def _instruction_outputs( + repo_root: Path, manifest: dict[str, object] +) -> tuple[dict[Path, str], list[str]]: + outputs: dict[Path, str] = {} + errors: list[str] = [] + source_value = manifest.get("canonical_instructions") + tools = manifest.get("tools", {}) + if not isinstance(tools, dict): + return outputs, ["Manifest tools must be an object"] + has_declared_docs = any( + isinstance(config, dict) and config.get("docs") + for config in tools.values() + ) + if source_value is None: + if has_declared_docs: + errors.append("Manifest canonical_instructions is required when tool docs are declared") + return outputs, errors + + source, error = _repo_path(repo_root, source_value, "canonical_instructions") + if error: + return outputs, [error] + if source is None or not source.is_file(): + return outputs, [f"Canonical instruction source not found: {source_value}"] + source_name = source.relative_to(repo_root.resolve()).as_posix() + try: + source_text = source.read_text(encoding="utf-8").rstrip() + except OSError as exception: + return outputs, [f"Cannot read canonical instruction source {source_value}: {exception}"] + + for tool_name, config in tools.items(): + if not isinstance(config, dict): + errors.append(f"Manifest tool {tool_name!r} must be an object") + continue + documents = config.get("docs", []) + if not isinstance(documents, list): + errors.append(f"Manifest docs for {tool_name!r} must be a list") + continue + addendum_text: str | None = None + addendum_name: str | None = None + addendum_value = config.get("instruction_addendum") + if addendum_value is not None: + addendum, error = _repo_path( + repo_root, addendum_value, f"instruction_addendum for {tool_name!r}" + ) + if error: + errors.append(error) + continue + if addendum is None or not addendum.is_file(): + errors.append(f"Instruction addendum not found for {tool_name!r}: {addendum_value}") + continue + try: + addendum_text = addendum.read_text(encoding="utf-8").rstrip() + except OSError as exception: + errors.append(f"Cannot read instruction addendum {addendum_value}: {exception}") + continue + addendum_name = addendum.relative_to(repo_root.resolve()).as_posix() + + for document_value in documents: + document, error = _repo_path( + repo_root, document_value, f"Generated doc for {tool_name!r}" + ) + if error: + errors.append(error) + continue + if document is None: + continue + if document == source: + errors.append(f"Generated doc for {tool_name!r} cannot replace its canonical source") + continue + if document.is_symlink(): + errors.append(f"Generated doc is a symlink; refusing to follow it: {document_value}") + continue + if document in outputs: + errors.append(f"Generated doc is declared more than once: {document_value}") + continue + sources = f"`{source_name}`" + if addendum_name is not None: + sources += f" and `{addendum_name}`" + header = GENERATED_DOCS_HEADER.format(sources=sources) + content = f"{header}\n\n{source_text}\n" + if addendum_text: + content += f"\n{addendum_text}\n" + outputs[document] = content + return outputs, errors + + +def _skill_globs(manifest: dict[str, object]) -> tuple[list[str], list[str]]: + errors: list[str] = [] + roots: list[object] = [manifest.get("canonical_skills", ".agents/skills")] + tools = manifest.get("tools", {}) + if not isinstance(tools, dict): + return [], ["Manifest tools must be an object"] + for tool_name, config in tools.items(): + if not isinstance(config, dict): + errors.append(f"Manifest tool {tool_name!r} must be an object") + continue + if config.get("symlink_strategy", "none") != "none": + skills_directory = config.get("skills_directory") + if skills_directory: + roots.append(skills_directory) + + globs: set[str] = set() + for root in roots: + if not isinstance(root, str) or not root: + errors.append("Skill directories in the manifest must be non-empty paths") + continue + path = Path(root) + if path.is_absolute() or ".." in path.parts: + errors.append(f"Skill directory must stay inside the repository: {root}") + continue + globs.add(f"{path.as_posix().rstrip('/')}/**/*") + return sorted(globs), errors + + +def _replace_skill_labeler_block(content: str, globs: list[str]) -> tuple[str | None, str | None]: + lines = content.splitlines(keepends=True) + starts = [index for index, line in enumerate(lines) if line.strip() == SKILL_LABELER_BEGIN] + ends = [index for index, line in enumerate(lines) if line.strip() == SKILL_LABELER_END] + if len(starts) != 1 or len(ends) != 1 or starts[0] >= ends[0]: + return None, "labeler config must contain one ordered generated-skill marker pair" + + block = [ + SKILL_LABELER_BEGIN, + "skills:", + " - changed-files:", + " - any-glob-to-any-file:", + *[f" - '{glob}'" for glob in globs], + SKILL_LABELER_END, + ] + generated = "\n".join(block) + "\n" + lines[starts[0] : ends[0] + 1] = generated.splitlines(keepends=True) + return "".join(lines), None + + +def _generated_outputs( + repo_root: Path, manifest: dict[str, object] +) -> tuple[dict[Path, str], list[str]]: + outputs, errors = _instruction_outputs(repo_root, manifest) + labeler_value = manifest.get("skill_labeler") + if labeler_value is None: + return outputs, errors + labeler, error = _repo_path(repo_root, labeler_value, "skill_labeler") + if error: + errors.append(error) + return outputs, errors + if labeler is None or labeler.is_symlink() or not labeler.is_file(): + errors.append(f"Skill labeler config is missing or not a regular file: {labeler_value}") + return outputs, errors + globs, glob_errors = _skill_globs(manifest) + errors.extend(glob_errors) + try: + content = labeler.read_text(encoding="utf-8") + except OSError as exception: + errors.append(f"Cannot read skill labeler config {labeler_value}: {exception}") + return outputs, errors + generated, error = _replace_skill_labeler_block(content, globs) + if error: + errors.append(f"{labeler_value}: {error}") + elif generated is not None: + outputs[labeler] = generated + return outputs, errors + + +def validate_generated_surfaces(repo_root: Path, manifest: dict[str, object]) -> list[str]: + """Reject drift in generated root instructions and manifest-derived labeler globs.""" + outputs, errors = _generated_outputs(repo_root, manifest) + if errors: + return errors + for path, expected in outputs.items(): + if not path.is_file(): + errors.append(f"Generated surface missing: {path.relative_to(repo_root)}; run agent-surface.py sync") + continue + try: + actual = path.read_text(encoding="utf-8") + except OSError as exception: + errors.append(f"Cannot read generated surface {path.relative_to(repo_root)}: {exception}") + continue + if actual != expected: + errors.append(f"Generated surface drift: {path.relative_to(repo_root)}; run agent-surface.py sync") + return errors + + +def sync_generated_surfaces(repo_root: Path, manifest: dict[str, object]) -> list[str]: + """Create or repair only the generated surfaces explicitly declared in the manifest.""" + outputs, errors = _generated_outputs(repo_root, manifest) + if errors: + return errors + for path in outputs: + if path.is_symlink() or (path.exists() and not path.is_file()): + errors.append(f"Generated surface is not a regular file: {path.relative_to(repo_root)}") + if errors: + return errors + for path, content in outputs.items(): + try: + if not path.is_file() or path.read_text(encoding="utf-8") != content: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + except OSError as exception: + errors.append(f"Cannot write generated surface {path.relative_to(repo_root)}: {exception}") + return errors diff --git a/scripts/setup-skills.sh b/scripts/setup-skills.sh index 97311494..98da4cd4 100755 --- a/scripts/setup-skills.sh +++ b/scripts/setup-skills.sh @@ -1,13 +1,12 @@ #!/usr/bin/env bash -# Setup skill symlinks for agent discovery. -# Reads manifest.json and creates symlinks for all declared tools. +# Setup manifest-managed skill links and generated agent surfaces. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="${REPO_ROOT:-$(dirname "$SCRIPT_DIR")}" - MANIFEST="$REPO_ROOT/.agents/manifest.json" AGENTS_DIR="$REPO_ROOT/.agents/skills" +PYTHON_BIN="${PYTHON_BIN:-python3}" if [ ! -d "$AGENTS_DIR" ]; then echo "No .agents/skills directory found — skipping." @@ -19,55 +18,12 @@ if [ ! -f "$MANIFEST" ]; then exit 0 fi -# Parse manifest with node (available in Node.js projects) -TOOLS=$(node -e " -const fs = require('fs'); -const path = require('path'); -const repoRoot = process.argv[1]; -const manifestPath = process.argv[2]; -const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); -const canonicalSkills = manifest.canonical_skills || '.agents/skills'; -const tools = manifest.tools || {}; -for (const [name, config] of Object.entries(tools)) { - if (config.symlink_strategy === 'none') continue; - const toolDir = config.directory || ''; - const skillsDir = config.skills_directory || ''; - if (!toolDir || !skillsDir) continue; - const linkParent = path.dirname(path.resolve(repoRoot, skillsDir)); - const target = path.relative(linkParent, path.resolve(repoRoot, canonicalSkills)); - console.log([name, toolDir, skillsDir, target].join('|')); -} -" -- "$REPO_ROOT" "$MANIFEST") - -while IFS='|' read -r tool_name tool_dir skills_dir skills_target; do - if [ -z "$tool_name" ] || [ -z "$tool_dir" ] || [ -z "$skills_dir" ] || [ -z "$skills_target" ]; then - continue - fi - - FULL_TOOL_DIR="$REPO_ROOT/$tool_dir" - FULL_SKILLS_DIR="$REPO_ROOT/$skills_dir" - - # Create tool directory if it doesn't exist - mkdir -p "$FULL_TOOL_DIR" - - # Create symlink for skills - if [ -L "$FULL_SKILLS_DIR" ]; then - echo "Symlink already exists: $FULL_SKILLS_DIR" - elif [ -e "$FULL_SKILLS_DIR" ]; then - echo "Warning: $FULL_SKILLS_DIR exists and is not a symlink. Skipping." - else - ln -s "$skills_target" "$FULL_SKILLS_DIR" - echo "Created symlink: $FULL_SKILLS_DIR -> $skills_target" - fi -done <<< "$TOOLS" - -# Refresh the skill reference tables (agents-docs/AVAILABLE_SKILLS.md and -# .agents/skills/README.md) so they never drift from SKILL.md frontmatter. -if command -v python3 >/dev/null 2>&1; then - python3 "$SCRIPT_DIR/generate-skills-docs.py" --root "$REPO_ROOT" \ - || echo "Warning: failed to regenerate skill docs (generator exit code $?)" >&2 -else - echo "Warning: python3 not found — skipping skill docs regeneration" >&2 +if ! command -v "$PYTHON_BIN" >/dev/null 2>&1; then + echo "ERROR: Python is required to sync agent surfaces." >&2 + exit 1 fi +REPO_ROOT="$REPO_ROOT" "$PYTHON_BIN" "$SCRIPT_DIR/agent-surface.py" sync +"$PYTHON_BIN" "$SCRIPT_DIR/generate-skills-docs.py" --root "$REPO_ROOT" + echo "Skills setup complete." diff --git a/tests/agent-surface-generated.bats b/tests/agent-surface-generated.bats new file mode 100755 index 00000000..5defb2fd --- /dev/null +++ b/tests/agent-surface-generated.bats @@ -0,0 +1,141 @@ +#!/usr/bin/env bash +# Regression coverage for manifest-generated instruction and labeler surfaces. + +bats_require_minimum_version 1.5.0 + +setup() { + export SCRIPT_DIR="$BATS_TEST_DIRNAME/../scripts" + export WORK="$BATS_TEST_TMPDIR/workspace" + mkdir -p "$WORK/.agents/skills" "$WORK/.agents/instructions" "$WORK/.github" "$WORK/scripts" + cp "$SCRIPT_DIR/agent-surface.py" "$WORK/scripts/agent-surface.py" + cp "$SCRIPT_DIR/agent_surface_generated.py" "$WORK/scripts/agent_surface_generated.py" +} + +write_fixture() { + mkdir -p "$WORK/.agents/skills/fixture-skill" + cat > "$WORK/.agents/manifest.json" <<'EOF' +{ + "version": "1.0.0", + "canonical_instructions": "AGENTS.md", + "canonical_skills": ".agents/skills", + "skill_labeler": ".github/labeler.yml", + "tools": { + "claude": { + "directory": ".claude", + "skills_directory": ".claude/skills", + "docs": ["CLAUDE.md"], + "instruction_addendum": ".agents/instructions/claude.md", + "symlink_strategy": "relative-entries" + }, + "gemini": { + "directory": ".gemini", + "skills_directory": ".gemini/skills", + "docs": ["GEMINI.md"], + "symlink_strategy": "relative-entries" + }, + "qwen": { + "directory": ".qwen", + "skills_directory": ".qwen/skills", + "docs": ["QWEN.md"], + "symlink_strategy": "relative-entries" + }, + "cursor": { + "directory": ".cursor", + "skills_directory": ".cursor/skills", + "symlink_strategy": "relative-directory" + }, + "windsurf": { + "directory": ".windsurf", + "skills_directory": ".windsurf/skills", + "symlink_strategy": "relative-directory" + } + } +} +EOF + printf '# Canonical instructions\n' > "$WORK/AGENTS.md" + printf '## Claude-only notes\n\nKeep these distinct.\n' > "$WORK/.agents/instructions/claude.md" + cat > "$WORK/.github/labeler.yml" <<'EOF' +# Documentation changes +# BEGIN GENERATED SKILLS LABELS +skills: + - changed-files: + - any-glob-to-any-file: [] +# END GENERATED SKILLS LABELS + +# Script changes +scripts: + - changed-files: + - any-glob-to-any-file: + - 'scripts/**/*' +EOF +} + +@test "sync generates root instructions and manifest-derived labeler globs idempotently" { + write_fixture + run python3 "$WORK/scripts/agent-surface.py" sync + [ "$status" -eq 0 ] + grep -q '^