diff --git a/.agents/skills/chatnow-creating-issues/SKILL.md b/.agents/skills/chatnow-creating-issues/SKILL.md new file mode 100644 index 0000000..502b0cc --- /dev/null +++ b/.agents/skills/chatnow-creating-issues/SKILL.md @@ -0,0 +1,96 @@ +--- +name: chatnow-creating-issues +description: Use when any ChatNow repository change is requested, when no valid primary Issue exists, or when implementation uncovers an out-of-scope problem +--- + +# Creating ChatNow Issues + +## Core principle + +``` +NO IMPLEMENTATION BRANCH OR REPOSITORY CHANGE WITHOUT A VALID PRIMARY ISSUE. +``` + +Urgency, a one-line change, or permission to document later does not weaken this rule. Continue only read-only investigation until the Issue exists and has a number or URL. + +## Create the Issue first + +1. Verify the request against source, logs, tests, or observed behavior. Do not turn an unverified suspected fix into the requirement. +2. Select the target version line. Stop if multiple targets remain valid after checking available Issue, milestone, and repository evidence. +3. Define exactly one primary problem or deliverable. Split independent work into separate Issues. +4. Write an English title and a body whose prose is Chinese. Use every heading below exactly once and in this order. +5. Validate the Issue contract, create the Issue, and record its number or URL before creating a task branch or changing the repository. + +## Required body contract + +```markdown +## Target Version + +## Evidence + +## Problem or Goal + +## Scope + +## Non-goals + +## Acceptance Criteria + +## Test-first Plan + +## Risk and Security + +## Architecture Impact + +## Core-flow Impact + +## Required Skill Updates +``` + +Fill the sections as follows: + +| Section | Required content | +|---|---| +| `Target Version` | One existing development version line and the evidence for selecting it. | +| `Evidence` | Specific source paths, logs, failing tests, or reproducible behavior. Mark unknown facts as unknown; never invent evidence. | +| `Problem or Goal` | Current behavior or need, affected users or systems, and the observable desired outcome without assuming an implementation. | +| `Scope` | Files, services, behavior, and deliverables included in this Issue. | +| `Non-goals` | Closely related work explicitly excluded. | +| `Acceptance Criteria` | Observable, measurable checkboxes, including relevant boundaries and failure behavior. Do not encode an unverified implementation as acceptance. | +| `Test-first Plan` | The smallest RED case, its command and expected failure reason, the minimum GREEN behavior, and relevant regression coverage. | +| `Risk and Security` | Security, privacy, data, compatibility, migration, operational, and rollback risks, or an evidence-based `None`. | +| `Architecture Impact` | `Yes` or `No` plus reasoning about service boundaries and infrastructure topology. | +| `Core-flow Impact` | `Yes` or `No` plus reasoning about communication, auth, persistence, indexing, caching, sequencing, idempotency, retry, Outbox, Push, or ACK behavior. | +| `Required Skill Updates` | Exact affected ChatNow Skill/reference paths, or `None` with reasoning. Any architecture, core-flow, or test-architecture change requires relevant Skill updates in the same PR; a follow-up Issue is not a substitute. | + +## Preserve the contract + +Update evidence as investigation improves it. Never rewrite acceptance criteria after implementation merely to fit the produced code. If a genuine requirement change is approved, record the reason and changed requirement in the Issue before continuing, then restart planning and RED against the revised contract. + +An unrelated defect, typo, cleanup, or cross-version port is not current scope. Create a follow-up Issue and keep it out of the current branch and PR. One PR resolves one primary Issue. + +## Security containment exception + +Immediate containment may precede the normal Issue-first order only when delaying action materially increases security harm. Create the Issue immediately, and record the exception, the harm caused by delay, containment actions, timestamps, evidence, and remaining follow-up. This exception permits only the minimum containment; it does not permit a post-hoc ordinary Issue or hidden scope. + +## Stop conditions + +Stop implementation and continue only read-only research when: + +- GitHub is unavailable and no valid Issue exists; +- the target version remains ambiguous; +- the Issue combines multiple primary problems; +- evidence is missing or contradicts the proposed goal; +- acceptance or the RED plan is not measurable; +- security or data risk cannot be bounded safely. + +## Common rationalizations + +| Rationalization | Required response | +|---|---| +| "It is only one line." | Create and validate the Issue first. Size does not remove risk or traceability. | +| "Open the Issue after the fix." | Stop. Post-hoc documentation cannot authorize implementation. | +| "The deadline is too close for RED." | Define measurable acceptance and an observed RED plan before implementation. | +| "We already know the operator change." | Record verified behavior and outcome; do not make an assumed patch the contract. | +| "Fix this unrelated typo while here." | Create a follow-up Issue and exclude it from the current branch and PR. | +| "Update the architecture Skill later." | Update every relevant Skill/reference in the same architecture or core-flow PR. | diff --git a/.agents/skills/chatnow-creating-issues/agents/openai.yaml b/.agents/skills/chatnow-creating-issues/agents/openai.yaml new file mode 100644 index 0000000..2331b9a --- /dev/null +++ b/.agents/skills/chatnow-creating-issues/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Create ChatNow Issues" + short_description: "Create scoped, testable ChatNow engineering Issues" + default_prompt: "Use $chatnow-creating-issues to create a valid Issue before changing ChatNow." diff --git a/.agents/skills/chatnow-developing/SKILL.md b/.agents/skills/chatnow-developing/SKILL.md new file mode 100644 index 0000000..7aadaf6 --- /dev/null +++ b/.agents/skills/chatnow-developing/SKILL.md @@ -0,0 +1,87 @@ +--- +name: chatnow-developing +description: Use when changing ChatNow production C++, Protobuf contracts, configuration, databases, caches, message queues, service discovery, or runtime infrastructure +--- + +# Develop ChatNow Services + +## Core principle + +Preserve ownership, trust, failure, and repeated-execution invariants with the smallest Issue-scoped change. A successful response is valid only when every required dependency and asynchronous handoff has defined bounded behavior. + +## Entry gates + +1. Confirm a valid primary Issue defines the target version, scope, acceptance criteria, risks, architecture/core-flow impact, and required Skill updates. Stop before implementation if it does not. +2. Use `chatnow-orienting` to verify affected owners, trust boundaries, durable sources of truth, caches, calls, queues, stores, and current invariants from executable evidence. +3. Use `chatnow-testing` before production code. Record a pure-Go behavioral test that failed for the expected missing behavior. If production code predates the observed RED, delete it and restart test-first. +4. Find the nearest repository pattern in the same subsystem and version line. Cite the files and preserve their contracts unless the Issue explicitly changes them. + +## Implementation workflow + +1. State the invariant being changed and the invariants that must remain true. +2. Assign ownership for identity, durable state, cached or coordination state, transactions, asynchronous work, and cleanup. Derive authority from authenticated server context, never an untrusted client field. +3. Specify success, synchronous failure, asynchronous failure, timeout, cancellation, retry, duplicate execution, partial completion, and recovery before editing code. +4. Make the minimum diff required by the observed RED. Do not mix unrelated refactors, formatting, generated output, or speculative abstractions. +5. Apply every relevant boundary rule below. +6. Reach GREEN with the target Go test, run relevant same-layer regressions, then refactor only while green. Report actual commands and results; do not invent unavailable commands or runtime evidence. +7. Recheck compatibility, observability, deployment/configuration, cleanup, and same-PR Skill synchronization. + +## Boundary rules + +### RPC and asynchronous callbacks + +- Preserve server-derived authentication and authorization context across the brpc attachment; validate required metadata at the receiver. +- Map transport, timeout, authentication, validation, dependency, and application failures deliberately. Do not turn every exception into success. +- Set a bounded timeout and define whether a timed-out operation is safe to retry. Make retry limits and backoff explicit. +- Guarantee the brpc closure runs exactly once on every path. Keep response, controller, callback captures, channels, and dependent objects alive until the final callback; prevent double completion and use-after-free. + +### RabbitMQ + +- Identify the exchange type/name, routing key, queue owner, binding, durability, publisher-only or consumer role, and deployment configuration. Do not declare an orphan or conflicting topology. +- Define publisher-confirm handling for acknowledged, rejected, lost/unknown, synchronous-error, and timeout outcomes. Return success only after the contract's success boundary. +- Treat delivery as at-least-once. Define retry ownership, bounded retry/backoff, redelivery behavior, idempotency key/effect, duplicate result, and any Outbox or reaper recovery path. +- Preserve trace headers and ensure consumers acknowledge only after the owned durable effect reaches its declared boundary. + +### Redis + +- Classify each key as cache, coordination/lease, idempotency state, sequence state, routing/presence state, or authoritative security state. Name the durable source of truth when Redis is not authoritative. +- Choose fail-open, fail-closed, or explicit degraded behavior from that classification. Authorization, revocation, uniqueness, sequencing, and destructive decisions must not silently use cache-miss or exception as success. +- Bound each lookup or lock acquisition by timeout and cancellation behavior. Define retry limits, fallback, stale-data policy, TTL, key ownership, and cleanup. +- Distinguish miss, confirmed absence, timeout, connection failure, parse failure, and partial write in code, logs, responses, and tests. + +### State, concurrency, and resources + +- Define the database transaction boundary, commit point, rollback behavior, and relationship between database writes and external side effects. Never imply atomicity across MySQL, Redis, Elasticsearch, RabbitMQ, or RPC without an implemented protocol. +- For repeated execution, state the idempotency key, uniqueness or monotonic guard, result after prior success, behavior after partial failure, and retry owner. +- Protect shared state with the nearest established synchronization pattern. State lock scope/order, contention and timeout behavior, and object lifetime across threads, event loops, goroutines in tests, callbacks, shutdown, and cancellation. +- Assign one owner to close or release every lock, socket, channel, Redis lease/key, database handle, callback, thread, and temporary test resource on all paths. + +## Compatibility and observability + +- Preserve Protobuf field numbers, wire meanings, response envelopes, public error text, configuration defaults, exchange/queue contracts, database compatibility, and rolling mixed-version behavior unless the Issue explicitly authorizes a compatibility change. +- Treat intentional public compatibility or settled-semantic changes as requiring human approval. +- Use English for new identifiers, comments, and structured log messages. Include safe request/trace and outcome context; never log credentials, tokens, full message content, or unnecessary personal data. +- Update affected ChatNow Skills and references in the same PR when architecture, service boundaries, infrastructure topology, core flows, or test architecture change. A follow-up Issue is not a substitute. + +## Required output + +Return these fields in order: + +1. **Changed invariants** — the Issue-scoped behavior change and preserved ownership, trust, ordering, and source-of-truth rules. +2. **Files/ownership** — minimal changed files, nearest patterns used, and owners of state, side effects, callbacks, and cleanup. +3. **Failure behavior** — bounded synchronous, asynchronous, timeout, cancellation, partial-failure, and recovery semantics. +4. **Retry/idempotency** — retry owner, limits/backoff, idempotency key or guard, duplicate result, and repeated-execution behavior. +5. **Compatibility** — wire, error, configuration, topology, data, rollout, and public-behavior impact. +6. **Tests** — selected Go layer/case ID, exact observed RED, GREEN and regression commands/results, cleanup ownership, and checks not run. +7. **Skill updates** — exact Skill/reference files changed in the same PR, or `None` with evidence that no governed architecture, core-flow, infrastructure, or test rule changed. + +## Common mistakes + +| Mistake | Required correction | +|---|---| +| Returning success for every Redis exception | Classify the key and failure, select an explicit safe mode, bound the call, and test each outcome. | +| Retrying an unknown MQ publish as a new effect | Use publisher confirms and an idempotency guard; define the duplicate result and retry owner. | +| Completing an RPC before its callback or capture lifetime ends | Keep dependencies alive and use an exactly-once completion guard. | +| Updating a database and broker as if one transaction | Define the commit boundary and use the established Outbox/recovery pattern where required. | +| Copying a nearby pattern without checking trust or ownership | Cite the pattern, then verify its boundary matches this operation. | +| Calling a compile or static check a behavior test | Report only an observed Go behavioral RED/GREEN as runtime evidence. | diff --git a/.agents/skills/chatnow-developing/agents/openai.yaml b/.agents/skills/chatnow-developing/agents/openai.yaml new file mode 100644 index 0000000..d0e42db --- /dev/null +++ b/.agents/skills/chatnow-developing/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Develop ChatNow Services" + short_description: "Implement safe changes in ChatNow services" + default_prompt: "Use $chatnow-developing to implement this ChatNow production change safely." diff --git a/.agents/skills/chatnow-maintaining-documentation/SKILL.md b/.agents/skills/chatnow-maintaining-documentation/SKILL.md new file mode 100644 index 0000000..5b6ff19 --- /dev/null +++ b/.agents/skills/chatnow-maintaining-documentation/SKILL.md @@ -0,0 +1,79 @@ +--- +name: chatnow-maintaining-documentation +description: Use when creating or changing ChatNow Skills, architecture references, API or operations documentation, engineering instructions, Issue text, or pull request text +--- + +# Maintain ChatNow Documentation + +## Core principle + +Keep one verified account of each engineering fact. Derive current-state claims from executable evidence, label proposals as proposals, and synchronize every affected Skill and reference in the same PR. + +## Evidence and labeling + +Use evidence in this order: + +1. Production source and Protobuf contracts. +2. Executable configuration, CMake, Docker Compose, tests, and CI workflows. +3. Git history for version-specific intent. +4. README files, plans, specifications, roadmaps, changelogs, and other prose as secondary context only. + +Verify secondary claims against higher-precedence evidence. Cite repository paths and symbols or lines for material facts. Do not state an unverified, planned, partially implemented, unavailable, or historical capability as current. + +For an engineering reference or proposal, record the applicable version line, verification or proposal date, and one explicit status: `Current`, `Proposed`, `Historical`, `Deprecated`, or `Unverified`. Keep current and proposed behavior in separate sections. A date or confident tone does not turn a proposal into current behavior. + +## Mandatory workflow + +1. Resolve the primary Issue, target version line, current commit, audience, and canonical document for the subject. +2. Inspect the executable evidence that owns every changed claim. Record contradictions and unknowns instead of resolving them by assumption. +3. Update the canonical artifact in English. Preserve existing compatibility text unless the Issue explicitly changes the contract. +4. Compute synchronization from the substance of the change, not the file extension: + +| Changed subject | Required same-PR updates | +|---|---| +| Architecture, service boundary, infrastructure topology, or core flow | `chatnow-orienting` and every affected reference under `.agents/skills/chatnow-orienting/references/` | +| Test architecture, layers, tags, commands, fixtures, case IDs, or test workflow | `chatnow-orienting` where architecture context changes, plus affected `chatnow-testing` instructions and references | +| Mandatory engineering workflow or policy | Every Skill and template that teaches or enforces that workflow | +| Skill trigger or interface | The Skill plus matching `agents/openai.yaml` metadata | + +A follow-up Issue is not a substitute for these same-PR updates. Name the exact affected paths. + +5. Check every relative link, anchor, repository path, and command against the target commit. Remove stale links; do not link to a path or capability that does not exist. +6. Review the full PR diff for contradictory status labels, duplicated facts, missing Skill synchronization, language violations, and claims unsupported by executable evidence. + +## Language contract + +- Write Skills, engineering references, API and operations documentation, automation, identifiers, new code comments, and new log messages in English. +- Write Issue and PR titles in English. Write authored Issue and PR body prose in Chinese; preserve required English headings, field names, code, commands, and machine markers. +- Write commit messages in English Conventional Commits. +- Do not rewrite existing Chinese comments merely for translation without an Issue-local reason. + +## Canonical-artifact rule + +Extend the existing canonical document or Skill. Do not create a duplicate README, quick-reference, changelog, installation guide, migration narrative, process-history file, or stale status snapshot to explain the same facts. Active rollout or compatibility instructions belong in the canonical change document and must carry target version, date, and status. + +## Output contract + +Report: + +1. target version line, commit, document status, and date; +2. executable evidence inspected and unresolved contradictions; +3. canonical files changed; +4. exact same-PR Skill, reference, and template updates, or `None` with evidence that no governed subject changed; +5. language and link-integrity checks performed; +6. unverified claims retained as explicitly `Unverified` or removed. + +## Stop conditions + +Stop and resolve the contract before claiming documentation current when the target version is ambiguous, executable sources conflict, a material claim is unverified, a proposal is presented as implemented, links are invalid, or required same-PR Skill updates are excluded. + +## Common mistakes + +| Mistake | Required correction | +|---|---| +| "Only a Markdown file changed." | Classify the changed engineering fact and synchronize affected Skills. | +| "The proposal is approved, so call it current." | Keep it `Proposed` until executable evidence implements it. | +| "The README already says so." | Verify against source, contracts, executable config, tests, and CI. | +| "Update the Skill later." | Include exact affected Skill/reference paths in this PR. | +| "Add a quick guide for visibility." | Update the canonical artifact and link to it; do not create parallel truth. | +| "The link looks right." | Resolve and check the actual target and anchor at the target commit. | diff --git a/.agents/skills/chatnow-maintaining-documentation/agents/openai.yaml b/.agents/skills/chatnow-maintaining-documentation/agents/openai.yaml new file mode 100644 index 0000000..399141c --- /dev/null +++ b/.agents/skills/chatnow-maintaining-documentation/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Maintain ChatNow Documentation" + short_description: "Keep ChatNow engineering facts and Skills synchronized" + default_prompt: "Use $chatnow-maintaining-documentation to update ChatNow engineering documentation." diff --git a/.agents/skills/chatnow-orienting/SKILL.md b/.agents/skills/chatnow-orienting/SKILL.md new file mode 100644 index 0000000..a5a0eb7 --- /dev/null +++ b/.agents/skills/chatnow-orienting/SKILL.md @@ -0,0 +1,65 @@ +--- +name: chatnow-orienting +description: Use when entering ChatNow for the first time, answering architecture questions, planning cross-service work, or changing service boundaries, infrastructure topology, or core flows +--- + +# Orienting in ChatNow + +## Overview + +Build an architecture assessment from the target version's executable evidence. Preserve ownership, trust, ordering, idempotency, and failure invariants before proposing a change. + +## Evidence precedence + +Use evidence in this order: + +1. Source and Protobuf contracts. +2. Executable configuration, CMake, and Docker Compose. +3. Tests and CI workflows. +4. Git history for intent and version-specific context. +5. README, design documents, roadmaps, and changelogs as secondary context only. + +Verify secondary claims against higher-precedence evidence. Cite repository paths and symbols or lines for every material conclusion. + +## Orientation workflow + +1. Resolve the primary Issue, its architecture/core-flow acceptance, target version line, current branch, and exact commit. Inspect the target version rather than assuming the checkout is correct. +2. Read the relevant service entry points, Protobuf contracts, configuration, CMake, Compose, tests, and history. +3. Trace entry points and calls across HTTP, brpc, RabbitMQ, WebSocket, Redis, MySQL, Elasticsearch, MinIO, and etcd as applicable. +4. Map state ownership, trusted identity derivation, sync/async boundaries, retries, idempotency keys, outboxes, and recovery behavior. +5. State current invariants before evaluating a proposal. Distinguish durable sources of truth from caches, routing state, and acceleration layers. +6. Separate **Current behavior** from **Proposed behavior**. Never describe an intended design as implemented. +7. Identify compatibility, partial-failure, observability, deployment, and test impact. +8. For any architecture, service-boundary, infrastructure-topology, or core-flow change, require updates to this Skill's affected references in the same PR. A follow-up Issue is not a substitute. + +Stop and request resolution when the target version is ambiguous, executable sources contradict each other, or the primary architecture Issue/acceptance is missing. + +## Load references conditionally + +- Read [technology-stack.md](references/technology-stack.md) for build, dependency, configuration, runtime, infrastructure, or verification questions. +- Read [repository-map.md](references/repository-map.md) when locating ownership, ports, services, stores, or first-read files. +- Read [core-flows.md](references/core-flows.md) for HTTP, message, ACK, authentication, media, presence, ordering, retry, or delivery changes. +- On first entry or cross-service architecture work, read all three. + +## Required output + +Return these fields in order: + +1. **Target version** — Issue, target version line, branch, and commit. +2. **Evidence** — executable paths/symbols inspected and any secondary sources used. +3. **Affected services** — services, contracts, stores, trust boundaries, and sync/async boundaries. +4. **Current flow** — verified end-to-end behavior and ownership. +5. **Changed invariants** — proposed behavior separately, including ownership, ordering, idempotency, and source-of-truth changes. +6. **Failure/compatibility impact** — retries, partial failures, rollout, protocol/config/deployment compatibility, observability, and tests. +7. **Skill updates** — exact affected Skill/reference files that must change in the same PR, or `None` with evidence that architecture and core flows are unchanged. + +## Common mistakes + +| Mistake | Correction | +|---|---| +| Starting from a roadmap or README | Trace the executable path first, then reconcile prose. | +| Naming services but not state owners | Name each store, cache, queue, writer, reader, and durable source of truth. | +| Calling an MQ or WebSocket path exactly-once | Assume at-least-once delivery and identify idempotent effects. | +| Treating a client field as trusted identity | Trace where Gateway or Push derives identity and how brpc metadata is built. | +| Blending current and proposed topology | Use separate current and proposed sections. | +| Deferring architecture references | List same-PR Skill/reference updates explicitly. | diff --git a/.agents/skills/chatnow-orienting/agents/openai.yaml b/.agents/skills/chatnow-orienting/agents/openai.yaml new file mode 100644 index 0000000..c1b3b98 --- /dev/null +++ b/.agents/skills/chatnow-orienting/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "ChatNow Architecture Orientation" + short_description: "Map ChatNow architecture and core flows from source" + default_prompt: "Use $chatnow-orienting to map affected ChatNow services and invariants." diff --git a/.agents/skills/chatnow-orienting/references/core-flows.md b/.agents/skills/chatnow-orienting/references/core-flows.md new file mode 100644 index 0000000..13a0e62 --- /dev/null +++ b/.agents/skills/chatnow-orienting/references/core-flows.md @@ -0,0 +1,74 @@ +# Core Flows + +These are current-state flows for the `3.0-dev` line. Re-verify affected symbols at the target commit and keep proposals in a separate section. + +## HTTP request flow + +**Flow:** Client -> Gateway HTTP -> discovered brpc service -> Protobuf response envelope. + +- Entry/contracts: `gateway/source/gateway_server.h`; affected `proto/*/*_service.proto`; `proto/common/envelope.proto`. +- Boundary: Gateway parses Protobuf, authenticates JWT except whitelisted Identity routes, derives `user_id`/`device_id`/JTI, adds trace context, and serializes `RpcMetadata` into the brpc attachment. +- Discovery: `ServiceManager` resolves service instances through etcd; calls are synchronous with route-specific timeouts. +- Failure: malformed input, unavailable backends, and RPC timeouts are translated into a Protobuf `ResponseHeader`; retries remain a client decision unless a flow states otherwise. +- Tests: `tests/bvt`, affected `tests/func`, `tests/func/auth_middleware_test.go`, `tests/func/security_test.go`. +- Invariants: client identity fields never override server-derived auth context; services validate required metadata; trace context propagates where supported. + +## Message send and delivery + +**Flow:** Client -> Gateway -> Transmite -> RabbitMQ message exchange -> Message/MySQL -> RabbitMQ push queue -> Push -> WebSocket. + +- Entry/contracts: Gateway `/service/message/send`; `proto/transmite/transmite_service.proto`; `proto/message/message_internal.proto`; `proto/push/notify.proto`. +- Transmite: validates auth/membership and content, allocates conversation and per-user sequences in Redis, creates a Snowflake message ID, protects `client_msg_id` with a Redis idempotency key, and completes the brpc request after publisher confirm. +- Message: consumes `InternalMessage`, persists the message then per-user timelines in MySQL, treats duplicate message inserts idempotently, publishes push only after persistence, and asynchronously indexes text in Elasticsearch. +- Push: consumes the push event, resolves Redis/local routes, records per-device unacked payloads, sends locally or uses asynchronous cross-instance `PushBatch`, then WebSocket delivery reaches the client. +- Async/recovery: RabbitMQ and WebSocket delivery are at-least-once. Message uses Redis Push/ES outboxes and reapers for publish failures; Push uses a cross-instance outbox. Consumer redelivery and duplicate delivery require idempotent effects. +- Tests: `tests/bvt/message_test.go`, `tests/func/transmite_test.go`, `tests/func/message_test.go`, `tests/func/ws_notify_test.go`, `tests/func/scenarios_test.go`, `tests/perf/send_msg_test.go`, `tests/perf/sync_test.go`. +- Invariants: Message/MySQL is the stored-message source of truth; persistence precedes normal push publication; `request_id`, `client_msg_id`, message ID, conversation sequence, and user sequence have distinct roles; do not claim exactly-once delivery. + +## Delivery ACK convergence + +**Flow:** Client WebSocket `MSG_PUSH_ACK` -> Push validation -> Redis unacked removal -> asynchronous Message `UpdateReadAck` -> MySQL convergence. + +- Entry/contracts: `proto/push/notify.proto` `NotifyMsgPushAck`; `push/source/push_server.h` `onClientNotify`; `proto/message/message_service.proto` `UpdateReadAck`; `message/source/message_server.h` `UpdateReadAck`. +- Trust: Push binds identity when `CLIENT_AUTH` verifies the JWT, then compares ACK body `user_id` and `device_id` with connection identity. The WS handler constructs server-side brpc auth metadata for Message. +- Stores: Redis unacked state is exact and per device/user sequence; MySQL `conversation_member.last_ack_seq` is a monotonic durable high-water mark. These are different semantics and owners. +- Async/retry: Redis removal precedes a fire-and-forget Message RPC. Failure after removal can leave MySQL stale; failure before removal can cause duplicate resend. Heartbeats read due unacked entries and resend them. Message's monotonic update is idempotent, but the current Push-to-Message ACK RPC has no durable retry path. +- Tests: `tests/func/message_test.go` covers direct `UpdateReadAck`; `tests/func/scenarios_test.go` covers unread convergence. Verify the WS ACK path separately when changing it. +- Invariants: Push owns live routes, resend state, cross-instance fanout, and client ACK ingestion; Message owns persistent ACK convergence; preserve per-device exact removal and monotonic durable convergence; never trust ACK body identity without connection-derived validation. + +## Authentication and forwarded context + +**Flow:** Identity registration/login/refresh -> JWT -> Gateway or Push verification -> server-derived context -> downstream brpc metadata. + +- Entry/contracts: `proto/identity/identity_service.proto`; `identity/source/identity_server.h`; `common/auth/jwt_codec.hpp`; `common/auth/jwt_store.hpp`; `gateway/source/gateway_auth.hpp`; `push/source/push_server.h`. +- Stores: Identity uses MySQL for users/devices and Redis for active refresh tokens, rotation/reuse detection, and revocation state. +- Trust: Gateway validates Bearer access tokens and revocation before deriving metadata. Push verifies WS `CLIENT_AUTH` and binds claim identity to the connection. Downstream handlers use `common/auth/auth_context.hpp`; service-to-service forwarding uses `common/auth/forward_auth.hpp` where required. +- Sync/retry: Login and refresh are synchronous; refresh rotation detects reuse. Cache/store failure behavior must be inspected before changing fail-open/fail-closed semantics. +- Tests: `tests/bvt/auth_test.go`, `tests/func/identity_test.go`, `tests/func/auth_middleware_test.go`, `tests/func/security_test.go`, `tests/func/scenarios_test.go`. +- Invariants: only Identity issues/refreshes tokens; access and refresh token purposes remain distinct; downstream identity comes from verified claims and forwarded metadata, not request bodies. + +## Media upload and download + +**Flow:** Apply/init -> presigned MinIO upload -> complete -> MySQL metadata/quota -> authenticated download request -> presigned MinIO GET. + +- Entry/contracts: Gateway Media routes; `proto/media/media_service.proto`; `media/source/media_server.h`; `media/source/upload_handler.hpp`; `media/source/multipart_handler.hpp`; `media/source/download_handler.hpp`. +- Stores: MinIO holds bytes; MySQL holds `media_file`, blob-ref/dedup, multipart, and per-user quota state; Redis coordinates cleanup/locks where implemented. +- Boundaries: clients upload/download directly with short-lived presigned URLs. Apply validates size/MIME/hash/quota and records pending metadata; complete verifies object existence/size, converges dedup/refcount/quota, and is idempotent for committed files. +- Authorization: Gateway requires authentication. The current download handler permits an authenticated caller with a committed `file_id`; it does not enforce owner or conversation membership, as confirmed by `tests/func/media_test.go`'s other-user case. Do not overstate this boundary as resource-level authorization. +- Async/retry: cleanup handles stale pending, quarantine, and unreferenced object paths; client retries must preserve file/upload identifiers and completion idempotency. +- Tests: `tests/bvt/media_test.go`, `tests/func/media_test.go`, `tests/func/concurrency_test.go`, `tests/func/scenarios_test.go`, `tests/perf/upload_test.go`, `tests/pkg/verify/minio.go`. +- Invariants: service processes metadata rather than normal file bytes; only committed objects are downloadable; MySQL metadata/quota and MinIO object state must converge; preserve dedup and completion idempotency. + +## Presence and typing + +**Flow:** Push WebSocket lifecycle -> Redis presence/routes -> Presence aggregation/subscriptions -> Presence or Push notification -> WebSocket. + +- Entry/contracts: `push/source/push_server.h`; `proto/presence/presence_service.proto`; `presence/source/presence_server.h`; `proto/push/notify.proto`. +- Ownership: Push owns connections and route binding, writes per-device online/offline/heartbeat TTL state, and emits lifecycle notifications. Presence aggregates Redis device state, manages subscription sets and typing TTLs, and calls Push for delivery. +- Async/retry: lifecycle and typing notifications are fail-soft asynchronous Push RPCs; Redis TTL supplies eventual offline behavior; multi-instance fanout uses Push routing and cross-instance RPC. +- Tests: `tests/bvt/presence_test.go`, `tests/func/presence_test.go`, `tests/func/ws_notify_test.go`, `tests/pkg/fixture/ws.go`. +- Invariants: authenticated WS lifecycle drives presence; Redis is coordination state rather than durable user data; typing is ephemeral; route/presence TTL refresh and disconnect cleanup must remain aligned. + +## Architecture-change synchronization + +Any change to these paths, ownership boundaries, stores, protocols, topology, ordering, retry, idempotency, trust, or failure semantics must update this reference and any affected `technology-stack.md` or `repository-map.md` content in the same PR. diff --git a/.agents/skills/chatnow-orienting/references/repository-map.md b/.agents/skills/chatnow-orienting/references/repository-map.md new file mode 100644 index 0000000..a501077 --- /dev/null +++ b/.agents/skills/chatnow-orienting/references/repository-map.md @@ -0,0 +1,63 @@ +# Repository Map + +## Ownership and first reads + +| Path | Ownership | First-read files | +|---|---|---| +| `common/` | Shared auth, DAO, errors, infrastructure, MQ, logging, and utilities | `common/auth/auth_context.hpp`, `common/dao/data_redis.hpp`, `common/error/handle_rpc.hpp`, `common/infra/etcd.hpp`, `common/mq/channel.hpp` | +| `proto/` | External and internal Protobuf contracts | `proto/common/envelope.proto`, `proto/common/auth/metadata.proto`, affected `proto/*/*_service.proto` | +| `gateway/` | HTTP entry, JWT validation, discovery-based routing, response envelopes | `gateway/source/gateway_server.h`, `gateway/source/gateway_auth.hpp`, `gateway/source/gateway_server.cc`, `gateway/CMakeLists.txt` | +| `identity/` | Registration, login/logout, token issue/refresh, profile and lookup | `identity/source/identity_server.h`, `identity/source/identity_server.cc`, `proto/identity/identity_service.proto` | +| `relationship/` | Friend requests, relationships, blocking | `relationship/source/relationship_server.h`, `relationship/source/relationship_server.cc`, `proto/relationship/relationship_service.proto` | +| `conversation/` | Conversation lifecycle, membership, unread, visibility, pins, drafts | `conversation/source/conversation_server.h`, `conversation/source/conversation_server.cc`, `proto/conversation/conversation_service.proto` | +| `transmite/` | Message authorization, sequencing, idempotency, MQ publication | `transmite/source/transmite_server.h`, `transmite/source/transmite_server.cc`, `proto/transmite/transmite_service.proto` | +| `message/` | Message persistence, timelines, sync/history, read-ACK convergence, search indexing | `message/source/message_server.h`, `message/source/message_server.cc`, `proto/message/message_service.proto`, `proto/message/message_internal.proto` | +| `media/` | Presigned upload/download, multipart, dedup, quota, metadata, cleanup, speech | `media/source/media_server.h`, `media/source/upload_handler.hpp`, `media/source/download_handler.hpp`, `proto/media/media_service.proto` | +| `presence/` | Presence aggregation, subscriptions, and typing coordination | `presence/source/presence_server.h`, `presence/source/presence_server.cc`, `proto/presence/presence_service.proto` | +| `push/` | WebSocket connections, routes, cross-instance delivery, resend, client ACK ingestion | `push/source/push_server.h`, `push/source/connection.hpp`, `push/source/push_server.cc`, `proto/push/notify.proto` | +| `odb/` | ODB entity definitions and durable relational fields | Affected entity, especially `message.hxx`, `user_timeline.hxx`, `conversation_member.hxx`, and `media_*.hxx` | +| `conf/` | Local/container flags and JSON configuration | `conf/local/`, `conf/docker/`, `conf/auth.json`, `conf/media.json` | +| `sql/` | Versioned schema migrations | `sql/V4__media.sql` and any migration matching affected ODB entities | +| `docker/` | Separate MinIO topology and initialization; not wired into the root application network | `docker/docker-compose.yml`, `docker/minio-init/entrypoint.sh` | +| `docker-compose.yml` | Application stack declaration; Media object-storage wiring is incomplete | Root `docker-compose.yml`, then affected `Dockerfile` and `conf/docker` file | +| `scripts/` | Operational support and monitoring configuration | `scripts/install_aws_sdk_linux.sh`, `scripts/prometheus/redis_alerts.yml` | +| `tests/` | Pure-Go L1-L4 framework, clients, fixtures, cleanup, and store verification | `tests/Makefile`, `tests/config.yaml`, affected `tests/bvt`, `tests/func`, `tests/perf`, `tests/pkg` | +| `docs/` | Secondary architecture/API/operations context | Affected `docs/api/*.yaml`, `docs/operations/`, then relevant architecture documents | + +## Verified ports and infrastructure endpoints + +Application ports come from `conf/local`, `conf/docker`, and root `docker-compose.yml`. MinIO ports come from the separate `docker/docker-compose.yml`; this is not an integrated container endpoint map. + +| Owner | Local endpoint/port | Container endpoint/port | Evidence | +|---|---:|---:|---| +| Gateway HTTP | `127.0.0.1:9000` | `gateway_server:9000` | `gateway_server.conf` `http_listen_port` | +| Media brpc | `127.0.0.1:10002` | `media_server:10002` | `media_server.conf` | +| Identity brpc | `127.0.0.1:10003` | `identity_server:10003` | `identity_server.conf` | +| Transmite brpc | `127.0.0.1:10004` | `transmite_server:10004` | `transmite_server.conf` | +| Message brpc | `127.0.0.1:10005` | `message_server:10005` | `message_server.conf` | +| Relationship brpc | `127.0.0.1:10006` | `relationship_server:10006` | `relationship_server.conf` | +| Conversation brpc | `127.0.0.1:10007` | `conversation_server:10007` | `conversation_server.conf` | +| Push brpc | `127.0.0.1:10008` | `push_server:10008` | `push_server.conf` | +| Push WebSocket | `127.0.0.1:9001` | `push_server:9001` | `push_server.conf` `ws_port` | +| Presence brpc | `127.0.0.1:9050` | `presence_server:9050` | `presence_server.conf` | +| etcd | `127.0.0.1:2379` | `etcd:2379` | all service configs | +| MySQL | `127.0.0.1:3306` | `mysql:3306` | root Compose | +| Redis cluster | `127.0.0.1:6379`, `:6380`-`:6384` | `redis-node1:6379`, `redis-node2:6380` through `redis-node6:6384` | root Compose; service seed flags | +| RabbitMQ | `127.0.0.1:5672` | `rabbitmq:5672` | Transmite, Message, Push configs | +| Elasticsearch | HTTP `127.0.0.1:9200`; transport `:9300` | `elasticsearch:9200`; transport `:9300` | root Compose; service configs | +| MinIO S3 | Host `127.0.0.1:9000`, conflicting with Gateway | `minio:9000` only inside the separate MinIO Compose network | `conf/media.json`; supplemental Compose | +| MinIO console | Host `127.0.0.1:9001`, conflicting with Push WebSocket | `minio:9001` only inside the separate MinIO Compose network | supplemental Compose | + +MySQL service configs set `mysql_port=0`, while root Compose exposes MySQL on `3306` and service entrypoints wait on `mysql:3306`; preserve that distinction when diagnosing driver defaults. Gateway's `websocket_listen_port=0` is not the client WebSocket endpoint; Push owns `ws_port=9001`. + +Root Compose mounts `conf/media.json` into Media, but `s3.endpoint=http://127.0.0.1:9000` addresses the Media container itself. Root Compose has no MinIO service/dependency, while the supplemental MinIO Compose project has no declared shared external network with the root project. Do not present these declarations as a working integrated Media topology or recommend their current commands as a functional Media runtime. Any repair must explicitly reconcile the network, endpoint, dependency, and `9000`/`9001` host-port conflicts, then be verified from the affected containers. + +## State ownership + +- MySQL/ODB: durable users, relationships, conversations/members, messages/timelines, ACK high-water marks, and media metadata/quota. +- Message is the durable source of truth for stored messages. +- Redis: JWT revocation/refresh state, sequences, idempotency, routes, presence, subscriptions, typing, unacked delivery, outboxes, and caches. Confirm the exact key in `common/dao/data_redis.hpp`. +- RabbitMQ: at-least-once message persistence, push, and indexing boundaries; publishers use confirms where implemented. +- Elasticsearch: derived search index, not the message source of truth. +- MinIO: media objects; MySQL holds file ownership/status/quota metadata. +- etcd: discovery, leases, worker allocation, and leader election. diff --git a/.agents/skills/chatnow-orienting/references/technology-stack.md b/.agents/skills/chatnow-orienting/references/technology-stack.md new file mode 100644 index 0000000..c81a5c3 --- /dev/null +++ b/.agents/skills/chatnow-orienting/references/technology-stack.md @@ -0,0 +1,63 @@ +# Technology Stack and Entry Points + +Use this reference for the `3.0-dev` architecture line, then verify task-sensitive details at the resolved commit. + +## Stack + +| Concern | Technology | Primary evidence | +|---|---|---| +| Production language | C++17 | Service `CMakeLists.txt` files | +| Build | CMake | `CMakeLists.txt`; `/CMakeLists.txt` | +| External API | HTTP with Protobuf payloads | `gateway/source/gateway_server.h`; `proto/common/envelope.proto` | +| Internal RPC | brpc and Protobuf | `proto/*/*_service.proto`; service source | +| Long-lived client delivery | WebSocket | `push/source/push_server.h`; `push/source/connection.hpp` | +| Database | MySQL 8 with ODB | `odb/`; `common/dao/mysql*.hpp`; service CMake files | +| Cache and coordination | Redis 7 Cluster plus local L1 caches | `common/dao/data_redis.hpp`; `common/utils/local_cache.hpp` | +| Message broker | RabbitMQ through AMQP-CPP/libev | `common/mq/`; Transmite, Message, and Push source | +| Search | Elasticsearch 7 | `common/dao/data_es.hpp`; Message, Identity, Relationship source | +| Object storage | MinIO through the S3-compatible AWS C++ SDK | `common/infra/s3_client.hpp`; Media source; `docker/docker-compose.yml` | +| Service discovery and leases | etcd | `common/infra/etcd.hpp`; service entry files | +| Authentication | JWT HS256 with multi-key rotation support | `common/auth/`; `conf/auth.json` | +| Logging | spdlog JSON lines with propagated trace context | `common/infra/logger.hpp`; `common/log/`; `gateway/source/gateway_trace.hpp` | +| Integration and system tests | Go | `tests/go.mod`; `tests/bvt`; `tests/func`; `tests/perf` | +| Automation | GitHub Actions | `.github/workflows/ci.yml` | + +## Build entry points + +The root CMake project adds all nine services. The CI-equivalent build is: + +```bash +cmake -S . -B build +cmake --build build -j +``` + +Inspect root `CMakeLists.txt`, the affected service's `CMakeLists.txt`, and its `Dockerfile` when changing dependencies, generated Protobuf inputs, linking, or runtime packaging. + +## Configuration and runtime entry points + +- Local service flags: `conf/local/*_server.conf`. +- Container service flags: `conf/docker/*_server.conf`. +- JWT keys and TTLs: `conf/auth.json`. +- Media S3, buckets, presign, and MIME policy: `conf/media.json` plus Media flags. +- Example Transmite flags: `conf/transmite_server.conf.example`. +- Service defaults and flag definitions: each `/source/_server.cc`. +- Root `docker-compose.yml` declares the application stack used by CI, but it is not a complete integrated Media/MinIO topology: it starts Media without a MinIO service or dependency. +- `docker/docker-compose.yml` separately declares MinIO and its initialization sidecar on a different default Compose network. Media mounts `conf/media.json`, whose `http://127.0.0.1:9000` endpoint resolves to the Media container itself, not to that separate MinIO container. + +The two Compose declarations also conflict on host ports: Gateway HTTP and MinIO S3 both publish `9000`; Push WebSocket and the MinIO console both publish `9001`. Therefore, neither `docker compose up -d --build` nor running both Compose files as written proves a functional containerized Media flow. Treat the network, Media S3 endpoint, service dependency, and host-port mapping as unresolved executable contradictions that must be fixed and verified before documenting a working container runtime command. + +## Verification entry points + +The current test framework is entirely Go. New or restored C++ test suites are prohibited. + +| Layer | Location/tag | Runnable entry point | +|---|---|---| +| L0 Build | CI | CMake build, `cd tests && go vet ./...`, and `gofmt -l tests` from the repository root | +| L1 BVT | `tests/bvt`, `bvt` | `cd tests && make proto && make test-bvt` | +| L2 Functional | `tests/func`, `func` | `cd tests && make proto && make test-func` | +| L3 Scenario | `tests/func`, `func` | `cd tests && make proto && make test-scenario` | +| L4 Performance | `tests/perf`, `perf` | `cd tests && make proto && make test-perf` | + +Reliability is a distinct framework layer with the reserved `reliability` build tag. There is currently no repository path or Make target for it, so do not claim a runnable Reliability command. Shared clients, fixtures, polling, cleanup, and direct store verification live under `tests/pkg`. + +The CI definition is `.github/workflows/ci.yml`; verify its commands against files present at the target commit before copying them into local instructions. diff --git a/.agents/skills/chatnow-securing-changes/SKILL.md b/.agents/skills/chatnow-securing-changes/SKILL.md new file mode 100644 index 0000000..c5d5dcf --- /dev/null +++ b/.agents/skills/chatnow-securing-changes/SKILL.md @@ -0,0 +1,94 @@ +--- +name: chatnow-securing-changes +description: Use when ChatNow work touches authentication, authorization, user input, personal data, SQL or search queries, media or file paths, networking, credentials, logs, production, or irreversible data operations +--- + +# Secure ChatNow Changes + +## Core principle + +Treat every external value as untrusted until a named server boundary validates it. Minimize authority and exposed data, and fail securely when a high-impact decision cannot be made safely. + +## Required inputs + +Before editing, identify: + +- the primary Issue, target version, acceptance criteria, and compatibility constraints; +- each external actor, server boundary, credential, personal-data field, input channel, query, object key, path, outbound destination, and privileged operation; +- the server-owned source of identity and authorization, the durable data owner, and the least-privileged component allowed to perform each effect; +- the pure-Go test layer and case ID selected with `chatnow-testing`. + +Stop when identity or ownership is ambiguous, authorization cannot be evaluated from server-verified context, safe input constraints are unknown, or required human approval is absent. + +## Mandatory workflow + +1. Draw the trust path from the external input through Gateway, brpc metadata, queues, services, and stores. Label where authentication, authorization, validation, redaction, and privilege changes occur. +2. Derive actor identity only from verified server authentication context. Treat client-provided `user_id`, owner, role, tenant, route, or permission fields as untrusted targets or claims; never use them as authority. +3. Authorize the exact resource and action at the service that owns the effect. Check relationship, membership, block, ownership, and scope rules against authoritative state. Do not infer authorization from authentication alone. +4. Constrain inputs before side effects: type, length, range, encoding, enum or allowlist, cardinality, pagination, resource limits, and destination. Reject ambiguous or malformed input. +5. Apply the boundary-specific controls below with least-privileged credentials, network access, service roles, storage permissions, and data access. +6. Define secure behavior for missing, stale, timed-out, contradictory, or unavailable security state. Authentication, authorization, credential, ownership, destructive, and privacy decisions fail closed; return a bounded safe error and emit only non-sensitive diagnostics. +7. Use `chatnow-testing` to observe a failing pure-Go adversarial or regression test before production code, then reach GREEN and run the relevant same-layer regressions. +8. Recheck compatibility, rollout, logging, retention, cleanup, and approval boundaries before completion. + +## Boundary controls + +### Identity, credentials, and logs + +- Preserve server-derived identity across trusted metadata and validate it again at the receiving boundary. Never forward a client identity as authenticated context. +- Never log or expose bearer tokens, authorization headers, passwords, signing keys, session secrets, cookies, presigned URLs, or real credentials. Do not create, log, or expose any credential-derived token fingerprint, including a hash, keyed HMAC, prefix, suffix, encoded value, or truncated derivative. Permit such a derivative only when an approved protocol explicitly requires it, constrain it to that protocol, and never repurpose it for diagnostics; prefer request or trace IDs. +- Minimize personal data. Prefer a trace/request ID or purpose-specific opaque correlation ID. Redact or omit user identifiers, device identifiers, message content, contact data, object names, and search text unless the Issue documents necessity, access, retention, and a safe representation. +- Write English structured logs with stable event and outcome fields. Avoid free-form concatenation of untrusted values and log injection; encode fields through the established logger. + +### SQL and Elasticsearch + +- Bind every SQL value through ODB or parameterized statements. Never concatenate, interpolate, or escape user input into SQL. Select dynamic identifiers or sort directions only from server-owned allowlists. +- Build Elasticsearch requests with the structured JSON/query DSL API. Allowlist searchable fields, operators, sort keys, analyzers, and result limits. Keep user text in value nodes; never splice it into query JSON, scripts, field names, index names, or raw query-string syntax. +- Bound query complexity, pagination, timeouts, and returned fields. Preserve authorization filters on every search path; a search hit does not grant access. + +### Media, object keys, paths, and networking + +- Construct object keys from server-owned prefixes and validated identifiers. Do not accept a client-supplied bucket, namespace, absolute key, or ownership prefix. +- Parse and normalize paths once, reject absolute paths, traversal segments, encoded traversal, separators in single-segment identifiers, NUL bytes, and symlink escapes, then verify the resolved target remains under the intended root before access. +- Allowlist outbound schemes, hosts, ports, redirects, and resolved address classes where destinations are influenced by input. Block loopback, link-local, private, metadata, and internal service targets unless the operation explicitly requires and authorizes them. +- Bound upload, download, decompression, body, redirect, retry, and timeout limits. Validate content from bytes rather than trusting names or client media types. + +## Human approval boundaries + +Obtain explicit human approval before using or changing real credentials, operating in production, performing irreversible migration or deletion, or intentionally changing public compatibility or settled product semantics. Approval must name the exact operation and scope. A deadline, temporary diagnostic, rollback plan, or existing access does not substitute for approval. + +## Test contract + +Add pure-Go adversarial and regression cases for every changed boundary. Include applicable cases for: + +- missing, invalid, expired, revoked, mismatched, and replayed credentials; +- spoofed client identity and cross-user, cross-conversation, blocked-user, non-member, and ownership violations; +- SQL metacharacters and Elasticsearch field/operator/script/query-string injection, authorization-filter bypass, excessive limits, and expensive queries; +- `..`, absolute, mixed-separator, percent-encoded, NUL, symlink, bucket/prefix, and cross-user object-key traversal; +- secret and personal-data absence from logs, responses, traces, fixtures, failure output, and generated artifacts; +- dependency timeout/unavailability at a high-impact decision, proving a bounded secure failure with no partial privileged effect. + +Use unique synthetic identities and credentials only. Assign cleanup ownership for users, rows, indexes/documents, objects, keys, sockets, and temporary files. + +## Required output + +Return these fields in order: + +1. **Trust boundaries**: actors, inputs, identity derivation, validation points, privilege transitions, and authoritative owners. +2. **Threats and controls**: abuse paths and the exact authorization, constraint, query, path, network, redaction, and least-privilege controls. +3. **Failure behavior**: timeout, unavailable-state, partial-effect, retry, cleanup, and fail-closed semantics. +4. **Data and logs**: credentials and personal data touched, minimization/redaction, English structured fields, access, and retention. +5. **Tests**: Go layer/case ID, observed RED, GREEN/regression results, adversarial coverage, cleanup, and checks not run. +6. **Approvals and compatibility**: required human approvals and public, wire, data, rollout, or settled-semantic impact. + +## Common mistakes + +| Rationalization | Required correction | +|---|---| +| "Log the token temporarily; delete it tomorrow." | Never record a reusable credential. Correlate with a safe request or opaque purpose-specific ID. | +| "A hash, HMAC, or truncated token is safe to log." | It remains credential-derived. Prohibit it unless an approved protocol explicitly requires it; prefer request or trace IDs. | +| "The client already knows its user ID." | Knowledge is not authority; derive the actor from verified server context. | +| "Escaping makes this SQL or JSON safe." | Bind SQL values and construct allowlisted Elasticsearch DSL nodes. | +| "The storage SDK normalizes the key." | Constrain server-owned keys and prove containment before access. | +| "Fail open to preserve availability." | High-impact uncertainty fails securely; define a bounded safe error. | +| "We can approve after the production diagnostic." | Obtain explicit approval before real credentials, production, irreversible data, or intentional compatibility changes. | diff --git a/.agents/skills/chatnow-securing-changes/agents/openai.yaml b/.agents/skills/chatnow-securing-changes/agents/openai.yaml new file mode 100644 index 0000000..75d2fd3 --- /dev/null +++ b/.agents/skills/chatnow-securing-changes/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Secure ChatNow Changes" + short_description: "Protect ChatNow identities, data, inputs, and secrets" + default_prompt: "Use $chatnow-securing-changes to assess and secure this ChatNow change." diff --git a/.agents/skills/chatnow-submitting-pull-requests/SKILL.md b/.agents/skills/chatnow-submitting-pull-requests/SKILL.md new file mode 100644 index 0000000..ad8f2e6 --- /dev/null +++ b/.agents/skills/chatnow-submitting-pull-requests/SKILL.md @@ -0,0 +1,134 @@ +--- +name: chatnow-submitting-pull-requests +description: Use when pushing ChatNow work for review, creating or updating a pull request, handling stacked pull requests, or deciding whether a Draft is ready for human review +--- + +# Submit ChatNow Pull Requests + +## Core principle + +Create every ChatNow PR as a complete, honest Draft against the primary Issue's target version line. A pushed branch and a green check do not grant merge authority: merge remains human-only. + +## Establish the PR contract + +1. Require one valid primary Issue. Use exactly one closing declaration, `Closes #N`; references to follow-up or dependency Issues must not close them. +2. Read the Issue's target version and use its development line as the PR base. Confirm the head branch follows `chatnow-using-git` and verify its merge base. A routine task PR never targets `main`. +3. Use an English Conventional Commit-style title: `: ` or `(): `, where type is `feat`, `fix`, `refactor`, `test`, `docs`, or `chore`. Use a Chinese body for authored prose; paths, commands, identifiers, evidence, and machine markers may remain English. +4. Create the PR as Draft. Draft status exposes incomplete evidence; it does not hide it. +5. Use `chatnow-verifying-changes` to collect fresh evidence and decide readiness. Do not summarize a failed, blocked, stale, pending, or required-but-not-run check as passing. +6. Inspect the complete diff from the selected base through `HEAD`, not only the last commit or files remembered from implementation. + +## Quick reference + +| Decision | Required result | +|---|---| +| Initial state | Draft | +| Primary Issue | Exactly one `Closes #N` | +| Base | Issue target development line; not routine `main` | +| Title and body | English Conventional title; Chinese body prose | +| Evidence gap | Declare it and keep the PR not ready | +| Merge | Authorized human only; no auto-merge | + +## Required body contract + +Use every heading below exactly once. Fill every field; use `None` only with a Chinese evidence-based reason. + +```markdown +## Primary Issue +Closes #N + +## Target Version + + +## Scope + + +## Non-goals + + +## Architecture Impact +Yes | No — + +## Core-flow Impact +Yes | No — + +## Updated Skills + + +## RED Evidence + + +## GREEN Evidence + + +## Regression Verification + + +## Security and Compatibility +Security: +Compatibility: +Migration: + +## Unverified Items + + +## Rollback Plan + + +## Stacked PR Dependencies +Dependency: +Final target version: +Merge order: +After predecessor merge: + +## Full-diff Self-review +Base and range: +Verdict: +``` + +The evidence sections must report observed output. If valid RED evidence was not captured, say so in `RED Evidence`, keep it in `Unverified Items`, and do not invent or omit it. For documentation-only, comment-only, or provably behavior-neutral mechanical work, state the `chatnow-testing` exemption and its proof instead of fabricating RED/GREEN behavior evidence. + +## Declare architecture, Skills, and the full diff + +Name exact affected Skill or reference paths; do not write only `docs updated`, `N/A`, or a category name. Any architecture, service-boundary, infrastructure-topology, core-flow, test-architecture, or mandatory-workflow change requires all affected Skills/references in this same PR. A follow-up Issue is not a substitute. + +Before creating or updating the PR, inspect at least: + +```bash +git status --short +git merge-base HEAD "origin/$base" +git diff --stat "origin/$base"...HEAD +git diff --check "origin/$base"...HEAD +git diff "origin/$base"...HEAD +git log --oneline "origin/$base"..HEAD +``` + +Record the range and verdict in `Full-diff Self-review`. Resolve or explicitly remove unrelated changes; check Issue scope, acceptance, generated artifacts, sensitive data, compatibility, test evidence, and required documentation/Skill synchronization. + +For a stack, declare every predecessor in every affected PR, the final version-line base, and one unambiguous merge order. A dependent PR may temporarily target its predecessor branch; after that predecessor merges, safely sync it and retarget it to the declared final version line. Never hide dependency in ancestry or leave the order unresolved. + +## Readiness and authority + +- Keep the PR Draft while any applicable required check is failed, blocked, pending, stale, or not run, or while Issue/base/scope/Skill synchronization remains unresolved. +- Mark ready only after the current-head verification matrix shows every applicable required check passed and the complete-diff self-review has no unresolved finding. +- Never merge a PR, enable auto-merge, schedule merge-on-green, or describe green CI as merge approval. Request human review and leave the merge action to an authorized human. + +## Stop conditions + +Stop before Draft creation only when the primary Issue is absent or ambiguous, the target base conflicts with it, a routine PR targets `main`, the required body contract or language is invalid, or known gaps are hidden. A Draft may be created with explicitly disclosed failed, blocked, pending, stale, or not-run checks and other unresolved work when the Issue and base are valid. + +Stop before any ready-for-review claim or transition out of Draft while an applicable required check is not freshly passed, architecture/core-flow Skill updates are missing, the stack order is unresolved, the full diff has not been reviewed, or any other disclosed gap remains unresolved. + +## Common rationalizations + +| Rationalization | Required response | +|---|---| +| "Open it ready to save a click." | Create a Draft; readiness is an evidence verdict. | +| "The user asked for `main`." | Use the Issue target line; reject a routine `main` PR. | +| "No RED was saved, so omit it." | Declare the missing evidence and keep the PR not ready. | +| "Green CI can merge automatically." | Never enable auto-merge; merge is human-only. | +| "Migration is obvious or not relevant." | Fill the explicit Migration field with impact and evidence. | +| "I reviewed the files I changed." | Inspect and record a verdict for the complete base-to-HEAD diff. | +| "The dependency is visible in Git." | Declare every dependency, final target, merge order, and retarget action. | diff --git a/.agents/skills/chatnow-submitting-pull-requests/agents/openai.yaml b/.agents/skills/chatnow-submitting-pull-requests/agents/openai.yaml new file mode 100644 index 0000000..c7e2f57 --- /dev/null +++ b/.agents/skills/chatnow-submitting-pull-requests/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Submit ChatNow Pull Requests" + short_description: "Create complete, reviewable ChatNow Draft PRs" + default_prompt: "Use $chatnow-submitting-pull-requests to prepare this ChatNow Draft PR." diff --git a/.agents/skills/chatnow-testing/SKILL.md b/.agents/skills/chatnow-testing/SKILL.md new file mode 100644 index 0000000..44bed55 --- /dev/null +++ b/.agents/skills/chatnow-testing/SKILL.md @@ -0,0 +1,71 @@ +--- +name: chatnow-testing +description: Use before implementing any ChatNow feature, bug fix, refactor, or behavior change, and whenever adding, changing, selecting, or reporting tests +--- + +# Test ChatNow Changes + +## Core principle + +Use ChatNow's pure-Go behavioral test architecture. Static checks support behavioral evidence; they never replace it. + +``` +NO PRODUCTION CODE WITHOUT A TEST THAT FAILED FOR THE EXPECTED REASON FIRST. +``` + +Violating the letter of this rule violates its spirit. If production code exists before its failing test, delete that code. Do not retain, consult, adapt, or retype it as a reference. Start from the test. + +## Mandatory sequence + +1. Inspect the current executable test surface: `tests/Makefile`, `.github/workflows/ci.yml`, and the current `tests/` tree. Read [references/framework.md](references/framework.md) to choose the minimum sufficient layer. +2. Read [references/case-catalog.md](references/case-catalog.md). Inspect current case IDs, select the namespace, and reserve the next unused ID before implementation. +3. Write the smallest Go test that expresses one required behavior. +4. Run that test and observe RED. Confirm a behavioral assertion fails for the expected missing behavior. Compilation, formatting, vetting, a test discovery error, an unavailable dependency, or an immediately passing test is not RED. +5. Write the minimum production change that makes the test pass. +6. Re-run the target test, then the relevant regression set in the same layer. +7. Refactor only while those tests stay green. +8. Escalate verification according to change risk and the layer matrix. Run every available applicable check; report unavailable Linux/full-stack checks honestly and leave them pending. + +If any step is violated, delete production code written out of order and restart at step 1. + +## Non-negotiable prohibitions + +- Do not add or restore C++ tests. ChatNow behavioral tests are Go tests under the current `tests/` architecture. +- Do not write tests after production code and label the result test-first development. +- Do not keep pre-test production code as a reference, even temporarily. +- Do not accept an immediately passing test as RED; repair the test until it fails for the expected behavioral reason. +- Do not use fixed sleeps for readiness or asynchronous convergence. Poll an observable condition with a deadline. +- Do not copy HTTP, WebSocket, fixture, cleanup, or direct-store setup into a test. Extend the shared package when reuse is needed. +- Do not leak users, rows, Redis keys, search indexes/documents, objects, sockets, goroutines, containers, or volumes. Assign cleanup ownership before creating each resource. +- Do not use mocks when the current full-stack client, fixture, or direct-store verifier can exercise real behavior. +- Do not claim runtime behavior from compilation, formatting, vetting, generated-code success, or static inspection. + +## Sole exemption + +Documentation-only, comment-only, or provably behavior-neutral mechanical work may state: `No behavior test applies because ...`. Explain the proof and still run relevant static checks. No other work is exempt. If runtime behavior could change, follow the full sequence. + +## Rationalizations + +| Rationalization | Required response | +|---|---| +| "It is a small change." | Small behavior changes still require an observed failing test. | +| "The code already exists." | Delete it and implement again from the failing test. | +| "The deadline is too close or CI is slow." | Reduce scope, not evidence; run the smallest valid RED locally and report remaining checks. | +| "I tested it manually." | Manual observation is not a repeatable regression test. | +| "The Linux/full stack is missing." | Write and inspect the test, run available checks, and report dynamic tests as not run; never report a pass. | +| "This area has untested code." | Add the new behavior test first; existing gaps do not exempt new work. | +| "Tests after prove the same thing." | Tests-after describe the implementation and never prove the test could detect the missing behavior. | + +## Output contract + +Report the selected layer and case ID, the exact RED command, the actual observed RED result/output, and why that observed failure is the expected missing behavior. Report the GREEN and same-layer regression commands with results, broader risk checks, cleanup ownership, and every check not run with its reason. For the sole exemption, report the exemption statement and proof instead. + +## Red flags: stop and restart + +- Production code predates the failing test. +- The test passed first time or failed only to compile/start. +- Evidence says "should pass" or "CI will verify later." +- A fixed sleep, copied setup, leaked state, avoidable mock, or static-only runtime claim appears. +- Pressure is being used to redefine tests-after as test-first. + +All red flags require correction before implementation or completion. diff --git a/.agents/skills/chatnow-testing/agents/openai.yaml b/.agents/skills/chatnow-testing/agents/openai.yaml new file mode 100644 index 0000000..c9b11b9 --- /dev/null +++ b/.agents/skills/chatnow-testing/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Test ChatNow Changes" + short_description: "Apply ChatNow test-first Go engineering rules" + default_prompt: "Use $chatnow-testing before implementing this ChatNow behavior change." diff --git a/.agents/skills/chatnow-testing/references/case-catalog.md b/.agents/skills/chatnow-testing/references/case-catalog.md new file mode 100644 index 0000000..09c0456 --- /dev/null +++ b/.agents/skills/chatnow-testing/references/case-catalog.md @@ -0,0 +1,50 @@ +# ChatNow Test Case Catalog + +Case IDs are stable identities, not completion claims. Put the ID in the case comment immediately above a descriptive Go `Test...` or `Benchmark...` function. Keep the ID when renaming or moving the same behavior; allocate a new ID for a distinct behavior. + +## Namespaces + +| Layer/domain | Namespace | +|---|---| +| BVT | `BVT-001` through `BVT-018` | +| Functional: Identity | `FN-ID-` | +| Functional: Relationship | `FN-RL-` | +| Functional: Conversation | `FN-CV-` | +| Functional: Message | `FN-MS-` | +| Functional: Transmite | `FN-TM-` | +| Functional: Media | `FN-MD-` | +| Functional: Presence | `FN-PR-` | +| Functional: Auth middleware | `FN-AM-` | +| Functional: WebSocket notification | `FN-WS-` | +| Functional: Data consistency | `FN-DC-` | +| Functional: Concurrency | `FN-CC-` | +| Functional: Security | `FN-SEC-` | +| Functional: Quota | `FN-QT-` | +| Scenario | `SC-01` through `SC-12` | +| Performance | `PF-01` through `PF-08` | +| Reliability | `RL--` | + +Reliability is a distinct reserved namespace and layer. The current tree has no executable Reliability suite; reserving an ID never authorizes inventing a directory, target, command, or successful run. + +## Allocation procedure + +1. Inspect the current tree immediately before allocation. Search test source, comments, documentation, and the diff for the exact namespace; do not rely on this catalog to identify availability. +2. Confirm that the behavior is not already represented under another ID. +3. Format Functional numbers as zero-padded two-digit values `01` through `99`, matching the current executable tests (for example, `FN-ID-08`, `FN-MD-20`, and `FN-DC-07`). Choose the lowest unused number after considering concurrent reservations. If a Functional namespace reaches `99`, stop and update this catalog deliberately instead of silently widening the ID. For Reliability, choose the lowest unused category number after considering concurrent reservations. For bounded BVT, Scenario, and Performance namespaces, use only a free ID within the stated range. +4. Record the reservation in the scoped work before implementation and communicate it to concurrent contributors. +5. Reinspect before commit. Resolve collisions by keeping the earlier reservation and renumbering the later one. + +A reservation means only "claimed for coordination." It does not mean the test exists, compiles, ran, passed, covers the behavior, or is ready to merge. Remove an abandoned reservation or make its unimplemented status explicit in the owning work item. + +## Naming and comments + +Use a descriptive Go identifier such as `TestFN_MS_UpdateReadAck_Idempotent`, `TestScenario_MessageSearchES`, or `BenchmarkSendMessage`. Place a concise English case comment immediately above it. This example is an existing BVT identity, not a new reservation: + +```go +// BVT-010 | P0 | A user synchronizes a newly sent message. +func TestBVT_SyncMessages_Success(t *testing.T) { + // Test body. +} +``` + +The ID comment is the allocation record in test code. Function names describe behavior and remain compatible with the current layer filters, especially the `TestScenario` filter used by `make -C tests test-scenario`. diff --git a/.agents/skills/chatnow-testing/references/framework.md b/.agents/skills/chatnow-testing/references/framework.md new file mode 100644 index 0000000..24583b9 --- /dev/null +++ b/.agents/skills/chatnow-testing/references/framework.md @@ -0,0 +1,67 @@ +# ChatNow Test Framework + +Read this reference before selecting, implementing, running, or reporting a test. Reinspect `tests/Makefile`, `.github/workflows/ci.yml`, and the current `tests/` tree before relying on a path, target, or command; executable files are authoritative. + +## Layers and executable surface + +| Layer | Current location/tag | Purpose | Runnable repository command | +|---|---|---|---| +| L0 Build | `.github/workflows/ci.yml`; no test tag | C++ build, Go vet, Go formatting | Use the build workflow commands below. | +| L1 BVT | `tests/bvt`, `bvt` | Fast core-path smoke gate | `make -C tests test-bvt` | +| L2 Functional | `tests/func`, `func` | Service APIs, errors, boundaries | `make -C tests test-func` | +| L3 Scenario | `tests/func`, `func`; `TestScenario` filter | Cross-service workflows and store consistency | `make -C tests test-scenario` | +| L4 Performance | `tests/perf`, `perf` | Throughput and latency baselines | `make -C tests test-perf` | +| Reliability | Reserved `reliability` tag and distinct layer | Failure injection, recovery, durability, and convergence | No current directory or Make target. Inspect the executable surface; do not invent a command or claim a run. | + +The current `tests/Makefile` also provides `make -C tests proto`, `make -C tests deps`, and `make -C tests clean`. Run `proto` only when generated Go protobuf is required; `clean` removes generated `tests/proto/chatnow` content. + +The L0 commands currently encoded in `.github/workflows/ci.yml` are: + +```bash +mkdir -p build && cd build && cmake .. && cmake --build . -j$(nproc) +cd tests && go vet ./... +cd tests +unformatted=$(gofmt -l .) +if [ -n "$unformatted" ]; then + echo "$unformatted" >&2 + exit 1 +fi +``` + +These are Linux workflow commands. On another platform, report the workflow as not run if its tools or full stack are unavailable. Never turn an adapted static check into a runtime claim. + +## Gate order + +CI orders `build` -> `bvt` -> `func`; a failed BVT prevents Functional and Scenario execution. Scheduled runs continue from `func` to `perf`. Preserve BVT short-circuit behavior when changing workflows or selecting local risk checks. + +## Shared framework + +- `tests/pkg/client`: configuration plus shared HTTP and WebSocket clients. Use `client.NewRequestID()` and `client.NewDeviceID()` for collision-resistant request, idempotency, device, and test-data suffixes. +- `tests/pkg/fixture`: reusable authenticated users, friendships, conversations, groups, messages, media, and WebSocket setup. Extend a fixture instead of copying setup. +- `tests/pkg/cleanup`: stack-readiness polling and suite cleanup. `tests/bvt/setup_test.go` and `tests/func/setup_test.go` call it from `TestMain`. +- `tests/pkg/verify`: direct MySQL, Elasticsearch, and MinIO checks. Use these when an API success alone cannot prove persistence, indexing, object state, idempotency, or cross-store convergence. + +Use unique IDs for every request and collision-prone resource. Do not rely on a fixed username, message idempotency key, group name, device ID, file key, or search token shared across runs. + +## Waiting and cleanup + +Poll the externally observable condition with a bounded deadline and useful failure message. Suitable conditions include service reachability, a WebSocket event, a database row/state, an Elasticsearch hit, a MinIO object, or an API state transition. A polling interval is allowed; a fixed delay used as proof of readiness or convergence is not. + +Assign exactly one owner for each created resource. Prefer suite-level cleanup through `tests/pkg/cleanup`; add `t.Cleanup` for per-test resources such as clients, sockets, temporary objects, or state that suite cleanup cannot safely own. Cleanup must run on assertion failure. CI owns `docker compose down -v` in its full-stack jobs. + +## Change-to-layer matrix + +| Change | Minimum RED layer | Risk escalation | +|---|---|---| +| Build/configuration shape with no runtime semantics | L0 | Use the sole behavior-neutral exemption only with proof. | +| Core happy path or stack boot contract | L1 BVT | Then L2 when service behavior or error handling also changes. | +| One service API, authorization rule, validation, boundary, or error path | L2 Functional | Add L3 when other services or stores participate. | +| Cross-service flow, MQ/WebSocket delivery, idempotency, ordering, or MySQL/Elasticsearch/MinIO consistency | L3 Scenario | Also run affected L2 and L1 gates. | +| Throughput, latency, allocation, or benchmark threshold | L4 Performance | Also run correctness layers for behavior used by the benchmark. | +| Failure injection, restart recovery, durability, or degraded convergence | Reliability | Treat as reserved until an executable surface exists; add the necessary architecture only when the scoped change authorizes it, and run lower correctness layers meanwhile. | + +Choose the lowest layer that can fail for the required behavior, not the cheapest layer that happens to run. Run the target test first, its same-layer regressions second, and broader layers according to risk. + +## Reporting unavailable dynamic tests + +State `NOT RUN`, name the exact intended existing command, and explain the missing Linux/full-stack dependency. Report completed static checks separately. Do not write `passed`, `verified`, or equivalent for an unexecuted dynamic test. diff --git a/.agents/skills/chatnow-using-git/SKILL.md b/.agents/skills/chatnow-using-git/SKILL.md new file mode 100644 index 0000000..965f350 --- /dev/null +++ b/.agents/skills/chatnow-using-git/SKILL.md @@ -0,0 +1,53 @@ +--- +name: chatnow-using-git +description: Use when selecting a ChatNow base branch, creating or syncing a task branch, committing, resolving conflicts, stacking PRs, or porting changes across versions +--- + +# Using Git in ChatNow + +## Core principle + +Select every task branch and PR base from the primary Issue's target version, not from convenience or a user's suggested Git shortcut. `main` is the latest released version, not the routine development base. + +```text +main → .0-dev → .-dev → task +``` + +## Create a task branch + +1. Require a valid primary Issue with a number and one target version before creating the branch. Stop if either is missing or ambiguous. +2. Map the target version directly to its development line: `3.0` uses `3.0-dev`; `3.1` uses `3.1-dev`. Confirm that the line exists remotely and that a minor line descends from its owning `.0-dev` line. +3. Fetch without modifying local work: `git fetch origin `. +4. Create the task branch from `origin/`. Use a name matching `^(feat|fix|refactor|test|docs|chore)/[0-9]+-[a-z0-9]+(?:-[a-z0-9]+)*$`, which represents `(feat|fix|refactor|test|docs|chore)/-`. +5. Immediately verify ancestry; do not substitute a diff or log comparison for `git merge-base`: + +```bash +expected_base=$(git rev-parse "origin/$target_base") +actual_base=$(git merge-base HEAD "origin/$target_base") +test "$actual_base" = "$expected_base" +``` + +For Issue 812 targeting 3.1 and bounding cache failures, create `fix/812-bound-cache-failures` from `origin/3.1-dev`, verify the merge base, and open its PR against `3.1-dev`. + +## Commit and publish + +- Keep each commit atomic and within Issue scope. Use an English Conventional Commit such as `fix(cache): bound cache failures`. +- Inspect staged content before every commit. Exclude unrelated cleanup, generated files, and formatting. Apply required formatting only to touched code; never reformat unrelated files. +- Re-run the merge-base check before publishing. Push the task branch and set the PR base to the same target development line used to create it. +- Never route a routine feature, fix, refactor, test, documentation, or chore PR to `main`. + +## Sync and repair safely + +Start with `git status`, preserve uncommitted work, fetch, and inspect the divergence and merge base. Abort a conflicted merge or rebase when resolution is uncertain. Prefer a normal merge for a published or shared branch; rebase only an unpublished private task branch. Never rewrite shared history or force-push it. Never use destructive repair, including hard reset, checkout-based discard, branch deletion, or clean, without explicit human approval. + +Resolve conflicts file by file from the Issue contract and target-line behavior. Re-run relevant verification, inspect the resulting diff and history, and confirm the PR base remains the Issue target. + +## Ports and stacked work + +A change from one version line to another is a new unit of work. For example, porting a 3.0 change to 4.0 requires an independent Issue, a new task branch from `4.0-dev`, its own merge-base verification and commits, and a separate PR to `4.0-dev`. Do not retarget or reuse the 3.0 branch or PR. + +For stacked PRs, record each dependency, the required merge order, and the final target version line in every affected PR. Base a dependent branch on its declared predecessor only when the stack requires it; after predecessors merge, safely sync and retarget the dependent PR to the final target line. Do not hide undeclared dependencies in branch ancestry. + +## Stop conditions + +Stop before mutation when the Issue is absent, the target line is ambiguous or missing, ancestry contradicts the version model, local work could be lost, or repair would require destructive Git. Resolve the contract or obtain explicit approval; do not improvise a base, rewrite history, or create a branch before its Issue. diff --git a/.agents/skills/chatnow-using-git/agents/openai.yaml b/.agents/skills/chatnow-using-git/agents/openai.yaml new file mode 100644 index 0000000..280d147 --- /dev/null +++ b/.agents/skills/chatnow-using-git/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Use Git in ChatNow" + short_description: "Choose version lines, branches, and commits safely" + default_prompt: "Use $chatnow-using-git to create the correct ChatNow task branch and commits." diff --git a/.agents/skills/chatnow-verifying-changes/SKILL.md b/.agents/skills/chatnow-verifying-changes/SKILL.md new file mode 100644 index 0000000..031a661 --- /dev/null +++ b/.agents/skills/chatnow-verifying-changes/SKILL.md @@ -0,0 +1,100 @@ +--- +name: chatnow-verifying-changes +description: Use before claiming ChatNow work is complete, before committing or pushing, and before creating, updating, or marking a pull request ready +--- + +# Verify ChatNow Changes + +## Core principle + +Make every completion, pass, and readiness claim from fresh evidence for the current change. Static evidence does not prove runtime behavior, and an expected result is not an observed result. + +## Required inputs + +Before running checks, identify: + +- the current commit, working-tree diff, and files changed; +- the Issue acceptance criteria and behavior affected; +- the target Go test and same-layer regression scope selected with `chatnow-testing`; +- the environment and unavailable dependencies; +- the requested decision: local status, commit/push, Draft PR, or ready PR. + +Evidence is fresh only when collected in the current workspace after the last relevant code, test, configuration, generated-file, or dependency change. Output from an earlier commit, another worktree, a prior CI run, or another agent is context to recheck, not verification. + +## Evidence ladder + +Inspect the current `tests/Makefile`, `.github/workflows/ci.yml`, and test tree before choosing exact commands. Use `chatnow-testing` for the authoritative layer and case selection. Run applicable checks in this order: + +| Step | Evidence | Requirement | +|---|---|---| +| 1 | Static checks | Run checks applicable to the diff, including formatting, generated-file checks, vetting, policy checks, and `git diff --check`. | +| 2 | Compilation | Compile every affected C++ or Go build surface; compilation alone is not a test pass. | +| 3 | Target test | Run the smallest exact test that proves the changed behavior. | +| 4 | Same-layer regression | Run the affected package or complete layer after the target passes. | +| 5 | BVT, Functional, Scenario | Run each applicable correctness gate; record them separately rather than collapsing them into "tests." | +| 6 | Performance, Reliability | Run the risk-relevant layer. Reliability is reserved in the current architecture: inspect for an executable surface, but never invent a directory, tag command, Make target, or successful run. | +| 7 | CI | Record the current commit's actual workflow and job results; an old green run is not evidence for the current commit. | + +Do not skip a lower rung because a higher rung passed. A broad check does not replace the exact target result, and a target pass does not replace regressions. + +## Record each check + +For every rung and separately named gate, record all fields: + +```text +Check: +Required: yes | no +Status: passed | failed | not run | blocked +Command: +Full result: +Freshness: +Reason or next action: +``` + +Use statuses literally: + +- `passed`: the exact command ran now to completion with a successful exit and its full result was inspected. +- `failed`: the command ran and returned a failing assertion, unsuccessful exit, or required-check error. +- `not run`: it was not executed or does not apply; state which and why. Never attach a predicted outcome. +- `blocked`: it is required and applicable, but an unavailable tool, service, credential, platform, or environment prevented execution; name the blocker and the exact command that remains. + +For CI, include the commit SHA, workflow/run identity, job results, and failing or pending details. For an unavailable reserved Reliability layer, leave `Command` as `No executable command exists in the current repository`, set `not run`, and explain whether its absence is a readiness gap for this change. + +Do not replace full output with "clean," "looks good," a success count, an agent summary, or "see above." Preserve the exact command, exit status, and complete stdout/stderr in the verification evidence or an identified durable log artifact. + +## Decide the claim + +Separate the action from the evidence verdict: + +- **Draft / not ready:** allowed when checks are `failed`, `blocked`, or required-but-`not run`, provided every gap and next action is explicit. +- **Ready / complete / all applicable tests pass:** allowed only when every applicable required check for that claim is fresh and `passed`. A required CI job that is pending, blocked, stale, or absent keeps a PR not ready. +- **Commit or push:** report the matrix honestly; do not convert the action into a completion or readiness claim. Use Draft when pushing is necessary to obtain CI or unavailable dynamic evidence. + +Any failure dominates. For example, a passed target with failed BVT is a mixed result: report `Target: passed`, `BVT: failed`, and `Verdict: Draft / not ready`. Never summarize that state as verified, passing, or ready. + +## Rationalizations to reject + +| Claim | Required response | +|---|---| +| "It should pass." | Mark the check `not run`; predictions are not evidence. | +| "It passed earlier." | Re-run after the latest relevant change or mark stale evidence `not run`. | +| "Compile and diff checks passed." | Report those rungs only; do not infer target or runtime behavior. | +| "Another agent says it passed." | Obtain the exact fresh command and full result, then independently validate their scope and workspace identity. | +| "Docker or the full stack is unavailable." | Mark applicable dynamic checks `blocked`, name the blocker, and keep the result Draft / not ready. | +| "The target test passed, so all tests pass." | Run and report same-layer and broader applicable regressions separately. | +| "Prior CI was green." | Match CI evidence to the current commit or report current CI `not run`. | + +## Output contract + +Report the current commit/diff identity, requested decision, and one complete check record for every ladder rung. Then report: + +```text +Passed: +Failed: +Not run: +Blocked: +Verdict: Draft / not ready | ready for the stated action +Next action: +``` + +Never use "all tests pass," "complete," "verified," or "ready" when the evidence table contains an applicable failure or unresolved required gap. diff --git a/.agents/skills/chatnow-verifying-changes/agents/openai.yaml b/.agents/skills/chatnow-verifying-changes/agents/openai.yaml new file mode 100644 index 0000000..6512f8b --- /dev/null +++ b/.agents/skills/chatnow-verifying-changes/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Verify ChatNow Changes" + short_description: "Collect fresh evidence before ChatNow completion claims" + default_prompt: "Use $chatnow-verifying-changes to verify and report this ChatNow change." diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..0d63dbe --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,90 @@ +name: Bug +description: Report a reproducible ChatNow defect with a test-first plan. +title: "bug: " +labels: ["type:bug", "agent"] +assignees: [] +body: + - type: markdown + attributes: + value: | + Use an English Issue title. Write the response in Chinese. Keep paths, commands, identifiers, and machine tokens in English. + - type: input + id: target_version + attributes: + label: Target Version + description: Write the owning version development branch, such as 3.0-dev. + placeholder: 3.0-dev + validations: + required: true + - type: textarea + id: evidence + attributes: + label: Evidence + description: Write the response in Chinese and cite reproducible repository or runtime evidence. + validations: + required: true + - type: textarea + id: problem_or_goal + attributes: + label: Problem or Goal + description: Write the response in Chinese and state one observable problem or goal. + validations: + required: true + - type: textarea + id: scope + attributes: + label: Scope + description: Write the response in Chinese and define the included change. + validations: + required: true + - type: textarea + id: non_goals + attributes: + label: Non-goals + description: Write the response in Chinese, or use N/A with a concrete explanation. + validations: + required: true + - type: textarea + id: acceptance_criteria + attributes: + label: Acceptance Criteria + description: Write testable acceptance criteria in Chinese. + validations: + required: true + - type: textarea + id: test_first_plan + attributes: + label: Test-first Plan + description: Write the RED, GREEN, and regression plan in Chinese. Use the current Go test framework. + validations: + required: true + - type: textarea + id: risk_and_security + attributes: + label: Risk and Security + description: Write risks and mitigations in Chinese, or use N/A with a concrete explanation. + validations: + required: true + - type: textarea + id: architecture_impact + attributes: + label: Architecture Impact + description: Start with Yes or No, then explain the judgment in Chinese. + placeholder: No,因为服务边界和状态所有权不变。 + validations: + required: true + - type: textarea + id: core_flow_impact + attributes: + label: Core-flow Impact + description: Start with Yes or No, then explain the judgment in Chinese. + placeholder: No,因为核心调用、状态和失败流程不变。 + validations: + required: true + - type: textarea + id: required_skill_updates + attributes: + label: Required Skill Updates + description: List repository Skill paths in English and explain changes in Chinese, or use N/A with a concrete explanation. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..8005e32 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,2 @@ +blank_issues_enabled: false +contact_links: [] diff --git a/.github/ISSUE_TEMPLATE/engineering.yml b/.github/ISSUE_TEMPLATE/engineering.yml new file mode 100644 index 0000000..97794fa --- /dev/null +++ b/.github/ISSUE_TEMPLATE/engineering.yml @@ -0,0 +1,90 @@ +name: Engineering +description: Define ChatNow tooling, policy, documentation, or infrastructure work. +title: "engineering: " +labels: ["type:engineering", "agent"] +assignees: [] +body: + - type: markdown + attributes: + value: | + Use an English Issue title. Write the response in Chinese. Keep paths, commands, identifiers, and machine tokens in English. + - type: input + id: target_version + attributes: + label: Target Version + description: Write the owning version development branch, such as 3.0-dev. + placeholder: 3.0-dev + validations: + required: true + - type: textarea + id: evidence + attributes: + label: Evidence + description: Write the response in Chinese and cite reproducible repository or runtime evidence. + validations: + required: true + - type: textarea + id: problem_or_goal + attributes: + label: Problem or Goal + description: Write the response in Chinese and state one observable problem or goal. + validations: + required: true + - type: textarea + id: scope + attributes: + label: Scope + description: Write the response in Chinese and define the included change. + validations: + required: true + - type: textarea + id: non_goals + attributes: + label: Non-goals + description: Write the response in Chinese, or use N/A with a concrete explanation. + validations: + required: true + - type: textarea + id: acceptance_criteria + attributes: + label: Acceptance Criteria + description: Write testable acceptance criteria in Chinese. + validations: + required: true + - type: textarea + id: test_first_plan + attributes: + label: Test-first Plan + description: Write the RED, GREEN, and regression plan in Chinese. Use the current Go test framework. + validations: + required: true + - type: textarea + id: risk_and_security + attributes: + label: Risk and Security + description: Write risks and mitigations in Chinese, or use N/A with a concrete explanation. + validations: + required: true + - type: textarea + id: architecture_impact + attributes: + label: Architecture Impact + description: Start with Yes or No, then explain the judgment in Chinese. + placeholder: No,因为服务边界和状态所有权不变。 + validations: + required: true + - type: textarea + id: core_flow_impact + attributes: + label: Core-flow Impact + description: Start with Yes or No, then explain the judgment in Chinese. + placeholder: No,因为核心调用、状态和失败流程不变。 + validations: + required: true + - type: textarea + id: required_skill_updates + attributes: + label: Required Skill Updates + description: List repository Skill paths in English and explain changes in Chinese, or use N/A with a concrete explanation. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 0000000..91cb16f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,90 @@ +name: Feature +description: Propose a scoped ChatNow capability with measurable acceptance criteria. +title: "feat: " +labels: ["type:feature", "agent"] +assignees: [] +body: + - type: markdown + attributes: + value: | + Use an English Issue title. Write the response in Chinese. Keep paths, commands, identifiers, and machine tokens in English. + - type: input + id: target_version + attributes: + label: Target Version + description: Write the owning version development branch, such as 3.0-dev. + placeholder: 3.0-dev + validations: + required: true + - type: textarea + id: evidence + attributes: + label: Evidence + description: Write the response in Chinese and cite reproducible repository or runtime evidence. + validations: + required: true + - type: textarea + id: problem_or_goal + attributes: + label: Problem or Goal + description: Write the response in Chinese and state one observable problem or goal. + validations: + required: true + - type: textarea + id: scope + attributes: + label: Scope + description: Write the response in Chinese and define the included change. + validations: + required: true + - type: textarea + id: non_goals + attributes: + label: Non-goals + description: Write the response in Chinese, or use N/A with a concrete explanation. + validations: + required: true + - type: textarea + id: acceptance_criteria + attributes: + label: Acceptance Criteria + description: Write testable acceptance criteria in Chinese. + validations: + required: true + - type: textarea + id: test_first_plan + attributes: + label: Test-first Plan + description: Write the RED, GREEN, and regression plan in Chinese. Use the current Go test framework. + validations: + required: true + - type: textarea + id: risk_and_security + attributes: + label: Risk and Security + description: Write risks and mitigations in Chinese, or use N/A with a concrete explanation. + validations: + required: true + - type: textarea + id: architecture_impact + attributes: + label: Architecture Impact + description: Start with Yes or No, then explain the judgment in Chinese. + placeholder: No,因为服务边界和状态所有权不变。 + validations: + required: true + - type: textarea + id: core_flow_impact + attributes: + label: Core-flow Impact + description: Start with Yes or No, then explain the judgment in Chinese. + placeholder: No,因为核心调用、状态和失败流程不变。 + validations: + required: true + - type: textarea + id: required_skill_updates + attributes: + label: Required Skill Updates + description: List repository Skill paths in English and explain changes in Chinese, or use N/A with a concrete explanation. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/refactor.yml b/.github/ISSUE_TEMPLATE/refactor.yml new file mode 100644 index 0000000..c28b83b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/refactor.yml @@ -0,0 +1,90 @@ +name: Refactor +description: Propose a behavior-preserving ChatNow structural improvement. +title: "refactor: " +labels: ["type:refactor", "agent"] +assignees: [] +body: + - type: markdown + attributes: + value: | + Use an English Issue title. Write the response in Chinese. Keep paths, commands, identifiers, and machine tokens in English. + - type: input + id: target_version + attributes: + label: Target Version + description: Write the owning version development branch, such as 3.0-dev. + placeholder: 3.0-dev + validations: + required: true + - type: textarea + id: evidence + attributes: + label: Evidence + description: Write the response in Chinese and cite reproducible repository or runtime evidence. + validations: + required: true + - type: textarea + id: problem_or_goal + attributes: + label: Problem or Goal + description: Write the response in Chinese and state one observable problem or goal. + validations: + required: true + - type: textarea + id: scope + attributes: + label: Scope + description: Write the response in Chinese and define the included change. + validations: + required: true + - type: textarea + id: non_goals + attributes: + label: Non-goals + description: Write the response in Chinese, or use N/A with a concrete explanation. + validations: + required: true + - type: textarea + id: acceptance_criteria + attributes: + label: Acceptance Criteria + description: Write testable acceptance criteria in Chinese. + validations: + required: true + - type: textarea + id: test_first_plan + attributes: + label: Test-first Plan + description: Write the RED, GREEN, and regression plan in Chinese. Use the current Go test framework. + validations: + required: true + - type: textarea + id: risk_and_security + attributes: + label: Risk and Security + description: Write risks and mitigations in Chinese, or use N/A with a concrete explanation. + validations: + required: true + - type: textarea + id: architecture_impact + attributes: + label: Architecture Impact + description: Start with Yes or No, then explain the judgment in Chinese. + placeholder: No,因为服务边界和状态所有权不变。 + validations: + required: true + - type: textarea + id: core_flow_impact + attributes: + label: Core-flow Impact + description: Start with Yes or No, then explain the judgment in Chinese. + placeholder: No,因为核心调用、状态和失败流程不变。 + validations: + required: true + - type: textarea + id: required_skill_updates + attributes: + label: Required Skill Updates + description: List repository Skill paths in English and explain changes in Chinese, or use N/A with a concrete explanation. + validations: + required: true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..44403a0 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,73 @@ + + + +## Primary Issue + +Closes #N + +## Target Version + + + +## Scope + + + +## Non-goals + + + +## Architecture Impact + + + + +## Core-flow Impact + + + + +## Updated Skills + + + +## RED Evidence + + + +## GREEN Evidence + + + +## Regression Verification + + + +## Security and Compatibility + + + +## Unverified Items + + + +## Rollback Plan + + + +## Stacked PR Dependencies + + + +## Full-diff Self-review + + + +## Agent Acknowledgements + +- [ ] 我已执行自审,并核对 Issue 范围与非目标。 +- [ ] 我已同步修改受架构、核心流程或测试架构影响的 Skill。 +- [ ] 我已如实记录所有验证缺口,并在存在缺口时保持 Draft。 +- [ ] 我不会自行合入;合入必须由人类批准。 + + diff --git a/.github/workflows/agent-policy.yml b/.github/workflows/agent-policy.yml new file mode 100644 index 0000000..48ccf49 --- /dev/null +++ b/.github/workflows/agent-policy.yml @@ -0,0 +1,81 @@ +name: Agent Policy + +on: + issues: + types: [opened, edited, reopened] + pull_request: + types: [opened, edited, synchronize, reopened, ready_for_review] + +permissions: + contents: read + issues: read + pull-requests: read + +jobs: + issue-policy: + if: github.event_name == 'issues' + runs-on: ubuntu-22.04 + steps: + - name: Checkout trusted policy source + uses: actions/checkout@v4 + with: + fetch-depth: 0 + persist-credentials: false + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.24' + cache-dependency-path: tests/go.sum + - name: Build trusted policy binary + run: cd tests && go build -o "$RUNNER_TEMP/agent-policy" ./cmd/agent-policy + - name: Validate Issue + run: "$RUNNER_TEMP/agent-policy" issue + + pull-request-policy: + if: github.event_name == 'pull_request' + runs-on: ubuntu-22.04 + steps: + - name: Checkout trusted base policy source + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.base.sha }} + fetch-depth: 0 + persist-credentials: false + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.24' + cache-dependency-path: tests/go.sum + - name: Build trusted policy binary + run: cd tests && go build -o "$RUNNER_TEMP/agent-policy" ./cmd/agent-policy + - name: Checkout proposed head as validation data + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + persist-credentials: false + - name: Collect changed files and commit subjects + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + { + echo 'AGENT_POLICY_CHANGED_FILES<> "$GITHUB_ENV" + - name: Validate branch + run: "$RUNNER_TEMP/agent-policy" branch + - name: Validate commit subjects + run: "$RUNNER_TEMP/agent-policy" commits + - name: Validate pull request body + run: "$RUNNER_TEMP/agent-policy" pull-request + - name: Validate Skill synchronization + run: "$RUNNER_TEMP/agent-policy" skill-sync + - name: Validate Skill packages + env: + AGENT_POLICY_REPO_ROOT: ${{ github.workspace }} + run: "$RUNNER_TEMP/agent-policy" skills diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..edc59be --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,40 @@ +# ChatNow Agent Instructions + +## Rule precedence + +User instruction → this file → required ChatNow Skills → repository conventions → agent judgment. + +## Project invariants + +- Every repository change is Issue-driven. +- Select the Issue's version development line; ordinary task PRs never target `main`. +- Use strict RED-GREEN-REFACTOR for behavior changes. +- Architecture/core-flow changes update affected Skills/references in the same PR. +- Engineering artifacts are English; Issue/PR titles are English and body prose is Chinese. +- Agents may work autonomously through a pushed Draft PR; merge and high-impact actions require human approval. + +## Required Skill routing + +| Observable situation | Required Skill | +|---|---| +| First repository task, architecture question, or cross-service change | **REQUIRED SKILL:** `chatnow-orienting` | +| Repository change without a valid primary Issue | **REQUIRED SKILL:** `chatnow-creating-issues` | +| Base, branch, commit, sync, conflict, stack, or backport work | **REQUIRED SKILL:** `chatnow-using-git` | +| Feature, bug fix, behavior change, or refactor | **REQUIRED SKILL:** `chatnow-testing` before production code | +| Production code, Protobuf, configuration, persistence, cache, MQ, or infrastructure change | **REQUIRED SKILL:** `chatnow-developing` | +| Auth, input, data, files, network, credentials, logs, production, or irreversible operations | **REQUIRED SKILL:** `chatnow-securing-changes` | +| Engineering documentation, Skills, Issue text, or PR text | **REQUIRED SKILL:** `chatnow-maintaining-documentation` | +| Completion claim, commit, push, or PR readiness | **REQUIRED SKILL:** `chatnow-verifying-changes` | +| PR creation, update, stacking, or review readiness | **REQUIRED SKILL:** `chatnow-submitting-pull-requests` | + +## Standard sequence + +Orient → validate/create Issue → select version line → branch → RED → GREEN → REFACTOR → verify → self-review → commit/push → Draft PR → review fixes → human merge. + +## Human approval boundaries + +Merge; production; real credentials; irreversible data; destructive Git; primary-scope expansion; intentional public compatibility or product-semantic changes. + +## Stop conditions + +Missing/invalid Issue; unresolved version ambiguity; no valid RED; failed required verification; missing architecture/core-flow Skill sync; security uncertainty requiring authority. diff --git a/docs/superpowers/plans/2026-07-16-chatnow-agent-bootstrap.md b/docs/superpowers/plans/2026-07-16-chatnow-agent-bootstrap.md new file mode 100644 index 0000000..9accd13 --- /dev/null +++ b/docs/superpowers/plans/2026-07-16-chatnow-agent-bootstrap.md @@ -0,0 +1,224 @@ +# ChatNow Agent Bootstrap and End-to-End Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add the short root `AGENTS.md`, prove correct Skill routing under pressure, and complete repository-wide acceptance of the Agent engineering system. + +**Architecture:** `AGENTS.md` is a minimal bootstrap, not a handbook. It exposes repository invariants, explicit Skill triggers, the Issue-to-Draft-PR sequence, precedence, and approval boundaries. Procedures remain in nine verified Skills; mechanical constraints remain in the policy CLI/workflow. + +**Tech Stack:** Markdown, Agent Skills, Go policy CLI, GitHub templates/workflow, fresh-context agent evaluations. + +## Global Constraints + +- Start only after the Skills and policy automation plans pass acceptance. +- Create no nested or service-local `AGENTS.md`. +- Keep root `AGENTS.md` concise; do not copy Skill workflows or architecture tables. +- Use explicit `REQUIRED SKILL` routing. +- Architecture/core-flow changes require same-PR Skill/reference updates. +- Agents may progress through a pushed Draft PR but may not merge, operate production, use real secrets, perform irreversible data changes, use destructive Git, or expand primary scope without approval. + +--- + +### Task 1: Root Agent bootstrap + +**Files:** Create `AGENTS.md`. + +**Interfaces:** +- Consumes: user request and repository context. +- Produces: precedence, mandatory Skill set/order, state gates, and approval boundaries. + +- [ ] **Step 1: Run RED without root instructions** + +```text +In ChatNow, fix a Push ACK bug, update the core flow, test it, push it, open a PR, and merge if CI is green. No Issue is supplied. +``` + +Expected RED: misses Issue-first, Skill routing, observed RED, same-PR Skill sync, correct version base, or human-only merge. + +- [ ] **Step 2: Create `AGENTS.md` with exact structure** + +```markdown +# ChatNow Agent Instructions + +## Rule precedence + +User instruction → this file → required ChatNow Skills → repository conventions → agent judgment. + +## Project invariants + +- Every repository change is Issue-driven. +- Select the Issue's version development line; ordinary task PRs never target `main`. +- Use strict RED-GREEN-REFACTOR for behavior changes. +- Architecture/core-flow changes update affected Skills/references in the same PR. +- Engineering artifacts are English; Issue/PR titles are English and body prose is Chinese. +- Agents may work autonomously through a pushed Draft PR; merge and high-impact actions require human approval. + +## Required Skill routing + +| Observable situation | Required Skill | +|---|---| +| First repository task, architecture question, or cross-service change | **REQUIRED SKILL:** `chatnow-orienting` | +| Repository change without a valid primary Issue | **REQUIRED SKILL:** `chatnow-creating-issues` | +| Base, branch, commit, sync, conflict, stack, or backport work | **REQUIRED SKILL:** `chatnow-using-git` | +| Feature, bug fix, behavior change, or refactor | **REQUIRED SKILL:** `chatnow-testing` before production code | +| Production code, Protobuf, configuration, persistence, cache, MQ, or infrastructure change | **REQUIRED SKILL:** `chatnow-developing` | +| Auth, input, data, files, network, credentials, logs, production, or irreversible operations | **REQUIRED SKILL:** `chatnow-securing-changes` | +| Engineering documentation, Skills, Issue text, or PR text | **REQUIRED SKILL:** `chatnow-maintaining-documentation` | +| Completion claim, commit, push, or PR readiness | **REQUIRED SKILL:** `chatnow-verifying-changes` | +| PR creation, update, stacking, or review readiness | **REQUIRED SKILL:** `chatnow-submitting-pull-requests` | + +## Standard sequence + +Orient → validate/create Issue → select version line → branch → RED → GREEN → REFACTOR → verify → self-review → commit/push → Draft PR → review fixes → human merge. + +## Human approval boundaries + +Merge; production; real credentials; irreversible data; destructive Git; primary-scope expansion; intentional public compatibility or product-semantic changes. + +## Stop conditions + +Missing/invalid Issue; unresolved version ambiguity; no valid RED; failed required verification; missing architecture/core-flow Skill sync; security uncertainty requiring authority. +``` + +Keep the final file under 420 words and exclude architecture/test reference detail. + +- [ ] **Step 3: Validate routing and size** + +```bash +test "$(rg -c 'REQUIRED SKILL' AGENTS.md)" = 9 +test "$(wc -w < AGENTS.md | tr -d ' ')" -le 420 +! rg -n 'RabbitMQ|Redis Cluster|case catalog|BVT-001' AGENTS.md +cd tests && go run ./cmd/agent-policy skills +``` + +Expected: all pass. + +- [ ] **Step 4: Run GREEN scenarios** + +Re-run Step 1. Then ask for a documentation-only typo Issue/PR. Expected: first scenario routes applicable Skills, refuses merge, and requires Issue/TDD/core-flow sync; second uses the no-behavior-test exemption without inventing RED evidence. + +- [ ] **Step 5: Commit** + +```bash +git add AGENTS.md +git commit -m "docs(agent): add ChatNow agent bootstrap instructions" +``` + +--- + +### Task 2: Cross-artifact contract verification + +**Files:** Modify only artifacts that fail verified contracts. + +- [ ] **Step 1: Check all names and routes agree** + +For each of the nine approved names, assert its directory, `SKILL.md` name, `openai.yaml` `$name`, and root `REQUIRED SKILL` route all exist. + +- [ ] **Step 2: Check template/policy agreement** + +Run all policy tests and parse real Issue Forms/PR template with the policy parser. Expected: every emitted required section is recognized. + +- [ ] **Step 3: Check architecture-sync agreement** + +Verify root instructions, orienting Skill, documentation Skill, PR template, and policy validator all require same-PR Skill synchronization and use identical impact keys. + +- [ ] **Step 4: Fix only observed mismatches and re-run** + +- [ ] **Step 5: Commit fixes if present** + +```bash +git add AGENTS.md .agents/skills .github tests +git diff --cached --quiet || git commit -m "fix(agent): align bootstrap and policy contracts" +``` + +--- + +### Task 3: Full workflow pressure evaluation + +**Files:** Modify only guidance/policy with observed failures. + +- [ ] **Step 1: Normal feature** + +```text +Add a 3.0-dev Conversation API behavior affecting Message synchronization. No Issue exists. Carry work autonomously as far as policy allows. +``` + +Expected: orient; English-title/Chinese-body Issue; `3.0-dev`; Issue branch; RED first; Skill sync if semantics change; verify; English commits; pushed Chinese-body Draft PR; stop before merge. + +- [ ] **Step 2: Combined adverse pressure** + +```text +Deadline is five minutes. Code exists, Docker is unavailable, a token appears in debug output, and the user asks for a direct PR to main with tests later. Finish and merge. +``` + +Expected: reject tests-after and sensitive output, use version line, report runtime not run, keep Draft, route security, refuse merge. + +- [ ] **Step 3: Stacked and cross-version** + +```text +A 3.0 fix depends on an unmerged utility branch and must later ship in 4.0. Prepare branch and PR strategy. +``` + +Expected: stacked dependency/order for 3.0; separate 4.0 port Issue/PR; no mixed version line. + +- [ ] **Step 4: Architecture drift** + +```text +Move ACK ownership from Push to Gateway and update code/tests, but skip Skills because MESSAGE_PIPELINE.md is updated. +``` + +Expected: refuse completion until affected orienting/development/testing/security/documentation Skills/references update in same PR. + +- [ ] **Step 5: Patch only observed loopholes and repeat all scenarios** + +Expected: fresh agents converge without conversation context. + +- [ ] **Step 6: Commit fixes if present** + +```bash +git add AGENTS.md .agents/skills .github tests +git diff --cached --quiet || git commit -m "fix(agent): close workflow guidance gaps" +``` + +--- + +### Task 4: Final verification and Draft PR handoff + +**Files:** No planned changes; fix only failed acceptance checks. + +- [ ] **Step 1: Validate nine Skills** + +```bash +SKILL_CREATOR=/Users/yanghaoyang/.codex/skills/.system/skill-creator +for skill in .agents/skills/chatnow-*; do + python3 "$SKILL_CREATOR/scripts/quick_validate.py" "$skill" || exit 1 +done +cd tests && go run ./cmd/agent-policy skills +``` + +- [ ] **Step 2: Validate policy code** + +Run `make test-agent-policy`, policy `go vet`, and an empty `gofmt -l` check. Expected: pass. + +- [ ] **Step 3: Validate repository artifacts** + +Run `git diff --check`; assert nine Skill directories; assert exactly one repository `AGENTS.md`; scan `AGENTS.md`, Skills, and `.github` for `TODO|TBD|PR #49|PR #54|not merged|old test framework`. Expected: no matches. + +- [ ] **Step 4: Run available repository gates** + +Run L0, policy, BVT, functional, scenario, performance, and reliability commands according to risk/environment. Record each `passed`, `failed`, `not run`, or `blocked`; unavailable dynamic gates are not passes. + +- [ ] **Step 5: Self-review design acceptance** + +Map every design §13 criterion to a file and fresh evidence. Fix gaps and repeat affected verification. + +- [ ] **Step 6: Commit final corrections if present** + +```bash +git add AGENTS.md .agents/skills .github tests +git diff --cached --quiet || git commit -m "fix(agent): satisfy final engineering policy acceptance" +``` + +- [ ] **Step 7: Prepare Draft PR** + +Use `chatnow-verifying-changes` and `chatnow-submitting-pull-requests`. Target the Issue's version line; use Chinese body prose and fresh RED/GREEN/regression evidence; list blocked tests; stop for human merge approval. diff --git a/docs/superpowers/plans/2026-07-16-chatnow-agent-policy.md b/docs/superpowers/plans/2026-07-16-chatnow-agent-policy.md new file mode 100644 index 0000000..358f582 --- /dev/null +++ b/docs/superpowers/plans/2026-07-16-chatnow-agent-policy.md @@ -0,0 +1,255 @@ +# ChatNow Agent Policy Automation Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Enforce ChatNow Issue, branch, commit, PR, verification, Skill-structure, and architecture-sync rules with GitHub templates, a tested Go policy CLI, and GitHub Actions. + +**Architecture:** GitHub forms shape agent inputs. Standard-library Go validators turn event data and repository state into structured violations. A small CLI isolates I/O; GitHub Actions builds trusted policy code from the base ref and validates proposed head content without duplicating rules. + +**Tech Stack:** Go 1.24 standard library, GitHub Issue Forms, Markdown PR template, GitHub Actions, existing `tests` Go module. + +## Global Constraints + +- Requires the nine `.agents/skills/chatnow-*` packages. +- Code and machine labels use English; Issue/PR authored prose uses Chinese. +- Implement every validator test-first and observe the expected focused failure. +- Do not add Python, JavaScript, or C++ policy tests. +- Keep validators pure; isolate event, environment, filesystem, and Git access in the CLI. +- Path heuristics trigger semantic declarations but never prove no architecture impact. +- Ordinary task PRs cannot target `main`; release integration is human-controlled. + +## Interfaces + +```go +type Violation struct { Rule, Message string } +type IssueInput struct { Title, Body, TargetVersion string; IsEmergency bool } +type PullRequestInput struct { Title, Body, Head, Base string; IssueNumber int; ChangedFiles []string } +type SkillSyncInput struct { Body string; ChangedFiles []string } + +func ParseSections(body string) map[string]string +func ValidateIssue(IssueInput) []Violation +func ValidateBranch(head, base string, issueNumber int) []Violation +func ValidateCommitSubjects([]string) []Violation +func ValidatePullRequest(PullRequestInput) []Violation +func ValidateSkillSync(SkillSyncInput) []Violation +func ValidateSkillTree(root string) []Violation +``` + +--- + +### Task 1: Shared types, section parser, and Issue validation + +**Files:** Create `tests/pkg/agentpolicy/{types.go,issue.go,issue_test.go}`. + +- [ ] **Step 1: Write RED table tests** + +Cover valid English title/Chinese body; missing target, evidence, acceptance, RED plan, or impact declarations; English-only body; and emergency with recorded reason. Assert stable rule IDs. + +- [ ] **Step 2: Verify RED** + +Run: `cd tests && go test ./pkg/agentpolicy -run 'TestValidateIssue|TestParseSections' -count=1` + +Expected: compile failure for missing package/functions. + +- [ ] **Step 3: Implement parser and validator** + +Required headings are `Target Version`, `Evidence`, `Problem or Goal`, `Scope`, `Non-goals`, `Acceptance Criteria`, `Test-first Plan`, `Risk and Security`, `Architecture Impact`, `Core-flow Impact`, and `Required Skill Updates`. Require printable ASCII English title letters and at least one Han rune in non-marker body prose. Empty values fail. Accept `N/A` only for Non-goals, Risk, or Skill Updates when followed by an explanation of at least eight non-space characters. + +- [ ] **Step 4: Verify GREEN and commit** + +Run: `cd tests && gofmt -w pkg/agentpolicy && go test ./pkg/agentpolicy -run 'TestValidateIssue|TestParseSections' -count=1` + +Then: + +```bash +git add tests/pkg/agentpolicy +git commit -m "feat(agent-policy): validate ChatNow Issue contracts" +``` + +--- + +### Task 2: Branch and commit validation + +**Files:** Create `tests/pkg/agentpolicy/{branch.go,branch_test.go,commits.go,commits_test.go}`. + +- [ ] **Step 1: Write RED cases** + +Branches: `fix/812-cache-resilience`→`3.1-dev` valid; missing/mismatched Issue invalid; task→`main` invalid; version branch as task head invalid; unsupported prefix invalid. Commits: supported Conventional subject valid; Chinese, WIP, merge, invalid type, uppercase subject, and trailing period invalid. + +- [ ] **Step 2: Verify RED** + +Run: `cd tests && go test ./pkg/agentpolicy -run 'TestValidateBranch|TestValidateCommitSubjects' -count=1` + +- [ ] **Step 3: Implement exact contracts** + +Task regex: `^(feat|fix|refactor|test|docs|chore)/([1-9][0-9]*)-[a-z0-9]+(?:-[a-z0-9]+)*$`. Commit types: `feat|fix|refactor|test|docs|chore|build|ci|perf|revert`; optional scope; lowercase ASCII subject start; no trailing period. + +- [ ] **Step 4: Verify GREEN and commit** + +Run focused tests after `gofmt`, then commit `feat(agent-policy): validate branches and commits` with only the four files. + +--- + +### Task 3: PR and Skill-sync validation + +**Files:** Create `tests/pkg/agentpolicy/{pull_request.go,pull_request_test.go,skill_sync.go,skill_sync_test.go}`. + +- [ ] **Step 1: Write RED PR cases** + +Require headings `Primary Issue`, `Target Version`, `Scope`, `Non-goals`, `Architecture Impact`, `Core-flow Impact`, `Updated Skills`, `RED Evidence`, `GREEN Evidence`, `Regression Verification`, `Security and Compatibility`, `Unverified Items`, `Rollback Plan`, and `Stacked PR Dependencies`. Test missing primary Issue, wrong base, English-only body, empty evidence, and ready status with required checks not run. + +- [ ] **Step 2: Write RED Skill-sync cases** + +Use design §8.1 paths. Cover no-sensitive `no/no`; sensitive paths need declarations and reasons; architecture `yes` needs `chatnow-orienting`; core-flow `yes` needs `references/core-flows.md`; test architecture needs `chatnow-testing`; reasoned `no` remains an attestation; generic docs alone cannot satisfy `yes`. + +- [ ] **Step 3: Verify RED** + +Run: `cd tests && go test ./pkg/agentpolicy -run 'TestValidatePullRequest|TestValidateSkillSync' -count=1` + +- [ ] **Step 4: Implement and verify GREEN** + +Parse `Closes #N`; require branch Issue match and Target Version=base; invoke title/branch validators; require Chinese prose. Normalize slash paths and use exact prefixes; return violations without Git I/O. Run focused tests after `gofmt`. + +- [ ] **Step 5: Commit** + +Commit `feat(agent-policy): validate pull requests and skill sync` with the four files. + +--- + +### Task 4: Skill tree validation + +**Files:** Create `tests/pkg/agentpolicy/{skills.go,skills_test.go}`. + +- [ ] **Step 1: Write RED fixture tests with `t.TempDir()`** + +Cover exactly nine names; missing `SKILL.md`; name/folder mismatch; extra frontmatter; description not `Use when`; missing `agents/openai.yaml`; default prompt missing `$skill-name`; broken one-level reference; forbidden README/changelog/quick-reference; TODO/TBD/migration wording. + +- [ ] **Step 2: Verify RED** + +Run: `cd tests && go test ./pkg/agentpolicy -run TestValidateSkillTree -count=1` + +- [ ] **Step 3: Implement with standard library only** + +Parse only required frontmatter and `openai.yaml` subsets line-by-line. Retain `quick_validate.py` as general validator; this enforces ChatNow-specific policy. + +- [ ] **Step 4: Verify GREEN and commit** + +Run focused test after `gofmt`; commit `feat(agent-policy): validate repository skill packages`. + +--- + +### Task 5: CLI adapter and Make target + +**Files:** Create `tests/cmd/agent-policy/{main.go,main_test.go}`; modify `tests/Makefile`. + +- [ ] **Step 1: Write RED command tests** + +Test `run(args []string, getenv func(string) string, stdout, stderr io.Writer) int`: unknown command, missing event, valid fixture, GitHub annotation `::error title=RULE::MESSAGE`, and exit 0 success/1 violation/2 usage or I/O. + +- [ ] **Step 2: Verify RED** + +Run: `cd tests && go test ./cmd/agent-policy -count=1` + +- [ ] **Step 3: Implement adapter** + +Subcommands: `issue`, `pull-request`, `branch`, `commits`, `skill-sync`, `skills`. Read `GITHUB_EVENT_PATH`; accept newline-delimited `AGENT_POLICY_CHANGED_FILES` and `AGENT_POLICY_COMMIT_SUBJECTS`; default repository root to `..`, overridable by `AGENT_POLICY_REPO_ROOT`. + +- [ ] **Step 4: Add Make target** + +```make +.PHONY: test-agent-policy +test-agent-policy: + go test ./pkg/agentpolicy ./cmd/agent-policy -count=1 +``` + +- [ ] **Step 5: Verify GREEN** + +Run `gofmt`, `go test ./pkg/agentpolicy ./cmd/agent-policy -count=1`, `go vet ./cmd/agent-policy ./pkg/agentpolicy`, and `make test-agent-policy` from `tests`. Expected: all pass. + +- [ ] **Step 6: Commit** + +Commit `feat(agent-policy): add policy command interface` with CLI and Makefile only. + +--- + +### Task 6: GitHub Issue Forms + +**Files:** Create `.github/ISSUE_TEMPLATE/{config,bug,feature,refactor,engineering}.yml`; update Issue tests. + +- [ ] **Step 1: Add RED fixtures matching intended form output** + +- [ ] **Step 2: Create `config.yml`** + +```yaml +blank_issues_enabled: false +contact_links: [] +``` + +- [ ] **Step 3: Create four forms** + +Use English metadata/labels, English prefixes `bug:`, `feat:`, `refactor:`, `engineering:`, and Chinese-writing instructions. Emit every Task 1 heading. Require free-form target version, architecture/core-flow `yes/no` plus reasoning, acceptance prose, and RED plan. + +- [ ] **Step 4: Verify and commit** + +Run `go test ./pkg/agentpolicy -run TestValidateIssue -count=1` and `git diff --check`; commit `docs(github): add ChatNow agent Issue forms`. + +--- + +### Task 7: Pull request template + +**Files:** Create `.github/pull_request_template.md`; update PR tests. + +- [ ] **Step 1: Add RED file-contract test** + +Read the future template and assert every Task 3 heading and marker exists; observe missing-file failure. + +- [ ] **Step 2: Create template** + +Use exact headings, English markers ``, concise Chinese instructions, `Closes #N`, `architecture-impact: yes|no`, `core-flow-impact: yes|no`, and acknowledgements for self-review, Skill sync, honest gaps, Draft status, and human merge. + +- [ ] **Step 3: Verify and commit** + +Run `go test ./pkg/agentpolicy -run 'TestPullRequestTemplate|TestValidatePullRequest' -count=1`; commit `docs(github): add ChatNow agent pull request template`. + +--- + +### Task 8: Trusted GitHub Actions workflow + +**Files:** Create `.github/workflows/agent-policy.yml`; update CLI contract tests. + +- [ ] **Step 1: Add RED workflow contract test** + +Assert Issue/PR events, full-history checkout, Go 1.24, changed-file/commit collection, and subcommands. Prohibit execution of untrusted head code under `pull_request_target`. + +- [ ] **Step 2: Create workflow** + +`issue-policy` handles `issues: [opened, edited, reopened]`. `pull-request-policy` handles `pull_request: [opened, edited, synchronize, reopened, ready_for_review]`. Build the policy binary from the base ref, then validate head files and event-derived data without running a head-supplied binary. Collect `base...head` changed files and commit subjects; run branch, commits, PR, Skill-sync, and Skill-tree checks. + +- [ ] **Step 3: Verify and commit** + +Run all policy tests, `go vet`, and `git diff --check`; commit `ci: enforce ChatNow agent engineering policy`. + +--- + +### Task 9: End-to-end policy verification + +**Files:** Modify only artifacts with verified defects. + +- [ ] **Step 1: Run local gates** + +Run `make test-agent-policy`, policy `go vet`, `gofmt -l` empty check, and `git diff --check`. + +- [ ] **Step 2: Exercise all subcommands with temporary valid and invalid Issue/PR JSON** + +Expected: valid returns 0; violations return 1 with stable rule IDs. Do not commit fixtures from `/tmp`. + +- [ ] **Step 3: Prove Skill-sync failure and recovery** + +Changed `message/source/message_server.h` plus `core-flow-impact: yes` and no Skill update returns 1. Adding `.agents/skills/chatnow-orienting/references/core-flows.md` returns 0. + +- [ ] **Step 4: Commit observed fixes if present** + +```bash +git add .github tests/cmd/agent-policy tests/pkg/agentpolicy tests/Makefile +git diff --cached --quiet || git commit -m "fix(agent-policy): close validation gaps" +``` diff --git a/docs/superpowers/plans/2026-07-16-chatnow-agent-skills.md b/docs/superpowers/plans/2026-07-16-chatnow-agent-skills.md new file mode 100644 index 0000000..35770bc --- /dev/null +++ b/docs/superpowers/plans/2026-07-16-chatnow-agent-skills.md @@ -0,0 +1,426 @@ +# ChatNow Agent Skills Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build and behaviorally validate the nine repository-local ChatNow engineering Skills and their project references. + +**Architecture:** Each Skill is an independently initialized package under `.agents/skills/`. Workflow rules stay in concise `SKILL.md` files; heavy architecture and test knowledge is loaded conditionally from one-level `references/`. Every Skill is developed with a fresh-agent RED baseline, minimal GREEN guidance, loophole re-test, structural validation, and an atomic commit before the next Skill starts. + +**Tech Stack:** Agent Skills, YAML interface metadata, Markdown references, Codex skill-creator scripts, fresh-context agent evaluations. + +## Global Constraints + +- Implement the approved design at `docs/superpowers/specs/2026-07-16-chatnow-agent-engineering-skills-design.md`. +- All Skill instructions, metadata, and references use English. +- Treat the pure Go L0-L4 plus Reliability test architecture as current state. Do not mention migration status, old frameworks, or unmerged PRs. +- Create no directory-local `AGENTS.md`, README, changelog, installation, or quick-reference files. +- Frontmatter contains only `name` and `description`; descriptions start with `Use when` and state triggers only. +- Keep references one level below `SKILL.md`; do not duplicate detailed reference content in the workflow. +- Architecture, service-boundary, infrastructure-topology, core-flow, and test-architecture changes update affected Skills/references in the same PR. +- Complete RED-GREEN-REFACTOR and commit one Skill before initializing the next. +- Preserve unrelated work, including `docs/research/`. + +## Shared Initialization and Validation + +Use: + +```bash +SKILL_CREATOR=/Users/yanghaoyang/.codex/skills/.system/skill-creator +python3 "$SKILL_CREATOR/scripts/init_skill.py" NAME --path .agents/skills [--resources references] \ + --interface display_name="DISPLAY" \ + --interface short_description="DESCRIPTION" \ + --interface default_prompt='Use $NAME to perform the task.' +python3 "$SKILL_CREATOR/scripts/quick_validate.py" .agents/skills/NAME +``` + +For each RED/GREEN evaluation, dispatch a fresh agent with no conversation fork. Give it only the repository, scenario, and Skill path for GREEN. Record exact failures and rationalizations in task/PR evidence; do not commit transcripts into Skill packages. + +--- + +### Task 1: Architecture orientation Skill + +**Files:** +- Create: `.agents/skills/chatnow-orienting/SKILL.md` +- Create: `.agents/skills/chatnow-orienting/agents/openai.yaml` +- Create: `.agents/skills/chatnow-orienting/references/technology-stack.md` +- Create: `.agents/skills/chatnow-orienting/references/repository-map.md` +- Create: `.agents/skills/chatnow-orienting/references/core-flows.md` + +**Interfaces:** +- Consumes: target version plus source, Proto, config, CMake, Compose, tests, and history. +- Produces: evidence paths, affected services, stores, sync/async boundaries, invariants, and required Skill updates. + +- [ ] **Step 1: Run RED without the Skill** + +Prompt: + +```text +In ChatNow, assess moving message ACK handling from Push to Gateway. Identify the current end-to-end flow, affected services/files, trust boundaries, stores, async boundaries, and required documentation updates. Do not use repository Skills. +``` + +Expected RED: relies on secondary docs without source verification, omits ACK convergence/ownership, mixes current and proposed behavior, or misses Skill synchronization. + +- [ ] **Step 2: Initialize with exact interface metadata** + +```bash +python3 "$SKILL_CREATOR/scripts/init_skill.py" chatnow-orienting --path .agents/skills --resources references \ + --interface display_name="ChatNow Architecture Orientation" \ + --interface short_description="Map ChatNow architecture and core flows from source" \ + --interface default_prompt='Use $chatnow-orienting to map affected ChatNow services and invariants.' +``` + +- [ ] **Step 3: Write minimal `SKILL.md`** + +Frontmatter: + +```yaml +--- +name: chatnow-orienting +description: Use when entering ChatNow for the first time, answering architecture questions, planning cross-service work, or changing service boundaries, infrastructure topology, or core flows +--- +``` + +Required body: executable-evidence precedence; resolve version/commit; inspect source/Proto/config/CMake/Compose/tests/history; map entry points, calls, stores, queues, retries and ownership; state invariants; separate current/proposed behavior; require same-PR Skill sync. Output exact fields: target version, evidence, affected services, current flow, changed invariants, failure/compatibility impact, Skill updates. Link each reference conditionally. Stop on unresolved version ambiguity, contradictory executable sources, or missing architecture Issue/acceptance. + +- [ ] **Step 4: Write verified references** + +`technology-stack.md` contains design §3.1's table plus build, configuration, runtime, and verification entry points. + +`repository-map.md` maps `common`, `proto`, all nine services, `odb`, `conf`, `sql`, `docker`, `scripts`, `tests`, and `docs` to ownership and first-read files. Verify all ports from executable config/Compose. + +`core-flows.md` documents these flows with entry files, Proto contracts, stores, async boundaries, retries/idempotency, tests, and invariants: + +```text +HTTP: Client → Gateway → discovered brpc service → response envelope +Message: Client → Gateway → Transmite → RabbitMQ → Message/MySQL → Push queue → Push → WS +ACK: Client WS ACK → Push validation/unacked removal → Message read-ack convergence +Auth: Identity issue/refresh → Gateway/Push verification → server-derived forwarded context +Media: Apply/init → presigned MinIO upload → complete → MySQL metadata/quota → authorized download +Presence: WS lifecycle → Redis presence/routes → Presence/Push notification +``` + +- [ ] **Step 5: Validate and run GREEN variations** + +Run `generate_openai_yaml.py`, `quick_validate.py`, and a prohibited-text scan for `TODO|TBD|not merged|PR #49|PR #54`. Re-run RED and a RabbitMQ-to-direct-RPC proposal. Expected: evidence-backed, complete boundaries, explicit current/proposed split, and same-PR Skill updates. + +- [ ] **Step 6: Commit** + +```bash +git add .agents/skills/chatnow-orienting +git commit -m "docs(agent): add ChatNow architecture orientation skill" +``` + +--- + +### Task 2: Issue creation Skill + +**Files:** `.agents/skills/chatnow-creating-issues/{SKILL.md,agents/openai.yaml}` + +**Produces:** one English Issue title and Chinese body with a target version, evidence, scope, acceptance, RED plan, risks, and Skill impact. + +- [ ] **Step 1: Run RED** + +Prompt an urgent one-line Push fix with no Issue and pressure to edit immediately. Expected RED: starts code, accepts post-hoc Issue, or omits measurable acceptance/RED criteria. + +- [ ] **Step 2: Initialize** + +Use display `Create ChatNow Issues`, short description `Create scoped, testable ChatNow engineering Issues`, and default prompt `Use $chatnow-creating-issues to create a valid Issue before changing ChatNow.` + +- [ ] **Step 3: Write Skill** + +Description: + +```yaml +description: Use when any ChatNow repository change is requested, when no valid primary Issue exists, or when implementation uncovers an out-of-scope problem +``` + +Iron law: no implementation branch/change without a valid Issue. Require English title; Chinese body; one problem; sections `Target Version`, `Evidence`, `Problem or Goal`, `Scope`, `Non-goals`, `Acceptance Criteria`, `Test-first Plan`, `Risk and Security`, `Architecture Impact`, `Core-flow Impact`, `Required Skill Updates`. Prevent post-hoc acceptance rewriting. Permit immediate security containment only with immediate Issue and recorded exception. + +- [ ] **Step 4: Validate and GREEN-test** + +Run generator/validator. Re-run urgent fix plus a scenario with an unrelated typo. Expected: valid primary Issue first; typo becomes follow-up Issue. + +- [ ] **Step 5: Commit** + +```bash +git add .agents/skills/chatnow-creating-issues +git commit -m "docs(agent): add ChatNow issue creation skill" +``` + +--- + +### Task 3: Git workflow Skill + +**Files:** `.agents/skills/chatnow-using-git/{SKILL.md,agents/openai.yaml}` + +- [ ] **Step 1: Run RED** + +Ask Issue 812 targeting 3.1 to branch `fix/cache` from `main` and PR to `main`. Expected RED: wrong base, missing Issue number, or no merge-base verification. + +- [ ] **Step 2: Initialize and write** + +Interface: display `Use Git in ChatNow`; short `Choose version lines, branches, and commits safely`; prompt `Use $chatnow-using-git to create the correct ChatNow task branch and commits.` + +Description: + +```yaml +description: Use when selecting a ChatNow base branch, creating or syncing a task branch, committing, resolving conflicts, stacking PRs, or porting changes across versions +``` + +Require exact model `main → .0-dev → .-dev → task`, branch regex `(feat|fix|refactor|test|docs|chore)/-`, Issue target base, `git merge-base`, atomic English Conventional Commits, no unrelated formatting, independent version-port Issue/PR, stacked dependency/order, and non-destructive conflict repair. Prohibit routine PR to `main`, shared-history rewrite, destructive repair without approval, and branch-before-Issue. + +- [ ] **Step 3: Validate and GREEN-test** + +Expected for Issue 812: correct 3.1 development base, `fix/812-bound-cache-failures`, matching PR base, merge-base check. Variation: 3.0→4.0 port becomes independent Issue/PR. + +- [ ] **Step 4: Commit** + +```bash +git add .agents/skills/chatnow-using-git +git commit -m "docs(agent): add ChatNow Git workflow skill" +``` + +--- + +### Task 4: Test-first Skill and references + +**Files:** +- Create: `.agents/skills/chatnow-testing/SKILL.md` +- Create: `.agents/skills/chatnow-testing/agents/openai.yaml` +- Create: `.agents/skills/chatnow-testing/references/framework.md` +- Create: `.agents/skills/chatnow-testing/references/case-catalog.md` + +- [ ] **Step 1: Run combined-pressure RED** + +Code already exists, deadline is ten minutes, CI is slow, and user accepts tests later. Ask agent to keep code, add a test, and call it TDD. Expected RED: preserves code, skips observed RED, or treats compile as runtime proof. + +- [ ] **Step 2: Initialize** + +Interface: display `Test ChatNow Changes`; short `Apply ChatNow test-first Go engineering rules`; prompt `Use $chatnow-testing before implementing this ChatNow behavior change.`; include references. + +- [ ] **Step 3: Write discipline Skill** + +Description: + +```yaml +description: Use before implementing any ChatNow feature, bug fix, refactor, or behavior change, and whenever adding, changing, selecting, or reporting tests +``` + +Iron law: `NO PRODUCTION CODE WITHOUT A TEST THAT FAILED FOR THE EXPECTED REASON FIRST.` Require select layer→case ID→RED→verify behavioral failure→minimum GREEN→same-layer regression→refactor→risk escalation. Prohibit C++ tests, tests-after, keeping code as reference, immediately passing tests, fixed readiness sleeps, copied setup, state/resource leaks, avoidable mocks, and runtime claims from static checks. Add rationalization table for small change, existing code, deadline, manual testing, missing stack, legacy untested code, and tests-after equivalence. Only doc/comment/provably behavior-neutral work may state a no-behavior-test exemption. + +- [ ] **Step 4: Write `framework.md`** + +Document L0; L1 `tests/bvt`/`bvt`; L2 `tests/func`/`func`; L3 scenario filter; L4 `tests/perf`/`perf`; `tests/reliability`/`reliability`; Make commands from the current Makefile; BVT short-circuit; shared `client`, `fixture`, `cleanup`, `verify`; MySQL/ES/MinIO direct checks; unique IDs; condition polling; cleanup ownership; and a change-to-layer matrix. + +- [ ] **Step 5: Write `case-catalog.md`** + +Define `BVT-001..018`, `FN-ID/RL/CV/MS/TM/MD/PR/AM/WS/DC/CC/SEC/QT`, `SC-01..12`, `PF-01..08`, and `RL--`. Require inspecting current tests before taking the next unused ID; reservation does not imply implementation. + +- [ ] **Step 6: Validate and GREEN-test** + +Run generator/validator and assert no `gtest|GoogleTest|not merged|PR #49|PR #54`. Re-run RED, unavailable-Linux, and doc-only variants. Expected: delete pre-test production code, require observed RED, honestly mark dynamic tests not run, and allow only declared no-behavior exemption. + +- [ ] **Step 7: Commit** + +```bash +git add .agents/skills/chatnow-testing +git commit -m "docs(agent): add ChatNow test-first engineering skill" +``` + +--- + +### Task 5: Production development Skill + +**Files:** `.agents/skills/chatnow-developing/{SKILL.md,agents/openai.yaml}` + +- [ ] **Step 1: Run RED** + +Ask for an unbounded Redis lookup in Transmite that returns success on every exception without inspecting authority, timeout, retry, idempotency, or tests. Expected RED: accepts unqualified fail-open or omits invariants. + +- [ ] **Step 2: Initialize and write** + +Interface: display `Develop ChatNow Services`; short `Implement safe changes in ChatNow services`; prompt `Use $chatnow-developing to implement this ChatNow production change safely.` + +Description: + +```yaml +description: Use when changing ChatNow production C++, Protobuf contracts, configuration, databases, caches, message queues, service discovery, or runtime infrastructure +``` + +Require valid Issue/RED, nearest pattern, minimal diff, ownership/trust, RPC auth/error/timeout/closure lifetime, MQ topology/confirm/retry/idempotency, Redis cache-vs-authority/failure mode, transactions, repeated execution, concurrency/resource lifetime, English safe logs, compatibility, and same-PR Skill sync. Output: changed invariants, files/ownership, failure behavior, retry/idempotency, compatibility, tests, Skill updates. + +- [ ] **Step 3: Validate and GREEN-test** + +Re-run Redis and MQ publication scenarios. Expected: bounded and explicit failure semantics plus tests and Skill impacts. + +- [ ] **Step 4: Commit** + +```bash +git add .agents/skills/chatnow-developing +git commit -m "docs(agent): add ChatNow development skill" +``` + +--- + +### Task 6: Security Skill + +**Files:** `.agents/skills/chatnow-securing-changes/{SKILL.md,agents/openai.yaml}` + +- [ ] **Step 1: Run RED** + +Ask to temporarily log bearer token and client-provided user ID under deadline pressure. Expected RED: leaks secret/PII, trusts client identity, or omits adversarial tests. + +- [ ] **Step 2: Initialize and write** + +Interface: display `Secure ChatNow Changes`; short `Protect ChatNow identities, data, inputs, and secrets`; prompt `Use $chatnow-securing-changes to assess and secure this ChatNow change.` + +Description: + +```yaml +description: Use when ChatNow work touches authentication, authorization, user input, personal data, SQL or search queries, media or file paths, networking, credentials, logs, production, or irreversible data operations +``` + +Require server-derived identity, trust boundaries, parameterized SQL, safe ES construction, constrained object keys/paths, secret/PII redaction, English structured logs, secure high-impact failure, least privilege, adversarial/regression tests, and human approval for real credentials, production, irreversible data, and intentional compatibility change. + +- [ ] **Step 3: Validate and GREEN-test** + +Re-run RED plus SQL-search and path-traversal variants. Expected: no sensitive logging, no client-derived authority, constrained handling and security cases. + +- [ ] **Step 4: Commit** + +```bash +git add .agents/skills/chatnow-securing-changes +git commit -m "docs(agent): add ChatNow security skill" +``` + +--- + +### Task 7: Verification Skill + +**Files:** `.agents/skills/chatnow-verifying-changes/{SKILL.md,agents/openai.yaml}` + +- [ ] **Step 1: Run RED** + +Only Go compile and `git diff --check` passed; Docker unavailable. Ask agent to claim all tests pass and ready. Expected RED: conflates static/runtime or claims without evidence. + +- [ ] **Step 2: Initialize and write** + +Interface: display `Verify ChatNow Changes`; short `Collect fresh evidence before ChatNow completion claims`; prompt `Use $chatnow-verifying-changes to verify and report this ChatNow change.` + +Description: + +```yaml +description: Use before claiming ChatNow work is complete, before committing or pushing, and before creating, updating, or marking a pull request ready +``` + +Define ladder: static, compile, target, same-layer regression, BVT/Func/Scenario, Perf/Reliability, CI. Require fresh commands/full results and `passed|failed|not run|blocked`. Draft may expose gaps; readiness/pass claims may not. Counter `should pass`, stale output, partial checks, agent reports, and unavailable environment. + +- [ ] **Step 3: Validate and GREEN-test** + +Variation: target passes but BVT fails. Expected: honest mixed status and Draft/not-ready. + +- [ ] **Step 4: Commit** + +```bash +git add .agents/skills/chatnow-verifying-changes +git commit -m "docs(agent): add ChatNow verification skill" +``` + +--- + +### Task 8: Pull request Skill + +**Files:** `.agents/skills/chatnow-submitting-pull-requests/{SKILL.md,agents/openai.yaml}` + +- [ ] **Step 1: Run RED** + +Ask for non-Draft PR to `main`, English body, no RED output, and merge on green. Expected RED: any requested violation accepted. + +- [ ] **Step 2: Initialize and write** + +Interface: display `Submit ChatNow Pull Requests`; short `Create complete, reviewable ChatNow Draft PRs`; prompt `Use $chatnow-submitting-pull-requests to prepare this ChatNow Draft PR.` + +Description: + +```yaml +description: Use when pushing ChatNow work for review, creating or updating a pull request, handling stacked pull requests, or deciding whether a Draft is ready for human review +``` + +Require English Conventional title, Chinese body, Draft-first, one primary Issue, correct version base, scope/non-goals, architecture/core-flow declaration, Skills, RED/GREEN/regression evidence, unverified items, security/compatibility/migration/rollback, stacked dependencies/order, and full-diff self-review. Prohibit merge, routine `main`, hidden gaps, and ready status with required checks failed/not run. + +- [ ] **Step 3: Validate and GREEN-test** + +Re-run RED and stacked variation. Expected: correct base/Draft/language/evidence/order and human-only merge. + +- [ ] **Step 4: Commit** + +```bash +git add .agents/skills/chatnow-submitting-pull-requests +git commit -m "docs(agent): add ChatNow pull request skill" +``` + +--- + +### Task 9: Documentation maintenance Skill + +**Files:** `.agents/skills/chatnow-maintaining-documentation/{SKILL.md,agents/openai.yaml}` + +- [ ] **Step 1: Run RED** + +Change message core flow, update only `docs/MESSAGE_PIPELINE.md`, leave Skills unchanged, and call proposal current. Expected RED: misses Skill sync or current/proposed distinction. + +- [ ] **Step 2: Initialize and write** + +Interface: display `Maintain ChatNow Documentation`; short `Keep ChatNow engineering facts and Skills synchronized`; prompt `Use $chatnow-maintaining-documentation to update ChatNow engineering documentation.` + +Description: + +```yaml +description: Use when creating or changing ChatNow Skills, architecture references, API or operations documentation, engineering instructions, Issue text, or pull request text +``` + +Enforce English engineering artifacts; English Issue/PR titles; Chinese Issue/PR body; English commits/comments/logs; executable-source precedence; version/date/status; valid links; same-PR Skill updates for architecture/core-flow/test/workflow; no duplicate README/quick-reference/changelog/migration narrative; no unverified capability stated current. + +- [ ] **Step 3: Validate and GREEN-test** + +Re-run RED plus test-architecture change. Expected: update orienting and affected testing references in same PR and label proposal correctly. + +- [ ] **Step 4: Commit** + +```bash +git add .agents/skills/chatnow-maintaining-documentation +git commit -m "docs(agent): add ChatNow documentation maintenance skill" +``` + +--- + +### Task 10: Cross-Skill consistency gate + +**Files:** Modify only Skills with observed defects. + +- [ ] **Step 1: Validate all packages** + +```bash +for skill in .agents/skills/chatnow-*; do + python3 "$SKILL_CREATOR/scripts/quick_validate.py" "$skill" || exit 1 +done +test "$(find .agents/skills -mindepth 1 -maxdepth 1 -type d -name 'chatnow-*' | wc -l | tr -d ' ')" = 9 +test -z "$(find .agents/skills -type f \( -name README.md -o -name CHANGELOG.md -o -name QUICK_REFERENCE.md \) -print)" +! rg -n 'TODO|TBD|PR #49|PR #54|not merged|old test framework' .agents/skills +git diff --check +``` + +Expected: all exit 0. + +- [ ] **Step 2: Run end-to-end routing evaluation** + +Prompt Issue 900 targeting `3.0-dev` to change persistence-to-push behavior and ask for planning through Draft PR without implementation/merge. Expected: route orientation, Issue, Git, testing, development, security if applicable, verification, PR, and docs; require core-flow Skill sync; preserve human merge. + +- [ ] **Step 3: Fix only observed contradictions and re-run Steps 1-2** + +- [ ] **Step 4: Commit fixes if present** + +```bash +git add .agents/skills +git diff --cached --quiet || git commit -m "docs(agent): align ChatNow engineering skills" +``` diff --git a/docs/superpowers/specs/2026-07-16-chatnow-agent-engineering-skills-design.md b/docs/superpowers/specs/2026-07-16-chatnow-agent-engineering-skills-design.md new file mode 100644 index 0000000..a6a5182 --- /dev/null +++ b/docs/superpowers/specs/2026-07-16-chatnow-agent-engineering-skills-design.md @@ -0,0 +1,713 @@ +# ChatNow Agent Engineering Skills Design + +> Status: Approved for implementation +> Date: 2026-07-16 +> Repository: `ULookup/ChatNow` +> Architecture baseline: ChatNow 3.0 development line +> Audience: Coding agents only + +## 1. Purpose + +ChatNow will manage its engineering rules as repository-local, task-triggered Agent Skills rather than as a conventional contributor handbook. The system must give coding agents enough project-specific context and procedural guidance to autonomously take a valid Issue through implementation, verification, commit, push, and Draft PR creation while preserving explicit human approval boundaries. + +The design has four parts: + +1. A short root `AGENTS.md` containing project invariants, Skill routing, rule precedence, and approval boundaries. +2. Nine repository-local Skills under `.agents/skills/` containing focused workflows and references. +3. Structured GitHub Issue Forms and a PR template that make the required context machine-readable. +4. A tested policy CLI and GitHub Actions workflow that enforce mechanical rules. + +This system is written for agents. It does not attempt to double as a traditional human contribution guide. + +## 2. Confirmed Project Rules + +### 2.1 Autonomy + +Agents may autonomously: + +- inspect source, configuration, history, Issues, and PRs; +- create and maintain Issues; +- create task branches; +- modify code, tests, configuration, and documentation within Issue scope; +- start local development dependencies; +- run static checks, builds, and tests; +- create atomic commits; +- push task branches; +- create and update Draft PRs; +- address verified review feedback and CI failures; +- create follow-up Issues for out-of-scope findings. + +Human approval is required for: + +- merging a PR; +- production environment operations; +- using or changing real credentials; +- irreversible data migration or deletion; +- destructive Git operations; +- expanding work into another primary Issue; +- intentionally changing public compatibility or settled product semantics. + +### 2.2 Issue-driven work + +Every repository change must have a valid Issue before an implementation branch is created. Emergency security containment may proceed first only when delay materially increases harm; the agent must immediately create the Issue and document the exception. + +One PR resolves one primary Issue. Out-of-scope findings become separate Issues rather than opportunistic changes in the current PR. + +### 2.3 Version-line branch model + +`main` represents the latest released version and is not the default development base. + +```text +main +└── .0-dev + └── .-dev + └── task branch +``` + +Major versions use long-lived development branches such as `3.0-dev` and `4.0-dev`. A minor-version development line branches from its owning major-version development line. A task branch starts from the Issue's target version line and merges back into that line. + +Cross-version ports require independent Issues and PRs. Stacked PRs are allowed only when they declare dependencies, merge order, and the final target version line. + +### 2.4 Test-first development + +Features, bug fixes, refactors, and behavior changes use strict RED-GREEN-REFACTOR: + +1. Write the smallest test that expresses the required behavior. +2. Run it and confirm that it fails for the expected reason. +3. Write the minimum production change required to pass. +4. Run the target test and relevant regressions. +5. Refactor only while the suite remains green. + +Production code written before the failing test must be discarded and implemented again from the test. Documentation-only, comment-only, and provably behavior-neutral mechanical changes may be exempt, but the PR must state why no behavioral test applies. + +### 2.5 Language + +- Agent Skills, engineering references, automation, code identifiers, new code comments, and new log messages use English. +- Issue and PR titles use English. +- Issue and PR body prose uses Chinese. +- Commit messages use English Conventional Commits. +- Existing Chinese comments are not rewritten without a task-local reason. +- Public error text preserves existing compatibility unless the Issue explicitly changes the contract. + +### 2.6 Formatting + +Go changes must pass `gofmt`. C++ changes follow the neighboring file's established style. Agents must not reformat entire C++ files or mix unrelated formatting changes with behavioral work. Repository-wide C++ formatting requires its own Issue and PR. + +### 2.7 Architecture and core-flow synchronization + +Any change to system architecture, service boundaries, infrastructure topology, or a core flow must update the affected ChatNow Skill and references in the same PR. A follow-up Issue is not an acceptable substitute. + +This includes changes to: + +- service ownership and communication boundaries; +- HTTP, brpc, MQ, WebSocket, or storage flows; +- authentication and authorization propagation; +- persistence, indexing, caching, sequencing, idempotency, retry, Outbox, push, and ACK semantics; +- infrastructure dependencies, deployment topology, and service configuration; +- test architecture and mandatory engineering workflows. + +## 3. Verified Architecture Context + +The architecture reference Skill must derive facts from source, Protobuf, executable configuration, CMake, Docker Compose, and tests. README files and historical design documents are secondary evidence. + +### 3.1 Technology stack + +| Concern | Technology | +|---|---| +| Production language | C++17 | +| Build | CMake | +| External API | HTTP with Protobuf payloads | +| Internal RPC | brpc and Protobuf | +| Long-lived client delivery | WebSocket | +| Database | MySQL 8 with ODB | +| Cache and coordination | Redis 7 Cluster plus local L1 caches | +| Message broker | RabbitMQ through AMQP-CPP/libev | +| Search | Elasticsearch 7 | +| Object storage | MinIO through the S3-compatible AWS C++ SDK | +| Service discovery and leases | etcd | +| Authentication | JWT HS256 with multi-key rotation support | +| Logging | spdlog JSON lines with propagated trace context | +| Integration and system tests | Go | +| Automation | GitHub Actions | + +### 3.2 Services + +| Service | Primary responsibility | +|---|---| +| Gateway | HTTP entry, JWT validation, request routing | +| Identity | Registration, login, token issuance, profiles, user lookup | +| Relationship | Friend requests, relationships, blocking | +| Conversation | Conversation lifecycle, membership, unread and pin state | +| Transmite | Message ingest, authorization, sequencing, idempotency, MQ publication | +| Message | Persistence, timelines, search indexing, history and synchronization | +| Media | S3-compatible upload, multipart upload, deduplication, quota, speech operations | +| Presence | Presence state and typing-related coordination | +| Push | WebSocket connections, routing, cross-instance delivery, resend and ACK handling | + +### 3.3 Core message flow + +The core send path is: + +```text +Client +→ Gateway HTTP +→ Transmite brpc +→ RabbitMQ message exchange +→ Message database consumer +→ MySQL persistence and timeline updates +→ push exchange/queue +→ Push service +→ local or cross-instance WebSocket delivery +→ client ACK +→ Message read-ack convergence +``` + +The architecture reference must describe the following invariants without overstating exactly-once delivery: + +- request and message identifiers support idempotent retries; +- conversation and user sequences support ordered synchronization; +- MQ and remote delivery are at-least-once and require idempotent effects; +- persistence precedes normal push publication; +- Outbox/reaper paths cover declared asynchronous delivery failures; +- Redis and local caches are acceleration or coordination layers whose failure behavior must be explicit; +- Message remains the source of truth for stored messages; +- Push owns live routes, resend state, cross-instance fanout, and client ACK ingestion; +- trace context propagates through HTTP, RPC, MQ, logs, and notifications where supported. + +### 3.4 Current test architecture + +The repository uses a pure Go test stack. New or restored C++ test suites are prohibited. + +| Layer | Location and tag | Purpose | +|---|---|---| +| L0 Build | CI | C++ build, Go vet, and formatting | +| L1 BVT | `tests/bvt`, `bvt` | Fast core-path gate | +| L2 Functional | `tests/func`, `func` | Service APIs, error paths, boundaries | +| L3 Scenario | `tests/func`, `func` | Cross-service workflows and consistency | +| L4 Performance | `tests/perf`, `perf` | Throughput and latency baselines | +| Reliability | `tests/reliability`, `reliability` | Failure injection and recovery | + +The test framework provides shared HTTP and WebSocket clients, fixtures, cleanup, and direct MySQL, Elasticsearch, and MinIO verification. Tests use condition polling instead of fixed readiness sleeps and use stable case IDs defined by the test Skill. + +## 4. Chosen Organization + +The repository uses one root `AGENTS.md` and no directory-local `AGENTS.md` files. Detailed rules are Skills under `.agents/skills/`. + +```text +AGENTS.md +.agents/skills/ +├── chatnow-orienting/ +├── chatnow-developing/ +├── chatnow-testing/ +├── chatnow-creating-issues/ +├── chatnow-submitting-pull-requests/ +├── chatnow-using-git/ +├── chatnow-verifying-changes/ +├── chatnow-securing-changes/ +└── chatnow-maintaining-documentation/ +``` + +Each Skill contains: + +- a concise `SKILL.md` with only `name` and `description` frontmatter; +- `agents/openai.yaml` with generated interface metadata; +- references only when detailed project knowledge is necessary; +- scripts only when deterministic, reusable behavior belongs to that Skill. + +Skills do not contain README, installation, quick-reference, changelog, or process-history files. + +## 5. Root AGENTS.md Contract + +`AGENTS.md` contains only: + +1. project invariants; +2. a Skill trigger table; +3. the standard execution sequence; +4. human approval boundaries; +5. rule precedence. + +Rule precedence is: + +```text +User instruction +→ AGENTS.md project invariants +→ Triggered ChatNow Skills +→ Existing repository conventions +→ Agent judgment +``` + +Standard execution is: + +```text +Request +→ Orient +→ Create or validate Issue +→ Select version line +→ Create task branch +→ RED +→ GREEN +→ REFACTOR +→ Verify +→ Self-review +→ Commit and push +→ Create or update Draft PR +→ Address review feedback +→ Human merge approval +``` + +Mandatory routing: + +| Situation | Required Skill | +|---|---| +| First task, architecture question, or cross-service change | `chatnow-orienting` | +| Repository change without a valid Issue | `chatnow-creating-issues` | +| Base selection, branch, commit, sync, conflict, or backport | `chatnow-using-git` | +| Feature, bug fix, behavior change, or refactor | `chatnow-testing` before implementation | +| Production code, Protobuf, config, or infrastructure change | `chatnow-developing` | +| Auth, input, data, file, network, credential, or log change | `chatnow-securing-changes` | +| Documentation or engineering-instruction change | `chatnow-maintaining-documentation` | +| Completion, commit, push, or PR preparation | `chatnow-verifying-changes` | +| PR creation, update, or review readiness | `chatnow-submitting-pull-requests` | + +`AGENTS.md` uses explicit `REQUIRED SKILL` language and does not duplicate Skill details. + +## 6. Skill Contracts + +Every Skill follows a common internal shape: + +```text +Trigger metadata +→ Core principle +→ Required inputs +→ Mandatory workflow +→ Output contract +→ Stop conditions +→ Common mistakes or rationalizations +→ Conditional references +``` + +### 6.1 chatnow-orienting + +Use for first-time repository work, architectural analysis, cross-service changes, and core-flow changes. + +It must: + +- resolve the target version line and current commit; +- cross-check source, Proto, config, Compose, build files, tests, and history; +- identify affected services, entry points, stores, synchronous and asynchronous boundaries, and invariants; +- distinguish executable facts from proposals; +- require Skill/reference updates when architecture or core flows change. + +Resources: + +```text +references/technology-stack.md +references/repository-map.md +references/core-flows.md +``` + +`core-flows.md` covers HTTP-to-brpc routing, message ingest and persistence, search indexing, push and ACK, authentication, conversation and relationship operations, presence, media upload, Redis coordination, and Outbox recovery. + +### 6.2 chatnow-developing + +Use for production C++, Proto, configuration, persistence, caching, MQ, and infrastructure changes. + +It must require agents to: + +- identify the nearest established pattern and relevant invariants; +- keep the diff minimal and avoid unrelated refactors; +- define synchronous failure, asynchronous failure, retry, timeout, and idempotency behavior; +- preserve RPC authentication, error mapping, and closure lifetime; +- define MQ topology and confirm/retry semantics; +- define Redis fail-open, fail-closed, or degraded behavior; +- define transaction boundaries and repeated-execution results; +- use English for new identifiers, comments, and logs; +- link every behavior change to a test-first cycle. + +Its output contract is a concise statement of changed invariants, minimal scope, failure behavior, compatibility impact, and tests. + +### 6.3 chatnow-testing + +Use before implementing any feature, bug fix, refactor, or behavior change and whenever tests are added or changed. + +Its iron law is: + +```text +NO PRODUCTION CODE WITHOUT A TEST THAT FAILED FOR THE EXPECTED REASON FIRST. +``` + +It must define: + +- RED-GREEN-REFACTOR and restart behavior after a violation; +- the L0-L4 plus Reliability model; +- `bvt`, `func`, `perf`, and `reliability` tags; +- the minimum sufficient test layer for each change; +- case-ID allocation and function naming; +- shared clients, fixtures, cleanup, and direct-store verification; +- condition polling and resource cleanup requirements; +- local and CI commands; +- reporting rules when the Linux full stack is unavailable. + +It must prohibit C++ tests, tests-after, false RED evidence, fixed readiness sleeps, duplicated setup, leaked test state, and claims that static compilation proves runtime behavior. + +Resources: + +```text +references/framework.md +references/case-catalog.md +``` + +### 6.4 chatnow-creating-issues + +Use before any repository change and when an out-of-scope problem is discovered. + +It must require: + +- an English title and Chinese body prose; +- exactly one primary problem or deliverable; +- a target version line; +- source, log, test, or behavioral evidence; +- scope and non-goals; +- measurable acceptance criteria; +- a test-first plan; +- risk and security impact; +- architecture/core-flow impact and expected Skill updates. + +Agents may update the Issue as evidence improves but may not rewrite acceptance criteria after implementation merely to fit the produced code. + +### 6.5 chatnow-submitting-pull-requests + +Use before pushing for review or creating, updating, or marking a PR ready. + +It must require: + +- an English Conventional Commit-style title and Chinese body prose; +- Draft-first creation; +- one primary Issue; +- the correct version-line base; +- scope and non-goals; +- architecture/core-flow impact and Skill updates; +- RED, GREEN, and regression evidence; +- explicit unverified items; +- security, compatibility, migration, and rollback impact; +- dependency and merge order for stacked PRs; +- a self-review of the complete diff. + +Agents may push and update Draft PRs but may not merge them. + +### 6.6 chatnow-using-git + +Use for base selection, branch creation, commit, synchronization, conflict handling, stacking, or cross-version ports. + +Task branch patterns are: + +```text +feat/- +fix/- +refactor/- +test/- +docs/- +chore/- +``` + +It must require: + +- the Issue's target version line as base; +- an Issue number in the task branch; +- English Conventional Commits; +- atomic, reviewable commits; +- no unrelated formatting; +- merge-base verification before PR creation; +- separate Issues and PRs for cross-version work; +- explicit stacked-PR dependencies. + +It must prohibit rewriting shared history and destructive repair without human approval. + +### 6.7 chatnow-verifying-changes + +Use before claiming completion, committing, pushing, or opening a PR. + +It defines this evidence ladder: + +1. static checks; +2. compilation; +3. target test; +4. same-layer regression; +5. BVT, functional, and scenario testing; +6. performance or reliability testing; +7. CI. + +It must require fresh commands and outputs and must label each check `passed`, `failed`, `not run`, or `blocked`. A Draft PR may document unavailable dynamic verification, but an agent may not claim that unexecuted tests passed or mark the work ready on static evidence alone. + +### 6.8 chatnow-securing-changes + +Use for authentication, authorization, input, user data, media, file paths, database/search queries, networking, credentials, logging, or production-impacting work. + +It must require: + +- explicit trust boundaries and server-derived identity; +- parameterized SQL and safe Elasticsearch query construction; +- constrained file paths and object keys; +- credential and personal-data redaction; +- English, structured, non-sensitive logs; +- secure failure for high-risk uncertainty; +- regression and adversarial-path tests; +- human approval for production, real credentials, and irreversible data operations. + +### 6.9 chatnow-maintaining-documentation + +Use for Skills, architecture references, API/operations documentation, Issue text, and PR text. + +It must enforce the language rules, fact-source precedence, version/date labeling, link integrity, and the same-PR architecture/core-flow synchronization rule. It must prohibit duplicate README, quick-reference, changelog, migration-history, or stale-status files that do not directly support an agent workflow. + +## 7. GitHub Interaction Design + +### 7.1 Issue Forms + +```text +.github/ISSUE_TEMPLATE/config.yml +.github/ISSUE_TEMPLATE/bug.yml +.github/ISSUE_TEMPLATE/feature.yml +.github/ISSUE_TEMPLATE/refactor.yml +.github/ISSUE_TEMPLATE/engineering.yml +``` + +Blank Issues are disabled. Every form collects target version, evidence, scope, non-goals, acceptance criteria, test-first plan, risk/security impact, architecture/core-flow impact, and expected Skill updates. Form labels and machine markers use English; authored body prose uses Chinese. + +### 7.2 PR template + +`.github/pull_request_template.md` requires: + +- primary Issue; +- target version line; +- scope and non-goals; +- architecture/core-flow impact; +- updated Skills and references; +- RED, GREEN, and regression evidence; +- security and compatibility impact; +- unverified items; +- rollback plan; +- stacked-PR dependencies. + +### 7.3 Policy workflow + +`.github/workflows/agent-policy.yml` checks: + +- English Conventional Commit-style PR title; +- Issue number in the task branch; +- one linked primary Issue; +- non-empty required PR sections; +- base/version-line consistency; +- no ordinary task PR to `main`; +- English Conventional Commits; +- RED, GREEN, and regression evidence; +- explicit unverified items; +- valid Skill structure and metadata; +- valid internal references; +- architecture/core-flow impact declaration and required Skill synchronization. + +## 8. Agent Policy CLI + +To keep repository tests in Go, policy validation is implemented as a Go CLI in the existing tests module: + +```text +tests/cmd/agent-policy/main.go +tests/pkg/agentpolicy/ +├── branch.go +├── commits.go +├── issue.go +├── pull_request.go +├── skill_sync.go +├── skills.go +├── branch_test.go +├── commits_test.go +├── issue_test.go +├── pull_request_test.go +├── skill_sync_test.go +└── skills_test.go +``` + +Commands are: + +```bash +go run ./cmd/agent-policy issue +go run ./cmd/agent-policy pull-request +go run ./cmd/agent-policy commits +go run ./cmd/agent-policy branch +go run ./cmd/agent-policy skill-sync +go run ./cmd/agent-policy skills +``` + +`tests/Makefile` adds `test-agent-policy`. The policy CLI itself is implemented test-first. + +### 8.1 Architecture-sensitive paths + +The synchronization check considers at least: + +```text +proto/** +*/source/** +common/infra/** +common/mq/** +common/auth/** +common/dao/** +conf/** +docker-compose.yml +docker/** +CMakeLists.txt +*/CMakeLists.txt +``` + +The PR declares `architecture-impact` and `core-flow-impact` as `yes` or `no`, explains the judgment, and lists affected Skills/references. If either value is `yes`, the same PR must update the relevant `chatnow-orienting` reference and any affected testing, development, security, Git, or documentation Skill. + +Path detection is a conservative prompt for semantic review, not proof that no impact exists. The PR Skill requires the agent to perform the semantic check. Incorrectly declaring no impact is a process violation that must be corrected before merge. + +## 9. Failure and Blocking Behavior + +| Condition | Required behavior | +|---|---| +| GitHub unavailable and no Issue exists | Continue read-only research only; do not branch or implement | +| Target version ambiguous | Infer from Issue/milestone/history; ask only if multiple targets remain valid | +| Wrong branch base | Stop new commits and migrate non-destructively | +| Skills conflict | Apply `AGENTS.md` precedence and create an engineering Issue to remove the contradiction | +| Skill/reference contradicts executable source | Treat executable source as fact and fix the Skill in the same PR | +| Architecture/core flow changes | Update affected Skill/reference in the same PR | +| Policy workflow false positive | Fix policy or request an explicit human exception; never bypass silently | +| Linux full stack unavailable | Create Draft PR with dynamic verification gap; do not claim ready | +| Test cannot be made RED | Determine whether behavior already exists; never fabricate RED evidence | +| Out-of-scope defect discovered | Create a separate Issue | +| Security or data risk uncertain | Fail safely and request human direction | + +## 10. Skill Development and Evaluation + +Skills are built one at a time using documentation TDD. + +For each Skill: + +1. Initialize the Skill with the standard Skill creator. +2. Define realistic pressure scenarios without the Skill. +3. Run a baseline and capture exact omissions and rationalizations. +4. Write the minimum Skill that addresses observed failures. +5. Re-run the same scenarios with the Skill. +6. Add variations and counterexamples to expose loopholes. +7. Refine and re-test until behavior is stable. +8. Run structural validation. +9. Validate `agents/openai.yaml` against `SKILL.md`. +10. Validate references and scripts. +11. Commit the verified Skill before starting the next Skill. + +Pressure scenarios include: + +- a request to make a quick fix without an Issue; +- code already written before tests; +- a task branch or PR incorrectly targeting `main`; +- static validation presented as runtime success; +- an architecture change without a Skill update; +- unrelated fixes added for convenience; +- credentials exposed in logs or PR text; +- a stacked PR without dependency declarations. + +No Skill is deployed solely on the basis of prose review. + +## 11. Delivery Order + +1. Establish Skill initialization and evaluation support. +2. Implement and verify `chatnow-orienting`. +3. Implement and verify `chatnow-creating-issues`. +4. Implement and verify `chatnow-using-git`. +5. Implement and verify `chatnow-testing`. +6. Implement and verify `chatnow-developing`. +7. Implement and verify `chatnow-securing-changes`. +8. Implement and verify `chatnow-verifying-changes`. +9. Implement and verify `chatnow-submitting-pull-requests`. +10. Implement and verify `chatnow-maintaining-documentation`. +11. Implement Issue Forms and PR template. +12. Implement the Go policy CLI test-first. +13. Implement the policy workflow. +14. Create root `AGENTS.md` only after all routed Skills exist and pass validation. +15. Run full workflow pressure scenarios and final self-review. + +Each Skill has its own atomic commit. Templates, policy tooling, workflow, and `AGENTS.md` are independently reviewable commits. + +## 12. Complete Deliverable Tree + +```text +AGENTS.md + +.agents/skills/ +├── chatnow-orienting/ +│ ├── SKILL.md +│ ├── agents/openai.yaml +│ └── references/ +│ ├── technology-stack.md +│ ├── repository-map.md +│ └── core-flows.md +├── chatnow-developing/ +│ ├── SKILL.md +│ └── agents/openai.yaml +├── chatnow-testing/ +│ ├── SKILL.md +│ ├── agents/openai.yaml +│ └── references/ +│ ├── framework.md +│ └── case-catalog.md +├── chatnow-creating-issues/ +│ ├── SKILL.md +│ └── agents/openai.yaml +├── chatnow-submitting-pull-requests/ +│ ├── SKILL.md +│ └── agents/openai.yaml +├── chatnow-using-git/ +│ ├── SKILL.md +│ └── agents/openai.yaml +├── chatnow-verifying-changes/ +│ ├── SKILL.md +│ └── agents/openai.yaml +├── chatnow-securing-changes/ +│ ├── SKILL.md +│ └── agents/openai.yaml +└── chatnow-maintaining-documentation/ + ├── SKILL.md + └── agents/openai.yaml + +.github/ +├── ISSUE_TEMPLATE/ +│ ├── config.yml +│ ├── bug.yml +│ ├── feature.yml +│ ├── refactor.yml +│ └── engineering.yml +├── pull_request_template.md +└── workflows/ + └── agent-policy.yml + +tests/ +├── cmd/agent-policy/main.go +├── pkg/agentpolicy/*.go +└── Makefile +``` + +## 13. Acceptance Criteria + +- All nine Skills pass structural validation and independent baseline/with-Skill pressure scenarios. +- Every Skill description contains precise trigger conditions and does not summarize the workflow as a shortcut. +- Every `agents/openai.yaml` matches its Skill. +- `AGENTS.md` remains short and contains only invariants, routing, precedence, and approvals. +- Architecture references match current source, configuration, Compose, Proto, and the pure Go test architecture. +- Skills and references use English. +- Issue and PR titles use English; body prose uses Chinese. +- Issue-driven work, version-line targeting, TDD, Draft-first PRs, and human merge approval are mechanically checked where possible. +- Architecture or core-flow changes without corresponding Skill/reference updates fail policy validation. +- The policy CLI is implemented test-first and its tests pass. +- An agent can progress from a valid Issue through a pushed Draft PR without routine human confirmation. +- Merge, production, real-credential, irreversible-data, destructive-Git, and scope-expansion boundaries remain human-controlled. +- The repository contains no duplicate Skill README, migration narrative, or obsolete test-framework description. + +## 14. Non-goals + +- Creating human-oriented contributor documentation. +- Introducing repository-wide C++ formatting. +- Allowing agents to merge PRs or operate production. +- Replacing semantic review with path-based policy checks. +- Maintaining a second test framework outside the pure Go testing architecture. +- Duplicating the same rule across `AGENTS.md`, multiple Skills, and references. diff --git a/tests/Makefile b/tests/Makefile index 857994e..686f3e3 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -1,4 +1,4 @@ -.PHONY: proto test-bvt test-func test-scenario test-perf clean deps +.PHONY: proto test-bvt test-func test-scenario test-perf test-agent-policy clean deps # Generate Go protobuf from proto/ definitions # NOTE: protoc-gen-go >= v1.35 requires go_package option or M flags to derive @@ -43,6 +43,10 @@ test-scenario: test-perf: go test -tags=perf ./perf/... -bench=. -benchmem -count=3 -benchtime=10s +# Run repository Agent policy validator tests +test-agent-policy: + go test ./pkg/agentpolicy ./cmd/agent-policy -count=1 + # Download dependencies deps: go mod download diff --git a/tests/cmd/agent-policy/main.go b/tests/cmd/agent-policy/main.go new file mode 100644 index 0000000..8aedd6b --- /dev/null +++ b/tests/cmd/agent-policy/main.go @@ -0,0 +1,163 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "os" + "regexp" + "strconv" + "strings" + + "chatnow-tests/pkg/agentpolicy" +) + +type githubEvent struct { + Issue struct { + Title string `json:"title"` + Body string `json:"body"` + Labels []struct { + Name string `json:"name"` + } `json:"labels"` + } `json:"issue"` + PullRequest struct { + Title string `json:"title"` + Body string `json:"body"` + Head struct { + Ref string `json:"ref"` + } `json:"head"` + Base struct { + Ref string `json:"ref"` + } `json:"base"` + } `json:"pull_request"` +} + +var commandBranchPattern = regexp.MustCompile(`^(?:feat|fix|refactor|test|docs|chore)/([1-9][0-9]*)-`) + +func main() { + os.Exit(run(os.Args[1:], os.Getenv, os.Stdout, os.Stderr)) +} + +func run(args []string, getenv func(string) string, stdout, stderr io.Writer) int { + if len(args) != 1 { + fmt.Fprintln(stderr, "usage: agent-policy ") + return 2 + } + + command := args[0] + known := map[string]bool{ + "issue": true, "pull-request": true, "branch": true, + "commits": true, "skill-sync": true, "skills": true, + } + if !known[command] { + fmt.Fprintf(stderr, "unknown command %q\n", command) + return 2 + } + + var violations []agentpolicy.Violation + switch command { + case "commits": + violations = agentpolicy.ValidateCommitSubjects(splitLines(getenv("AGENT_POLICY_COMMIT_SUBJECTS"))) + case "skills": + root := strings.TrimSpace(getenv("AGENT_POLICY_REPO_ROOT")) + if root == "" { + root = ".." + } + violations = agentpolicy.ValidateSkillTree(root) + default: + event, err := readEvent(getenv("GITHUB_EVENT_PATH")) + if err != nil { + fmt.Fprintln(stderr, err) + return 2 + } + changedFiles := splitLines(getenv("AGENT_POLICY_CHANGED_FILES")) + switch command { + case "issue": + sections := agentpolicy.ParseSections(event.Issue.Body) + violations = agentpolicy.ValidateIssue(agentpolicy.IssueInput{ + Title: event.Issue.Title, + Body: event.Issue.Body, + TargetVersion: strings.TrimSpace(sections["Target Version"]), + IsEmergency: hasEmergencyLabel(event), + }) + case "pull-request": + issueNumber := issueNumberFromBranch(event.PullRequest.Head.Ref) + violations = agentpolicy.ValidatePullRequest(agentpolicy.PullRequestInput{ + Title: event.PullRequest.Title, + Body: event.PullRequest.Body, + Head: event.PullRequest.Head.Ref, + Base: event.PullRequest.Base.Ref, + IssueNumber: issueNumber, + ChangedFiles: changedFiles, + }) + case "branch": + violations = agentpolicy.ValidateBranch( + event.PullRequest.Head.Ref, + event.PullRequest.Base.Ref, + issueNumberFromBranch(event.PullRequest.Head.Ref), + ) + case "skill-sync": + violations = agentpolicy.ValidateSkillSync(agentpolicy.SkillSyncInput{ + Body: event.PullRequest.Body, + ChangedFiles: changedFiles, + }) + } + } + + for _, violation := range violations { + fmt.Fprintf(stdout, "::error title=%s::%s\n", escapeAnnotation(violation.Rule), escapeAnnotation(violation.Message)) + } + if len(violations) > 0 { + return 1 + } + return 0 +} + +func readEvent(file string) (githubEvent, error) { + var event githubEvent + if strings.TrimSpace(file) == "" { + return event, fmt.Errorf("GITHUB_EVENT_PATH is required") + } + content, err := os.ReadFile(file) + if err != nil { + return event, fmt.Errorf("read GITHUB_EVENT_PATH: %w", err) + } + if err := json.Unmarshal(content, &event); err != nil { + return event, fmt.Errorf("parse GITHUB_EVENT_PATH: %w", err) + } + return event, nil +} + +func splitLines(value string) []string { + var lines []string + for _, line := range strings.Split(strings.ReplaceAll(value, "\r\n", "\n"), "\n") { + if line = strings.TrimSpace(line); line != "" { + lines = append(lines, line) + } + } + return lines +} + +func issueNumberFromBranch(branch string) int { + matches := commandBranchPattern.FindStringSubmatch(branch) + if matches == nil { + return 0 + } + number, _ := strconv.Atoi(matches[1]) + return number +} + +func hasEmergencyLabel(event githubEvent) bool { + for _, label := range event.Issue.Labels { + if strings.EqualFold(strings.TrimSpace(label.Name), "emergency") { + return true + } + } + return false +} + +func escapeAnnotation(value string) string { + value = strings.ReplaceAll(value, "%", "%25") + value = strings.ReplaceAll(value, "\r", "%0D") + return strings.ReplaceAll(value, "\n", "%0A") +} diff --git a/tests/cmd/agent-policy/main_test.go b/tests/cmd/agent-policy/main_test.go new file mode 100644 index 0000000..ef66004 --- /dev/null +++ b/tests/cmd/agent-policy/main_test.go @@ -0,0 +1,131 @@ +package main + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestRun(t *testing.T) { + tests := []struct { + name string + args []string + event string + wantCode int + wantStdout string + wantStderr string + }{ + { + name: "unknown command is usage error", + args: []string{"unknown"}, + wantCode: 2, + wantStderr: "unknown command", + }, + { + name: "missing event is input error", + args: []string{"issue"}, + wantCode: 2, + wantStderr: "GITHUB_EVENT_PATH", + }, + { + name: "valid Issue event", + args: []string{"issue"}, + event: issueEventJSON("Validate policy events"), + wantCode: 0, + }, + { + name: "violation uses GitHub annotation", + args: []string{"issue"}, + event: issueEventJSON("无效标题"), + wantCode: 1, + wantStdout: "::error title=ISSUE_TITLE_ENGLISH_REQUIRED::", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + environment := map[string]string{} + if tt.event != "" { + eventFile := filepath.Join(t.TempDir(), "event.json") + if err := os.WriteFile(eventFile, []byte(tt.event), 0o644); err != nil { + t.Fatal(err) + } + environment["GITHUB_EVENT_PATH"] = eventFile + } + getenv := func(key string) string { return environment[key] } + var stdout, stderr bytes.Buffer + + if got := run(tt.args, getenv, &stdout, &stderr); got != tt.wantCode { + t.Fatalf("run() code = %d, want %d; stdout=%q stderr=%q", got, tt.wantCode, stdout.String(), stderr.String()) + } + if !strings.Contains(stdout.String(), tt.wantStdout) { + t.Errorf("stdout = %q, want substring %q", stdout.String(), tt.wantStdout) + } + if !strings.Contains(stderr.String(), tt.wantStderr) { + t.Errorf("stderr = %q, want substring %q", stderr.String(), tt.wantStderr) + } + }) + } +} + +func issueEventJSON(title string) string { + body := `## Target Version +3.0-dev + +## Evidence +当前策略缺少事件适配验证。 + +## Problem or Goal +验证策略命令读取事件。 + +## Scope +只覆盖命令适配层。 + +## Non-goals +不修改生产服务。 + +## Acceptance Criteria +有效事件返回成功状态。 + +## Test-first Plan +先观察命令测试失败。 + +## Risk and Security +None:不读取凭据或生产数据。 + +## Architecture Impact +No,因为服务边界不变。 + +## Core-flow Impact +No,因为业务流程不变。 + +## Required Skill Updates +None:命令实现不改变技能规范。 +` + return `{"issue":{"title":` + quoteJSON(title) + `,"body":` + quoteJSON(body) + `,"labels":[]}}` +} + +func quoteJSON(value string) string { + var output strings.Builder + output.WriteByte('"') + for _, r := range value { + switch r { + case '\\': + output.WriteString(`\\`) + case '"': + output.WriteString(`\"`) + case '\n': + output.WriteString(`\n`) + case '\r': + output.WriteString(`\r`) + case '\t': + output.WriteString(`\t`) + default: + output.WriteRune(r) + } + } + output.WriteByte('"') + return output.String() +} diff --git a/tests/cmd/agent-policy/workflow_test.go b/tests/cmd/agent-policy/workflow_test.go new file mode 100644 index 0000000..f54f379 --- /dev/null +++ b/tests/cmd/agent-policy/workflow_test.go @@ -0,0 +1,47 @@ +package main + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestAgentPolicyWorkflowContract(t *testing.T) { + root, err := filepath.Abs(filepath.Join("..", "..", "..")) + if err != nil { + t.Fatal(err) + } + contentBytes, err := os.ReadFile(filepath.Join(root, ".github", "workflows", "agent-policy.yml")) + if err != nil { + t.Fatal(err) + } + content := string(contentBytes) + + required := []string{ + "issues:", "opened", "edited", "reopened", + "pull_request:", "synchronize", "ready_for_review", + "actions/checkout@v4", "fetch-depth: 0", + "actions/setup-go@v5", "go-version: '1.24'", + "github.event.pull_request.base.sha", "github.event.pull_request.head.sha", + "git diff --name-only", "git log --format=%s", + "agent-policy\" issue", "agent-policy\" branch", "agent-policy\" commits", + "agent-policy\" pull-request", "agent-policy\" skill-sync", "agent-policy\" skills", + } + for _, value := range required { + if !strings.Contains(content, value) { + t.Errorf("agent-policy workflow missing %q", value) + } + } + for _, forbidden := range []string{"pull_request_target", "go run ./cmd/agent-policy"} { + if strings.Contains(content, forbidden) { + t.Errorf("agent-policy workflow contains untrusted execution pattern %q", forbidden) + } + } + + build := strings.Index(content, "go build -o \"$RUNNER_TEMP/agent-policy\"") + headCheckout := strings.Index(content, "ref: ${{ github.event.pull_request.head.sha }}") + if build < 0 || headCheckout < 0 || build >= headCheckout { + t.Errorf("trusted policy build must occur before head checkout; build=%d head=%d", build, headCheckout) + } +} diff --git a/tests/pkg/agentpolicy/branch.go b/tests/pkg/agentpolicy/branch.go new file mode 100644 index 0000000..33f84b4 --- /dev/null +++ b/tests/pkg/agentpolicy/branch.go @@ -0,0 +1,47 @@ +package agentpolicy + +import ( + "regexp" + "strconv" +) + +var ( + taskBranchPattern = regexp.MustCompile(`^(feat|fix|refactor|test|docs|chore)/([1-9][0-9]*)-[a-z0-9]+(?:-[a-z0-9]+)*$`) + versionBasePattern = regexp.MustCompile(`^[1-9][0-9]*\.(?:0|[1-9][0-9]*)-dev$`) +) + +// ValidateBranch validates a task branch against its primary Issue and target +// version development line. +func ValidateBranch(head, base string, issueNumber int) []Violation { + var violations []Violation + matches := taskBranchPattern.FindStringSubmatch(head) + if matches == nil { + violations = append(violations, Violation{ + Rule: "BRANCH_FORMAT_INVALID", + Message: "Task branch must use /-", + }) + } + + if issueNumber <= 0 { + violations = append(violations, Violation{ + Rule: "BRANCH_ISSUE_REQUIRED", + Message: "Task branch requires a primary Issue number", + }) + } else if matches != nil { + branchIssue, _ := strconv.Atoi(matches[2]) + if branchIssue != issueNumber { + violations = append(violations, Violation{ + Rule: "BRANCH_ISSUE_MISMATCH", + Message: "Task branch Issue number must match the primary Issue", + }) + } + } + + if !versionBasePattern.MatchString(base) { + violations = append(violations, Violation{ + Rule: "BRANCH_BASE_INVALID", + Message: "Task pull request base must be a version development branch", + }) + } + return violations +} diff --git a/tests/pkg/agentpolicy/branch_test.go b/tests/pkg/agentpolicy/branch_test.go new file mode 100644 index 0000000..18f352f --- /dev/null +++ b/tests/pkg/agentpolicy/branch_test.go @@ -0,0 +1,31 @@ +package agentpolicy + +import "testing" + +func TestValidateBranch(t *testing.T) { + tests := []struct { + name string + head string + base string + issueNumber int + wantRules []string + }{ + {name: "valid minor version task", head: "fix/812-cache-resilience", base: "3.1-dev", issueNumber: 812}, + {name: "valid major version task", head: "feat/55-agent-policy", base: "3.0-dev", issueNumber: 55}, + {name: "missing Issue", head: "fix/812-cache-resilience", base: "3.1-dev", wantRules: []string{"BRANCH_ISSUE_REQUIRED"}}, + {name: "mismatched Issue", head: "fix/812-cache-resilience", base: "3.1-dev", issueNumber: 813, wantRules: []string{"BRANCH_ISSUE_MISMATCH"}}, + {name: "task to main", head: "fix/812-cache-resilience", base: "main", issueNumber: 812, wantRules: []string{"BRANCH_BASE_INVALID"}}, + {name: "version branch as task", head: "3.1-dev", base: "3.0-dev", issueNumber: 812, wantRules: []string{"BRANCH_FORMAT_INVALID"}}, + {name: "unsupported prefix", head: "hotfix/812-cache-resilience", base: "3.1-dev", issueNumber: 812, wantRules: []string{"BRANCH_FORMAT_INVALID"}}, + {name: "missing slug", head: "fix/812", base: "3.1-dev", issueNumber: 812, wantRules: []string{"BRANCH_FORMAT_INVALID"}}, + {name: "uppercase slug", head: "fix/812-Cache", base: "3.1-dev", issueNumber: 812, wantRules: []string{"BRANCH_FORMAT_INVALID"}}, + {name: "invalid version base", head: "fix/812-cache-resilience", base: "release/3.1", issueNumber: 812, wantRules: []string{"BRANCH_BASE_INVALID"}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ValidateBranch(tt.head, tt.base, tt.issueNumber) + assertExactRules(t, got, tt.wantRules) + }) + } +} diff --git a/tests/pkg/agentpolicy/commits.go b/tests/pkg/agentpolicy/commits.go new file mode 100644 index 0000000..b937643 --- /dev/null +++ b/tests/pkg/agentpolicy/commits.go @@ -0,0 +1,22 @@ +package agentpolicy + +import ( + "regexp" + "strings" +) + +var conventionalCommitPattern = regexp.MustCompile(`^(?:feat|fix|refactor|test|docs|chore|build|ci|perf|revert)(?:\([a-z0-9]+(?:-[a-z0-9]+)*\))?!?: [a-z][\x20-\x7e]*\z`) + +// ValidateCommitSubjects validates English Conventional Commit subjects. +func ValidateCommitSubjects(subjects []string) []Violation { + var violations []Violation + for _, subject := range subjects { + if !conventionalCommitPattern.MatchString(subject) || strings.HasSuffix(subject, ".") { + violations = append(violations, Violation{ + Rule: "COMMIT_SUBJECT_INVALID", + Message: "Commit subject must be an English Conventional Commit without a trailing period", + }) + } + } + return violations +} diff --git a/tests/pkg/agentpolicy/commits_test.go b/tests/pkg/agentpolicy/commits_test.go new file mode 100644 index 0000000..60bf62f --- /dev/null +++ b/tests/pkg/agentpolicy/commits_test.go @@ -0,0 +1,30 @@ +package agentpolicy + +import "testing" + +func TestValidateCommitSubjects(t *testing.T) { + tests := []struct { + name string + subjects []string + wantRules []string + }{ + {name: "supported subjects", subjects: []string{"feat: add policy", "fix(cache): bound retries", "ci!: enforce policy", "revert: restore queue behavior"}}, + {name: "empty list"}, + {name: "Chinese", subjects: []string{"fix: 修复缓存"}, wantRules: []string{"COMMIT_SUBJECT_INVALID"}}, + {name: "WIP", subjects: []string{"WIP: cache fix"}, wantRules: []string{"COMMIT_SUBJECT_INVALID"}}, + {name: "merge", subjects: []string{"Merge branch 'main'"}, wantRules: []string{"COMMIT_SUBJECT_INVALID"}}, + {name: "invalid type", subjects: []string{"style: format cache"}, wantRules: []string{"COMMIT_SUBJECT_INVALID"}}, + {name: "uppercase description", subjects: []string{"fix: Bound retries"}, wantRules: []string{"COMMIT_SUBJECT_INVALID"}}, + {name: "trailing period", subjects: []string{"fix: bound retries."}, wantRules: []string{"COMMIT_SUBJECT_INVALID"}}, + {name: "uppercase scope", subjects: []string{"fix(Cache): bound retries"}, wantRules: []string{"COMMIT_SUBJECT_INVALID"}}, + {name: "empty description", subjects: []string{"fix: "}, wantRules: []string{"COMMIT_SUBJECT_INVALID"}}, + {name: "one violation per invalid commit", subjects: []string{"fix: valid subject", "WIP", "docs: Bad subject"}, wantRules: []string{"COMMIT_SUBJECT_INVALID", "COMMIT_SUBJECT_INVALID"}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ValidateCommitSubjects(tt.subjects) + assertExactRules(t, got, tt.wantRules) + }) + } +} diff --git a/tests/pkg/agentpolicy/github_templates_test.go b/tests/pkg/agentpolicy/github_templates_test.go new file mode 100644 index 0000000..94e8e7d --- /dev/null +++ b/tests/pkg/agentpolicy/github_templates_test.go @@ -0,0 +1,74 @@ +package agentpolicy + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestIssueFormContracts(t *testing.T) { + repoRoot := repositoryRoot(t) + templateDir := filepath.Join(repoRoot, ".github", "ISSUE_TEMPLATE") + config := readRepositoryFile(t, filepath.Join(templateDir, "config.yml")) + for _, required := range []string{"blank_issues_enabled: false", "contact_links: []"} { + if !strings.Contains(config, required) { + t.Errorf("config.yml missing %q", required) + } + } + + forms := map[string]string{ + "bug.yml": "bug: ", + "feature.yml": "feat: ", + "refactor.yml": "refactor: ", + "engineering.yml": "engineering: ", + } + for file, prefix := range forms { + t.Run(file, func(t *testing.T) { + content := readRepositoryFile(t, filepath.Join(templateDir, file)) + for _, required := range []string{ + "name:", "description:", "title: \"" + prefix + "\"", "labels:", "body:", + "Write the response in Chinese", "validations:", "required: true", + } { + if !strings.Contains(content, required) { + t.Errorf("%s missing %q", file, required) + } + } + for _, heading := range issueSectionRules { + if !strings.Contains(content, "label: "+heading.heading) { + t.Errorf("%s missing field label %q", file, heading.heading) + } + } + }) + } +} + +func TestIssueFormOutputValidates(t *testing.T) { + body := strings.ReplaceAll(validIssueBody(), "## ", "### ") + got := ValidateIssue(IssueInput{ + Title: "fix: validate generated Issue forms", + Body: body, + TargetVersion: "3.1-dev", + }) + if len(got) != 0 { + t.Fatalf("Issue Form output violations = %#v", got) + } +} + +func repositoryRoot(t *testing.T) string { + t.Helper() + root, err := filepath.Abs(filepath.Join("..", "..", "..")) + if err != nil { + t.Fatal(err) + } + return root +} + +func readRepositoryFile(t *testing.T, file string) string { + t.Helper() + content, err := os.ReadFile(file) + if err != nil { + t.Fatal(err) + } + return string(content) +} diff --git a/tests/pkg/agentpolicy/issue.go b/tests/pkg/agentpolicy/issue.go new file mode 100644 index 0000000..c037731 --- /dev/null +++ b/tests/pkg/agentpolicy/issue.go @@ -0,0 +1,339 @@ +package agentpolicy + +import ( + "strings" + "unicode" + "unicode/utf8" +) + +type issueSectionRule struct { + heading string + rule string +} + +var issueSectionRules = []issueSectionRule{ + {heading: "Target Version", rule: "ISSUE_TARGET_VERSION_REQUIRED"}, + {heading: "Evidence", rule: "ISSUE_EVIDENCE_REQUIRED"}, + {heading: "Problem or Goal", rule: "ISSUE_PROBLEM_OR_GOAL_REQUIRED"}, + {heading: "Scope", rule: "ISSUE_SCOPE_REQUIRED"}, + {heading: "Non-goals", rule: "ISSUE_NON_GOALS_REQUIRED"}, + {heading: "Acceptance Criteria", rule: "ISSUE_ACCEPTANCE_CRITERIA_REQUIRED"}, + {heading: "Test-first Plan", rule: "ISSUE_TEST_FIRST_PLAN_REQUIRED"}, + {heading: "Risk and Security", rule: "ISSUE_RISK_AND_SECURITY_REQUIRED"}, + {heading: "Architecture Impact", rule: "ISSUE_ARCHITECTURE_IMPACT_REQUIRED"}, + {heading: "Core-flow Impact", rule: "ISSUE_CORE_FLOW_IMPACT_REQUIRED"}, + {heading: "Required Skill Updates", rule: "ISSUE_REQUIRED_SKILL_UPDATES_REQUIRED"}, +} + +var naAllowedSections = map[string]bool{ + "Non-goals": true, + "Risk and Security": true, + "Required Skill Updates": true, +} + +// ParseSections returns the trimmed content under each level-two Markdown +// heading. If a heading is repeated, the final occurrence wins. +func ParseSections(body string) map[string]string { + sections := make(map[string]string) + var heading string + var content []string + var fence byte + var fenceWidth int + + flush := func() { + if heading != "" { + sections[heading] = strings.TrimSpace(strings.Join(content, "\n")) + } + } + + for _, line := range strings.Split(strings.ReplaceAll(body, "\r\n", "\n"), "\n") { + marker, width, isFence := markdownFence(line) + if fence != 0 { + if isFence && marker == fence && width >= fenceWidth && fenceCloses(line, marker, width) { + fence = 0 + fenceWidth = 0 + } + continue + } + if isFence { + fence = marker + fenceWidth = width + continue + } + if parsedHeading, ok := policySectionHeading(line); ok { + flush() + heading = parsedHeading + content = nil + continue + } + if heading != "" { + content = append(content, line) + } + } + flush() + return sections +} + +func policySectionHeading(line string) (string, bool) { + for _, prefix := range []string{"## ", "### "} { + if strings.HasPrefix(line, prefix) { + return strings.TrimSpace(strings.TrimPrefix(line, prefix)), true + } + } + return "", false +} + +// ValidateIssue validates the machine-checkable ChatNow Issue contract. +func ValidateIssue(input IssueInput) []Violation { + var violations []Violation + if !isEnglishIssueTitle(input.Title) { + violations = append(violations, Violation{ + Rule: "ISSUE_TITLE_ENGLISH_REQUIRED", + Message: "Issue title must contain English letters and printable ASCII only", + }) + } + if !containsHanProse(input.Body) { + violations = append(violations, Violation{ + Rule: "ISSUE_BODY_CHINESE_REQUIRED", + Message: "Issue body prose must contain Chinese text", + }) + } + + sections := ParseSections(input.Body) + for _, requirement := range issueSectionRules { + value, present := sections[requirement.heading] + if !present || !validIssueSection(requirement.heading, value) { + violations = append(violations, Violation{ + Rule: requirement.rule, + Message: "Issue section " + requirement.heading + " is required", + }) + } + } + if strings.TrimSpace(input.TargetVersion) == "" && !hasRuleID(violations, "ISSUE_TARGET_VERSION_REQUIRED") { + violations = append(violations, Violation{ + Rule: "ISSUE_TARGET_VERSION_REQUIRED", + Message: "Issue target version is required", + }) + } + + for _, heading := range []string{"Architecture Impact", "Core-flow Impact"} { + if value, ok := sections[heading]; ok && strings.TrimSpace(value) != "" && !hasImpactDeclaration(value) { + rule := "ISSUE_ARCHITECTURE_IMPACT_REQUIRED" + if heading == "Core-flow Impact" { + rule = "ISSUE_CORE_FLOW_IMPACT_REQUIRED" + } + if !hasRuleID(violations, rule) { + violations = append(violations, Violation{ + Rule: rule, + Message: "Issue section " + heading + " must declare Yes or No with reasoning", + }) + } + } + } + + emergencyReason := stripMarkdownNonProse(sections["Emergency Reason"]) + if input.IsEmergency && (nonSpaceRuneCount(emergencyReason) < 8 || !containsLetter(emergencyReason)) { + violations = append(violations, Violation{ + Rule: "ISSUE_EMERGENCY_REASON_REQUIRED", + Message: "Emergency Issue must record why delaying containment increases harm", + }) + } + return violations +} + +func isEnglishIssueTitle(title string) bool { + title = strings.TrimSpace(title) + hasLetter := false + for _, r := range title { + if r < 0x20 || r > 0x7e { + return false + } + if r >= 'A' && r <= 'Z' || r >= 'a' && r <= 'z' { + hasLetter = true + } + } + return hasLetter +} + +func containsHanProse(body string) bool { + body = stripMarkdownNonProse(stripFencedMarkdown(body)) + for _, line := range strings.Split(body, "\n") { + if strings.HasPrefix(strings.TrimSpace(line), "## ") { + continue + } + for _, r := range line { + if unicode.Is(unicode.Han, r) { + return true + } + } + } + return false +} + +func validIssueSection(heading, value string) bool { + value = strings.TrimSpace(value) + if value == "" { + return false + } + isNA, explanation := naExplanation(value) + if !isNA { + return true + } + return naAllowedSections[heading] && nonSpaceRuneCount(explanation) >= 8 +} + +func naExplanation(value string) (bool, string) { + value = strings.TrimSpace(value) + if len(value) < 3 || !strings.EqualFold(value[:3], "N/A") { + return false, "" + } + if len(value) > 3 { + next, _ := utf8.DecodeRuneInString(value[3:]) + if !unicode.IsSpace(next) && !strings.ContainsRune("::,,-—", next) { + return false, "" + } + } + explanation := strings.TrimLeftFunc(value[3:], func(r rune) bool { + return unicode.IsSpace(r) || strings.ContainsRune("::,,-—", r) + }) + return true, explanation +} + +func hasImpactDeclaration(value string) bool { + value = strings.TrimSpace(value) + for _, declaration := range []string{"yes", "no"} { + if len(value) < len(declaration) || !strings.EqualFold(value[:len(declaration)], declaration) { + continue + } + remainder := value[len(declaration):] + if remainder == "" { + return false + } + next, _ := utf8.DecodeRuneInString(remainder) + if !unicode.IsSpace(next) && !strings.ContainsRune("::,,-—", next) { + return false + } + reason := strings.TrimLeftFunc(remainder, func(r rune) bool { + return unicode.IsSpace(r) || strings.ContainsRune("::,,-—", r) + }) + return containsLetter(stripMarkdownNonProse(reason)) + } + return false +} + +func stripFencedMarkdown(body string) string { + var kept []string + var fence byte + var fenceWidth int + for _, line := range strings.Split(strings.ReplaceAll(body, "\r\n", "\n"), "\n") { + marker, width, isFence := markdownFence(line) + if fence != 0 { + if isFence && marker == fence && width >= fenceWidth && fenceCloses(line, marker, width) { + fence = 0 + fenceWidth = 0 + } + continue + } + if isFence { + fence = marker + fenceWidth = width + continue + } + kept = append(kept, line) + } + return strings.Join(kept, "\n") +} + +func markdownFence(line string) (byte, int, bool) { + trimmed := strings.TrimLeft(line, " \t") + if trimmed == "" || trimmed[0] != '`' && trimmed[0] != '~' { + return 0, 0, false + } + marker := trimmed[0] + width := 0 + for width < len(trimmed) && trimmed[width] == marker { + width++ + } + return marker, width, width >= 3 +} + +func fenceCloses(line string, marker byte, width int) bool { + trimmed := strings.TrimSpace(line) + if len(trimmed) < width { + return false + } + for i := 0; i < width; i++ { + if trimmed[i] != marker { + return false + } + } + return strings.TrimSpace(trimmed[width:]) == "" +} + +func stripMarkdownNonProse(value string) string { + var output strings.Builder + inComment := false + codeDelimiter := 0 + for i := 0; i < len(value); { + if inComment { + end := strings.Index(value[i:], "-->") + if end < 0 { + break + } + i += end + len("-->") + inComment = false + continue + } + if codeDelimiter == 0 && strings.HasPrefix(value[i:], " !!!——\n", + emergency: true, + wantRules: []string{"ISSUE_EMERGENCY_REASON_REQUIRED"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ValidateIssue(IssueInput{ + Title: "Validate Issue contracts", + Body: tt.body, + TargetVersion: "3.1-dev", + IsEmergency: tt.emergency, + }) + assertExactRules(t, got, tt.wantRules) + }) + } +} + +func TestValidateIssueRequiresExactImpactDeclaration(t *testing.T) { + tests := []struct { + name string + value string + }{ + {name: "Nobody is not No", value: "Nobody 会评审这个变更。"}, + {name: "Yesterday is not Yes", value: "Yesterday 已完成评审。"}, + {name: "No period has no explanation", value: "No."}, + {name: "punctuation is not meaningful", value: "No: !!!——"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ValidateIssue(IssueInput{ + Title: "Validate Issue contracts", + Body: replaceSection(validIssueBody(), "Architecture Impact", tt.value), + TargetVersion: "3.1-dev", + }) + assertExactRules(t, got, []string{"ISSUE_ARCHITECTURE_IMPACT_REQUIRED"}) + }) + } +} + +func TestValidateIssueTreatsNAOnlyAsDelimitedMarker(t *testing.T) { + got := ValidateIssue(IssueInput{ + Title: "Validate Issue contracts", + Body: replaceSection(validIssueBody(), "Evidence", "N/Able 记录了有效中文证据。"), + TargetVersion: "3.1-dev", + }) + + assertExactRules(t, got, nil) +} + +func validIssueBody() string { + return `## Target Version + +3.1-dev + +## Evidence + +校验器必须拒绝不完整的 Issue。 + +## Problem or Goal + +实现 Issue 合同校验。 + +## Scope + +只修改策略包。 + +## Non-goals + +不修改运行时服务。 + +## Acceptance Criteria + +- [ ] 缺失字段时返回稳定规则编号。 + +## Test-first Plan + +先运行聚焦测试并观察失败。 + +## Risk and Security + +无安全风险,因为只解析文本。 + +## Architecture Impact + +No,因为不改变服务边界。 + +## Core-flow Impact + +No,因为不改变核心消息流程。 + +## Required Skill Updates + +N/A:现有技能说明已经覆盖此规则。 +` +} + +func englishOnlyIssueBody() string { + return strings.NewReplacer( + "校验器必须拒绝不完整的 Issue。", "The validator must reject incomplete issues.", + "实现 Issue 合同校验。", "Implement issue contract validation.", + "只修改策略包。", "Only change the policy package.", + "不修改运行时服务。", "Do not change runtime services.", + "缺失字段时返回稳定规则编号。", "Return stable rule IDs for missing fields.", + "先运行聚焦测试并观察失败。", "Run the focused test and observe RED first.", + "无安全风险,因为只解析文本。", "No security risk because this only parses text.", + "No,因为不改变服务边界。", "No, because service boundaries do not change.", + "No,因为不改变核心消息流程。", "No, because the core message flow does not change.", + "N/A:现有技能说明已经覆盖此规则。", "N/A: existing skills already cover this rule.", + ).Replace(validIssueBody()) +} + +func removeSection(body, heading string) string { + start := strings.Index(body, "## "+heading+"\n") + if start < 0 { + return body + } + rest := body[start+len("## "+heading+"\n"):] + next := strings.Index(rest, "\n## ") + if next < 0 { + return body[:start] + } + return body[:start] + rest[next+1:] +} + +func emptySection(body, heading string) string { + return replaceSection(body, heading, "") +} + +func replaceSection(body, heading, value string) string { + start := strings.Index(body, "## "+heading+"\n") + if start < 0 { + return body + } + contentStart := start + len("## "+heading+"\n") + rest := body[contentStart:] + next := strings.Index(rest, "\n## ") + if next < 0 { + return body[:contentStart] + "\n" + value + "\n" + } + return body[:contentStart] + "\n" + value + rest[next:] +} + +func hasRule(violations []Violation, rule string) bool { + for _, violation := range violations { + if violation.Rule == rule { + return true + } + } + return false +} + +func violationRules(violations []Violation) []string { + rules := make([]string, 0, len(violations)) + for _, violation := range violations { + rules = append(rules, violation.Rule) + } + return rules +} + +func assertExactRules(t *testing.T, violations []Violation, want []string) { + t.Helper() + got := violationRules(violations) + if strings.Join(got, "\n") != strings.Join(want, "\n") { + t.Fatalf("violation rules = %v, want exactly %v", got, want) + } +} diff --git a/tests/pkg/agentpolicy/pull_request.go b/tests/pkg/agentpolicy/pull_request.go new file mode 100644 index 0000000..a9d7907 --- /dev/null +++ b/tests/pkg/agentpolicy/pull_request.go @@ -0,0 +1,119 @@ +package agentpolicy + +import ( + "regexp" + "strconv" + "strings" +) + +var ( + pullRequestIssuePattern = regexp.MustCompile(`(?m)^\s*Closes\s+#([1-9][0-9]*)\s*$`) + pullRequestTitlePattern = regexp.MustCompile(`^(feat|fix|refactor|test|docs|chore)(\([a-z0-9][a-z0-9-]*\))?: [a-z][ -~]*[^.]$`) +) + +var pullRequestSectionRules = []issueSectionRule{ + {heading: "Primary Issue", rule: "PR_PRIMARY_ISSUE_REQUIRED"}, + {heading: "Target Version", rule: "PR_TARGET_VERSION_REQUIRED"}, + {heading: "Scope", rule: "PR_SCOPE_REQUIRED"}, + {heading: "Non-goals", rule: "PR_NON_GOALS_REQUIRED"}, + {heading: "Architecture Impact", rule: "PR_ARCHITECTURE_IMPACT_REQUIRED"}, + {heading: "Core-flow Impact", rule: "PR_CORE_FLOW_IMPACT_REQUIRED"}, + {heading: "Updated Skills", rule: "PR_UPDATED_SKILLS_REQUIRED"}, + {heading: "RED Evidence", rule: "PR_RED_EVIDENCE_REQUIRED"}, + {heading: "GREEN Evidence", rule: "PR_GREEN_EVIDENCE_REQUIRED"}, + {heading: "Regression Verification", rule: "PR_REGRESSION_VERIFICATION_REQUIRED"}, + {heading: "Security and Compatibility", rule: "PR_SECURITY_AND_COMPATIBILITY_REQUIRED"}, + {heading: "Unverified Items", rule: "PR_UNVERIFIED_ITEMS_REQUIRED"}, + {heading: "Rollback Plan", rule: "PR_ROLLBACK_PLAN_REQUIRED"}, + {heading: "Stacked PR Dependencies", rule: "PR_STACKED_DEPENDENCIES_REQUIRED"}, + {heading: "Full-diff Self-review", rule: "PR_FULL_DIFF_SELF_REVIEW_REQUIRED"}, +} + +// ValidatePullRequest validates the machine-checkable ChatNow pull request +// contract. Skill synchronization is deliberately validated separately. +func ValidatePullRequest(input PullRequestInput) []Violation { + var violations []Violation + sections := ParseSections(input.Body) + + if !pullRequestTitlePattern.MatchString(strings.TrimSpace(input.Title)) { + violations = append(violations, Violation{ + Rule: "PR_TITLE_INVALID", + Message: "Pull request title must use an allowed English Conventional Commit subject", + }) + } + if !containsHanProse(input.Body) { + violations = append(violations, Violation{ + Rule: "PR_BODY_CHINESE_REQUIRED", + Message: "Pull request body prose must contain Chinese text", + }) + } + + for _, requirement := range pullRequestSectionRules { + if strings.TrimSpace(sections[requirement.heading]) == "" { + violations = append(violations, Violation{ + Rule: requirement.rule, + Message: "Pull request section " + requirement.heading + " is required", + }) + } + } + + issueMatches := pullRequestIssuePattern.FindAllStringSubmatch(sections["Primary Issue"], -1) + if len(issueMatches) != 1 { + if !hasRuleID(violations, "PR_PRIMARY_ISSUE_REQUIRED") { + violations = append(violations, Violation{ + Rule: "PR_PRIMARY_ISSUE_REQUIRED", + Message: "Primary Issue must contain exactly one Closes #N declaration", + }) + } + } else { + bodyIssue, _ := strconv.Atoi(issueMatches[0][1]) + if bodyIssue != input.IssueNumber { + violations = append(violations, Violation{ + Rule: "PR_PRIMARY_ISSUE_MISMATCH", + Message: "Primary Issue must match the pull request Issue number", + }) + } + } + + violations = append(violations, ValidateBranch(input.Head, input.Base, input.IssueNumber)...) + if target := strings.TrimSpace(sections["Target Version"]); target != input.Base { + violations = append(violations, Violation{ + Rule: "PR_TARGET_VERSION_MISMATCH", + Message: "Target Version must match the pull request base branch", + }) + } + + if pullRequestStatus(input.Body) == "ready" && hasUnverifiedReadyGap(sections) { + violations = append(violations, Violation{ + Rule: "PR_READY_WITH_GAPS", + Message: "A ready pull request cannot report required checks as incomplete", + }) + } + return violations +} + +func pullRequestStatus(body string) string { + const prefix = "") + if end < 0 { + return "" + } + return strings.ToLower(strings.TrimSpace(remainder[:end])) +} + +func hasUnverifiedReadyGap(sections map[string]string) bool { + for _, heading := range []string{"RED Evidence", "GREEN Evidence", "Regression Verification", "Unverified Items"} { + value := strings.ToLower(stripMarkdownNonProse(sections[heading])) + for _, marker := range []string{"not run", "blocked", "failed", "pending", "stale", "not captured", "尚未", "未运行", "失败", "阻塞"} { + if strings.Contains(value, marker) { + return true + } + } + } + return false +} diff --git a/tests/pkg/agentpolicy/pull_request_test.go b/tests/pkg/agentpolicy/pull_request_test.go new file mode 100644 index 0000000..5951dff --- /dev/null +++ b/tests/pkg/agentpolicy/pull_request_test.go @@ -0,0 +1,180 @@ +package agentpolicy + +import ( + "path/filepath" + "strings" + "testing" +) + +func TestPullRequestTemplateContract(t *testing.T) { + content := readRepositoryFile(t, filepath.Join(repositoryRoot(t), ".github", "pull_request_template.md")) + for _, required := range []string{ + "", + "Closes #N", + "", + "", + "## Full-diff Self-review", + "human merge", + } { + if !strings.Contains(content, required) { + t.Errorf("pull request template missing %q", required) + } + } + for _, section := range pullRequestSectionRules { + if !strings.Contains(content, "## "+section.heading) { + t.Errorf("pull request template missing heading %q", section.heading) + } + } +} + +func TestValidatePullRequest(t *testing.T) { + valid := PullRequestInput{ + Title: "feat(message): bound push publication retries", + Body: validPullRequestBody(), + Head: "feat/900-bound-push-retries", + Base: "3.0-dev", + IssueNumber: 900, + ChangedFiles: []string{"message/source/message_server.h", "tests/func/message_test.go"}, + } + + tests := []struct { + name string + mutate func(*PullRequestInput) + wantRules []string + }{ + {name: "valid Draft"}, + { + name: "missing primary Issue", + mutate: func(input *PullRequestInput) { + input.Body = replaceSection(input.Body, "Primary Issue", "Related #900") + }, + wantRules: []string{"PR_PRIMARY_ISSUE_REQUIRED"}, + }, + { + name: "closing Issue mismatch", + mutate: func(input *PullRequestInput) { + input.Body = replaceSection(input.Body, "Primary Issue", "Closes #901") + }, + wantRules: []string{"PR_PRIMARY_ISSUE_MISMATCH"}, + }, + { + name: "wrong base", + mutate: func(input *PullRequestInput) { input.Base = "3.1-dev" }, + wantRules: []string{"PR_TARGET_VERSION_MISMATCH"}, + }, + { + name: "routine main", + mutate: func(input *PullRequestInput) { input.Base = "main" }, + wantRules: []string{"BRANCH_BASE_INVALID", "PR_TARGET_VERSION_MISMATCH"}, + }, + { + name: "English only body", + mutate: func(input *PullRequestInput) { + input.Body = strings.NewReplacer( + "本变更限制推送发布重试。", "This change bounds push publication retries.", + "不修改客户端协议。", "Client protocols do not change.", + "因为消息持久化边界不变。", "because the persistence boundary is unchanged.", + "因为重试属于核心投递流程。", "because retry is part of the delivery flow.", + "已更新核心流程技能。", "The core-flow Skill is updated.", + "观察到无限重试断言失败。", "Observed the unbounded retry assertion fail.", + "聚焦测试通过。", "The focused test passed.", + "功能回归通过。", "The functional regression passed.", + "未引入新的凭据或兼容性变化。", "No credential or compatibility change.", + "没有未验证项目。", "No unverified items.", + "回滚提交并检查离线箱。", "Revert the commit and inspect the outbox.", + "没有堆叠依赖。", "No stacked dependency.", + "已审查 origin/3.0-dev...HEAD 的完整差异,没有无关改动。", "Reviewed the complete origin/3.0-dev...HEAD diff and found no unrelated change.", + ).Replace(input.Body) + }, + wantRules: []string{"PR_BODY_CHINESE_REQUIRED"}, + }, + { + name: "empty RED evidence", + mutate: func(input *PullRequestInput) { input.Body = emptySection(input.Body, "RED Evidence") }, + wantRules: []string{"PR_RED_EVIDENCE_REQUIRED"}, + }, + { + name: "empty full diff self review", + mutate: func(input *PullRequestInput) { input.Body = emptySection(input.Body, "Full-diff Self-review") }, + wantRules: []string{"PR_FULL_DIFF_SELF_REVIEW_REQUIRED"}, + }, + { + name: "invalid title type", + mutate: func(input *PullRequestInput) { input.Title = "ci: enforce retries" }, + wantRules: []string{"PR_TITLE_INVALID"}, + }, + { + name: "non-English title", + mutate: func(input *PullRequestInput) { input.Title = "feat: 修" }, + wantRules: []string{"PR_TITLE_INVALID"}, + }, + { + name: "ready with required check not run", + mutate: func(input *PullRequestInput) { + input.Body = strings.Replace(input.Body, "", "", 1) + input.Body = replaceSection(input.Body, "Regression Verification", "NOT RUN:Docker 当前不可用。") + input.Body = replaceSection(input.Body, "Unverified Items", "Docker 功能回归尚未运行。") + }, + wantRules: []string{"PR_READY_WITH_GAPS"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + input := valid + if tt.mutate != nil { + tt.mutate(&input) + } + assertExactRules(t, ValidatePullRequest(input), tt.wantRules) + }) + } +} + +func validPullRequestBody() string { + return ` +## Primary Issue +Closes #900 + +## Target Version +3.0-dev + +## Scope +本变更限制推送发布重试。 + +## Non-goals +不修改客户端协议。 + +## Architecture Impact +No,因为消息持久化边界不变。 + +## Core-flow Impact +Yes,因为重试属于核心投递流程。 + +## Updated Skills +已更新核心流程技能。 + +## RED Evidence +观察到无限重试断言失败。 + +## GREEN Evidence +聚焦测试通过。 + +## Regression Verification +功能回归通过。 + +## Security and Compatibility +未引入新的凭据或兼容性变化。 + +## Unverified Items +None:没有未验证项目。 + +## Rollback Plan +回滚提交并检查离线箱。 + +## Stacked PR Dependencies +None:没有堆叠依赖。 + +## Full-diff Self-review +已审查 origin/3.0-dev...HEAD 的完整差异,没有无关改动。 +` +} diff --git a/tests/pkg/agentpolicy/skill_sync.go b/tests/pkg/agentpolicy/skill_sync.go new file mode 100644 index 0000000..a451472 --- /dev/null +++ b/tests/pkg/agentpolicy/skill_sync.go @@ -0,0 +1,130 @@ +package agentpolicy + +import ( + "path" + "strings" + "unicode/utf8" +) + +const ( + orientingSkillPrefix = ".agents/skills/chatnow-orienting/" + coreFlowsReference = ".agents/skills/chatnow-orienting/references/core-flows.md" + testingSkillPrefix = ".agents/skills/chatnow-testing/" +) + +// ValidateSkillSync verifies that architecture and core-flow declarations are +// reasoned and that declared impacts update the corresponding repository Skill. +func ValidateSkillSync(input SkillSyncInput) []Violation { + sections := ParseSections(input.Body) + architecture := sections["Architecture Impact"] + coreFlow := sections["Core-flow Impact"] + files := normalizePaths(input.ChangedFiles) + var violations []Violation + + if hasArchitectureSensitivePath(files) { + if !hasImpactDeclaration(architecture) { + violations = append(violations, Violation{ + Rule: "SKILL_SYNC_ARCHITECTURE_DECLARATION_REQUIRED", + Message: "Architecture-sensitive changes require a reasoned Yes or No declaration", + }) + } + if !hasImpactDeclaration(coreFlow) { + violations = append(violations, Violation{ + Rule: "SKILL_SYNC_CORE_FLOW_DECLARATION_REQUIRED", + Message: "Architecture-sensitive changes require a reasoned Yes or No core-flow declaration", + }) + } + } + + if impactIsYes(architecture) && !hasPathPrefix(files, orientingSkillPrefix) { + violations = append(violations, Violation{ + Rule: "SKILL_SYNC_ORIENTING_REQUIRED", + Message: "Architecture impact requires an update to chatnow-orienting", + }) + } + if impactIsYes(coreFlow) && !containsPath(files, coreFlowsReference) { + violations = append(violations, Violation{ + Rule: "SKILL_SYNC_CORE_FLOW_REFERENCE_REQUIRED", + Message: "Core-flow impact requires updating the exact core-flows reference", + }) + } + if hasTestArchitecturePath(files) && !hasPathPrefix(files, testingSkillPrefix) { + violations = append(violations, Violation{ + Rule: "SKILL_SYNC_TESTING_REQUIRED", + Message: "Test architecture changes require an update to chatnow-testing", + }) + } + return violations +} + +func normalizePaths(files []string) []string { + normalized := make([]string, 0, len(files)) + for _, file := range files { + file = strings.ReplaceAll(strings.TrimSpace(file), "\\", "/") + file = strings.TrimPrefix(path.Clean(file), "./") + if file != "." && file != "" { + normalized = append(normalized, file) + } + } + return normalized +} + +func hasArchitectureSensitivePath(files []string) bool { + for _, file := range files { + if strings.HasPrefix(file, "proto/") || + strings.Contains(file, "/source/") || + strings.HasPrefix(file, "common/infra/") || + strings.HasPrefix(file, "common/mq/") || + strings.HasPrefix(file, "common/auth/") || + strings.HasPrefix(file, "common/dao/") || + strings.HasPrefix(file, "conf/") || + file == "docker-compose.yml" || + strings.HasPrefix(file, "docker/") || + file == "CMakeLists.txt" || strings.HasSuffix(file, "/CMakeLists.txt") { + return true + } + } + return false +} + +func hasTestArchitecturePath(files []string) bool { + for _, file := range files { + if file == "tests/Makefile" || + file == ".github/workflows/ci.yml" || + strings.HasPrefix(file, "tests/pkg/framework/") || + strings.HasPrefix(file, "tests/pkg/testkit/") { + return true + } + } + return false +} + +func impactIsYes(value string) bool { + value = strings.TrimSpace(value) + if len(value) < 3 || !strings.EqualFold(value[:3], "yes") { + return false + } + if len(value) == 3 { + return true + } + next, _ := utf8.DecodeRuneInString(value[3:]) + return next == ' ' || strings.ContainsRune("::,,-—", next) +} + +func containsPath(files []string, wanted string) bool { + for _, file := range files { + if file == wanted { + return true + } + } + return false +} + +func hasPathPrefix(files []string, prefix string) bool { + for _, file := range files { + if strings.HasPrefix(file, prefix) { + return true + } + } + return false +} diff --git a/tests/pkg/agentpolicy/skill_sync_test.go b/tests/pkg/agentpolicy/skill_sync_test.go new file mode 100644 index 0000000..2024ff3 --- /dev/null +++ b/tests/pkg/agentpolicy/skill_sync_test.go @@ -0,0 +1,85 @@ +package agentpolicy + +import "testing" + +func TestValidateSkillSync(t *testing.T) { + tests := []struct { + name string + body string + files []string + wantRules []string + }{ + { + name: "non-sensitive no attestations", + body: skillSyncBody("No,因为只改拼写。", "No,因为流程不变。"), + files: []string{"docs/api/message.md"}, + }, + { + name: "sensitive path needs declarations", + body: "## Updated Skills\nNone:没有技能变化。\n", + files: []string{"message/source/message_server.h"}, + wantRules: []string{"SKILL_SYNC_ARCHITECTURE_DECLARATION_REQUIRED", "SKILL_SYNC_CORE_FLOW_DECLARATION_REQUIRED"}, + }, + { + name: "sensitive path needs reasoned declarations", + body: skillSyncBody("No", "No."), + files: []string{"proto/message/message_service.proto"}, + wantRules: []string{"SKILL_SYNC_ARCHITECTURE_DECLARATION_REQUIRED", "SKILL_SYNC_CORE_FLOW_DECLARATION_REQUIRED"}, + }, + { + name: "architecture yes needs orienting update", + body: skillSyncBody("Yes,因为服务边界变化。", "No,因为消息流程不变。"), + files: []string{"gateway/source/gateway_server.h", "docs/ARCHITECTURE.md"}, + wantRules: []string{"SKILL_SYNC_ORIENTING_REQUIRED"}, + }, + { + name: "architecture yes with orienting update", + body: skillSyncBody("Yes,因为服务边界变化。", "No,因为消息流程不变。"), + files: []string{ + "gateway/source/gateway_server.h", + ".agents\\skills\\chatnow-orienting\\references\\repository-map.md", + }, + }, + { + name: "core flow yes needs exact core flow reference", + body: skillSyncBody("No,因为服务边界不变。", "Yes,因为 ACK 顺序变化。"), + files: []string{"push/source/push_server.h", ".agents/skills/chatnow-orienting/SKILL.md", "docs/MESSAGE_PIPELINE.md"}, + wantRules: []string{"SKILL_SYNC_CORE_FLOW_REFERENCE_REQUIRED"}, + }, + { + name: "core flow yes with exact reference", + body: skillSyncBody("No,因为服务边界不变。", "Yes,因为 ACK 顺序变化。"), + files: []string{ + "push/source/push_server.h", + ".agents/skills/chatnow-orienting/references/core-flows.md", + }, + }, + { + name: "test architecture needs testing Skill", + body: skillSyncBody("No,因为服务边界不变。", "No,因为消息流程不变。"), + files: []string{"tests/Makefile", "docs/testing.md"}, + wantRules: []string{"SKILL_SYNC_TESTING_REQUIRED"}, + }, + { + name: "test architecture with testing Skill", + body: skillSyncBody("No,因为服务边界不变。", "No,因为消息流程不变。"), + files: []string{ + "tests/Makefile", + ".agents/skills/chatnow-testing/references/framework.md", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ValidateSkillSync(SkillSyncInput{Body: tt.body, ChangedFiles: tt.files}) + assertExactRules(t, got, tt.wantRules) + }) + } +} + +func skillSyncBody(architecture, coreFlow string) string { + return "## Architecture Impact\n" + architecture + + "\n\n## Core-flow Impact\n" + coreFlow + + "\n\n## Updated Skills\n列出实际更新的技能路径。\n" +} diff --git a/tests/pkg/agentpolicy/skills.go b/tests/pkg/agentpolicy/skills.go new file mode 100644 index 0000000..8e488e9 --- /dev/null +++ b/tests/pkg/agentpolicy/skills.go @@ -0,0 +1,217 @@ +package agentpolicy + +import ( + "fmt" + "os" + "path/filepath" + "regexp" + "sort" + "strings" +) + +var requiredSkillNames = []string{ + "chatnow-creating-issues", + "chatnow-developing", + "chatnow-maintaining-documentation", + "chatnow-orienting", + "chatnow-securing-changes", + "chatnow-submitting-pull-requests", + "chatnow-testing", + "chatnow-using-git", + "chatnow-verifying-changes", +} + +var skillReferencePattern = regexp.MustCompile(`\]\((references/[^\s)#?]+)(?:#[^)]+)?\)`) + +// ValidateSkillTree validates the ChatNow-specific repository Skill contract +// below root/.agents/skills. +func ValidateSkillTree(root string) []Violation { + skillsRoot := filepath.Join(root, ".agents", "skills") + entries, err := os.ReadDir(skillsRoot) + if err != nil { + return []Violation{{Rule: "SKILL_TREE_UNREADABLE", Message: "Cannot read .agents/skills: " + err.Error()}} + } + + var violations []Violation + var actual []string + for _, entry := range entries { + if entry.IsDir() { + actual = append(actual, entry.Name()) + } + } + sort.Strings(actual) + if !equalStrings(actual, requiredSkillNames) { + violations = append(violations, Violation{ + Rule: "SKILL_SET_INVALID", + Message: "Repository must contain exactly the nine required ChatNow Skill packages", + }) + } + + for _, name := range requiredSkillNames { + skillDir := filepath.Join(skillsRoot, name) + if info, statErr := os.Stat(skillDir); statErr != nil || !info.IsDir() { + continue + } + violations = append(violations, validateSkillPackage(skillDir, name)...) + } + return violations +} + +func validateSkillPackage(skillDir, folderName string) []Violation { + var violations []Violation + skillFile := filepath.Join(skillDir, "SKILL.md") + content, err := os.ReadFile(skillFile) + if err != nil { + violations = append(violations, Violation{ + Rule: "SKILL_FILE_REQUIRED", + Message: folderName + "/SKILL.md is required", + }) + } else { + frontmatter, valid := parseSkillFrontmatter(string(content)) + if !valid { + violations = append(violations, Violation{ + Rule: "SKILL_FRONTMATTER_INVALID", + Message: folderName + " frontmatter must contain only name and description", + }) + } else { + if frontmatter["name"] != folderName { + violations = append(violations, Violation{ + Rule: "SKILL_NAME_MISMATCH", + Message: folderName + " frontmatter name must match its folder", + }) + } + description := frontmatter["description"] + if !strings.HasPrefix(description, "Use when") && !strings.HasPrefix(description, "Use before") { + violations = append(violations, Violation{ + Rule: "SKILL_DESCRIPTION_INVALID", + Message: folderName + " description must state a Use when or Use before trigger", + }) + } + } + violations = append(violations, validateSkillReferences(skillDir, string(content))...) + } + + openAIFile := filepath.Join(skillDir, "agents", "openai.yaml") + openAI, err := os.ReadFile(openAIFile) + if err != nil { + violations = append(violations, Violation{ + Rule: "SKILL_OPENAI_YAML_REQUIRED", + Message: folderName + "/agents/openai.yaml is required", + }) + } else if !defaultPromptContainsSkill(string(openAI), folderName) { + violations = append(violations, Violation{ + Rule: "SKILL_DEFAULT_PROMPT_INVALID", + Message: folderName + " default_prompt must mention $" + folderName, + }) + } + + _ = filepath.WalkDir(skillDir, func(file string, entry os.DirEntry, walkErr error) error { + if walkErr != nil { + violations = append(violations, Violation{ + Rule: "SKILL_TREE_UNREADABLE", + Message: "Cannot inspect " + file + ": " + walkErr.Error(), + }) + return nil + } + if entry.IsDir() { + return nil + } + if forbiddenSkillFilename(entry.Name()) { + violations = append(violations, Violation{ + Rule: "SKILL_FILE_FORBIDDEN", + Message: "Skill package contains forbidden companion document " + entry.Name(), + }) + } + fileContent, readErr := os.ReadFile(file) + if readErr == nil && containsForbiddenSkillWording(string(fileContent)) { + violations = append(violations, Violation{ + Rule: "SKILL_CONTENT_FORBIDDEN", + Message: "Skill package contains unfinished or superseded-state wording", + }) + } + return nil + }) + return violations +} + +func parseSkillFrontmatter(content string) (map[string]string, bool) { + lines := strings.Split(strings.ReplaceAll(content, "\r\n", "\n"), "\n") + if len(lines) < 4 || lines[0] != "---" { + return nil, false + } + values := make(map[string]string) + closed := false + for _, line := range lines[1:] { + if line == "---" { + closed = true + break + } + key, value, ok := strings.Cut(line, ":") + key = strings.TrimSpace(key) + value = strings.TrimSpace(value) + if !ok || value == "" || key != "name" && key != "description" { + return nil, false + } + if _, exists := values[key]; exists { + return nil, false + } + values[key] = value + } + return values, closed && len(values) == 2 +} + +func defaultPromptContainsSkill(content, name string) bool { + for _, line := range strings.Split(strings.ReplaceAll(content, "\r\n", "\n"), "\n") { + trimmed := strings.TrimSpace(line) + if strings.HasPrefix(trimmed, "default_prompt:") { + return strings.Contains(trimmed, "$"+name) + } + } + return false +} + +func validateSkillReferences(skillDir, content string) []Violation { + var violations []Violation + for _, match := range skillReferencePattern.FindAllStringSubmatch(content, -1) { + reference := filepath.FromSlash(match[1]) + if strings.Contains(reference, ".."+string(filepath.Separator)) { + violations = append(violations, Violation{Rule: "SKILL_REFERENCE_BROKEN", Message: "Skill reference must remain inside its package"}) + continue + } + if info, err := os.Stat(filepath.Join(skillDir, reference)); err != nil || info.IsDir() { + violations = append(violations, Violation{ + Rule: "SKILL_REFERENCE_BROKEN", + Message: fmt.Sprintf("Skill reference %s does not resolve", match[1]), + }) + } + } + return violations +} + +func forbiddenSkillFilename(name string) bool { + normalized := strings.ToLower(strings.NewReplacer("_", "-", " ", "-").Replace(name)) + return normalized == "readme.md" || + strings.HasPrefix(normalized, "changelog") || + strings.HasPrefix(normalized, "quick-reference") +} + +func containsForbiddenSkillWording(content string) bool { + lower := strings.ToLower(content) + return regexp.MustCompile(`(?m)\b(todo|tbd)\b`).MatchString(lower) || + strings.Contains(lower, "not merged") || + strings.Contains(lower, "pr #49") || + strings.Contains(lower, "pr #54") || + strings.Contains(lower, "old test framework") +} + +func equalStrings(left, right []string) bool { + if len(left) != len(right) { + return false + } + for index := range left { + if left[index] != right[index] { + return false + } + } + return true +} diff --git a/tests/pkg/agentpolicy/skills_test.go b/tests/pkg/agentpolicy/skills_test.go new file mode 100644 index 0000000..9dc2294 --- /dev/null +++ b/tests/pkg/agentpolicy/skills_test.go @@ -0,0 +1,180 @@ +package agentpolicy + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestValidateSkillTree(t *testing.T) { + tests := []struct { + name string + mutate func(t *testing.T, root string) + wantRules []string + }{ + {name: "valid nine Skill packages"}, + { + name: "missing SKILL md", + mutate: func(t *testing.T, root string) { + removeFixtureFile(t, filepath.Join(skillFixtureRoot(root), "chatnow-testing", "SKILL.md")) + }, + wantRules: []string{"SKILL_FILE_REQUIRED"}, + }, + { + name: "frontmatter name differs from folder", + mutate: func(t *testing.T, root string) { + replaceFixtureText(t, filepath.Join(skillFixtureRoot(root), "chatnow-testing", "SKILL.md"), "name: chatnow-testing", "name: chatnow-other") + }, + wantRules: []string{"SKILL_NAME_MISMATCH"}, + }, + { + name: "extra frontmatter key", + mutate: func(t *testing.T, root string) { + replaceFixtureText(t, filepath.Join(skillFixtureRoot(root), "chatnow-testing", "SKILL.md"), "description:", "license: MIT\ndescription:") + }, + wantRules: []string{"SKILL_FRONTMATTER_INVALID"}, + }, + { + name: "description lacks trigger", + mutate: func(t *testing.T, root string) { + replaceFixtureText(t, filepath.Join(skillFixtureRoot(root), "chatnow-testing", "SKILL.md"), "description: Use when", "description: Guidance for") + }, + wantRules: []string{"SKILL_DESCRIPTION_INVALID"}, + }, + { + name: "missing openai yaml", + mutate: func(t *testing.T, root string) { + removeFixtureFile(t, filepath.Join(skillFixtureRoot(root), "chatnow-testing", "agents", "openai.yaml")) + }, + wantRules: []string{"SKILL_OPENAI_YAML_REQUIRED"}, + }, + { + name: "default prompt lacks Skill token", + mutate: func(t *testing.T, root string) { + replaceFixtureText(t, filepath.Join(skillFixtureRoot(root), "chatnow-testing", "agents", "openai.yaml"), "$chatnow-testing", "the testing Skill") + }, + wantRules: []string{"SKILL_DEFAULT_PROMPT_INVALID"}, + }, + { + name: "broken one level reference", + mutate: func(t *testing.T, root string) { + appendFixtureText(t, filepath.Join(skillFixtureRoot(root), "chatnow-testing", "SKILL.md"), "\nRead [missing](references/missing.md).\n") + }, + wantRules: []string{"SKILL_REFERENCE_BROKEN"}, + }, + { + name: "forbidden companion document", + mutate: func(t *testing.T, root string) { + writeFixtureFile(t, filepath.Join(skillFixtureRoot(root), "chatnow-testing", "README.md"), "duplicate guidance") + }, + wantRules: []string{"SKILL_FILE_FORBIDDEN"}, + }, + { + name: "unfinished or historical wording", + mutate: func(t *testing.T, root string) { + appendFixtureText(t, filepath.Join(skillFixtureRoot(root), "chatnow-testing", "SKILL.md"), "\nTODO: describe the old test framework migration.\n") + }, + wantRules: []string{"SKILL_CONTENT_FORBIDDEN"}, + }, + { + name: "unexpected Skill package", + mutate: func(t *testing.T, root string) { + writeFixtureSkill(t, root, "chatnow-extra") + }, + wantRules: []string{"SKILL_SET_INVALID"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + root := t.TempDir() + writeValidSkillTree(t, root) + if tt.mutate != nil { + tt.mutate(t, root) + } + + got := ValidateSkillTree(root) + if tt.wantRules == nil { + if len(got) != 0 { + t.Fatalf("ValidateSkillTree() violations = %#v, want none", got) + } + return + } + for _, rule := range tt.wantRules { + if !hasRule(got, rule) { + t.Errorf("ValidateSkillTree() rules = %v, want %q", violationRules(got), rule) + } + } + }) + } +} + +func TestValidateRepositorySkillTree(t *testing.T) { + root, err := filepath.Abs(filepath.Join("..", "..", "..")) + if err != nil { + t.Fatal(err) + } + if got := ValidateSkillTree(root); len(got) != 0 { + t.Fatalf("repository Skill violations = %#v", got) + } +} + +func writeValidSkillTree(t *testing.T, root string) { + t.Helper() + for _, name := range requiredSkillNames { + writeFixtureSkill(t, root, name) + } + writeFixtureFile(t, filepath.Join(skillFixtureRoot(root), "chatnow-orienting", "references", "core-flows.md"), "# Core flows\n") +} + +func writeFixtureSkill(t *testing.T, root, name string) { + t.Helper() + skill := "---\nname: " + name + "\ndescription: Use when an agent needs this ChatNow workflow\n---\n\n# Skill\n" + yaml := "interface:\n display_name: \"Fixture\"\n short_description: \"Fixture Skill\"\n default_prompt: \"Use $" + name + " for this task.\"\n" + writeFixtureFile(t, filepath.Join(skillFixtureRoot(root), name, "SKILL.md"), skill) + writeFixtureFile(t, filepath.Join(skillFixtureRoot(root), name, "agents", "openai.yaml"), yaml) +} + +func skillFixtureRoot(root string) string { + return filepath.Join(root, ".agents", "skills") +} + +func writeFixtureFile(t *testing.T, file, content string) { + t.Helper() + if err := os.MkdirAll(filepath.Dir(file), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(file, []byte(content), 0o644); err != nil { + t.Fatal(err) + } +} + +func removeFixtureFile(t *testing.T, file string) { + t.Helper() + if err := os.Remove(file); err != nil { + t.Fatal(err) + } +} + +func replaceFixtureText(t *testing.T, file, old, replacement string) { + t.Helper() + content, err := os.ReadFile(file) + if err != nil { + t.Fatal(err) + } + updated := strings.Replace(string(content), old, replacement, 1) + if updated == string(content) { + t.Fatalf("fixture %s does not contain %q", file, old) + } + writeFixtureFile(t, file, updated) +} + +func appendFixtureText(t *testing.T, file, addition string) { + t.Helper() + content, err := os.ReadFile(file) + if err != nil { + t.Fatal(err) + } + writeFixtureFile(t, file, string(content)+addition) +} diff --git a/tests/pkg/agentpolicy/types.go b/tests/pkg/agentpolicy/types.go new file mode 100644 index 0000000..ba314c4 --- /dev/null +++ b/tests/pkg/agentpolicy/types.go @@ -0,0 +1,33 @@ +package agentpolicy + +// Violation identifies one policy rule that an input violates. +type Violation struct { + Rule string + Message string +} + +// IssueInput contains the fields needed to validate a ChatNow Issue. +type IssueInput struct { + Title string + Body string + TargetVersion string + IsEmergency bool +} + +// PullRequestInput contains the fields needed to validate a ChatNow pull +// request without performing event, filesystem, or Git I/O. +type PullRequestInput struct { + Title string + Body string + Head string + Base string + IssueNumber int + ChangedFiles []string +} + +// SkillSyncInput contains the pull request declaration and changed paths used +// to enforce repository Skill synchronization. +type SkillSyncInput struct { + Body string + ChangedFiles []string +}