From 9db718e921743c134f7d2c4a3691bd56b86aa05d Mon Sep 17 00:00:00 2001 From: gracefullight <11773683+gracefullight@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:37:44 +0000 Subject: [PATCH] chore(deps): update oh-my-agent skills --- .agents/agents/architecture-reviewer.md | 3 +- .agents/agents/backend-engineer.md | 7 +- .agents/agents/db-engineer.md | 10 +- .agents/agents/debug-investigator.md | 2 +- .agents/agents/docs-curator.md | 2 +- .agents/agents/frontend-engineer.md | 12 +- .agents/agents/mobile-engineer.md | 24 +- .agents/agents/pm-planner.md | 9 +- .agents/agents/qa-reviewer.md | 8 +- .agents/agents/refactor-engineer.md | 2 +- .agents/agents/research-explorer.md | 2 +- .agents/agents/tf-infra-engineer.md | 2 +- .agents/agents/variants/claude.json | 3 + .agents/hooks/core/agentmemory-client.ts | 40 +- .agents/hooks/core/hook-output.ts | 114 +- .agents/hooks/core/keyword-detector.ts | 348 ++++-- .agents/hooks/core/prompt-input.ts | 45 + .agents/hooks/core/scm-guard.ts | 250 ++++ .agents/hooks/core/serena-primer.ts | 8 +- .agents/hooks/core/skill-injector.ts | 8 +- .agents/hooks/core/state-boundary.ts | 17 +- .agents/hooks/core/state-marker.ts | 4 +- .agents/hooks/core/test-filter.ts | 38 +- .agents/hooks/core/triggers.json | 138 +-- .agents/hooks/core/types.ts | 13 +- .agents/hooks/variants/antigravity.json | 5 - .agents/hooks/variants/claude.json | 27 +- .agents/hooks/variants/codex.json | 24 +- .agents/hooks/variants/commandcode.json | 10 + .agents/hooks/variants/cursor.json | 27 + .agents/hooks/variants/grok.json | 17 +- .../hooks/variants/hook-variant.schema.json | 9 + .agents/hooks/variants/kimi.json | 17 +- .agents/hooks/variants/kiro.json | 18 +- .agents/hooks/variants/opencode/oma.ts | 472 ++++++-- .agents/hooks/variants/pi/README.md | 45 +- .agents/hooks/variants/pi/index.ts | 126 +- .agents/hooks/variants/qwen.json | 24 +- .agents/mcp.json | 12 +- .agents/rules/backend.md | 2 +- .agents/rules/commit.md | 3 +- .agents/rules/database.md | 2 + .agents/rules/market.md | 6 +- .agents/rules/mobile.md | 18 +- .../_shared/conditional/quality-score.md | 2 + .../_shared/core/api-contracts/README.md | 4 +- .agents/skills/_shared/core/context-budget.md | 4 +- .../skills/_shared/core/context-loading.md | 6 +- .../skills/_shared/core/session-metrics.md | 6 +- .../skills/_shared/core/vendor-detection.md | 4 +- .agents/skills/_shared/runtime/event-spec.md | 2 + .../execution-protocols/antigravity.md | 20 +- .../runtime/execution-protocols/claude.md | 14 +- .../runtime/execution-protocols/codex.md | 10 +- .../execution-protocols/commandcode.md | 10 +- .../runtime/execution-protocols/grok.md | 8 +- .../runtime/execution-protocols/kimi.md | 25 +- .../runtime/execution-protocols/kiro.md | 20 +- .../runtime/execution-protocols/opencode.md | 22 +- .../_shared/runtime/execution-protocols/pi.md | 15 +- .../runtime/execution-protocols/qwen.md | 13 +- .../skills/_shared/runtime/memory-protocol.md | 29 +- .agents/skills/_version.json | 4 +- .agents/skills/oma-academic-writer/SKILL.md | 4 +- .../resources/academic-verb-tiers.md | 52 +- .../resources/anti-ai-checklist.md | 16 +- .../resources/hedging-guide.md | 10 +- .../resources/sentence-structure-reference.md | 18 +- .agents/skills/oma-architecture/SKILL.md | 15 +- .../resources/api-evolution.md | 41 + .../oma-architecture/resources/checklist.md | 9 +- .../oma-architecture/resources/examples.md | 39 + .../resources/execution-protocol.md | 29 +- .../resources/methodology-selection.md | 9 + .../resources/migration-patterns.md | 28 + .../resources/output-templates.md | 26 + .../resources/stakeholder-synthesis.md | 29 +- .agents/skills/oma-backend/SKILL.md | 10 +- .../skills/oma-backend/resources/checklist.md | 2 +- .../oma-backend/resources/error-playbook.md | 4 +- .../skills/oma-backend/resources/examples.md | 2 +- .agents/skills/oma-brainstorm/SKILL.md | 46 +- .../oma-brainstorm/resources/triz-lite.md | 117 ++ .agents/skills/oma-coordination/SKILL.md | 27 +- .../oma-coordination/resources/examples.md | 4 +- .agents/skills/oma-db/SKILL.md | 8 +- .agents/skills/oma-db/resources/checklist.md | 4 + .../skills/oma-db/resources/error-playbook.md | 25 + .agents/skills/oma-db/resources/examples.md | 30 + .../oma-db/resources/execution-protocol.md | 11 +- .../oma-db/resources/migration-playbook.md | 82 ++ .../skills/oma-db/resources/query-tuning.md | 57 + .agents/skills/oma-debug/SKILL.md | 4 +- .../resources/bug-report-template.md | 8 + .../skills/oma-debug/resources/checklist.md | 2 +- .../oma-debug/resources/common-patterns.md | 11 +- .../resources/debugging-checklist.md | 6 +- .../oma-debug/resources/error-playbook.md | 2 +- .../oma-debug/resources/execution-protocol.md | 5 +- .agents/skills/oma-deepsec/SKILL.md | 6 +- .../skills/oma-deepsec/resources/config.md | 8 + .../skills/oma-deepsec/resources/pr-review.md | 14 +- .../skills/oma-deepsec/resources/scanning.md | 6 +- .../skills/oma-deepsec/resources/triage.md | 2 +- .agents/skills/oma-design/SKILL.md | 22 +- .../oma-design/resources/design-md-spec.md | 2 +- .../resources/execution-protocol.md | 76 +- .../oma-design/resources/getdesign-fetcher.md | 2 + .../resources/prompt-enhancement.md | 131 +++ .agents/skills/oma-dev-workflow/SKILL.md | 20 +- .../resources/api-workflows.md | 8 +- .../resources/database-patterns.md | 29 +- .../resources/i18n-patterns.md | 6 +- .../resources/release-coordination.md | 22 +- .../resources/validation-pipeline.md | 48 +- .agents/skills/oma-docs/SKILL.md | 89 +- .agents/skills/oma-frontend/SKILL.md | 15 +- .../oma-frontend/resources/checklist.md | 3 + .../oma-frontend/resources/error-playbook.md | 6 +- .../resources/execution-protocol.md | 4 +- .../skills/oma-frontend/resources/snippets.md | 72 +- .../oma-frontend/resources/tech-stack.md | 61 + .agents/skills/oma-hwp/SKILL.md | 15 +- .agents/skills/oma-hwp/config/hwp-config.yaml | 5 +- .../oma-hwp/resources/balanced-tables.ts | 39 + .../oma-hwp/resources/execution-protocol.md | 34 +- .../oma-hwp/resources/flatten-tables.ts | 46 +- .../oma-hwp/resources/troubleshooting.md | 10 +- .agents/skills/oma-image/SKILL.md | 24 +- .../skills/oma-image/config/image-config.yaml | 13 + .../skills/oma-image/resources/checklist.md | 1 + .../oma-image/resources/execution-protocol.md | 13 +- .../skills/oma-image/resources/prompt-tips.md | 8 +- .../oma-image/resources/vendor-matrix.md | 18 +- .agents/skills/oma-market/SKILL.md | 60 +- .../skills/oma-market/resources/checklist.md | 2 +- .../oma-market/resources/error-playbook.md | 39 +- .../skills/oma-market/resources/examples.md | 74 +- .../resources/execution-protocol.md | 2 +- .../oma-market/resources/frameworks/pestel.md | 12 +- .../resources/frameworks/porters-5f.md | 10 +- .../resources/operator-packs/discovery.md | 12 +- .../resources/operator-packs/pain.md | 2 +- .../resources/operator-packs/positive.md | 2 +- .../oma-market/resources/output-laws.md | 10 +- .agents/skills/oma-mobile/SKILL.md | 9 +- .../skills/oma-mobile/resources/checklist.md | 28 +- .../oma-mobile/resources/error-playbook.md | 42 +- .../skills/oma-mobile/resources/examples.md | 47 +- .../resources/execution-protocol.md | 18 +- .../oma-mobile/resources/screen-template.dart | 267 +++-- .../resources/screen-template.swift | 72 +- .../oma-mobile/resources/screen-template.tsx | 177 +++ .../skills/oma-mobile/resources/tech-stack.md | 8 +- .../variants/flutter/api-template.dart | 145 ++- .../oma-mobile/variants/flutter/snippets.md | 245 +++- .../oma-mobile/variants/flutter/tech-stack.md | 42 +- .../variants/react-native/api-template.ts | 209 +++- .../variants/react-native/snippets.md | 437 +++++-- .../variants/react-native/stack.yaml | 2 +- .../variants/react-native/tech-stack.md | 27 +- .../oma-mobile/variants/swift-ios/snippets.md | 262 ++++- .../oma-mobile/variants/swift-ios/stack.yaml | 8 +- .../variants/swift-ios/tech-stack.md | 10 +- .agents/skills/oma-observability/SKILL.md | 8 +- .../resources/anti-patterns.md | 2 +- .../resources/boundaries/cross-application.md | 4 +- .../resources/boundaries/multi-tenant.md | 4 +- .../resources/boundaries/release.md | 2 +- .../oma-observability/resources/checklist.md | 4 +- .../oma-observability/resources/examples.md | 4 +- .../resources/execution-protocol.md | 6 +- .../resources/incident-forensics.md | 22 +- .../resources/intent-rules.md | 2 +- .../resources/layers/L3-network.md | 4 +- .../resources/layers/L4-transport.md | 2 +- .../layers/L7-application/crash-analytics.md | 2 +- .../layers/L7-application/mobile-rum.md | 6 +- .../resources/layers/L7-application/waf.md | 4 +- .../layers/L7-application/web-rum.md | 4 +- .../resources/layers/mesh.md | 12 +- .../oma-observability/resources/matrix.md | 8 +- .../resources/meta-observability.md | 8 +- .../resources/signals/audit.md | 2 +- .../resources/signals/cost.md | 6 +- .../resources/signals/logs.md | 2 +- .../resources/signals/metrics.md | 10 +- .../resources/signals/privacy.md | 10 +- .../resources/signals/profiles.md | 6 +- .../resources/signals/traces.md | 10 +- .../oma-observability/resources/standards.md | 16 +- .../resources/vendor-categories.md | 6 +- .agents/skills/oma-orchestrator/SKILL.md | 33 +- .../oma-orchestrator/config/cli-config.yaml | 14 + .../resources/memory-schema.md | 20 +- .../resources/subagent-prompt-template.md | 10 +- .../skills/oma-orchestrator/scripts/verify.sh | 12 + .agents/skills/oma-pdf/SKILL.md | 13 +- .../oma-pdf/resources/execution-protocol.md | 59 +- .agents/skills/oma-pm/SKILL.md | 4 +- .../skills/oma-pm/resources/error-playbook.md | 8 +- .agents/skills/oma-pm/resources/examples.md | 23 +- .../oma-pm/resources/execution-protocol.md | 4 + .../oma-pm/resources/plan-phase-protocol.md | 2 +- .../oma-pm/resources/task-template.json | 19 +- .agents/skills/oma-qa/resources/checklist.md | 2 +- .agents/skills/oma-qa/resources/examples.md | 4 +- .../oma-qa/resources/execution-protocol.md | 2 +- .agents/skills/oma-recap/SKILL.md | 22 +- .../oma-recap/resources/output-formats.md | 8 +- .agents/skills/oma-refactor/SKILL.md | 8 +- .../oma-refactor/resources/definition.md | 27 +- .../oma-refactor/resources/governance.md | 4 + .agents/skills/oma-scholar/SKILL.md | 38 +- .../oma-scholar/config/scholar-config.yaml | 11 +- .../oma-scholar/resources/api-endpoints.md | 2 +- .../skills/oma-scholar/resources/checklist.md | 3 +- .../resources/execution-protocol.md | 39 +- .../resources/fallback-providers.md | 34 +- .../oma-scholar/resources/sidecar-spec.md | 33 +- .../resources/upstream-spec-cache.md | 1039 +++++++++++++++-- .agents/skills/oma-scm/SKILL.md | 48 +- .agents/skills/oma-scm/config/cm-config.yaml | 3 +- .../skills/oma-scm/config/commit-config.yaml | 26 +- .../oma-scm/resources/conventional-commits.md | 7 +- .agents/skills/oma-search/SKILL.md | 11 +- .../skills/oma-search/resources/checklist.md | 1 + .../oma-search/resources/error-playbook.md | 9 +- .../skills/oma-search/resources/examples.md | 14 +- .../resources/execution-protocol.md | 15 +- .../oma-search/resources/trust-registry.md | 172 +-- .agents/skills/oma-skill-creator/SKILL.md | 13 +- .../resources/ssl-lite-template.md | 12 + .../resources/validation-checklist.md | 33 +- .agents/skills/oma-slide/SKILL.md | 59 +- .../oma-slide/resources/assets/deck-stage.js | 77 +- .../oma-slide/resources/canva-integration.md | 4 +- .../skills/oma-slide/resources/checklist.md | 4 + .../oma-slide/resources/design-doctrine.md | 11 +- .../skills/oma-slide/resources/fixed-stage.md | 29 +- .../resources/generation-protocol.md | 81 +- .agents/skills/oma-tf-infra/SKILL.md | 14 +- .../oma-tf-infra/resources/checklist.md | 5 +- .../resources/cost-optimization.md | 61 +- .../oma-tf-infra/resources/error-playbook.md | 16 +- .../resources/execution-protocol.md | 13 +- .../resources/multi-cloud-examples.md | 55 +- .../resources/policy-testing-examples.md | 45 +- .agents/skills/oma-translator/SKILL.md | 30 +- .../resources/anti-ai-patterns.md | 14 +- .../resources/translation-rubric.md | 2 +- .agents/skills/oma-voice/SKILL.md | 21 +- .../skills/oma-voice/config/voice-config.yaml | 2 + .../oma-voice/resources/execution-protocol.md | 14 +- .../oma-voice/resources/voice-matrix.md | 2 +- .agents/workflows/architecture.md | 25 +- .agents/workflows/brainstorm.md | 87 +- .agents/workflows/convert.md | 5 +- .agents/workflows/debug.md | 4 +- .agents/workflows/deepinit.md | 10 +- .agents/workflows/deepsec.md | 4 +- .agents/workflows/design.md | 10 +- .agents/workflows/docs.md | 28 +- .agents/workflows/explain.md | 74 ++ .agents/workflows/orchestrate.md | 28 +- .agents/workflows/plan.md | 20 +- .agents/workflows/ralph.md | 10 +- .agents/workflows/recap.md | 10 +- .agents/workflows/review.md | 6 +- .agents/workflows/schedule.md | 12 +- .agents/workflows/scm.md | 27 +- .agents/workflows/stack-set.md | 65 +- .agents/workflows/tools.md | 46 - .agents/workflows/ultrawork.md | 197 ++-- .../resources/multi-review-protocol.md | 44 +- .../ultrawork/resources/phase-gates.md | 4 +- .agents/workflows/video.md | 29 +- .agents/workflows/work.md | 26 +- .claude/agents/architecture-reviewer.md | 3 +- .claude/agents/backend-engineer.md | 7 +- .claude/agents/db-engineer.md | 10 +- .claude/agents/debug-investigator.md | 2 +- .claude/agents/docs-curator.md | 2 +- .claude/agents/frontend-engineer.md | 14 +- .claude/agents/mobile-engineer.md | 26 +- .claude/agents/pm-planner.md | 9 +- .claude/agents/qa-reviewer.md | 8 +- .claude/agents/refactor-engineer.md | 2 +- .claude/agents/research-explorer.md | 4 +- .claude/agents/tf-infra-engineer.md | 2 +- .claude/rules/backend.md | 2 +- .claude/rules/commit.md | 3 +- .claude/rules/database.md | 2 + .claude/rules/market.md | 6 +- .claude/rules/mobile.md | 18 +- .claude/settings.json | 14 +- .claude/skills/explain/SKILL.md | 1 + .codex/agents/architecture-reviewer.toml | 3 +- .codex/agents/backend-engineer.toml | 7 +- .codex/agents/db-engineer.toml | 10 +- .codex/agents/debug-investigator.toml | 2 +- .codex/agents/docs-curator.toml | 2 +- .codex/agents/frontend-engineer.toml | 12 +- .codex/agents/mobile-engineer.toml | 24 +- .codex/agents/pm-planner.toml | 9 +- .codex/agents/qa-reviewer.toml | 8 +- .codex/agents/refactor-engineer.toml | 2 +- .codex/agents/research-explorer.toml | 2 +- .codex/agents/tf-infra-engineer.toml | 2 +- .codex/config.toml | 1 - .codex/hooks.json | 2 +- .codex/skills/explain/SKILL.md | 1 + .cursor/agents/architecture-reviewer.md | 3 +- .cursor/agents/backend-engineer.md | 7 +- .cursor/agents/db-engineer.md | 10 +- .cursor/agents/debug-investigator.md | 2 +- .cursor/agents/docs-curator.md | 2 +- .cursor/agents/frontend-engineer.md | 14 +- .cursor/agents/mobile-engineer.md | 26 +- .cursor/agents/pm-planner.md | 9 +- .cursor/agents/qa-reviewer.md | 8 +- .cursor/agents/refactor-engineer.md | 2 +- .cursor/agents/research-explorer.md | 2 +- .cursor/agents/tf-infra-engineer.md | 2 +- .cursor/hooks.json | 20 + .cursor/hooks/filter-test-output.sh | 52 + .cursor/rules/backend.mdc | 2 +- .cursor/rules/commit.mdc | 3 +- .cursor/rules/database.mdc | 2 + .cursor/rules/market.mdc | 6 +- .cursor/rules/mobile.mdc | 18 +- .cursor/skills/explain/SKILL.md | 1 + .github/prompts/docs.prompt.md | 2 +- .github/prompts/explain.prompt.md | 7 + .github/prompts/ultrawork.prompt.md | 2 +- .github/skills/explain/SKILL.md | 1 + .gitignore | 2 + .opencode/agents/architecture-reviewer.md | 3 +- .opencode/agents/backend-engineer.md | 7 +- .opencode/agents/db-engineer.md | 10 +- .opencode/agents/debug-investigator.md | 2 +- .opencode/agents/docs-curator.md | 2 +- .opencode/agents/frontend-engineer.md | 14 +- .opencode/agents/mobile-engineer.md | 26 +- .opencode/agents/pm-planner.md | 9 +- .opencode/agents/qa-reviewer.md | 8 +- .opencode/agents/refactor-engineer.md | 2 +- .opencode/agents/research-explorer.md | 2 +- .opencode/agents/tf-infra-engineer.md | 2 +- .opencode/plugins/oma/agentmemory-client.ts | 40 +- .opencode/plugins/oma/hook-output.ts | 114 +- .opencode/plugins/oma/keyword-detector.ts | 348 ++++-- .opencode/plugins/oma/oma.ts | 472 ++++++-- .opencode/plugins/oma/prompt-input.ts | 45 + .opencode/plugins/oma/scm-guard.ts | 250 ++++ .opencode/plugins/oma/serena-primer.ts | 8 +- .opencode/plugins/oma/skill-injector.ts | 8 +- .opencode/plugins/oma/state-boundary.ts | 17 +- .opencode/plugins/oma/state-marker.ts | 4 +- .opencode/plugins/oma/test-filter.ts | 38 +- .opencode/plugins/oma/triggers.json | 138 +-- .opencode/plugins/oma/types.ts | 13 +- .opencode/skills/explain/SKILL.md | 1 + .qwen/settings.json | 11 +- .qwen/skills/explain/SKILL.md | 1 + AGENTS.md | 7 +- CLAUDE.md | 7 +- 367 files changed, 8826 insertions(+), 2718 deletions(-) create mode 100644 .agents/hooks/core/prompt-input.ts create mode 100644 .agents/hooks/core/scm-guard.ts create mode 100644 .agents/skills/oma-architecture/resources/api-evolution.md create mode 100644 .agents/skills/oma-architecture/resources/migration-patterns.md create mode 100644 .agents/skills/oma-brainstorm/resources/triz-lite.md create mode 100644 .agents/skills/oma-db/resources/migration-playbook.md create mode 100644 .agents/skills/oma-db/resources/query-tuning.md create mode 100644 .agents/skills/oma-hwp/resources/balanced-tables.ts create mode 100644 .agents/skills/oma-mobile/resources/screen-template.tsx create mode 100644 .agents/workflows/explain.md create mode 120000 .claude/skills/explain/SKILL.md create mode 120000 .codex/skills/explain/SKILL.md create mode 100755 .cursor/hooks/filter-test-output.sh create mode 120000 .cursor/skills/explain/SKILL.md create mode 100644 .github/prompts/explain.prompt.md create mode 120000 .github/skills/explain/SKILL.md create mode 100644 .opencode/plugins/oma/prompt-input.ts create mode 100644 .opencode/plugins/oma/scm-guard.ts create mode 120000 .opencode/skills/explain/SKILL.md create mode 120000 .qwen/skills/explain/SKILL.md diff --git a/.agents/agents/architecture-reviewer.md b/.agents/agents/architecture-reviewer.md index 0c7bed66..6d029a69 100644 --- a/.agents/agents/architecture-reviewer.md +++ b/.agents/agents/architecture-reviewer.md @@ -12,6 +12,7 @@ You are an Architecture Specialist. Diagnose the architectural concern before re Follow the vendor-specific execution protocol: - Write results to project root `.agents/results/result-architecture.md` (orchestrated: `result-architecture-{sessionId}.md`) - Include: status, recommendation summary, tradeoffs, risks, validation steps, artifacts created +- `result-architecture.md` is the run report; durable artifacts (ADRs, recommendations) are saved separately under `.agents/results/architecture/` and linked from the report — the report does not replace them @@ -41,4 +42,4 @@ CHARTER_CHECK: 4. Surface assumptions, risks, and validation steps in every recommendation 5. Save ADRs or architecture notes under `.agents/results/architecture/` when material 6. Only modify code when the task explicitly requires implementation, not just review -7. Never modify `.agents/` files +7. Never modify `.agents/` files (SSOT) — run outputs under `.agents/results/` and `.agents/state/memories/` are the only exceptions diff --git a/.agents/agents/backend-engineer.md b/.agents/agents/backend-engineer.md index 4fec2624..32048652 100644 --- a/.agents/agents/backend-engineer.md +++ b/.agents/agents/backend-engineer.md @@ -44,8 +44,9 @@ Router (HTTP) → Service (Business Logic) → Repository (Data Access) → Mode 3. Follow Repository → Service → Router pattern (no business logic in routes) 4. Validate all inputs with the project's validation library 5. Parameterized queries only (no string interpolation in SQL) -6. JWT + bcrypt for auth +6. JWT + Argon2id for auth (bcrypt acceptable for legacy compatibility) 7. Async/await consistently 8. Custom exceptions via centralized error module -9. Document out-of-scope dependencies for other agents -10. Never modify `.agents/` files +9. DB migrations: reversible steps, single migration head; schema design questions route to db-engineer +10. Document out-of-scope dependencies for other agents +11. Never modify `.agents/` files (SSOT) — run outputs under `.agents/results/` and `.agents/state/memories/` are the only exceptions diff --git a/.agents/agents/db-engineer.md b/.agents/agents/db-engineer.md index d7b36af3..037a053a 100644 --- a/.agents/agents/db-engineer.md +++ b/.agents/agents/db-engineer.md @@ -27,6 +27,10 @@ CHARTER_CHECK: - Success criteria: {measurable criteria} - Assumptions: {defaults applied} ``` + +- LOW: proceed with assumptions +- MEDIUM: list options, proceed with most likely +- HIGH: set status blocked, list questions, DO NOT write code ## Rules @@ -39,5 +43,7 @@ CHARTER_CHECK: 6. Integrity as first-class: entity, domain, referential, business-rule 7. Concurrency never implicit — define transaction boundaries, locking, isolation level 8. Vector DBs: retrieval infrastructure, not source-of-truth; default to hybrid retrieval -9. Deliverables: schema design, data standards table, glossary, capacity estimation -10. Never modify `.agents/` files +9. Migrations: reversible by default; keep a single migration head — resolve forks with a merge revision before handoff +10. Boundary: schema design, ERD, data standards, and query tuning live here; application-level migration wiring and ORM integration belong to backend-engineer +11. Deliverables: schema design, data standards table, glossary, capacity estimation +12. Never modify `.agents/` files (SSOT) — run outputs under `.agents/results/` and `.agents/state/memories/` are the only exceptions diff --git a/.agents/agents/debug-investigator.md b/.agents/agents/debug-investigator.md index b6acf290..7bcd41ed 100644 --- a/.agents/agents/debug-investigator.md +++ b/.agents/agents/debug-investigator.md @@ -49,4 +49,4 @@ CHARTER_CHECK: 4. Every fix gets a regression test 5. Search for similar patterns after fixing 6. Document out-of-scope findings for other agents -7. Never modify `.agents/` files +7. Never modify `.agents/` files (SSOT) — run outputs under `.agents/results/` and `.agents/state/memories/` are the only exceptions diff --git a/.agents/agents/docs-curator.md b/.agents/agents/docs-curator.md index 3718ca6c..1fb40843 100644 --- a/.agents/agents/docs-curator.md +++ b/.agents/agents/docs-curator.md @@ -53,7 +53,7 @@ This agent is a write-capable peer of `backend-engineer` / `frontend-engineer`. 1. Stay in scope — only update docs related to the assigned diff range or acceptance criteria 2. Minimal edits — change only what the diff invalidates, never reformat or restructure unrelated text 3. Never modify code (`*.ts`, `*.tsx`, `*.py`, `*.go`, etc.) — surface mismatches for `backend-engineer` / `frontend-engineer` instead -4. Never modify `.agents/` files — SSOT protection +4. Never modify `.agents/` files (SSOT) — run outputs under `.agents/results/` and `.agents/state/memories/` are the only exceptions 5. Never touch secret-bearing files even if surfaced in diffs (`.env*`, `*.pem`, `*.key`, `id_rsa*`) 6. Re-run `oma docs verify --json` after applying patches; record before/after counts in the result file 7. ARB-based localization (`packages/i18n/`): edit ARB source, never regenerate localization code diff --git a/.agents/agents/frontend-engineer.md b/.agents/agents/frontend-engineer.md index e47f5994..134bdf73 100644 --- a/.agents/agents/frontend-engineer.md +++ b/.agents/agents/frontend-engineer.md @@ -1,12 +1,12 @@ --- name: frontend-engineer -description: React/Next.js/TypeScript frontend implementation. Use for UI, components, styling work. +description: React/Next.js/Angular/TypeScript frontend implementation. Use for UI, components, styling work. skills: - oma-frontend - oma-design --- -You are a Frontend Specialist. +You are a Frontend Specialist. Detect the project's framework and existing conventions (package.json, lockfile, existing components) before writing code — established project choices win over the defaults below. ## Execution Protocol @@ -28,6 +28,10 @@ CHARTER_CHECK: - Success criteria: {measurable criteria} - Assumptions: {defaults applied} ``` + +- LOW: proceed with assumptions +- MEDIUM: list options, proceed with most likely +- HIGH: set status blocked, list questions, DO NOT write code ## Architecture @@ -41,8 +45,8 @@ FSD-lite: root `src/` + feature `src/features/*/` 3. Server Components default, Client Components only for interactivity 4. Accessibility mandatory (semantic HTML, ARIA, keyboard nav) 5. TailwindCSS v4 for styling, design tokens 1:1 mapping -6. Libraries: luxon (dates), ahooks (hooks), es-toolkit (utils), jotai (client state), TanStack Query (server state) +6. Library defaults (greenfield; existing project choices win): luxon (dates), ahooks (hooks), es-toolkit (utils), jotai (client state), TanStack Query (server state) 7. Absolute imports with `@/` 8. Write tests for custom logic (>90% coverage target) 9. Document out-of-scope dependencies for other agents -10. Never modify `.agents/` files +10. Never modify `.agents/` files (SSOT) — run outputs under `.agents/results/` and `.agents/state/memories/` are the only exceptions diff --git a/.agents/agents/mobile-engineer.md b/.agents/agents/mobile-engineer.md index dce9e504..5419fd71 100644 --- a/.agents/agents/mobile-engineer.md +++ b/.agents/agents/mobile-engineer.md @@ -1,6 +1,6 @@ --- name: mobile-engineer -description: Flutter/React Native mobile implementation. Use for mobile app, widgets, platform feature work. +description: Flutter/React Native/Swift native mobile implementation. Use for mobile app, widgets, SwiftUI, platform feature work. skills: - oma-mobile --- @@ -27,20 +27,26 @@ CHARTER_CHECK: - Success criteria: {measurable criteria} - Assumptions: {defaults applied} ``` + +- LOW: proceed with assumptions +- MEDIUM: list options, proceed with most likely +- HIGH: set status blocked, list questions, DO NOT write code ## Architecture -Clean Architecture: domain → data → presentation +Clean Architecture: domain → data → presentation (Swift native: App/Core/Features/Shared) ## Rules 1. Stay in scope — only work on assigned mobile tasks -2. Riverpod/Bloc for state management +2. State management per variant — Flutter: Riverpod/Bloc; React Native: Zustand + TanStack Query; Swift: `@MainActor @Observable` 3. Material Design 3 (Android) + iOS HIG (iOS) -4. All controllers disposed properly -5. Dio with interceptors, offline-first architecture -6. 60fps target performance -7. Write widget tests and integration tests -8. Document out-of-scope dependencies for other agents -9. Never modify `.agents/` files +4. Dispose controllers / cancel structured tasks properly +5. Transport client with interceptors (Dio / axios / generated Client) + repository-layer response cache, offline-first architecture +6. Secrets in secure storage only — never plain prefs or MMKV +7. 60fps target performance +8. Write widget/component tests and integration tests +9. ARB-based localization: edit ARB source files only, never generated localization code +10. Document out-of-scope dependencies for other agents +11. Never modify `.agents/` files (SSOT) — run outputs under `.agents/results/` and `.agents/state/memories/` are the only exceptions diff --git a/.agents/agents/pm-planner.md b/.agents/agents/pm-planner.md index bc08426d..87674c8b 100644 --- a/.agents/agents/pm-planner.md +++ b/.agents/agents/pm-planner.md @@ -38,8 +38,8 @@ CHARTER_CHECK: 1. **Gather**: Requirements (users, features, constraints, deployment target) 2. **Analyze**: Technical feasibility using codebase analysis 3. **Contracts**: Define API contracts using template `.agents/skills/_shared/core/api-contracts/template.md`; save the generated contract to `.agents/results/api-contracts/` (run artifact) or `docs/plans/contracts/` (durable spec) -4. **Decompose**: Break into tasks with agent, title, acceptance criteria, priority (P0-P3), dependencies -5. **Output**: Save to `.agents/results/plan-{sessionId}.json` +4. **Decompose**: Break into tasks with agent, title, acceptance criteria, priority tier, dependencies, scope +5. **Output**: Save to `.agents/results/plan-{sessionId}.json` (manual non-orchestrated runs: `plan.json`) ## Task Format @@ -47,8 +47,9 @@ Each task must include: - `agent`: assigned domain agent - `title`: what to do - `acceptance_criteria`: testable conditions -- `priority`: P0 (critical) to P3 (nice-to-have) +- `priority`: execution tier — 1 = independent (runs first), 2 = depends on tier 1, etc. (lower runs first) - `dependencies`: task IDs that must complete first +- `scope`: directory prefixes this task's agent may modify (used to detect boundary violations in parallel runs) ## Rules @@ -57,4 +58,4 @@ Each task must include: 3. Minimize dependencies for maximum parallelism 4. Security and testing are part of every task (not separate) 5. Each task completable by a single agent -6. Never modify `.agents/` files +6. Never modify `.agents/` files (SSOT) — run outputs under `.agents/results/` and `.agents/state/memories/` are the only exceptions diff --git a/.agents/agents/qa-reviewer.md b/.agents/agents/qa-reviewer.md index 88289c39..6989d63e 100644 --- a/.agents/agents/qa-reviewer.md +++ b/.agents/agents/qa-reviewer.md @@ -27,6 +27,10 @@ CHARTER_CHECK: - Must NOT do: modify source code, skip severity levels, report unverified findings - Success criteria: {all files reviewed, findings with file:line references} ``` + +- LOW: proceed with assumptions +- MEDIUM: list options, proceed with most likely +- HIGH: set status blocked, list questions, DO NOT start the review ## Review Priority Order @@ -60,11 +64,11 @@ Report findings with severity levels: 1. Every finding: file:line, description, fix 2. Severity: CRITICAL, HIGH, MEDIUM, LOW -3. Run automated tools first (`npm audit`, lint, type-check) +3. Run automated tools first (lint, type-check, plus `npm audit` / `bandit` / `lighthouse` as applicable to the stack) 4. No false positives — verify each finding 5. Provide remediation code, not just descriptions 6. PASS verdict: zero CRITICAL, HIGH, and MEDIUM issues 7. WARNING verdict: zero CRITICAL and HIGH, but MEDIUM issues exist 8. FAIL verdict: any CRITICAL or HIGH issue found 9. Never modify source code — review only -10. Never modify `.agents/` files +10. Never modify `.agents/` files (SSOT) — run outputs under `.agents/results/` and `.agents/state/memories/` are the only exceptions diff --git a/.agents/agents/refactor-engineer.md b/.agents/agents/refactor-engineer.md index c33e0664..6f117d11 100644 --- a/.agents/agents/refactor-engineer.md +++ b/.agents/agents/refactor-engineer.md @@ -52,4 +52,4 @@ CHARTER_CHECK: 6. Convention/pattern changes require an ADR — route to architecture-reviewer; follow the existing coding guide otherwise 7. Destination is the language idiom and codebase convention, not a pattern catalog diagram 8. Document out-of-scope findings for other agents -9. Never modify `.agents/` files +9. Never modify `.agents/` files (SSOT) — run outputs under `.agents/results/` and `.agents/state/memories/` are the only exceptions diff --git a/.agents/agents/research-explorer.md b/.agents/agents/research-explorer.md index 53aa3abe..9b06cb37 100644 --- a/.agents/agents/research-explorer.md +++ b/.agents/agents/research-explorer.md @@ -61,4 +61,4 @@ CHARTER_CHECK: 6. Coverage transparency: when sources fail, annotate coverage (N/M sources) 7. Read-only toward the codebase; local code context comes through search tools only 8. Document out-of-scope findings for other agents -9. Never modify `.agents/` files +9. Never modify `.agents/` files (SSOT) — run outputs under `.agents/results/` and `.agents/state/memories/` are the only exceptions diff --git a/.agents/agents/tf-infra-engineer.md b/.agents/agents/tf-infra-engineer.md index 88734a22..f6f9fdce 100644 --- a/.agents/agents/tf-infra-engineer.md +++ b/.agents/agents/tf-infra-engineer.md @@ -42,4 +42,4 @@ CHARTER_CHECK: 5. Do not hardcode secrets in `.tf` files or examples 6. Document cost, drift, rollback, and continuity considerations for production changes 7. Never run destructive operations without explicit user approval -8. Never modify `.agents/` files +8. Never modify `.agents/` files (SSOT) — run outputs under `.agents/results/` and `.agents/state/memories/` are the only exceptions diff --git a/.agents/agents/variants/claude.json b/.agents/agents/variants/claude.json index 59cd1096..dd3f8ffd 100644 --- a/.agents/agents/variants/claude.json +++ b/.agents/agents/variants/claude.json @@ -41,6 +41,9 @@ "docs-curator": { "tools": "Read, Write, Edit, Bash, Grep, Glob", "maxTurns": 15 + }, + "research-explorer": { + "tools": "Read, Write, Edit, Bash, Grep, Glob, WebSearch, WebFetch" } } } diff --git a/.agents/hooks/core/agentmemory-client.ts b/.agents/hooks/core/agentmemory-client.ts index f81ecb09..50962bd6 100644 --- a/.agents/hooks/core/agentmemory-client.ts +++ b/.agents/hooks/core/agentmemory-client.ts @@ -6,10 +6,6 @@ import https from "node:https"; import { homedir } from "node:os"; import { basename, join } from "node:path"; -// AgentMemory's published version line moved from 0.11/0.12 (original design -// target) to 0.9.x service builds; accept 0.9.x and the 0.1x.x range. -const SUPPORTED = /^0\.(9|1\d)\./; - function endpointUrl(): string | null { if (process.env.OMA_NO_AGENTMEMORY === "1") return null; if (process.env.AGENTMEMORY_URL) return process.env.AGENTMEMORY_URL; @@ -106,36 +102,12 @@ export async function isAgentMemoryReachable(): Promise { try { const response = await requestAgentMemory(url, "/agentmemory/health"); - if (response.statusCode < 200 || response.statusCode >= 300) { - reachable = false; - return reachable; - } - const headerVersion = response.headers["x-agentmemory-version"]; - const version = Array.isArray(headerVersion) - ? headerVersion[0] - : headerVersion; - // Recent AgentMemory releases expose the version only in the health body, - // not the `x-agentmemory-version` header. - let isAgentMemory = false; - let bodyVersion: string | undefined; - try { - const parsed = JSON.parse(response.body) as { - service?: unknown; - status?: unknown; - version?: unknown; - }; - isAgentMemory = - parsed.service === "agentmemory" || - parsed.status === "healthy" || - parsed.status === "ok"; - if (typeof parsed.version === "string") bodyVersion = parsed.version; - } catch { - // Non-JSON body — fall back to the header check below. - } - const resolvedVersion = version ?? bodyVersion; - reachable = - isAgentMemory || - (resolvedVersion !== undefined && SUPPORTED.test(resolvedVersion)); + // Capability-based acceptance: any 2xx health response from the + // explicitly configured endpoint counts as reachable. Version pinning + // proved brittle (the published line already jumped from 0.11/0.12 + // design targets to 0.9.x service builds), so payload shape and version + // are no longer gating. + reachable = response.statusCode >= 200 && response.statusCode < 300; return reachable; } catch { reachable = false; diff --git a/.agents/hooks/core/hook-output.ts b/.agents/hooks/core/hook-output.ts index 5fd76dcf..3a77cc2e 100644 --- a/.agents/hooks/core/hook-output.ts +++ b/.agents/hooks/core/hook-output.ts @@ -8,6 +8,11 @@ import type { Vendor } from "./types.ts"; export function makePromptOutput( vendor: Vendor, additionalContext: string, + // Native hook event the context is injected for. Defaults to the prompt-submit + // event; the dispatch layer passes "SessionStart" for session-start injection + // (commandcode / cursor sessionStart) so the emitted hookSpecificOutput names + // the correct event. Standalone core-script callers use the default. + hookEventName: string = "UserPromptSubmit", ): string { switch (vendor) { case "antigravity": @@ -19,33 +24,43 @@ export function makePromptOutput( injectSteps: [{ ephemeralMessage: additionalContext }], }); case "claude": - case "commandcode": + case "commandcode": { // Official Claude Code docs (code.claude.com/docs/en/hooks) specify // `hookSpecificOutput.additionalContext` — the top-level field is kept // for back-compat with older builds that read it. // commandcode (Command Code, commandcode.ai) mirrors the Claude hook - // dialect, but has NO prompt event (only PreToolUse/PostToolUse/Stop), - // so this branch never fires for it — kept for Vendor exhaustiveness. - return JSON.stringify({ + // dialect. It has no prompt-submit event, but DOES inject context on + // SessionStart (additionalContext) — dispatch passes hookEventName + // "SessionStart" for that path. + const hookSpecificOutput: Record = { + hookEventName, additionalContext, - hookSpecificOutput: { - hookEventName: "UserPromptSubmit", - additionalContext, - }, - }); + }; + // Claude Code re-scans skill/command directories after SessionStart hooks + // complete when the output sets `reloadSkills` (docs: SessionStart + // hookSpecificOutput.reloadSkills). It is Claude-only and only meaningful + // for SessionStart; this builder is called solely when context was + // actually injected, so the "only when injecting" condition is inherent. + if (vendor === "claude" && hookEventName === "SessionStart") { + hookSpecificOutput.reloadSkills = true; + } + return JSON.stringify({ additionalContext, hookSpecificOutput }); + } case "codex": return JSON.stringify({ hookSpecificOutput: { - hookEventName: "UserPromptSubmit", + hookEventName, additionalContext, }, }); case "cursor": + // Cursor reads the top-level `additional_context` (sessionStart) / + // `additionalContext`; the hookSpecificOutput block is informational. return JSON.stringify({ additionalContext, additional_context: additionalContext, hookSpecificOutput: { - hookEventName: "UserPromptSubmit", + hookEventName, additionalContext, }, }); @@ -70,7 +85,7 @@ export function makePromptOutput( // Qwen Code fork uses hookSpecificOutput (same as Codex) return JSON.stringify({ hookSpecificOutput: { - hookEventName: "UserPromptSubmit", + hookEventName, additionalContext, }, }); @@ -82,18 +97,27 @@ export function makeBlockOutput(vendor: Vendor, reason: string): string { case "claude": case "codex": case "commandcode": - case "cursor": case "kiro": case "qwen": return JSON.stringify({ decision: "block", reason }); + case "cursor": + // Cursor's `stop` hook ignores Claude-style `{decision:"block"}`. It + // re-enters the loop via `{followup_message}`, which is auto-submitted as + // the next turn (capped by the entry's loop_limit). Cursor's only + // block-producing chain is `stop` — its sole preToolUse handler + // (test-filter) never blocks, only mutates — so followup_message is + // always the correct dialect here. + return JSON.stringify({ followup_message: reason }); case "antigravity": // agy Stop: `decision:"continue"` re-enters the loop (= block the stop); // `reason` is injected as a system message. (Any other value allows stop.) return JSON.stringify({ decision: "continue", reason }); case "pi": - // pi has no stop-blocking event (agent_end is notification-only), so - // persistent-mode never runs under pi. This shape mirrors pi's native - // tool_call block return for completeness/forward-compat. + // pi's bridge implements persistent-mode via agent_settled + + // pi.sendUserMessage: it runs the persistent-mode subprocess (which + // resolves to the Claude dialect `{decision:"block", reason}`) and also + // accepts this `{block:true, reason}` shape, re-submitting `reason` as the + // next turn so the workflow continues. return JSON.stringify({ block: true, reason }); case "grok": // Grok Stop hooks are generally advisory. Emit block decision + rich @@ -116,6 +140,64 @@ export function makeBlockOutput(vendor: Vendor, reason: string): string { } } +/** + * Pre-tool DENY dialect — used when a PreToolUse handler blocks a tool call + * (scm-guard). Distinct from makeBlockOutput, whose dialects are Stop-shaped + * (e.g. cursor's followup_message re-submits a turn instead of denying). + * scm-guard is wired for claude/codex/qwen/kimi/kiro/grok/cursor (plus the + * opencode/pi bridges, which read the claude dialect from the standalone + * entry); the remaining cases are best-effort so the switch stays exhaustive. + */ +export function makePreToolDenyOutput(vendor: Vendor, reason: string): string { + switch (vendor) { + case "claude": + case "codex": + case "commandcode": + case "qwen": + // Claude-documented PreToolUse deny shape (Codex/Qwen follow the dialect). + return JSON.stringify({ + hookSpecificOutput: { + hookEventName: "PreToolUse", + permissionDecision: "deny", + permissionDecisionReason: reason, + }, + }); + case "kimi": + // Kimi honours both the Claude-style decision keys and the + // hookSpecificOutput deny form (same rationale as makeBlockOutput). + return JSON.stringify({ + decision: "block", + reason, + hookSpecificOutput: { + hookEventName: "PreToolUse", + permissionDecision: "deny", + permissionDecisionReason: reason, + }, + }); + case "kiro": + return JSON.stringify({ decision: "block", reason }); + case "grok": + // Grok PreToolUse output is a gate decision (see makePreToolOutput). + return JSON.stringify({ decision: "deny", reason }); + case "antigravity": + // agy PreToolUse output is a gate decision; deny analog of the allow gate. + return JSON.stringify({ decision: "deny", reason }); + case "cursor": + // Documented preToolUse output (cursor.com/docs/hooks): permission + // allow|deny, user_message shown in the client, agent_message sent to + // the agent. Cursor also accepts Claude's nested hookSpecificOutput + // form, but the native flat shape is authoritative. + return JSON.stringify({ + permission: "deny", + user_message: reason, + agent_message: reason, + }); + case "pi": + // pi's bridge accepts {block:true, reason} on tool_call interception. + return JSON.stringify({ block: true, reason }); + } +} + export function makePreToolOutput( vendor: Vendor, updatedInput: Record, diff --git a/.agents/hooks/core/keyword-detector.ts b/.agents/hooks/core/keyword-detector.ts index 223fcb15..e3368141 100644 --- a/.agents/hooks/core/keyword-detector.ts +++ b/.agents/hooks/core/keyword-detector.ts @@ -25,6 +25,7 @@ import { agyConversationId, isAgyInput, readAgyPrompt } from "./agy-input.ts"; import { UNKNOWN_SESSION_ID, VENDORS } from "./constants.ts"; import { clearGrokContext } from "./grok-context.ts"; import { makePromptOutput } from "./hook-output.ts"; +import { isRelayedAgentMessage, normalizePromptInput } from "./prompt-input.ts"; // triggers.json is imported statically: the bundler inlines it into the oma // binary (bundled `oma hook` path needs no file on disk), while a standalone // bun run resolves the sibling file next to this module (pi / direct run). @@ -154,6 +155,11 @@ export function isGenuineUserPrompt(input: Record): boolean { return true; } +// ── Guard: relayed inter-agent messages ────────────────────── +// Shared with skill-injector — see prompt-input.ts. Re-exported here so +// existing imports/tests keep resolving from this module. +export { isRelayedAgentMessage }; + // ── Guard 3: Reinforcement suppression ─────────────────────── const REINFORCEMENT_WINDOW_MS = 60_000; // 60 seconds @@ -349,24 +355,84 @@ export function escapeRegex(s: string): string { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } +/** + * Merge a language-keyed keyword/pattern bank into a single flat list: + * universal ("*") + English (the universal default) + the configured + * language's own entries (skipped when lang === "en" to avoid duplicates). + * Shared by buildPatterns and buildRawPatterns — both keyword banks and + * pattern banks use this exact `Record` shape. + */ +export function collectLangEntries( + bank: Record, + lang: string, +): string[] { + return [ + ...(bank["*"] ?? []), + ...(bank.en ?? []), + ...(lang !== "en" ? (bank[lang] ?? []) : []), + ]; +} + +/** + * Keyword-plus-compiled-regex pair. Kept 1:1 with the literal keyword string + * (as authored in triggers.json) so callers that need the ACTUAL matched + * keyword text — e.g. specificity ranking — don't have to re-derive it from + * match[0], which would require re-deriving buildPatterns' word-boundary + * peeling logic (fragile, and wrong for CJK where no boundary is added). + */ +export interface KeywordPatternEntry { + regex: RegExp; + keyword: string; +} + +export function buildPatternEntries( + keywords: Record, + lang: string, + cjkScripts: string[], +): KeywordPatternEntry[] { + return collectLangEntries(keywords, lang).map((kw) => { + const escaped = escapeRegex(kw).replace(/\s+/g, "\\s+"); + const regex = + cjkScripts.includes(lang) || /[^\p{ASCII}]/u.test(kw) + ? new RegExp(escaped, "i") + : new RegExp(`(?:^|[^\\w-])${escaped}(?:$|[^\\w-])`, "i"); + return { regex, keyword: kw }; + }); +} + export function buildPatterns( keywords: Record, lang: string, cjkScripts: string[], ): RegExp[] { - const allKeywords = [ - ...(keywords["*"] ?? []), - ...(keywords.en ?? []), - ...(lang !== "en" ? (keywords[lang] ?? []) : []), - ]; + return buildPatternEntries(keywords, lang, cjkScripts).map((e) => e.regex); +} - return allKeywords.map((kw) => { - const escaped = escapeRegex(kw).replace(/\s+/g, "\\s+"); - if (cjkScripts.includes(lang) || /[^\p{ASCII}]/u.test(kw)) { - return new RegExp(escaped, "i"); +/** + * Raw-pattern-plus-source pair — mirrors KeywordPatternEntry for the + * `patterns` (intent regex) field. `source` is the raw regex string itself: + * unlike keyword entries, a raw pattern has no fixed "keyword" — its + * specificity is however much text it actually matched (match[0]). + */ +export interface RawPatternEntry { + regex: RegExp; + source: string; +} + +export function buildRawPatternEntries( + patterns: Record | undefined, + lang: string, +): RawPatternEntry[] { + if (!patterns) return []; + const compiled: RawPatternEntry[] = []; + for (const raw of collectLangEntries(patterns, lang)) { + try { + compiled.push({ regex: new RegExp(raw, "iu"), source: raw }); + } catch { + // Skip invalid regex — surfaces during config edit, not at runtime } - return new RegExp(`(?:^|[^\\w-])${escaped}(?:$|[^\\w-])`, "i"); - }); + } + return compiled; } /** @@ -379,21 +445,7 @@ export function buildRawPatterns( patterns: Record | undefined, lang: string, ): RegExp[] { - if (!patterns) return []; - const all = [ - ...(patterns["*"] ?? []), - ...(patterns.en ?? []), - ...(lang !== "en" ? (patterns[lang] ?? []) : []), - ]; - const compiled: RegExp[] = []; - for (const raw of all) { - try { - compiled.push(new RegExp(raw, "iu")); - } catch { - // Skip invalid regex — surfaces during config edit, not at runtime - } - } - return compiled; + return buildRawPatternEntries(patterns, lang).map((e) => e.regex); } export function buildInformationalPatterns(config: TriggerConfig): RegExp[] { @@ -752,6 +804,93 @@ export function deactivateAllPersistentModes( } } +// ── Specificity ranking ─────────────────────────────────────── + +/** + * One surviving pattern match for one workflow, carrying everything the + * ranking rules in `pickWinningCandidate` need. Built once per successful + * `pattern.exec()` across ALL workflows in `config.workflows` — the matching + * loop no longer returns on the first hit; it collects every candidate + * first and lets specificity decide the winner. + */ +export interface WorkflowCandidate { + workflow: string; + persistent: boolean; + /** Index of the match within the cleaned (stripped/normalized) text. */ + matchIndex: number; + matchText: string; + /** Position re-located in the ORIGINAL prompt — used for tie-break #3. */ + origIndex: number; + /** Length of the specific text that matched (trimmed keyword or, for a + * `patterns` intent-regex hit, the trimmed match[0] span) — rule #1. */ + keywordLength: number; + /** Whether the specificity text contains whitespace — rule #2. */ + isMultiWord: boolean; + /** Index of this workflow in triggers.json `workflows` — rule #4 (final + * tiebreak, preserves the pre-ranking first-declared-wins behavior). */ + declarationIndex: number; + /** True if any suppression filter (RC3 technical-reference is a hard drop + * and never reaches this point; informational-context / pasted-content / + * reinforcement) applies to this specific match. */ + suppressed: boolean; +} + +/** + * Pick the winning candidate among all workflow matches collected for a + * prompt, or `null` if none survive. + * + * Ranking order (only consulted on a tie with the previous rule): + * 1. Longest matched keyword/phrase wins — "deepsec pr review" (18 chars) + * beats "review" (6 chars) even though "review" also literally matches + * as a substring of the same sentence. + * 2. A multi-word/compound match beats a single-word match of equal + * length (defensive tiebreak; rule 1 already separates most real + * cases since compound phrases are almost always longer). + * 3. Earliest match position in the ORIGINAL prompt wins — mirrors the + * existing "keyword near the front = command position" heuristic used + * elsewhere in this file (RC2 pasted-content guard). + * 4. Final tiebreak: declaration order in triggers.json `workflows` — + * i.e. the original pre-ranking first-match-wins behavior, kept only + * as a last resort when two workflows are otherwise indistinguishable. + * + * DESIGN DECISION (round-1 trigger demotion, part A): suppression is + * evaluated PER CANDIDATE, not globally. A candidate flagged `suppressed` + * (by the informational-context window, the persistent-mode pasted-content + * limit, or reinforcement) is simply removed from the ranking pool — it + * does NOT veto a *different*, unsuppressed candidate from a more generic + * workflow. Reasoning: every existing suppression filter in this file + * already operates locally, on one match at a time (a suppressed hit in one + * workflow's pattern loop has always fallen through to the next + * pattern/workflow, never blocking unrelated matches elsewhere in the same + * prompt) — ranking preserves that locality instead of upgrading it into a + * prompt-wide veto. The alternative (any suppressed specific match blocks + * the whole prompt from firing anything) would silence genuine, independent + * requests that merely share a sentence with a meta-mention of a more + * specific workflow. See triggers-corpus.json for a case that locks this in: + * a prompt that asks an informational "what is X" question about a specific + * workflow AND, separately, makes a genuine generic request — the generic + * request still fires. + */ +export function pickWinningCandidate( + candidates: WorkflowCandidate[], +): WorkflowCandidate | null { + const eligible = candidates.filter((c) => !c.suppressed); + if (eligible.length === 0) return null; + eligible.sort((a, b) => { + if (b.keywordLength !== a.keywordLength) { + return b.keywordLength - a.keywordLength; + } + if (a.isMultiWord !== b.isMultiWord) { + return a.isMultiWord ? -1 : 1; + } + if (a.origIndex !== b.origIndex) { + return a.origIndex - b.origIndex; + } + return a.declarationIndex - b.declarationIndex; + }); + return eligible[0] ?? null; +} + // ── Pure handler (canonical ABI) ───────────────────────────── /** @@ -778,6 +917,9 @@ export async function run( if (!prompt.trim()) return null; if (startsWithSlashCommand(prompt)) return null; + // Relayed inter-agent messages carry another agent's text, not a user + // request — their content must not drive workflow keyword detection. + if (isRelayedAgentMessage(prompt)) return null; const config = loadConfig(); const lang = detectLanguage(projectDir); @@ -804,73 +946,125 @@ export async function run( // Skip persistent workflows entirely if the prompt is an analytical question const analytical = isAnalyticalQuestion(cleaned); - for (const [workflow, def] of Object.entries(config.workflows)) { + // shouldSkipAllWorkflows does not depend on the workflow being evaluated — + // hoisted out of the loop (was re-checked on every iteration pre-ranking). + if (shouldSkipAllWorkflows(cleaned)) return null; + + // Position guard must reflect the user's ACTUAL prompt, not the + // content-stripped text. stripCodeBlocks/stripSystemEchoes remove quoted + // and code spans, which shrinks the text and pulls keywords toward the + // front — defeating the "deep in a long prompt = not an instruction" + // heuristic (a keyword genuinely at char 245 of a discussion can appear + // at char 179 after stripping, slipping under PERSISTENT_MATCH_LIMIT). + const origPrompt = normalizeForMatching(prompt); + + // Collect every surviving match across every workflow first — specificity + // ranking (see pickWinningCandidate) decides the winner, replacing the old + // declaration-order first-match-wins loop. + const candidates: WorkflowCandidate[] = []; + const workflowEntries = Object.entries(config.workflows); + + for ( + let declarationIndex = 0; + declarationIndex < workflowEntries.length; + declarationIndex++ + ) { + const entry = workflowEntries[declarationIndex]; + if (!entry) continue; + const [workflow, def] = entry; if (excluded.has(workflow)) continue; - if (shouldSkipAllWorkflows(cleaned)) continue; const workflowPredicate = KEYWORD_SKIP_PREDICATES[workflow]; if (workflowPredicate?.(cleaned)) continue; if (analytical && def.persistent) continue; - const patterns = [ - ...buildPatterns(def.keywords, lang, config.cjkScripts), - ...buildRawPatterns(def.patterns, lang), - ]; + const reinforced = isReinforcementSuppressed(kwState, workflow); - for (const pattern of patterns) { - const match = pattern.exec(cleaned); - if (!match) continue; + const considerMatch = (regex: RegExp, specificityText: string) => { + const match = regex.exec(cleaned); + if (!match) return; // RC3: compound technical tokens (ralph:verify, ralph.md, // workflows/ralph) reference the workflow as an artifact, not a run - // request. - if (isTechnicalReference(cleaned, match.index, match[0])) continue; - if (isInformationalContext(cleaned, match.index, infoPatterns)) continue; - // Position guard must reflect the user's ACTUAL prompt, not the - // content-stripped text. stripCodeBlocks/stripSystemEchoes remove quoted - // and code spans, which shrinks the text and pulls keywords toward the - // front — defeating the "deep in a long prompt = not an instruction" - // heuristic (a keyword genuinely at char 245 of a discussion can appear - // at char 179 after stripping, slipping under PERSISTENT_MATCH_LIMIT). - // Re-locate the matched keyword in the original prompt for the check. - const origPrompt = normalizeForMatching(prompt); + // request — dropped entirely, never becomes a candidate. + if (isTechnicalReference(cleaned, match.index, match[0])) return; + + // Re-locate the matched keyword in the original prompt for the + // pasted-content position guard. const origIndex = origPrompt.indexOf(match[0]); const posIndex = origIndex >= 0 ? origIndex : match.index; - if (isPastedContent(posIndex, def.persistent, origPrompt.length)) - continue; - if (isReinforcementSuppressed(kwState, workflow)) continue; + const informational = isInformationalContext( + cleaned, + match.index, + infoPatterns, + ); + const pasted = isPastedContent( + posIndex, + def.persistent, + origPrompt.length, + ); + const text = specificityText.trim(); + + candidates.push({ + workflow, + persistent: def.persistent, + matchIndex: match.index, + matchText: match[0], + origIndex: posIndex, + keywordLength: text.length, + isMultiWord: /\s/.test(text), + declarationIndex, + suppressed: informational || pasted || reinforced, + }); + }; + + for (const { regex, keyword } of buildPatternEntries( + def.keywords, + lang, + config.cjkScripts, + )) { + considerMatch(regex, keyword); + } + for (const { regex, source } of buildRawPatternEntries( + def.patterns, + lang, + )) { + considerMatch(regex, source); + } + } - if (def.persistent) { - activateMode(projectDir, workflow, sessionId); - } - await activateL1WorkflowSession(projectDir, workflow, vendor, sessionId); - const updatedState = recordKwTrigger(kwState, workflow); - saveKwState(projectDir, updatedState); - - const contextLines = [ - `[OMA WORKFLOW: ${workflow.toUpperCase()}]`, - `User intent matches the /${workflow} workflow.`, - `Read and follow \`.agents/workflows/${workflow}.md\` step by step.`, - `User request: ${prompt}`, - `IMPORTANT: Start the workflow IMMEDIATELY. Do not ask for confirmation.`, - ]; - - if (config.extensionRouting) { - const extensions = detectExtensions(prompt); - const agent = resolveAgentFromExtensions( - extensions, - config.extensionRouting, - ); - if (agent) { - contextLines.push(`[OMA AGENT HINT: ${agent}]`); - } - } + const winner = pickWinningCandidate(candidates); + if (!winner) return null; + + const { workflow } = winner; + + if (winner.persistent) { + activateMode(projectDir, workflow, sessionId); + } + await activateL1WorkflowSession(projectDir, workflow, vendor, sessionId); + const updatedState = recordKwTrigger(kwState, workflow); + saveKwState(projectDir, updatedState); + + const contextLines = [ + `[OMA WORKFLOW: ${workflow.toUpperCase()}]`, + `User intent matches the /${workflow} workflow.`, + `Read and follow \`.agents/workflows/${workflow}.md\` step by step.`, + `User request: ${prompt}`, + `IMPORTANT: Start the workflow IMMEDIATELY. Do not ask for confirmation.`, + ]; - return { type: "context", additionalContext: contextLines.join("\n") }; + if (config.extensionRouting) { + const extensions = detectExtensions(prompt); + const agent = resolveAgentFromExtensions( + extensions, + config.extensionRouting, + ); + if (agent) { + contextLines.push(`[OMA AGENT HINT: ${agent}]`); } } - return null; + return { type: "context", additionalContext: contextLines.join("\n") }; } // ── Standalone entry (pi subprocess / direct bun invocation) ── @@ -890,7 +1084,7 @@ async function main() { const vendor = detectVendor(input); const projectDir = getProjectDir(vendor, input); const sessionId = getSessionId(input); - let prompt = (input.prompt as string) ?? ""; + let prompt = normalizePromptInput(input.prompt); // agy's PreInvocation stdin carries no `prompt` — recover the user request // from the transcript. PreInvocation fires before every model call, so only diff --git a/.agents/hooks/core/prompt-input.ts b/.agents/hooks/core/prompt-input.ts new file mode 100644 index 00000000..7f185805 --- /dev/null +++ b/.agents/hooks/core/prompt-input.ts @@ -0,0 +1,45 @@ +// Shared normalizer for the raw `prompt` field delivered on hook stdin. +// +// Most host CLIs deliver `prompt` as a plain string, but some (e.g. Kimi Code +// CLI's UserPromptSubmit payload) deliver it as a ContentPart[] array such as +// `[{ "type": "text", "text": "hello" }]`. The standalone hook scripts cast it +// with `(input.prompt as string) ?? ""`, so on those vendors the downstream +// string methods throw and the top-level catch swallows the error — the entire +// prompt chain silently no-ops. Route every raw read through this helper so the +// array shape collapses to the equivalent string. + +// Relayed inter-agent messages (teammate reports, task notifications, idle +// notifications) are transported through the prompt channel but are not user +// intent: their content routinely carries workflow keywords and skill trigger +// words, producing false activations. Detect the transport envelopes +// conservatively so prompt-driven handlers can skip them. +const RELAY_ENVELOPE_PREFIXES = [ + " trimmed.startsWith(p))) return true; + return trimmed.slice(0, 200).includes('"type":"idle_notification"'); +} + +/** Coerce a raw stdin `prompt` field to a string across vendor payload shapes. */ +export function normalizePromptInput(prompt: unknown): string { + if (typeof prompt === "string") return prompt; + if (Array.isArray(prompt)) { + return prompt + .filter( + (p): p is { type: string; text: string } => + !!p && + typeof p === "object" && + (p as { type?: unknown }).type === "text" && + typeof (p as { text?: unknown }).text === "string", + ) + .map((p) => p.text) + .join(" "); + } + return ""; +} diff --git a/.agents/hooks/core/scm-guard.ts b/.agents/hooks/core/scm-guard.ts new file mode 100644 index 00000000..fc07f24c --- /dev/null +++ b/.agents/hooks/core/scm-guard.ts @@ -0,0 +1,250 @@ +// PreToolUse hook — Block staging of likely-secret files (oma-scm enforcement). +// Enforces `forbidden_patterns` from oma-scm's commit-config.yaml mechanically: +// before this hook the pattern list was advisory-only (model compliance). +// Works with: Claude Code, Codex CLI, Cursor, Grok, Kimi, Kiro, Qwen Code. +// +// Scope decisions: +// - Only `git add` segments are guarded. Untracked secrets must pass through +// `git add` to ever reach a commit (`git commit ` / `-a` only touch +// already-tracked files), so staging is the single choke point. +// - Broad staging (`git add -A` / `git add .`) is NOT blocked here: the rule +// ("never without explicit permission") depends on user consent the hook +// cannot observe. It stays an agent-level guardrail. +// - Escape hatch: a command containing `OMA_SCM_ALLOW_SECRETS=1` bypasses the +// guard, mirroring SKILL.md Guardrail 0 (explicit user override — surface +// once, then proceed). + +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { makePreToolDenyOutput } from "./hook-output.ts"; +import type { HandlerCtx, HandlerResult, HookInput, Vendor } from "./types.ts"; +import { getProjectDir } from "./vendor-detect.ts"; + +// --- Defaults (mirror .agents/skills/oma-scm/config/commit-config.yaml) --- +// Used when the config file is absent (global installs without project config, +// or repos that removed the skill). Keep in sync with the yaml SSOT. + +const DEFAULT_FORBIDDEN_PATTERNS = [ + "*.env", + "*.env.*", + "credentials.json", + "secrets.yaml", + "*.pem", + "*.key", + ".env.local", + "*.p12", + "*.pfx", + "id_rsa*", + "id_ed25519*", + ".npmrc", + "service-account*.json", + "*.keystore", + "*.jks", + "*.tfvars", + "*.tfstate", + "*.tfstate.*", + ".netrc", + ".pypirc", +]; + +const DEFAULT_ALLOWED_EXCEPTIONS = ["*.example", "*.sample", "*.template"]; + +const BYPASS_TOKEN = "OMA_SCM_ALLOW_SECRETS=1"; + +const CONFIG_RELPATH = join( + ".agents", + "skills", + "oma-scm", + "config", + "commit-config.yaml", +); + +// --- Config loading (regex-based, consistent with core's yaml handling) --- + +/** + * Extract a top-level yaml string-list section (`key:` followed by `- "item"` + * lines) without a yaml dependency — core handlers must stay standalone. + */ +export function extractYamlList(content: string, key: string): string[] | null { + const lines = content.split(/\r?\n/); + const start = lines.findIndex((l) => new RegExp(`^${key}:\\s*$`).test(l)); + if (start === -1) return null; + const items: string[] = []; + for (let i = start + 1; i < lines.length; i++) { + const line = lines[i] ?? ""; + if (/^\s*(#|$)/.test(line)) continue; // comments / blanks inside the list + const item = line.match(/^\s+-\s+["']?([^"']+)["']?\s*$/)?.[1]; + if (!item) break; // end of the list block + items.push(item); + } + return items; +} + +interface GuardConfig { + forbidden: string[]; + exceptions: string[]; +} + +function loadGuardConfig(projectDir: string): GuardConfig { + const configPath = join(projectDir, CONFIG_RELPATH); + if (!existsSync(configPath)) { + return { + forbidden: DEFAULT_FORBIDDEN_PATTERNS, + exceptions: DEFAULT_ALLOWED_EXCEPTIONS, + }; + } + try { + const content = readFileSync(configPath, "utf-8"); + return { + forbidden: + extractYamlList(content, "forbidden_patterns") ?? + DEFAULT_FORBIDDEN_PATTERNS, + exceptions: + extractYamlList(content, "allowed_exceptions") ?? + DEFAULT_ALLOWED_EXCEPTIONS, + }; + } catch { + return { + forbidden: DEFAULT_FORBIDDEN_PATTERNS, + exceptions: DEFAULT_ALLOWED_EXCEPTIONS, + }; + } +} + +// --- Glob matching (basename semantics, like .gitignore basename patterns) --- + +function globToRegExp(pattern: string): RegExp { + const escaped = pattern + .replace(/[.+^${}()|[\]\\]/g, "\\$&") + .replace(/\*/g, "[^/]*") + .replace(/\?/g, "[^/]"); + return new RegExp(`^${escaped}$`); +} + +function basename(path: string): string { + const trimmed = path.replace(/\/+$/, ""); + const idx = trimmed.lastIndexOf("/"); + return idx === -1 ? trimmed : trimmed.slice(idx + 1); +} + +export function matchesForbidden(path: string, config: GuardConfig): boolean { + const name = basename(path); + if (!name) return false; + if (config.exceptions.some((p) => globToRegExp(p).test(name))) return false; + return config.forbidden.some((p) => globToRegExp(p).test(name)); +} + +// --- Command parsing --- + +/** + * Extract candidate pathspec tokens from every `git add` segment of a shell + * command. Naive quoting-aware-ish tokenizer: segments split on shell control + * operators; option tokens (`-…`) and the `--` separator are skipped. + */ +export function extractGitAddPaths(command: string): string[] { + const paths: string[] = []; + const segments = command.split(/&&|\|\||;|\||\n/); + for (const segment of segments) { + const m = segment.match(/\bgit\s+(?:[-\w]+=\S+\s+)*add\b(.*)$/); + if (!m) continue; + const rest = m[1] ?? ""; + // Tokenize, honoring simple single/double quotes. + const tokens = rest.match(/"[^"]*"|'[^']*'|\S+/g) ?? []; + for (const raw of tokens) { + const token = raw.replace(/^["']|["']$/g, ""); + if (token === "--") continue; + if (token.startsWith("-")) continue; // options (incl. -A/--all; see header) + paths.push(token); + } + } + return paths; +} + +// ── Pure handler (canonical ABI) ───────────────────────────── + +/** + * Pure decision function — blocks `git add` of likely-secret files. + * Returns a `block` HandlerResult when a staged path matches + * `forbidden_patterns` (minus `allowed_exceptions`), else `null` (fail-open). + */ +export async function run( + input: HookInput, + _ctx: HandlerCtx, +): Promise { + if (input.kind !== "pre_tool") return null; + + const { toolName, toolInput, cwd: projectDir } = input; + + if ( + toolName !== "Bash" && + toolName !== "run_shell_command" && + toolName !== "Shell" && + toolName !== "execute_bash" + ) + return null; + + const command = toolInput.command as string | undefined; + if (!command) return null; + if (!/\bgit\b/.test(command)) return null; + if (command.includes(BYPASS_TOKEN)) return null; + + const candidates = extractGitAddPaths(command); + if (candidates.length === 0) return null; + + const config = loadGuardConfig(projectDir); + const flagged = candidates.filter((p) => matchesForbidden(p, config)); + if (flagged.length === 0) return null; + + return { + type: "block", + reason: + `[oma scm-guard] Blocked: staging likely-secret file(s): ${flagged.join(", ")} ` + + `(matched forbidden_patterns in ${CONFIG_RELPATH}). ` + + `Surface this to the user; if they explicitly approve committing these files, ` + + `re-run the command prefixed with ${BYPASS_TOKEN}.`, + }; +} + +// ── Standalone entry (pi subprocess / direct bun invocation) ── + +interface PreToolUseInput { + tool_name: string; + tool_input: { + command?: string; + [key: string]: unknown; + }; + [key: string]: unknown; +} + +function main() { + const inputFile = process.env.OMA_HOOK_INPUT_FILE; + const raw = inputFile + ? readFileSync(inputFile, "utf-8") + : readFileSync(0, "utf-8"); + if (!raw.trim()) process.exit(0); + + const parsed: PreToolUseInput = JSON.parse(raw); + // Standalone path is vendor-agnostic here; claude covers the common dialect. + const vendor: Vendor = "claude"; + const projectDir = getProjectDir(vendor, parsed); + + const hookInput: HookInput = { + kind: "pre_tool", + toolName: parsed.tool_name, + toolInput: { ...(parsed.tool_input ?? {}) }, + cwd: projectDir, + }; + + run(hookInput, { vendor, cwd: projectDir }) + .then((result) => { + if (result && result.type === "block") { + console.log(makePreToolDenyOutput(vendor, result.reason)); + } + process.exit(0); + }) + .catch(() => process.exit(0)); +} + +if (import.meta.main) { + main(); +} diff --git a/.agents/hooks/core/serena-primer.ts b/.agents/hooks/core/serena-primer.ts index 6605ac2e..df2aabc4 100644 --- a/.agents/hooks/core/serena-primer.ts +++ b/.agents/hooks/core/serena-primer.ts @@ -25,6 +25,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { agyConversationId, isAgyInput, readAgyPrompt } from "./agy-input.ts"; import { makePromptOutput } from "./hook-output.ts"; +import { normalizePromptInput } from "./prompt-input.ts"; import type { HandlerCtx, HandlerResult, HookInput, Vendor } from "./types.ts"; import { getProjectDir, inferVendorFromScriptPath } from "./vendor-detect.ts"; @@ -136,7 +137,10 @@ export async function run( const { cwd: projectDir, sid: sessionId = "unknown" } = ctx; if (!isSerenaProject(projectDir)) return null; - if (!claimSession(projectDir, sessionId)) return null; + // Compaction keeps the session id, so the session-once claim would skip + // exactly the turn that just lost the primer from context — force re-inject. + const forced = input.source === "compact"; + if (!claimSession(projectDir, sessionId) && !forced) return null; return { type: "context", additionalContext: primerContext() }; } @@ -190,7 +194,7 @@ async function main() { const vendor = detectVendor(input); const projectDir = getProjectDir(vendor, input); const sessionId = getSessionId(input); - let prompt = (input.prompt as string) ?? ""; + let prompt = normalizePromptInput(input.prompt); // agy's PreInvocation stdin carries no `prompt`; recover it and only act on // the first invocation of a turn. diff --git a/.agents/hooks/core/skill-injector.ts b/.agents/hooks/core/skill-injector.ts index 550fbd88..66c8139f 100644 --- a/.agents/hooks/core/skill-injector.ts +++ b/.agents/hooks/core/skill-injector.ts @@ -24,6 +24,7 @@ import { basename, dirname, join } from "node:path"; import { agyConversationId, isAgyInput, readAgyPrompt } from "./agy-input.ts"; import { toPosixPath } from "./fs-utils.ts"; import { makePromptOutput } from "./hook-output.ts"; +import { isRelayedAgentMessage, normalizePromptInput } from "./prompt-input.ts"; // triggers.json is imported statically: bundler inlines it into the oma binary; // standalone bun runs resolve the sibling file (pi / direct run). import embeddedTriggers from "./triggers.json" with { type: "json" }; @@ -463,6 +464,11 @@ export async function run( if (!prompt.trim()) return null; + // Relayed inter-agent messages (teammate reports, idle notifications) are + // not user intent — their content routinely contains skill trigger words + // and produced false skill suggestions. Same guard as keyword-detector. + if (isRelayedAgentMessage(prompt)) return null; + // Claude-specific: slash-skill resolution must run BEFORE the slash early-exit // and persistent-workflow guard (same order as the original standalone path). if (vendor === "claude") { @@ -514,7 +520,7 @@ async function main() { const vendor = detectVendor(input); const projectDir = getProjectDir(vendor, input); const sessionId = getSessionId(input); - let prompt = (input.prompt as string) ?? ""; + let prompt = normalizePromptInput(input.prompt); // agy's PreInvocation stdin carries no `prompt`; recover it from the // transcript, and only act on the first invocation of a turn. diff --git a/.agents/hooks/core/state-boundary.ts b/.agents/hooks/core/state-boundary.ts index 8700b102..ddc1bd00 100644 --- a/.agents/hooks/core/state-boundary.ts +++ b/.agents/hooks/core/state-boundary.ts @@ -6,6 +6,7 @@ import { agyConversationId, isAgyInput } from "./agy-input.ts"; import { syncGrokContext } from "./grok-context.ts"; import { makePromptOutput } from "./hook-output.ts"; import { writeInjectLog } from "./inject-log.ts"; +import { normalizePromptInput } from "./prompt-input.ts"; import { emitEvent, type OmaEvent, readEvents } from "./state-emit.ts"; import { getActiveSid, readIndex, setLastSession } from "./state-marker.ts"; import type { HandlerCtx, HandlerResult, HookInput, Vendor } from "./types.ts"; @@ -98,6 +99,7 @@ export async function onBoundary( vendor: Vendor, vendorSid: string, promptText?: string, + forced = false, ): Promise { const idx = readIndex(projectDir); const previous = idx.lastSession; @@ -105,7 +107,9 @@ export async function onBoundary( !previous || previous.vendor !== vendor || previous.vendorSid !== vendorSid; const statelessTurnFlush = vendor === "kiro" && vendorSid === "unknown"; - if (!boundary && !statelessTurnFlush) { + // `forced` = post-compaction SessionStart: the session id is unchanged (no + // boundary), but the snapshot was just compacted out of context — re-emit. + if (!boundary && !statelessTurnFlush && !forced) { setLastSession(projectDir, vendor, vendorSid); return null; } @@ -122,7 +126,9 @@ export async function onBoundary( vendorSid, payload: { reason: !boundary - ? "stateless-vendor-turn" + ? statelessTurnFlush + ? "stateless-vendor-turn" + : "post-compact-rehydration" : previous ? "vendor-session-transition" : "session-created", @@ -189,12 +195,15 @@ export async function run( const { vendor, cwd: projectDir, sid: vendorSid = "unknown" } = ctx; // input.kind === "prompt" is guaranteed by the guard above; the user prompt is - // the primary recall signal for boundary rehydration. + // the primary recall signal for boundary rehydration. A post-compaction + // SessionStart (source === "compact") forces re-emission: the session id is + // unchanged, but the snapshot was just compacted out of the context window. const rendered = await onBoundary( projectDir, vendor, vendorSid, input.prompt, + input.source === "compact", ); if (!rendered) return null; return { type: "context", additionalContext: rendered }; @@ -218,7 +227,7 @@ async function main() { // Delegate to run() — single logic source. const hookInput: HookInput = { kind: "prompt", - prompt: (input.prompt as string) ?? "", + prompt: normalizePromptInput(input.prompt), cwd: projectDir, }; const ctxVal: HandlerCtx = { vendor, cwd: projectDir, sid: vendorSid }; diff --git a/.agents/hooks/core/state-marker.ts b/.agents/hooks/core/state-marker.ts index 97ef1b28..e495ad6a 100644 --- a/.agents/hooks/core/state-marker.ts +++ b/.agents/hooks/core/state-marker.ts @@ -64,7 +64,9 @@ export function atomicWriteJson(path: string, value: unknown): void { ensureParent(path); const tmp = `${path}.${process.pid}.${Date.now()}.tmp`; writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, "utf-8"); - const fd = openSync(tmp, "r"); + // Open read-write ("r+"): fsyncSync on a read-only handle fails with EPERM + // on Windows, aborting the metadata write. See issue #613. + const fd = openSync(tmp, "r+"); try { fsyncSync(fd); } finally { diff --git a/.agents/hooks/core/test-filter.ts b/.agents/hooks/core/test-filter.ts index f6ee917f..1ab21637 100644 --- a/.agents/hooks/core/test-filter.ts +++ b/.agents/hooks/core/test-filter.ts @@ -103,24 +103,48 @@ export async function run( const { toolName, toolInput, cwd: projectDir } = input; const { vendor } = ctx; - // Claude-family uses Bash; some CLIs use run_shell_command. - if (toolName !== "Bash" && toolName !== "run_shell_command") return null; + // Claude-family uses Bash; some CLIs use run_shell_command; Cursor names its + // terminal tool "Shell" (matches cursor.json's preToolUse matcher); Kiro's + // canonical shell tool is execute_bash (the agent-JSON matcher name). + if ( + toolName !== "Bash" && + toolName !== "run_shell_command" && + toolName !== "Shell" && + toolName !== "execute_bash" + ) + return null; const command = toolInput.command as string | undefined; if (!command) return null; + // The rewrite below is Bash-only (`set -o pipefail`, subshell, pipe to + // bash). On Windows the host shell is PowerShell/cmd, which fails to parse + // it before the test runner even starts (#618). Losing the failure filter + // is acceptable; breaking `npm test` is not. + if (process.platform === "win32") return null; + + // Hook re-entry guard: a command already piping through the filter script + // must pass through unchanged, not get wrapped a second time (#618). + if (command.includes("filter-test-output.sh")) return null; + const isTestCommand = TEST_PATTERNS.some((p) => p.test(command)); if (!isTestCommand) return null; const isExcluded = EXCLUDE_PATTERNS.some((p) => p.test(command)); if (isExcluded) return null; - const filterScript = join( - projectDir, + // Resolve the filter script: vendor hook dir first, then the opencode + // bridge dir (opencode has no core Vendor identity — its subprocess payload + // detects as claude, whose hook dir is absent in opencode-only installs), + // then the SSOT core dir as the last resort. + const filterScript = [ getHookDir(vendor), - "filter-test-output.sh", - ); - if (!existsSync(filterScript)) return null; + join(".opencode", "plugins", "oma"), + join(".agents", "hooks", "core"), + ] + .map((dir) => join(projectDir, dir, "filter-test-output.sh")) + .find((p) => existsSync(p)); + if (!filterScript) return null; const filteredCmd = `set -o pipefail; (${command}) 2>&1 | bash "${filterScript}"`; const updatedInput: Record = { diff --git a/.agents/hooks/core/triggers.json b/.agents/hooks/core/triggers.json index 73c6a592..8e5bff82 100644 --- a/.agents/hooks/core/triggers.json +++ b/.agents/hooks/core/triggers.json @@ -830,13 +830,7 @@ "persistent": true, "keywords": { "*": ["work", "step by step"], - "en": [ - "one by one", - "guide me", - "walk me through", - "manual mode", - "one step at a time" - ], + "en": ["one by one", "one step at a time"], "ko": [ "단계별", "단계별로", @@ -845,83 +839,21 @@ "하나씩 해줘", "순서대로 해줘", "차근차근", - "차근차근 해줘", - "수동으로 해줘" - ], - "ja": [ - "ステップバイステップ", - "一歩ずつ", - "ガイドして", - "手動で", - "一つずつ", - "順番にやって", - "手順を教えて" - ], - "zh": [ - "逐步", - "一步一步", - "指导我", - "手动", - "一个一个", - "按顺序", - "带我做" - ], - "es": [ - "paso a paso", - "guíame", - "uno por uno", - "modo manual", - "de a uno", - "llévame paso a paso" - ], - "fr": [ - "étape par étape", - "guide-moi", - "un par un", - "mode manuel", - "accompagne-moi", - "montre-moi étape par étape" + "차근차근 해줘" ], + "ja": ["ステップバイステップ", "一歩ずつ", "一つずつ", "順番にやって"], + "zh": ["逐步", "一步一步", "一个一个", "按顺序"], + "es": ["paso a paso", "uno por uno", "de a uno"], + "fr": ["étape par étape", "un par un"], "de": [ "schritt für schritt", - "führe mich", "eins nach dem anderen", - "manueller modus", - "zeig mir wie", "der reihe nach" ], - "pt": [ - "passo a passo", - "me guie", - "um por um", - "modo manual", - "me acompanhe", - "me mostre passo a passo" - ], - "ru": [ - "шаг за шагом", - "направь меня", - "по одному", - "ручной режим", - "покажи по шагам", - "веди меня" - ], - "nl": [ - "stap voor stap", - "begeleid me", - "een voor een", - "handmatige modus", - "leid me erdoor", - "stap voor stap uitleg" - ], - "pl": [ - "krok po kroku", - "poprowadź mnie", - "jeden po drugim", - "tryb ręczny", - "przeprowadź mnie", - "pokaż krok po kroku" - ] + "pt": ["passo a passo", "um por um"], + "ru": ["шаг за шагом", "по одному"], + "nl": ["stap voor stap", "een voor een"], + "pl": ["krok po kroku", "jeden po drugim"] } }, "deepinit": { @@ -2601,14 +2533,32 @@ "rebase workflow", "git worktree" ], - "ko": ["머지 충돌 해결", "리베이스해줘", "워크트리 써줘"], + "ko": [ + "머지 충돌 해결", + "리베이스해줘", + "워크트리 써줘", + "브랜칭 전략", + "브랜치 전략 세워줘", + "릴리즈 태그 달아줘", + "체리픽해줘" + ], "ja": [ "マージ衝突解決", "リベースして", "リリースタグ", - "worktree使って" + "worktree使って", + "ブランチ戦略", + "チェリーピック", + "リリースタグ付けて" ], - "zh": ["解决合并冲突", "帮我 rebase", "打发布标签", "用 worktree"] + "zh": [ + "解决合并冲突", + "帮我 rebase", + "打发布标签", + "用 worktree", + "分支策略", + "帮我 cherry-pick" + ] } }, "oma-search": { @@ -3062,7 +3012,10 @@ "기술 부채", "클래스 분리", "메서드 추출", - "코드 정리해줘" + "코드 정리해줘", + "특성화 테스트", + "핫스팟", + "리팩토링 계획" ], "ja": [ "リファクタリング", @@ -3071,7 +3024,10 @@ "コードスメル", "技術的負債", "クラスを分割", - "メソッド抽出" + "メソッド抽出", + "特性化テスト", + "ホットスポット", + "リファクタリング計画" ], "zh": [ "重构", @@ -3080,7 +3036,10 @@ "代码异味", "技术债", "拆分类", - "提取方法" + "提取方法", + "特征化测试", + "热点", + "重构计划" ] } } @@ -3116,7 +3075,6 @@ "should you", "could we", "would you", - "what if", "what about", "why build", "why create", @@ -3127,7 +3085,10 @@ "뭐야", "뭐임", "무엇", - "어떻게", + "어떻게 동작", + "어떻게 작동", + "어떻게 되는지", + "어떻게 하는지", "설명해", "알려줘", "키워드", @@ -3143,13 +3104,12 @@ "왜 만들", "어떻게 만들", "어떨까", - "하면 좋을", + "하면 좋을지", "한다면", "할까요", "보강할", "에 대해", "에 대한", - "한번 봐", "깊게 봐", "코드를 한번", "그 워크플로우", @@ -3225,7 +3185,7 @@ "wykryto" ] }, - "excludedWorkflows": ["tools", "stack-set", "schedule"], + "excludedWorkflows": ["tools", "stack-set", "schedule", "convert"], "cjkScripts": ["ko", "ja", "zh"], "extensionRouting": { "frontend-engineer": [ diff --git a/.agents/hooks/core/types.ts b/.agents/hooks/core/types.ts index 5ae6a749..a58ffda1 100644 --- a/.agents/hooks/core/types.ts +++ b/.agents/hooks/core/types.ts @@ -54,7 +54,18 @@ export interface ModeState { * Discriminated on `kind`; produced by adapters.ts normalizeInput(). */ export type HookInput = - | { kind: "prompt"; prompt: string; cwd: string } + | { + kind: "prompt"; + prompt: string; + cwd: string; + /** + * SessionStart trigger source (claude: startup|resume|clear|compact). + * `compact` lets session-once handlers (serena-primer, state-boundary) + * force re-injection: compaction keeps the session id, so their normal + * dedup would otherwise skip exactly the turn that lost the context. + */ + source?: string; + } | { kind: "pre_tool"; toolName: string; diff --git a/.agents/hooks/variants/antigravity.json b/.agents/hooks/variants/antigravity.json index 0c9f5d65..f2331482 100644 --- a/.agents/hooks/variants/antigravity.json +++ b/.agents/hooks/variants/antigravity.json @@ -25,11 +25,6 @@ "timeout": 3 } ], - "PreToolUse": { - "hook": "test-filter.ts", - "matcher": "run_command", - "timeout": 5 - }, "Stop": { "hook": "persistent-mode.ts", "timeout": 5 diff --git a/.agents/hooks/variants/claude.json b/.agents/hooks/variants/claude.json index b9a8a41f..bbccd01c 100644 --- a/.agents/hooks/variants/claude.json +++ b/.agents/hooks/variants/claude.json @@ -6,6 +6,16 @@ "projectDirEnv": "CLAUDE_PROJECT_DIR", "runtime": "bun", "events": { + "SessionStart": [ + { + "hook": "serena-primer.ts", + "timeout": 3 + }, + { + "hook": "state-boundary.ts", + "timeout": 5 + } + ], "UserPromptSubmit": [ { "hook": "keyword-detector.ts", @@ -24,11 +34,18 @@ "timeout": 2 } ], - "PreToolUse": { - "hook": "test-filter.ts", - "matcher": "Bash", - "timeout": 5 - }, + "PreToolUse": [ + { + "hook": "scm-guard.ts", + "matcher": "Bash", + "timeout": 3 + }, + { + "hook": "test-filter.ts", + "matcher": "Bash", + "timeout": 5 + } + ], "Stop": { "hook": "persistent-mode.ts", "timeout": 5 diff --git a/.agents/hooks/variants/codex.json b/.agents/hooks/variants/codex.json index 0609cabb..90ff320b 100644 --- a/.agents/hooks/variants/codex.json +++ b/.agents/hooks/variants/codex.json @@ -24,21 +24,21 @@ "timeout": 3 } ], - "PreToolUse": { - "hook": "test-filter.ts", - "matcher": "Bash", - "timeout": 5 - }, + "PreToolUse": [ + { + "hook": "scm-guard.ts", + "matcher": "Bash", + "timeout": 3 + }, + { + "hook": "test-filter.ts", + "matcher": "Bash", + "timeout": 5 + } + ], "Stop": { "hook": "persistent-mode.ts", "timeout": 5 } - }, - "featureFlags": { - "file": ".codex/config.toml", - "section": "features", - "flags": { - "hooks": true - } } } diff --git a/.agents/hooks/variants/commandcode.json b/.agents/hooks/variants/commandcode.json index 150f1ef9..67b18195 100644 --- a/.agents/hooks/variants/commandcode.json +++ b/.agents/hooks/variants/commandcode.json @@ -6,6 +6,16 @@ "projectDirEnv": null, "runtime": "bun", "events": { + "SessionStart": [ + { + "hook": "serena-primer.ts", + "timeout": 3 + }, + { + "hook": "state-boundary.ts", + "timeout": 5 + } + ], "Stop": { "hook": "persistent-mode.ts", "timeout": 5 diff --git a/.agents/hooks/variants/cursor.json b/.agents/hooks/variants/cursor.json index 8feb024d..8601cb19 100644 --- a/.agents/hooks/variants/cursor.json +++ b/.agents/hooks/variants/cursor.json @@ -24,6 +24,33 @@ "hook": "serena-primer.ts", "timeout": 3 } + ], + "preToolUse": [ + { + "hook": "scm-guard.ts", + "matcher": "Shell", + "timeout": 3 + }, + { + "hook": "test-filter.ts", + "matcher": "Shell", + "timeout": 5 + } + ], + "stop": { + "hook": "persistent-mode.ts", + "timeout": 5, + "loopLimit": null + }, + "sessionStart": [ + { + "hook": "serena-primer.ts", + "timeout": 3 + }, + { + "hook": "state-boundary.ts", + "timeout": 5 + } ] }, "extra": { diff --git a/.agents/hooks/variants/grok.json b/.agents/hooks/variants/grok.json index a0ee2bf6..2d09dac3 100644 --- a/.agents/hooks/variants/grok.json +++ b/.agents/hooks/variants/grok.json @@ -24,11 +24,18 @@ "timeout": 3 } ], - "PreToolUse": { - "hook": "test-filter.ts", - "matcher": "run_terminal_cmd", - "timeout": 5 - }, + "PreToolUse": [ + { + "hook": "scm-guard.ts", + "matcher": "Bash", + "timeout": 3 + }, + { + "hook": "test-filter.ts", + "matcher": "Bash", + "timeout": 5 + } + ], "Stop": { "hook": "persistent-mode.ts", "timeout": 5 diff --git a/.agents/hooks/variants/hook-variant.schema.json b/.agents/hooks/variants/hook-variant.schema.json index 4387c983..4ac20566 100644 --- a/.agents/hooks/variants/hook-variant.schema.json +++ b/.agents/hooks/variants/hook-variant.schema.json @@ -86,6 +86,11 @@ "flatHookEntries": { "type": "boolean", "description": "When true, settings hook entries are written as flat {command, timeout[, matcher]} objects under each event key (Cursor hooks.json format) instead of nested {matcher, hooks:[...]} groups (Claude Code format)." + }, + "skipSettingsMerge": { + "type": "boolean", + "description": "When true, the generic installer copies hook scripts and writes the oma-hook.sh wrapper but does NOT merge hook entries into settingsFile. Used by Kiro, whose CLI reads hooks from a dedicated agent config (.kiro/agents/oma-hooks.json via applyKiroOmaHooksAgent) — the generic .kiro/settings/cli.json merge would be dead config Kiro never reads. `extra` (statusLine/permissions) is still applied when present.", + "default": false } }, "$defs": { @@ -109,6 +114,10 @@ "minimum": 1, "maximum": 30, "default": 5 + }, + "loopLimit": { + "type": ["integer", "null"], + "description": "Cursor `stop` hook only: caps how many times the hook may auto-resubmit a followup_message (Cursor's loop_limit). null = uncapped; omit for Cursor's default (5). Ignored by other vendors." } } }, diff --git a/.agents/hooks/variants/kimi.json b/.agents/hooks/variants/kimi.json index ed163eae..3caae586 100644 --- a/.agents/hooks/variants/kimi.json +++ b/.agents/hooks/variants/kimi.json @@ -25,11 +25,18 @@ "timeout": 3 } ], - "PreToolUse": { - "hook": "test-filter.ts", - "matcher": "Bash", - "timeout": 5 - }, + "PreToolUse": [ + { + "hook": "scm-guard.ts", + "matcher": "Bash", + "timeout": 3 + }, + { + "hook": "test-filter.ts", + "matcher": "Bash", + "timeout": 5 + } + ], "Stop": { "hook": "persistent-mode.ts", "timeout": 5 diff --git a/.agents/hooks/variants/kiro.json b/.agents/hooks/variants/kiro.json index 8a01e24d..d170a37b 100644 --- a/.agents/hooks/variants/kiro.json +++ b/.agents/hooks/variants/kiro.json @@ -5,6 +5,7 @@ "settingsFile": ".kiro/settings/cli.json", "projectDirEnv": null, "runtime": "bun", + "skipSettingsMerge": true, "events": { "userPromptSubmit": [ { @@ -24,11 +25,18 @@ "timeout": 3 } ], - "preToolUse": { - "hook": "test-filter.ts", - "matcher": "shell", - "timeout": 5 - }, + "preToolUse": [ + { + "hook": "scm-guard.ts", + "matcher": "shell", + "timeout": 3 + }, + { + "hook": "test-filter.ts", + "matcher": "shell", + "timeout": 5 + } + ], "stop": { "hook": "persistent-mode.ts", "timeout": 5 diff --git a/.agents/hooks/variants/opencode/oma.ts b/.agents/hooks/variants/opencode/oma.ts index 2e0e5c69..a488524c 100644 --- a/.agents/hooks/variants/opencode/oma.ts +++ b/.agents/hooks/variants/opencode/oma.ts @@ -2,34 +2,60 @@ * oh-my-agent — opencode (Sst opencode) plugin bridge. * * SSOT source. At install time `installOpencodePlugin` copies this file to - * `.opencode/plugins/oma/oma.ts` alongside the core hook scripts. opencode - * auto-discovers plugins under each `.opencode/plugins` subdirectory. + * `.opencode/plugins/oma/oma.ts` alongside the core hook scripts, and + * `registerOpencodePlugin` adds it to `.opencode/opencode.jsonc` (the nested + * subdir is invisible to opencode's flat auto-discovery). * * Why a bridge instead of a per-vendor variants JSON entry: opencode does NOT * register settings-file hooks like the other vendors. It loads in-process - * TypeScript plugins and dispatches plugin event handlers. So rather than the - * generic `installHooksFromVariant` path (events → settings file → `bun - * `. @@ -104,7 +109,7 @@ For `viewer.html`, the CLI injects: |---|---| | Slide root | `position: absolute; inset: 0; width: 1920px; height: 1080px;` | | Safe zones | Left/right margin ≥ 80px; top/bottom margin ≥ 60px | -| Body text | 28–36 px minimum; heading 64–120 px | +| Body text | 28–36 px doctrine floor; heading 64–120 px. (The validator hard-fails only below 18 px — 18–27 px passes the gate but is reserved for captions/labels.) | | Icon / decorative image | explicit `width`/`height` in px | | Background gradients | allowed (CSS); rasterized to PNG at PPTX export | | Clipping / overflow | `overflow: hidden` on `.slide` prevents bleed-out | @@ -223,10 +228,17 @@ sees the true 1920×1080 px layout. `viewport-base.css` `@media print` rules: Result: one clean 1920×1080 slide per printed page. -## 10. Presenter View (postMessage API) +## 10. Presenter View (embedded notes panel + postMessage API) -When `viewer.html` opens the deck inside an `