Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .cursor/rules/octobot-cloud.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ alwaysApply: true
- Before Python, `OctoBot`, or pytest: `source .cursor/env.sh`
- **Pytest failure:** follow [When tests fail](.cursor/skills/octobot-cloud/reference-pytest.md#when-tests-fail) in `reference-pytest.md`, starting with [step 1 — visible logs](.cursor/skills/octobot-cloud/reference-pytest.md#1-re-run-with-visible-logs); do not patch symptoms (mocks, seeds, timeouts) until the failure boundary is identified.
- **Tentacles:** edit only `packages/tentacles/`; after changes run `bash .cursor/reinstall-tentacles.sh`. Never commit repo-root `tentacles/`.
- **Git:** checkout `dev` (or user base) → feature branch → commit → open PR to **`dev`** unless the user specifies another target.
- **Git:** checkout `dev` (or user base) → feature branch → commit → open PR to **`dev`** unless the user specifies another target. Do not commit agent plan files (`PLAN-*.md`, `*.plan.md`, `.cursor/plans/`); use Plan mode or chat only — delete scratch plans before staging (`path.deny_agent_plans`). Do **not** edit agent docs (`**/AGENTS.md`, `.cursor/skills/**`, `.cursor/rules/**`, `.cursor/README.md`, `CONTRIBUTING-agent.md`, `tools/**/README.md`, `tools/**/ARCHITECTURE.md`) unless the task owns them; if diffs are out of scope, `git checkout origin/<base> -- <paths>` before commit (`agent_docs.no_regression_vs_merge_base`).
- **Node UI tests:** Vitest (`npm test`) + Python/API tests; do not add `node_web_interface/e2e/` or Playwright specs (`path.deny_node_web_playwright_e2e`). Optional live UI QA via skill **agent-seed**, not committed e2e.
- Before handoff: `python -m tools.extended_linter --base origin/dev` (use `origin/<base>` matching the PR target).
- No `pip install` / `npm install` to fix imports. No edits under `user/` or secret/env files.
- Cross-package or tentacles work: read root `AGENTS.md` and colocated `AGENTS.md` for every area you touch (`octobot/`, `packages/<name>/`, `packages/tentacles/`).
Expand Down
4 changes: 2 additions & 2 deletions .cursor/skills/cloud-roadmap/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Also when the user invokes **cloud-roadmap** or asks for a **cloud-safe multi-st
2. Skim root `AGENTS.md` and every colocated `AGENTS.md` for packages in scope.
3. If not in Plan mode but the user wants a roadmap-style plan, use the same rules in the reply or switch to Plan + `CreatePlan`.

**Output:** submit via **CreatePlan** only. Do **not** write `.cursor/roadmaps/*.md` unless the user asks to persist a file.
**Output:** submit via **CreatePlan** only. Do **not** write `.cursor/roadmaps/*.md` unless the user asks to persist a file. Never commit plan artifacts in the repo (`PLAN-*.md`, `*.plan.md`, `.cursor/plans/`) — `path.deny_agent_plans`.

## Clarify first; never guess

Expand Down Expand Up @@ -97,7 +97,7 @@ Pytest matrix: [reference-pytest.md](../octobot-cloud/reference-pytest.md) (skil
| Unit (Python) | Test path + enumerated behaviors | Package `tests/` or `tools/tests/…` per `AGENTS.md` **Tests** |
| Unit (Node UI) | TS under `packages/tentacles/Services/Interfaces/node_web_interface/` | `src/**/__tests__`; cwd that directory; `npm test` (vitest). Profile **`ui-node-web`** if build env needed. |
| Functional / integration | Cross-package or I/O flows | `tools/tests`, integration dirs, tentacles-dependent pytest |
| UI functional | Browser / seeded grid | **agent-seed** after vitest on logic |
| UI functional | Browser / seeded grid | Default **N/A — Vitest + API tests**; **agent-seed** only when the user explicitly wants manual `/app` QA — **not** new Playwright `e2e/` files in git (`path.deny_node_web_playwright_e2e`) |

**Milestone catalog**

Expand Down
11 changes: 9 additions & 2 deletions .cursor/skills/octobot-cloud/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ Multi-step **roadmap** plans (title or description contains `roadmap`): author w
1. `source .cursor/env.sh` before Python tooling.
2. Checkout **`dev`** (or user base) → create feature branch → implement → commit → PR to **`dev`** (match `--base` on extended_linter to PR target).
3. After `packages/tentacles/` edits: `bash .cursor/reinstall-tentacles.sh`.
4. Before handoff: `python -m tools.extended_linter --base origin/dev` (or `origin/<base_ref>`).
5. Run targeted pytest per [reference-pytest.md](reference-pytest.md) (CI matrix). **If any test fails,** start with [step 1 — visible logs](reference-pytest.md#1-re-run-with-visible-logs), then follow [When tests fail](reference-pytest.md#when-tests-fail) **before** changing production code or the harness.
4. Before commit: remove any scratch plan files (`PLAN-*.md`, `*.plan.md`, `.cursor/plans/`) from the tree; do not stage them (`path.deny_agent_plans`). Do not change agent docs (see `octobot-cloud.mdc` path list) unless the task owns them — otherwise restore from `origin/<base>` (`agent_docs.no_regression_vs_merge_base`).
5. After `git rebase` or fast-forward onto `origin/dev`, re-checkout unrelated agent-doc paths from `origin/dev` if the working tree still has stale copies. Squash commits must reflect the **current** branch tip, not an old cloud workspace snapshot.
6. Before handoff: `python -m tools.extended_linter --base origin/dev` (or `origin/<base_ref>`).
7. Run targeted pytest per [reference-pytest.md](reference-pytest.md) (CI matrix). **If any test fails,** start with [step 1 — visible logs](reference-pytest.md#1-re-run-with-visible-logs), then follow [When tests fail](reference-pytest.md#when-tests-fail) **before** changing production code or the harness.

## Install and env

Expand Down Expand Up @@ -59,6 +61,11 @@ See **[reference-pytest.md](reference-pytest.md)** for cwd and PYTHONPATH per CI
3. Find the **failure boundary** (last good layer vs first bad layer); do not widen mocks, seeds, or timeouts until that boundary is clear.
4. Apply the full protocol in [When tests fail](reference-pytest.md#when-tests-fail) before editing production code or the harness.

## Node UI tests (agents)

- **In PRs:** Vitest in `packages/tentacles/Services/Interfaces/node_web_interface/` (`npm test`) plus Python/API tests (`node_api_interface`, `packages/node`, tentacles pytest as needed). **Do not** add Playwright e2e under `e2e/` or `test:e2e` scripts (`path.deny_node_web_playwright_e2e`).
- CI runs Vitest for tentacles `package.json` projects; e2e is not in the matrix.

## Node UI manual QA (agent seed)

**Not required** for normal pytest / `ci-tentacles` package work — only when the task uses the live Node web UI.
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,8 @@ installer/

# ai
analysis/

# Agent session plans (never commit; see path.deny_agent_plans in extended_linter)
**/PLAN-*.md
**/*.plan.md
.cursor/plans/
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ source .cursor/env.sh
python -m tools.extended_linter --base origin/dev
```

Use `origin/<base>` matching your PR target. Policy: `tools/extended_linter/config/policy.yaml` — see `tools/extended_linter/ARCHITECTURE.md`.
Use `origin/<base>` matching your PR target. Policy: `tools/extended_linter/config/policy.yaml` — see `tools/extended_linter/ARCHITECTURE.md`. No agent plan files or Node UI Playwright e2e in the diff (`path.deny_agent_plans`, `path.deny_node_web_playwright_e2e`). No accidental agent-doc regressions (`agent_docs.no_regression_vs_merge_base`; see `octobot-cloud.mdc`).

## Architecture for agents

Expand Down
8 changes: 5 additions & 3 deletions CONTRIBUTING-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ See [`.cursor/README.md`](.cursor/README.md). Install profile **`ci-tentacles`**
1. Branch from `dev` (or user-specified base).
2. For cross-package work, read root [`AGENTS.md`](AGENTS.md) and each colocated `AGENTS.md` for packages you touch.
3. Run `python -m tools.extended_linter --base origin/<pr-base>` before handoff.
4. CI: **OctoBot-CI** job **`extended_linter`** (`pytest tools/tests` with wheel + tentacles; PR policy with `--skip-tentacles-reinstall`, no `cloud-install`). Package **`tests`** matrix unchanged.
4. Do not commit agent session plans (`path.deny_agent_plans`: `PLAN-*.md`, `*.plan.md`, `.cursor/plans/`). Node UI: Vitest + Python/API tests only — no Playwright e2e under `node_web_interface/e2e/` (`path.deny_node_web_playwright_e2e`).
5. **Agent docs:** edit `AGENTS.md`, `.cursor/skills/**`, `.cursor/rules/**`, `.cursor/README.md`, `CONTRIBUTING-agent.md`, and `tools/**/README.md` / `tools/**/ARCHITECTURE.md` only when the PR owns them. `extended_linter` blocks shrink/backdated **Last reviewed** (where present) vs merge-base (`agent_docs.no_regression_vs_merge_base`). Large trims need explicit reviewer intent.
6. CI: **OctoBot-CI** job **`extended_linter`** (`pytest tools/tests` with wheel + tentacles; PR policy with `--skip-tentacles-reinstall`, no `cloud-install`). Package **`tests`** matrix unchanged.

## Adding a policy rule

Expand All @@ -20,9 +22,9 @@ See [tools/extended_linter/README.md#adding-a-rule](tools/extended_linter/README
3. Add tests under `tools/tests/extended_linter/layers/`.
4. Summarize in `octobot-cloud` skill only for human context; YAML is the contract.

## AGENTS.md hygiene (guidance, not CI in v1)
## AGENTS.md hygiene

When you change package boundaries (owns, public API, test cwd), update the matching colocated `AGENTS.md` (`octobot/`, `packages/<name>/`, or `packages/tentacles/`) and bump **Last reviewed**. CI does not enforce this yet.
When you change package boundaries (owns, public API, test cwd), update the matching colocated `AGENTS.md` (`octobot/`, `packages/<name>/`, or `packages/tentacles/`) and bump **Last reviewed**. `extended_linter` enforces no shrink/backdate on agent-doc paths vs merge-base (`agent_docs.no_regression_vs_merge_base`); it does not require updates when boundaries change.

### AGENTS.md sections (new packages)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ npm ci # install from the committed package-lock.json
npm run generate-client # regenerate the typed API client from openapi.json
npm run dev # Vite dev server
npm run build # tsc + vite build -> dist/
npm test
npm test # Vitest — this is the CI test gate for this package
```

Do not add Playwright e2e under `e2e/` in this repo (agent policy: Vitest + Python/API tests; live UI QA via [`tools/agent_seed/README.md`](../../../../../tools/agent_seed/README.md)).

`openapi.json` is generated by the `node_api_interface` tentacle (`python build_openapi.py`) and is
git-ignored. Generate it once before `npm run generate-client` if it is missing.

Expand Down
10 changes: 7 additions & 3 deletions tools/extended_linter/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ flowchart TB
hook[hooks.tentacles]
git[layers.git_scope]
path[layers.path_policy]
agentDocs[layers.agent_docs_policy]
diff[layers.diff_policy]
report[reporting.format]
config[config.policy.yaml]
Expand All @@ -17,6 +18,7 @@ flowchart TB
engine --> git
engine --> hook
engine --> path
engine --> agentDocs
engine --> diff
cli --> report
```
Expand All @@ -25,8 +27,9 @@ flowchart TB
2. **Git layer** — verify `--base` ref; list changed paths + unified diff
3. **Tentacles hook** — if `packages/tentacles/**` in diff, run `reinstall-tentacles.sh` (not a policy violation). Requires cloud install / `env.sh`. **CI** passes `--skip-tentacles-reinstall`; agents run the hook by default.
4. **Path layer** — `layers.path_policy.run(changed_paths, policy)`
5. **Diff layer** — `layers.diff_policy.run(diff_text, policy)`
6. **Report** — `reporting.format` in CLI
5. **Agent docs layer** — `layers.agent_docs_policy.run(...)` for policy `path_globs` (AGENTS.md, `.cursor/skills/**/SKILL.md`, `.cursor/rules/**`, tools README/ARCHITECTURE, etc.): shrink / Last reviewed vs merge-base; uses `git show` like `git_scope`
6. **Diff layer** — `layers.diff_policy.run(diff_text, policy)`
7. **Report** — `reporting.format` in CLI

## RunContext

Expand All @@ -38,7 +41,7 @@ flowchart TB
|--------|------------|
| `cli.py` | `config.loader`, `engine.runner`, `reporting.format` |
| `engine.runner` | `config`, `layers`, `hooks`, `domain` |
| `layers/*` | `domain` only (git layer may use `subprocess`) |
| `layers/*` | `domain` only (`git_scope` and `agent_docs_policy` may use `subprocess` for `git`; `agent_docs_policy` may import `path_policy` for globs) |
| `hooks/*` | `domain.paths`; `subprocess` for scripts |
| `reporting/*` | `domain.models` |
| `config.loader` | PyYAML only |
Expand All @@ -54,6 +57,7 @@ flowchart TB
| Change | Location |
|--------|----------|
| New path deny / glob | `config/policy.yaml` + optional `kind` in `layers/path_policy.py` |
| Agent docs regression vs merge-base | `config/policy.yaml` `agent_docs_rules` + `layers/agent_docs_policy.py` |
| New diff regex | `config/policy.yaml` `patterns` or `kind` in `layers/diff_policy.py` |
| New pre-check side effect | `hooks/` + call from `engine/runner.py` only |
| CLI flags | `cli.py` |
Expand Down
23 changes: 23 additions & 0 deletions tools/extended_linter/config/policy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,29 @@ path_rules:
- "**/secrets.json"
- "**/*service-account*.json"
hint: Use secrets manager / local only
- rule_id: path.deny_agent_plans
globs:
- "**/PLAN-*.md"
- "**/*.plan.md"
- ".cursor/plans/**"
hint: Agent/session plans are not product code; keep in Cursor Plan UI or chat; never commit
- rule_id: path.deny_node_web_playwright_e2e
globs:
- "packages/tentacles/Services/Interfaces/node_web_interface/e2e/**"
hint: Node UI — use Vitest (npm test) + Python/API tests; no Playwright e2e in repo; manual UI QA via agent-seed

agent_docs_rules:
- rule_id: agent_docs.no_regression_vs_merge_base
shrink_line_threshold: 5
path_globs:
- "**/AGENTS.md"
- ".cursor/skills/**/SKILL.md"
- ".cursor/README.md"
- ".cursor/rules/**"
- "CONTRIBUTING-agent.md"
- "tools/**/README.md"
- "tools/**/ARCHITECTURE.md"
hint: Do not shrink or backdate agent docs unless this PR owns them; restore from origin/<base> or merge-base

diff_rules:
- rule_id: diff.no_pip_install
Expand Down
9 changes: 9 additions & 0 deletions tools/extended_linter/engine/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import tools.extended_linter.domain.models as domain_models
import tools.extended_linter.engine.context as engine_context
import tools.extended_linter.hooks.tentacles as hooks_tentacles
import tools.extended_linter.layers.agent_docs_policy as layer_agent_docs
import tools.extended_linter.layers.diff_policy as layer_diff
import tools.extended_linter.layers.git_scope as layer_git
import tools.extended_linter.layers.path_policy as layer_path
Expand Down Expand Up @@ -39,6 +40,14 @@ def run(config: RunnerConfig) -> list[domain_models.Violation]:
):
hooks_tentacles.run_reinstall(context.repo_root)
context.violations.extend(layer_path.run(context.changed_paths, context.policy))
context.violations.extend(
layer_agent_docs.run(
context.repo_root,
context.merge_base_sha,
context.changed_paths,
context.policy,
)
)
context.diff_text = layer_git.unified_diff(context.repo_root, context.merge_base_sha)
context.violations.extend(layer_diff.run(context.diff_text, context.policy))
return context.violations
154 changes: 154 additions & 0 deletions tools/extended_linter/layers/agent_docs_policy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import datetime
import pathlib
import re
import subprocess
import typing

import tools.extended_linter.domain.models as domain_models
import tools.extended_linter.domain.paths as domain_paths
import tools.extended_linter.layers.path_policy as layer_path_policy

_LAST_REVIEWED_SECTION = re.compile(
r"##\s+Last reviewed\s*\n(?:.*\n)*?-\s*(\d{4}-\d{2}-\d{2})",
re.IGNORECASE,
)

_DEFAULT_PATH_GLOBS = [
"**/AGENTS.md",
".cursor/skills/**/SKILL.md",
".cursor/README.md",
".cursor/rules/**",
"CONTRIBUTING-agent.md",
"tools/**/README.md",
"tools/**/ARCHITECTURE.md",
]


def parse_last_reviewed_date(content: str) -> datetime.date | None:
match = _LAST_REVIEWED_SECTION.search(content)
if not match:
return None
try:
return datetime.date.fromisoformat(match.group(1))
except ValueError:
return None


def should_flag_shrink(
base_line_count: int,
head_line_count: int,
shrink_line_threshold: int,
) -> bool:
return head_line_count < base_line_count - shrink_line_threshold


def matches_agent_doc_path(path: str, path_globs: list[str]) -> bool:
normalized = domain_paths.normalize_path(path)
for pattern in path_globs:
if layer_path_policy._matches_glob(normalized, pattern):
return True
return False


def regression_reasons(
base_content: str,
head_content: str,
shrink_line_threshold: int,
doc_path: str = "agent doc",
) -> list[str]:
reasons: list[str] = []
base_lines = base_content.splitlines()
head_lines = head_content.splitlines()
if should_flag_shrink(len(base_lines), len(head_lines), shrink_line_threshold):
reasons.append(
f"Agent doc `{doc_path}` shrank by {len(base_lines) - len(head_lines)} lines "
f"(threshold {shrink_line_threshold})"
)
base_reviewed = parse_last_reviewed_date(base_content)
head_reviewed = parse_last_reviewed_date(head_content)
if base_reviewed is not None and head_reviewed is not None:
if head_reviewed < base_reviewed:
reasons.append(
f"Last reviewed regressed ({head_reviewed.isoformat()} "
f"< {base_reviewed.isoformat()})"
)
return reasons


def _git_show_blob(
repo_root: pathlib.Path,
object_ref: str,
path: str,
) -> str | None:
result = subprocess.run(
["git", "show", f"{object_ref}:{path}"],
cwd=repo_root,
capture_output=True,
text=True,
check=False,
)
if result.returncode != 0:
return None
return result.stdout


def _rule_config(
policy: dict[str, typing.Any],
) -> tuple[str, str, int, list[str]]:
rules = policy.get("agent_docs_rules") or []
if not rules:
return (
"agent_docs.no_regression_vs_merge_base",
"Do not shrink or backdate agent docs",
5,
list(_DEFAULT_PATH_GLOBS),
)
rule = rules[0]
globs = rule.get("path_globs") or list(_DEFAULT_PATH_GLOBS)
return (
rule.get("rule_id", "agent_docs.no_regression_vs_merge_base"),
rule.get(
"hint",
"Do not shrink or backdate agent docs unless this PR owns them; "
"restore from origin/<base> or merge-base",
),
int(rule.get("shrink_line_threshold", 5)),
list(globs),
)


def run(
repo_root: pathlib.Path,
merge_base_sha: str,
changed_paths: list[str],
policy: dict[str, typing.Any],
) -> list[domain_models.Violation]:
rule_id, hint, shrink_threshold, path_globs = _rule_config(policy)
violations: list[domain_models.Violation] = []
for path in changed_paths:
if not matches_agent_doc_path(path, path_globs):
continue
normalized = domain_paths.normalize_path(path)
base_content = _git_show_blob(repo_root, merge_base_sha, normalized)
if base_content is None:
continue
head_path = repo_root / normalized
if not head_path.is_file():
continue
head_content = head_path.read_text(encoding="utf-8")
for reason in regression_reasons(
base_content,
head_content,
shrink_threshold,
doc_path=normalized,
):
violations.append(
domain_models.Violation(
rule_id=rule_id,
file=normalized,
line=None,
hint=hint,
detail=reason,
)
)
return violations
Loading
Loading