diff --git a/.github/scripts/ci_changed_tests.py b/.github/scripts/ci_changed_tests.py deleted file mode 100644 index 73a851ca..00000000 --- a/.github/scripts/ci_changed_tests.py +++ /dev/null @@ -1,272 +0,0 @@ -#!/usr/bin/env python3 -""" -Find pytest node IDs for tests whose definitions changed between two commits. - -This script is designed for CI optimization: -- It detects changed lines via `git diff --unified=0` -- Parses Python test files with `ast` -- Returns pytest node IDs for changed test functions/methods -- Falls back conservatively to class-wide or file-wide selection when needed - -Usage: - python ci_changed_tests.py [...] - -Outputs to $GITHUB_OUTPUT: - has_changed=true|false - test_ids< - - EOF -""" - -from __future__ import annotations - -import ast -import os -import re -import subprocess -import sys -from typing import Iterable - - -HUNK_RE = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@") - - -def warn(message: str) -> None: - print(f"Warning: {message}", file=sys.stderr) - - -def unique_preserve_order(items: Iterable[str]) -> list[str]: - return list(dict.fromkeys(items)) - - -def get_changed_line_numbers(base_sha: str, head_sha: str, filepath: str) -> set[int]: - """ - Return line numbers in the new version of `filepath` that changed - between `base_sha` and `head_sha`. - - For pure deletions (`count == 0`), add the start line as a deletion anchor. - """ - try: - result = subprocess.run( - ["git", "diff", "--unified=0", f"{base_sha}...{head_sha}", "--", filepath], - capture_output=True, - text=True, - check=True, - ) - except subprocess.CalledProcessError as e: - warn( - f"git diff failed for {filepath!r} " - f"(base={base_sha}, head={head_sha}, returncode={e.returncode})" - ) - if e.stderr: - warn(e.stderr.strip()) - return set() - - changed: set[int] = set() - - for line in result.stdout.splitlines(): - m = HUNK_RE.match(line) - if not m: - continue - - start = int(m.group(1)) - count = int(m.group(2)) if m.group(2) is not None else 1 - - if count == 0: - # Deletion-only hunk: mark the anchor line in the new file. - changed.add(start) - else: - changed.update(range(start, start + count)) - - return changed - - -def get_docstring_lines(tree: ast.AST) -> set[int]: - """ - Return line numbers occupied by docstring expressions. - """ - docstring_lines: set[int] = set() - - for node in ast.walk(tree): - if ( - isinstance(node, ast.Expr) - and isinstance(node.value, ast.Constant) - and isinstance(node.value.value, str) - and hasattr(node, "lineno") - and hasattr(node, "end_lineno") - and node.end_lineno is not None - ): - docstring_lines.update(range(node.lineno, node.end_lineno + 1)) - - return docstring_lines - - -def filter_meaningful_changed_lines( - source_lines: list[str], - changed_lines: set[int], - docstring_lines: set[int], -) -> set[int]: - """ - Keep only changed lines that are: - - within file bounds - - non-empty - - not comments - - not docstrings - """ - meaningful = { - ln - for ln in changed_lines - if 1 <= ln <= len(source_lines) - and source_lines[ln - 1].strip() - and not source_lines[ln - 1].strip().startswith("#") - } - - return meaningful - docstring_lines - - -def line_range_for_node(node: ast.AST) -> set[int]: - """ - Return the line range for a node, including decorators if present. - """ - if not hasattr(node, "lineno") or not hasattr(node, "end_lineno") or node.end_lineno is None: - return set() - - start = node.lineno - - decorator_list = getattr(node, "decorator_list", None) - if decorator_list: - first_decorator = decorator_list[0] - if hasattr(first_decorator, "lineno"): - start = first_decorator.lineno - - return set(range(start, node.end_lineno + 1)) - - -def is_test_function(node: ast.AST) -> bool: - return isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name.startswith("test_") - - -def is_test_class(node: ast.AST) -> bool: - return isinstance(node, ast.ClassDef) and node.name.startswith("Test") - - -def find_test_node_ids(filepath: str, changed_lines: set[int]) -> list[str]: - """ - Return pytest node IDs affected by changed lines in `filepath`. - - Behavior: - - Directly changed test functions/methods -> return those node IDs - - Class-level meaningful changes affecting a test class -> return all test methods in that class - - Parse failure -> return [filepath] as conservative fallback - - If nothing meaningful remains after filtering -> return [] - """ - try: - with open(filepath, encoding="utf-8") as f: - source = f.read() - tree = ast.parse(source, filename=filepath) - except Exception as e: - warn(f"failed to parse {filepath!r}: {e}") - return [filepath] - - source_lines = source.splitlines() - docstring_lines = get_docstring_lines(tree) - changed_lines = filter_meaningful_changed_lines(source_lines, changed_lines, docstring_lines) - - if not changed_lines: - return [] - - node_ids: list[str] = [] - - for node in tree.body: - if is_test_class(node): - class_lines = set(range(node.lineno, node.end_lineno + 1)) - class_changed_lines = class_lines & changed_lines - - if not class_changed_lines: - continue - - changed_methods = [ - child - for child in node.body - if is_test_function(child) and (line_range_for_node(child) & changed_lines) - ] - - if changed_methods: - node_ids.extend( - f"{filepath}::{node.name}::{method.name}" - for method in changed_methods - ) - continue - - # No test method itself changed, but something meaningful inside the class did. - # Be conservative: if any child node overlaps changed lines, run all test methods in class. - child_code_lines: set[int] = set() - all_test_methods: list[ast.AST] = [] - - for child in node.body: - if hasattr(child, "lineno") and hasattr(child, "end_lineno") and child.end_lineno is not None: - child_code_lines.update(range(child.lineno, child.end_lineno + 1)) - if is_test_function(child): - all_test_methods.append(child) - - if child_code_lines & class_changed_lines: - node_ids.extend( - f"{filepath}::{node.name}::{method.name}" - for method in all_test_methods - ) - - elif is_test_function(node): - if line_range_for_node(node) & changed_lines: - node_ids.append(f"{filepath}::{node.name}") - - return unique_preserve_order(node_ids) or [filepath] - - -def write_github_output(has_changed: bool, test_ids: list[str]) -> None: - github_output = os.environ.get("GITHUB_OUTPUT") - if not github_output: - return - - with open(github_output, "a", encoding="utf-8") as f: - f.write(f"has_changed={'true' if has_changed else 'false'}\n") - f.write("test_ids< None: - if len(sys.argv) < 4: - print("Usage: ci_changed_tests.py [...]", file=sys.stderr) - sys.exit(1) - - base_sha, head_sha = sys.argv[1], sys.argv[2] - test_files = sys.argv[3:] - - all_ids: list[str] = [] - - for filepath in test_files: - changed_lines = get_changed_line_numbers(base_sha, head_sha, filepath) - if not changed_lines: - continue - - all_ids.extend(find_test_node_ids(filepath, changed_lines)) - - all_ids = unique_preserve_order(all_ids) - - if not all_ids: - print("No changed tests found.") - write_github_output(False, []) - return - - print("Changed test node IDs:") - for node_id in all_ids: - print(f" {node_id}") - - write_github_output(True, all_ids) - - -if __name__ == "__main__": - main() diff --git a/.github/scripts/post_pr_comment.js b/.github/scripts/post_pr_comment.js deleted file mode 100644 index acf8a285..00000000 --- a/.github/scripts/post_pr_comment.js +++ /dev/null @@ -1,207 +0,0 @@ -'use strict'; -const fs = require('fs'); - -module.exports = async ({ github, context }) => { - const rawOutput = fs.readFileSync('pytest_output.txt', 'utf8'); - const exitCode = process.env.EXIT_CODE; - const passed = exitCode === '0'; - const status = passed ? 'βœ… All tests passed' : '❌ Some tests failed'; - - // Parse pytest final summary line for authoritative counts - // e.g. "4 failed, 261 passed, 2 skipped in 4.06s" - function parsePytestSummary(output) { - const lines = output.split('\n'); - for (let i = lines.length - 1; i >= 0; i--) { - const line = lines[i]; - if (/^=+\s+/.test(line) && /\d+\s+(passed|failed)/.test(line)) { - const counts = { passed: 0, failed: 0, error: 0, skipped: 0, xfailed: 0, xpassed: 0 }; - for (const [, n, k] of line.matchAll(/(\d+)\s+(passed|failed|error|skipped|xfailed|xpassed)/g)) { - counts[k] = parseInt(n); - } - counts.total = Object.values(counts).reduce((a, b) => a + b, 0); - return counts; - } - } - return null; - } - - // Parse SKIPPED entries from pytest's "short test summary info" section - // Lines like: "SKIPPED [1] agents/tests/test_foo.py:175: reason text" - // This section exists in the main pytest run output and covers both - // runtime skips (with reason) and collection-level skips. - function parseShortSummarySkips(output) { - const entries = []; - let inSummary = false; - for (const line of output.split('\n')) { - if (/=+\s+short test summary info\s+=+/.test(line)) { inSummary = true; continue; } - if (inSummary) { - if (/^={3,}/.test(line)) break; - const m = line.match(/^SKIPPED\s+\[\d+\]\s+(.+?):(\d+):\s+(.+)/); - if (m) entries.push({ file: m[1], line: parseInt(m[2]), reason: m[3].trim() }); - } - } - return entries; - } - - // Parse individual test results from verbose pytest output - // e.g. "tests/foo.py::TestClass::test_bar PASSED [ 50%]" - // e.g. "tests/foo.py::TestClass::test_bar SKIPPED (reason) [ 50%]" - const testResults = []; - for (const line of rawOutput.split('\n')) { - const m = line.match(/^(\S+::\S+)\s+(PASSED|FAILED|ERROR|SKIPPED|XFAIL|XPASS)(?:\s+\(([^)]*)\))?/); - if (m) testResults.push({ id: m[1], result: m[2], reason: m[3] || null }); - } - - const pytestSummary = parsePytestSummary(rawOutput); - - - const resultIcon = { PASSED: 'βœ…', FAILED: '❌', ERROR: 'πŸ’₯', SKIPPED: '⏭️', XFAIL: 'πŸ”•', XPASS: '⚠️', 'NOT RUN': 'βšͺ' }; - - // Parse skip reasons from the short test summary section of the main output - const shortSummarySkips = parseShortSummarySkips(rawOutput); - - const resultById = {}; - for (const t of testResults) { - resultById[t.id] = t.result; - } - - // Tests Added/Modified in This PR β€” per-test PASS/FAIL and summary - const hasChanged = process.env.HAS_CHANGED === 'true'; - const changedIds = (process.env.TEST_IDS || '').trim().split(/\s+/).filter(Boolean); - - // For parameterized tests, ci_changed_tests.py outputs the base function name - // (e.g. "test_foo") but pytest IDs include params (e.g. "test_foo[param1-param2]"). - // Resolve a changed ID to its aggregate result across all matching param variants. - const resolveResult = (id) => { - if (resultById[id]) return resultById[id]; - const prefix = id + '['; - const variants = testResults.filter(t => t.id.startsWith(prefix)); - if (variants.length === 0) return 'NOT RUN'; - if (variants.some(t => t.result === 'FAILED' || t.result === 'ERROR')) return 'FAILED'; - if (variants.every(t => t.result === 'PASSED')) return 'PASSED'; - if (variants.every(t => t.result === 'SKIPPED' || t.result === 'XFAIL')) return 'SKIPPED'; - return variants[0].result; - }; - - let changedSection = ''; - if (hasChanged && changedIds.length > 0) { - const changedPass = changedIds.filter(id => resolveResult(id) === 'PASSED').length; - const notPassedRows = changedIds - .map(id => ({ id, res: resolveResult(id) })) - .filter(({ res }) => res !== 'PASSED') - .map(({ id, res }) => `| ${resultIcon[res]} ${res} | \`${id}\` |`); - const notableTable = notPassedRows.length > 0 - ? ['', '| Result | Test |', '|--------|------|', ...notPassedRows, ''].join('\n') - : ''; - changedSection = [ - '### Tests Added/Modified in This PR', - '', - `**${changedPass} / ${changedIds.length} passed**`, - notableTable, - ].join('\n'); - } - - // Full Test Results: summary counts + failed + skipped tests - const failedTests = testResults.filter(t => t.result === 'FAILED' || t.result === 'ERROR'); - const skippedTests = testResults.filter(t => t.result === 'SKIPPED' || t.result === 'XFAIL'); - const totalPass = testResults.filter(t => t.result === 'PASSED').length; - const totalFail = failedTests.length; - const totalSkip = skippedTests.length; - - // Detect collection-skipped: short summary entries whose file has no collected tests - const collectedFiles = new Set(testResults.map(t => t.id.split('::')[0])); - const collectionSkips = shortSummarySkips.filter(s => !collectedFiles.has(s.file)); - const summarySkipped = pytestSummary ? pytestSummary.skipped : 0; - const collectionSkipCount = Math.max(0, summarySkipped - totalSkip); - - // Build corrected totals using pytest summary as authoritative source - const authoritativeTotal = pytestSummary ? pytestSummary.total : testResults.length; - const correctedTotal = testResults.length + collectionSkipCount; - - // Warn if corrected total still doesn't match summary after reconciliation - const stillMismatched = pytestSummary && correctedTotal !== authoritativeTotal; - - const runUrl = `https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.CI_RUN_ID || process.env.GITHUB_RUN_ID}`; - - let tableSection = ''; - if (testResults.length > 0) { - const correctedSkip = totalSkip + collectionSkipCount; - const skipPart = correctedSkip > 0 ? `, ${correctedSkip} skipped` : ''; - const summary = `**${totalPass} passed, ${totalFail} failed${skipPart}** (${authoritativeTotal} total) β€” [See CI logs](${runUrl})`; - const notableRows = [ - ...failedTests.map(t => `| ${resultIcon[t.result]} ${t.result} | \`${t.id}\` |`), - ...skippedTests.map(t => `| ${resultIcon[t.result]} ${t.result} | \`${t.id}\` |`), - ...(collectionSkipCount > 0 - ? (collectionSkips.length > 0 - ? collectionSkips.map(s => `| ⏭️ SKIPPED (collection) | \`${s.file}:${s.line}\` |`) - : [`| ⏭️ SKIPPED (collection) | ${collectionSkipCount} test(s) skipped during collection β€” names unavailable |`]) - : []), - ]; - const notableTable = notableRows.length > 0 - ? ['', '| Result | Test |', '|--------|------|', ...notableRows, ''].join('\n') - : ''; - tableSection = ['### Full Test Results', '', summary, notableTable].join('\n'); - } - - // Warning if test counts still don't reconcile after correction - const warningSection = stillMismatched - ? [ - '---', - '### ⚠️ Count Mismatch Warning', - '', - `pytest reported **${authoritativeTotal} total** tests in its summary, but the reconciled count is **${correctedTotal}**.`, - 'This may indicate XFAIL/XPASS results or other collection anomalies not reflected above.', - '', - ].join('\n') - : ''; - - const headSha = process.env.HEAD_SHA; - const shortSha = headSha.slice(0, 7); - const commitUrl = `https://github.com/${process.env.GITHUB_REPOSITORY}/commit/${headSha}`; - - const marker = ''; - const bodyParts = [ - marker, - '## ' + status, - '', - `> Ran on commit [\`${shortSha}\`](${commitUrl})`, - '', - ]; - - if (warningSection) bodyParts.push(warningSection); - if (changedSection) bodyParts.push(changedSection); - if (tableSection) bodyParts.push(tableSection); - - const LIMIT = 60000; - const TRUNCATION = `\n\n> ⚠️ Comment truncated β€” [See CI logs](${runUrl}) for the full results.`; - - let body = bodyParts.join('\n'); - if (body.length > LIMIT) { - const cutAt = body.lastIndexOf('\n', LIMIT - TRUNCATION.length); - body = body.slice(0, cutAt > 0 ? cutAt : LIMIT - TRUNCATION.length) + TRUNCATION; - } - - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: parseInt(process.env.PR_NUMBER) || context.issue.number, - }); - - const existing = comments.find(c => c.body && c.body.includes(marker)); - - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - body, - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: parseInt(process.env.PR_NUMBER) || context.issue.number, - body, - }); - } -}; diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..949eeeac --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,40 @@ +name: CI + +on: + push: + branches: [main, feat/go-migration] + pull_request: + +permissions: + contents: read + +jobs: + build-and-test: + name: Build and test + runs-on: [self-hosted, rune-ci] + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: gofmt + run: | + unformatted=$(gofmt -l -e .) + if [ -n "$unformatted" ]; then + echo "::error::gofmt found unformatted files:" + echo "$unformatted" + gofmt -d -e . + exit 1 + fi + + - name: go vet + run: go vet ./... + + - name: go build + run: go build ./... + + - name: go test + run: go test -race -count=1 ./... diff --git a/.github/workflows/pr-comment.yml b/.github/workflows/pr-comment.yml deleted file mode 100644 index c23fc6f2..00000000 --- a/.github/workflows/pr-comment.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: Post PR Comment - -on: - workflow_run: - workflows: ["PR Tests"] - types: [completed] - -permissions: - contents: read - actions: read - pull-requests: write - issues: write - -jobs: - post-comment: - if: ${{ github.event.workflow_run.event == 'pull_request' }} - runs-on: [self-hosted, rune-ci] - - steps: - - name: Checkout trusted default branch - uses: actions/checkout@v4 - with: - ref: main - persist-credentials: false - - - name: Download test results - uses: actions/download-artifact@v4 - with: - name: test-results - github-token: ${{ github.token }} - run-id: ${{ github.event.workflow_run.id }} - - - name: Validate PR metadata - run: | - python3 - <<'EOF' - import json, re - with open("pr_info.json", "r", encoding="utf-8") as f: - info = json.load(f) - - assert re.fullmatch(r"[1-9][0-9]*", str(info.get("PR_NUMBER", ""))) - assert re.fullmatch(r"[0-9a-fA-F]{40}", str(info.get("HEAD_SHA", ""))) - EOF - - - name: Post PR comment - uses: actions/github-script@v7 - env: - CI_RUN_ID: ${{ github.event.workflow_run.id }} - with: - github-token: ${{ secrets.CI_BOT_TOKEN }} - script: | - const fs = require('fs'); - const info = JSON.parse(fs.readFileSync('pr_info.json', 'utf8')); - process.env.EXIT_CODE = String(info.EXIT_CODE ?? ''); - process.env.HAS_CHANGED = String(info.HAS_CHANGED ?? 'false'); - process.env.TEST_IDS = String(info.TEST_IDS ?? ''); - process.env.HEAD_SHA = String(info.HEAD_SHA ?? ''); - process.env.PR_NUMBER = String(info.PR_NUMBER ?? ''); - - const fn = require('./.github/scripts/post_pr_comment.js'); - await fn({ github, context }); diff --git a/.github/workflows/pr-tests.yml b/.github/workflows/pr-tests.yml deleted file mode 100644 index 09bb22a2..00000000 --- a/.github/workflows/pr-tests.yml +++ /dev/null @@ -1,97 +0,0 @@ -# Test detection rules (ci_changed_tests.py + grep pattern): -# - Files must live under a `tests/` subdirectory and be named `test_*.py` -# - Test classes must start with `Test` -# - Test functions/methods must start with `test_` -name: PR Tests - -on: - pull_request: - types: [opened, synchronize, reopened] - -permissions: - contents: read - -jobs: - run-tests: - name: Run all tests - runs-on: [self-hosted, rune-ci] - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - uses: actions/setup-python@v5 - with: - python-version: '3.12' - - - name: Detect changed test node IDs - id: detect - run: | - BASE=${{ github.event.pull_request.base.sha }} - HEAD=${{ github.event.pull_request.head.sha }} - CHANGED=$(git diff --name-only "$BASE" "$HEAD" | grep -E '(^|/)tests/test_[^/]+\.py$' || true) - - if [ -z "$CHANGED" ]; then - echo "has_changed=false" >> "$GITHUB_OUTPUT" - echo "test_ids=" >> "$GITHUB_OUTPUT" - else - python .github/scripts/ci_changed_tests.py "$BASE" "$HEAD" $CHANGED - fi - - - name: Cache pip - uses: actions/cache@v4 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }} - restore-keys: | - ${{ runner.os }}-pip- - - - name: Install dependencies - run: pip install -r requirements.txt - - - name: Run all tests - id: pytest - run: | - TEST_DIRS=$(find . -type d -name "tests" | grep -v __pycache__ | grep -v "/.git/" | tr '\n' ' ') \ - && python -m pytest $TEST_DIRS \ - -v --tb=long -rs \ - 2>&1 | tee pytest_output.txt - echo "exit_code=${PIPESTATUS[0]}" >> "$GITHUB_OUTPUT" - env: - PYTHONPATH: ${{ github.workspace }} - continue-on-error: true - - - name: Save PR info - if: always() - run: | - python3 - <<'EOF' - import json, os - info = { - 'PR_NUMBER': os.environ['PR_NUMBER'], - 'EXIT_CODE': os.environ['EXIT_CODE'], - 'HAS_CHANGED': os.environ['HAS_CHANGED'], - 'TEST_IDS': os.environ['TEST_IDS'], - 'HEAD_SHA': os.environ['HEAD_SHA'], - } - with open('pr_info.json', 'w') as f: - json.dump(info, f) - EOF - env: - PR_NUMBER: ${{ github.event.pull_request.number }} - EXIT_CODE: ${{ steps.pytest.outputs.exit_code }} - HAS_CHANGED: ${{ steps.detect.outputs.has_changed }} - TEST_IDS: ${{ steps.detect.outputs.test_ids }} - HEAD_SHA: ${{ github.event.pull_request.head.sha }} - - - name: Upload test results - if: always() - uses: actions/upload-artifact@v4 - with: - name: test-results - path: | - pytest_output.txt - pr_info.json - - - name: Fail if tests failed - if: always() && steps.pytest.outputs.exit_code != '0' - run: exit 1 diff --git a/cmd/rune-mcp/main.go b/cmd/rune-mcp/main.go index c2afa0b0..5f3f2f9d 100644 --- a/cmd/rune-mcp/main.go +++ b/cmd/rune-mcp/main.go @@ -4,7 +4,8 @@ // Spawn model: Claude Code launches one instance per session via stdio. // Lifecycle: starting β†’ waiting_for_vault β†’ active ↔ dormant. // Tools: 8 MCP tools (capture, recall, batch_capture, capture_history, -// delete_capture, vault_status, diagnostics, reload_pipelines). +// +// delete_capture, vault_status, diagnostics, reload_pipelines). // // Wiring: Deps holds a State manager + 3 services. Adapter clients (vault / // envector / embedder) are populated on the services by the boot loop after diff --git a/internal/adapters/config/dormant.go b/internal/adapters/config/dormant.go index 7860bd90..4efd0be2 100644 --- a/internal/adapters/config/dormant.go +++ b/internal/adapters/config/dormant.go @@ -59,7 +59,7 @@ func SaveToPath(cfg *Config, path string) error { // - "not_configured" β€” config.json missing, fresh install // - "vault_unconfigured" β€” config exists but Vault.Endpoint/Token empty // - "user_deactivated" β€” already-dormant config picked up by boot -// (idempotent path, just refreshes timestamp) +// (idempotent path, just refreshes timestamp) func MarkDormant(reason string) error { cfg, err := Load() if err != nil { diff --git a/internal/adapters/config/loader.go b/internal/adapters/config/loader.go index d26025a6..d3c1fc72 100644 --- a/internal/adapters/config/loader.go +++ b/internal/adapters/config/loader.go @@ -3,8 +3,9 @@ // Python: agents/common/config.py (365 LoC) β€” Go reduced from 7 sections to 3. // // Dropped sections (per scope SOT β€” docs/v04/overview/architecture.md): -// envector / embedding / llm / scribe / retriever β€” moved to Vault bundle -// (memory only) or external embedder process. +// +// envector / embedding / llm / scribe / retriever β€” moved to Vault bundle +// (memory only) or external embedder process. package config import ( @@ -33,8 +34,9 @@ type VaultConfig struct { } // FilePerms β€” per rune-mcp.md Β§Config: -// ~/.rune/ 0700 -// ~/.rune/config.json 0600 +// +// ~/.rune/ 0700 +// ~/.rune/config.json 0600 const ( DirPerm = 0700 FilePerm = 0600 diff --git a/internal/adapters/embedder/client_test.go b/internal/adapters/embedder/client_test.go index f62e8012..c9016c63 100644 --- a/internal/adapters/embedder/client_test.go +++ b/internal/adapters/embedder/client_test.go @@ -29,8 +29,8 @@ type fakeRuned struct { infoFn func(*runedv1.InfoRequest) (*runedv1.InfoResponse, error) healthFn func(*runedv1.HealthRequest) (*runedv1.HealthResponse, error) - infoCalls int32 // atomic β€” Info should be invoked exactly once across the lifetime of an infoCache - embedCalls int32 // atomic β€” used by retry test to count attempts + infoCalls int32 // atomic β€” Info should be invoked exactly once across the lifetime of an infoCache + embedCalls int32 // atomic β€” used by retry test to count attempts embedBatchCalls int32 // atomic β€” used by batch-split test } diff --git a/internal/adapters/envector/client.go b/internal/adapters/envector/client.go index 33edf1b0..557d3d6c 100644 --- a/internal/adapters/envector/client.go +++ b/internal/adapters/envector/client.go @@ -47,7 +47,7 @@ type Client interface { Insert(ctx context.Context, req InsertRequest) (*InsertResult, error) Score(ctx context.Context, vec []float32) ([][]byte, error) GetMetadata(ctx context.Context, refs []MetadataRef, fields []string) ([]MetadataEntry, error) - OpenIndex(ctx context.Context) error // opens (or creates) the server-side index + OpenIndex(ctx context.Context) error // opens (or creates) the server-side index GetIndexList(ctx context.Context) ([]string, error) // used by diagnostics + warmup Close() error } @@ -65,7 +65,7 @@ type ClientConfig struct { } type client struct { - sdk *envector.Client + sdk *envector.Client keys *envector.Keys idx *envector.Index cfg ClientConfig diff --git a/internal/adapters/envector/errors_test.go b/internal/adapters/envector/errors_test.go index e2666003..a2723c0b 100644 --- a/internal/adapters/envector/errors_test.go +++ b/internal/adapters/envector/errors_test.go @@ -18,45 +18,45 @@ func TestMapSDKError_Nil(t *testing.T) { func TestMapSDKError_SDKSentinels(t *testing.T) { tests := []struct { - name string - err error - wantCode string + name string + err error + wantCode string wantRetry bool }{ { - name: "ErrKeysNotForEncrypt", - err: envector.ErrKeysNotForEncrypt, - wantCode: "DECRYPTOR_UNAVAILABLE", + name: "ErrKeysNotForEncrypt", + err: envector.ErrKeysNotForEncrypt, + wantCode: "DECRYPTOR_UNAVAILABLE", wantRetry: false, }, { - name: "ErrKeysNotForDecrypt", - err: envector.ErrKeysNotForDecrypt, - wantCode: "DECRYPTOR_UNAVAILABLE", + name: "ErrKeysNotForDecrypt", + err: envector.ErrKeysNotForDecrypt, + wantCode: "DECRYPTOR_UNAVAILABLE", wantRetry: false, }, { - name: "ErrKeysNotForRegister", - err: envector.ErrKeysNotForRegister, - wantCode: "KEY_NOT_FOR_REGISTER", + name: "ErrKeysNotForRegister", + err: envector.ErrKeysNotForRegister, + wantCode: "KEY_NOT_FOR_REGISTER", wantRetry: false, }, { - name: "ErrClientClosed", - err: envector.ErrClientClosed, - wantCode: "ENVECTOR_CONNECTION_LOST", + name: "ErrClientClosed", + err: envector.ErrClientClosed, + wantCode: "ENVECTOR_CONNECTION_LOST", wantRetry: true, }, { - name: "ErrKeysNotFound", - err: envector.ErrKeysNotFound, - wantCode: "ENVECTOR_KEYS_NOT_FOUND", + name: "ErrKeysNotFound", + err: envector.ErrKeysNotFound, + wantCode: "ENVECTOR_KEYS_NOT_FOUND", wantRetry: false, }, { - name: "ErrKeysRequired", - err: envector.ErrKeysRequired, - wantCode: "ENVECTOR_KEYS_REQUIRED", + name: "ErrKeysRequired", + err: envector.ErrKeysRequired, + wantCode: "ENVECTOR_KEYS_REQUIRED", wantRetry: false, }, } @@ -142,7 +142,7 @@ func TestMapSDKError_GenericError(t *testing.T) { } } -//--- Error types ---// +// --- Error types ---// func TestError_ErrorString(t *testing.T) { tests := []struct { name string diff --git a/internal/domain/errors.go b/internal/domain/errors.go index f3e73fa7..d10961a2 100644 --- a/internal/domain/errors.go +++ b/internal/domain/errors.go @@ -8,16 +8,16 @@ import "errors" // Code enum β€” 8 codes. const ( - CodeInternal = "INTERNAL_ERROR" - CodeVaultConnection = "VAULT_CONNECTION_ERROR" - CodeVaultDecryption = "VAULT_DECRYPTION_ERROR" - CodeEnvectorConnection = "ENVECTOR_CONNECTION_ERROR" - CodeEnvectorInsert = "ENVECTOR_INSERT_ERROR" - CodePipelineNotReady = "PIPELINE_NOT_READY" - CodeInvalidInput = "INVALID_INPUT" - CodeEmbedderUnreachable = "EMBEDDER_UNREACHABLE" // Go-specific (D30) - CodeEmptyEmbedText = "EMPTY_EMBED_TEXT" // D5 β€” dedicated code for missing embed text - CodeExtractionMissing = "EXTRACTION_MISSING" // D14 β€” agent must provide pre_extraction + CodeInternal = "INTERNAL_ERROR" + CodeVaultConnection = "VAULT_CONNECTION_ERROR" + CodeVaultDecryption = "VAULT_DECRYPTION_ERROR" + CodeEnvectorConnection = "ENVECTOR_CONNECTION_ERROR" + CodeEnvectorInsert = "ENVECTOR_INSERT_ERROR" + CodePipelineNotReady = "PIPELINE_NOT_READY" + CodeInvalidInput = "INVALID_INPUT" + CodeEmbedderUnreachable = "EMBEDDER_UNREACHABLE" // Go-specific (D30) + CodeEmptyEmbedText = "EMPTY_EMBED_TEXT" // D5 β€” dedicated code for missing embed text + CodeExtractionMissing = "EXTRACTION_MISSING" // D14 β€” agent must provide pre_extraction ) // RuneError β€” MCP error response body (Python make_error equivalent). diff --git a/internal/domain/extraction.go b/internal/domain/extraction.go index aad202f2..6e3e6576 100644 --- a/internal/domain/extraction.go +++ b/internal/domain/extraction.go @@ -61,7 +61,6 @@ func (r *ExtractionResult) IsBundle() bool { return r.GroupType == "bundle" && len(r.Phases) > 1 } - // ParseExtractionFromAgent builds Detection + ExtractionResult from the flat // CaptureRequest.Extracted dict sent by the agent. Wire β†’ internal conversion. // diff --git a/internal/domain/schema.go b/internal/domain/schema.go index ffbccf8b..ae2f4cdf 100644 --- a/internal/domain/schema.go +++ b/internal/domain/schema.go @@ -210,9 +210,9 @@ type Payload struct { // DecisionRecord β€” Β§3. Python: decision_record.py:L166-213. // envector.Insert metadata의 decrypted payload. type DecisionRecord struct { - SchemaVersion string `json:"schema_version"` // fixed "2.1" - ID string `json:"id"` - Type string `json:"type"` // fixed "decision_record" + SchemaVersion string `json:"schema_version"` // fixed "2.1" + ID string `json:"id"` + Type string `json:"type"` // fixed "decision_record" Domain Domain `json:"domain"` Sensitivity Sensitivity `json:"sensitivity"` diff --git a/internal/lifecycle/boot.go b/internal/lifecycle/boot.go index c18e398f..8b02af02 100644 --- a/internal/lifecycle/boot.go +++ b/internal/lifecycle/boot.go @@ -187,7 +187,7 @@ const ( // - vault endpoint/token empty β†’ terminal Dormant (await /rune:configure) // - vault dial / GetAgentManifest β†’ state=WaitingForVault, exp backoff retry // - keymanager / embedder / envector init β†’ exp backoff retry (might be -// transient β€” daemon down, etc.) +// transient β€” daemon down, etc.) // - other config error (parse fail) β†’ exp backoff retry (user might be editing) // - ctx cancellation β†’ return immediately // diff --git a/internal/mcp/tools.go b/internal/mcp/tools.go index 09f484c0..c8c2e733 100644 --- a/internal/mcp/tools.go +++ b/internal/mcp/tools.go @@ -2,8 +2,9 @@ // owns Deps injection + state-aware response shaping. // // Spec: -// docs/v04/spec/components/rune-mcp.md (MCP server κ΅¬ν˜„) -// docs/v04/spec/flows/{capture,recall,lifecycle}.md +// +// docs/v04/spec/components/rune-mcp.md (MCP server κ΅¬ν˜„) +// docs/v04/spec/flows/{capture,recall,lifecycle}.md // // SDK: github.com/modelcontextprotocol/go-sdk v1.5.0+ (D2). Stdio transport. // Input schema is auto-inferred from the Go input struct (jsonschema tags @@ -200,4 +201,3 @@ func isValidToolName(name string) bool { } return true } - diff --git a/internal/policy/record_builder.go b/internal/policy/record_builder.go index 5cdf200d..839c39f5 100644 --- a/internal/policy/record_builder.go +++ b/internal/policy/record_builder.go @@ -17,7 +17,7 @@ import ( // MAX_INPUT_CHARS β€” Python L227. Truncate cleanText before extraction. const MaxInputChars = 12_000 -// QuotePatterns β€” 4 regex (Python L72-77): double "", single '', Japanese γ€Œγ€, +// QuotePatterns β€” 4 regex (Python L72-77): double "", single ”, Japanese γ€Œγ€, // French «». Min 10 chars. var QuotePatterns = []*regexp.Regexp{ regexp.MustCompile(`"([^"]{10,})"`), diff --git a/internal/policy/rerank.go b/internal/policy/rerank.go index ffb77f52..511c75c9 100644 --- a/internal/policy/rerank.go +++ b/internal/policy/rerank.go @@ -41,7 +41,6 @@ var TimeRanges = map[domain.TimeScope]time.Duration{ // // BIT-IDENTICAL REQUIREMENT: Python timedelta.days is integer floor. // Go Hours()/24 is float β€” must math.Floor to match. -// func ApplyRecencyWeighting(hits []domain.SearchHit, now time.Time) []domain.SearchHit { for i := range hits { r := &hits[i] diff --git a/internal/service/capture.go b/internal/service/capture.go index 91118c17..2ee35fc5 100644 --- a/internal/service/capture.go +++ b/internal/service/capture.go @@ -3,9 +3,10 @@ // delegate to these services; business logic lives here, not in handlers. // // Spec: -// docs/v04/spec/flows/capture.md (7-phase) -// docs/v04/spec/flows/recall.md (7-phase) -// docs/v04/spec/flows/lifecycle.md (6 tools) +// +// docs/v04/spec/flows/capture.md (7-phase) +// docs/v04/spec/flows/recall.md (7-phase) +// docs/v04/spec/flows/lifecycle.md (6 tools) package service import ( diff --git a/internal/service/lifecycle.go b/internal/service/lifecycle.go index 0504ad4b..e07f2c8d 100644 --- a/internal/service/lifecycle.go +++ b/internal/service/lifecycle.go @@ -112,10 +112,10 @@ type DiagnosticsResult struct { // EnvInfo β€” OS, Go runtime version, cwd. type EnvInfo struct { - OS string `json:"os"` - Runtime string `json:"runtime"` - CWD string `json:"cwd"` - GOArch string `json:"goarch"` + OS string `json:"os"` + Runtime string `json:"runtime"` + CWD string `json:"cwd"` + GOArch string `json:"goarch"` } // VaultInfo β€” subset exposed in diagnostics. @@ -168,10 +168,10 @@ func (s *LifecycleService) Diagnostics(ctx context.Context) *DiagnosticsResult { // Environment cwd, _ := os.Getwd() r.Environment = EnvInfo{ - OS: runtime.GOOS, - Runtime: runtime.Version(), - CWD: cwd, - GOArch: runtime.GOARCH, + OS: runtime.GOOS, + Runtime: runtime.Version(), + CWD: cwd, + GOArch: runtime.GOARCH, } // Config state @@ -490,11 +490,12 @@ const WarmupTimeout = 60 * time.Second // // TODO: currently a no-op for state recovery β€” only envector warmup probe runs. // Full re-init requires: -// 1. internal/lifecycle/boot.go::RunBootLoop body (Vault.GetAgentManifest + bundle setup -// + envector.NewClient + state=Active transition) -// 2. wiring here to re-trigger boot logic on call (state.SetState(Starting) + -// RunBootLoop re-invoke, or a shared _init_pipelines helper called from both -// startup and this function) +// 1. internal/lifecycle/boot.go::RunBootLoop body (Vault.GetAgentManifest + bundle setup +// + envector.NewClient + state=Active transition) +// 2. wiring here to re-trigger boot logic on call (state.SetState(Starting) + +// RunBootLoop re-invoke, or a shared _init_pipelines helper called from both +// startup and this function) +// // Until both land, /rune:activate cannot recover from dormant or trigger first-time // pipeline init. func (s *LifecycleService) ReloadPipelines(ctx context.Context) (*ReloadPipelinesResult, error) { diff --git a/internal/service/recall.go b/internal/service/recall.go index 3a644646..bff67af3 100644 --- a/internal/service/recall.go +++ b/internal/service/recall.go @@ -635,9 +635,9 @@ func calculateConfidence(results []domain.SearchHit) float64 { } certaintyWeights := map[string]float64{ - "supported": 1.0, - "partially_supported": 0.6, - "unknown": 0.3, + "supported": 1.0, + "partially_supported": 0.6, + "unknown": 0.3, } totalScore := 0.0