diff --git a/README.md b/README.md index 730001fe..a0fd1793 100644 --- a/README.md +++ b/README.md @@ -184,6 +184,23 @@ If `AGENTS.md` already exists it is never overwritten. See `plugins/aem/cloud-service/skills/ensure-agents-md/` for the skill, template, and module catalog. +### AEM as a Cloud Service — aem-agentkit (beta) + +The `aem-agentkit` skill complements `ensure-agents-md` by layering everything beyond the root `AGENTS.md` needed for agentic workflows across Claude Code, Cursor, GitHub Copilot, Codex, Continue.dev, Cline, Windsurf, and Augment Code. It writes only into agent-meta locations and never modifies customer source code. Scope: **AEM as a Cloud Service only** — the skill exits early on 6.5 LTS / AMS / on-premise layouts. + +- Per-module `AGENTS.md` in each detected AEM module (focused context the agent loads only when working in that module, recursive for nested AEM monorepos) +- Machine-readable codified context under `.aem/context/`: component catalog, OSGi services / Sling Models / Sling Servlets index, derived conventions with evidence pointers, anti-patterns with absolute Cloud Service documentation links, glossary, test patterns, canonical API namespaces, run manifest (every file written + every heuristic decision) +- Silent IDE detection — writes project-scoped subagents (`.claude/agents/aem-*.md`) and slash commands (`.claude/commands/*.md`) for Claude, rule files (`.cursor/rules/aem-*.mdc`) for Cursor, scoped instructions (`.github/instructions/aem-*.instructions.md`) for GitHub Copilot, rules (`.continue/rules/aem-*.md`) for Continue, plus concatenated rule files for Cline / Windsurf / Augment. A single canonical role-prompt is projected into each format so the content is identical across IDEs. +- Non-destructive `.mcp.json` / `.cursor/mcp.json` placeholders when missing (inert by construction — no `command` field, `_TODO_` key prefix) +- Embedded guardrails (search-before-create, verify-before-import, no `/libs` writes, stop-on-red, honor indexes after writing code) +- Idempotent, marker-based, byte-for-byte non-destructive — `git diff` after a run shows zero changes to pre-existing files. Customer opt-out via a `_disable_agentkit` file at the workspace root, with explicit single-archetype-vs-monorepo handling. +- Deterministic by construction — realpath + workspace boundary checks, SHA-256 canonical-body marker checksums, atomic `.tmp` + `rename(2)` writes, exhaustive Unicode sanitization, sorted-key JSON, bounded file walks (100,000 files / depth 32 / 10,000 per subtree), advisory workspace lock — all performed by the deterministic helper documented in `references/helpers.md`. +- Beta. Verify all outputs before applying them to production projects. + +`aem-agentkit` does not replace `ensure-agents-md`; the two are complementary. When the root `AGENTS.md` is missing and `ensure-agents-md` is available, `aem-agentkit` defers to it as step 0. When `ensure-agents-md` is not installed, `aem-agentkit` proceeds with everything else and emits a one-line notice. + +See `plugins/aem/cloud-service/skills/aem-agentkit/` for the skill, references, templates, and tool-specific projection rules. + ### AEM Workflow Workflow skills cover the full AEM Granite Workflow Engine lifecycle — from designing and implementing workflows to production debugging and incident triaging. Like Dispatcher, they are split by runtime flavor: diff --git a/package.json b/package.json index c182191e..91811cfa 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "type": "module", "description": "Adobe skills for AI coding agents", "scripts": { - "validate": "find plugins -name SKILL.md -exec dirname {} \\; | xargs -I {} skills-ref validate {}" + "validate": "find plugins -name SKILL.md -exec dirname {} \\; | xargs -I {} skills-ref validate {}", + "test:aem-agentkit-helper": "bash plugins/aem/cloud-service/skills/aem-agentkit/tests/run-tests.sh" }, "devDependencies": { "@semantic-release/changelog": "^6.0.3", diff --git a/plugins/aem/cloud-service/skills/aem-agentkit/README.md b/plugins/aem/cloud-service/skills/aem-agentkit/README.md new file mode 100644 index 00000000..ff263cdf --- /dev/null +++ b/plugins/aem/cloud-service/skills/aem-agentkit/README.md @@ -0,0 +1,123 @@ +# aem-agentkit (beta) + +Bootstrap an **AEM as a Cloud Service** repository for agentic workflows. + +> **Beta Skill**: This skill is in beta and under active development. +> Results should be reviewed carefully before use in production. +> Report issues at https://github.com/adobe/skills/issues + +This skill writes a small set of agent-meta files at the workspace root and +inside existing modules so coding agents and any harness on top of them can +work on the customer's repository with high reliability and low +hallucination. It never modifies customer source code. + +**Scope:** AEM as a Cloud Service only. The skill exits early on AEM 6.5 +LTS, AMS, and on-premise AEM layouts. + +See [`SKILL.md`](./SKILL.md) for the full contract. + +## What gets created + +### Universal layer (always written if missing) + +| Path | Purpose | +|---|---| +| `/AGENTS.md` | Focused per-module context (sized for one task) | +| `.aem/context/components.json` | Machine-readable component catalog | +| `.aem/context/osgi-services.json` | Sling Models, OSGi services, Sling Servlets | +| `.aem/context/conventions.md` | Derived conventions with evidence pointers | +| `.aem/context/avoid.md` | Anti-patterns detected in the repo | +| `.aem/context/glossary.md` | Domain disambiguation | +| `.aem/context/test-patterns.md` | How this project writes tests | +| `.aem/context/aem-api-namespaces.md` | Canonical AEM as a Cloud Service API package roots (verify-before-import support) | +| `.aem/context/README.md` | Index of the above | +| `.aem/context/.agentkit-manifest.json` | Run manifest: every file written, post-write checksum, every heuristic decision | +| `.aem/context/.agentkit.lock` | Workspace advisory lock so parallel invocations exit cleanly | + +### Tool-specific layer (silent auto-detection) + +| Tool | Detection signal | Tool-specific artifacts | +|---|---|---| +| Claude Code | `.claude/` dir or `CLAUDE.md` | `.claude/agents/aem-*.md`, `.claude/commands/.md`, `.mcp.json` | +| Cursor | `.cursor/` dir | `.cursor/rules/aem-*.mdc`, `.cursor/mcp.json` | +| GitHub Copilot | `.github/copilot-instructions.md` or `.github/*.yml` workflow | `.github/instructions/aem-*.instructions.md` (Copilot-instructions written only if missing) | +| Codex | (universal layer is sufficient) | — | +| Continue.dev | `.continue/` dir | `.continue/rules/aem-*.md` | +| Cline | `.clinerules` or `.vscode/extensions.json` listing the Cline extension | `.clinerules` (only when missing) | +| Windsurf | `.windsurfrules` or `.codeium/` directory | `.windsurfrules` (only when missing) | +| Augment Code | `.augment/` directory or pre-existing `augment.md` | `augment.md` (only when missing) | +| Aider, Gemini CLI, Zed, Factory, Jules, Devin, Amp, Kilo, RooCode, Warp, JetBrains Junie, Ona, Phoenix | (universal layer is sufficient — read `AGENTS.md` natively) | — | + +A single canonical role-prompt source is projected into each tool's format +so the content seen by the agent is identical regardless of IDE. The +deferred-role inline fallback (for the concatenated single-file +projections — Cline / Windsurf / Augment) writes a sibling +`.aem-roles-extra.md` so the customer always has every role body on +disk, not behind a pointer to the published skill bundle. + +## What never changes + +Customer Java, HTL, JSP, JS/TS/CSS, dispatcher configuration, FileVault XML, +`pom.xml`, content `.json`, OSGi config files, `README`, `CONTRIBUTING`, +`LICENSE`, the root `AGENTS.md` / `CLAUDE.md`, or any other pre-existing file +lacking the marker comment. See `SKILL.md` § "Hard guarantee" for the exact +allow-list. + +## Relationship to `ensure-agents-md` + +`aem-agentkit` does not replace `ensure-agents-md`; they are complementary. +`ensure-agents-md` owns the root `AGENTS.md` + `CLAUDE.md`. `aem-agentkit` +owns everything else. If root `AGENTS.md` is missing and `ensure-agents-md` +is available, `aem-agentkit` defers to it as step 0. If it is not +available, `aem-agentkit` proceeds with everything except the root +`AGENTS.md` and emits a one-line notice. + +## Status + +Beta. Skill version `1.0.0-beta`. Generated JSON files carry +`schemaVersion: "1"`. Marker contract, migration rules, and the +deterministic-helper version pin are documented in +[`references/upgrade-and-migration.md`](./references/upgrade-and-migration.md) +and [`references/helpers.md`](./references/helpers.md). + +Verify all outputs before applying to production projects. + +## End-to-end agentic workflow coverage + +This skill covers the **bootstrap** phase of an end-to-end agentic +workflow on AEM as a Cloud Service. Other phases are handled by sibling +skills already published in the `aem-cloud-service` plugin +(`plugins/aem/cloud-service/skills/` in [adobe/skills](https://github.com/adobe/skills)): + +| Phase | Public sibling skill | +|---|---| +| Bootstrap (this skill) | `aem-agentkit` — per-module AGENTS.md, codified context, tool-specific routing | +| Root context | `ensure-agents-md` — root AGENTS.md + CLAUDE.md | +| Pattern transformation | `best-practices` — Cloud Service patterns, legacy-to-cloud transformations | +| Component scaffolding | `create-component` — opinionated component scaffolds | +| Migration orchestration | `migration` — BPA / CAM orchestration on top of `best-practices` | +| Workflow authoring | `aem-workflow` — Granite Workflow model design, development, triggering, debugging, triaging | +| Dispatcher | `dispatcher` — config authoring, advisory, incident response, performance tuning, security hardening | +| Content distribution | `content-distribution` — Sling distribution and replication | +| Rapid Development | `aem-rde` — RDE deploy, log inspection, snapshots, troubleshooting via `aio aem rde` | + +The bootstrap this skill produces (per-module `AGENTS.md`, codified +context under `.aem/context/`, project-scoped subagents and rules) is +read by every later-phase skill. A customer who has installed the +`aem-cloud-service` plugin (which bundles every skill above) and run +`aem-agentkit` has end-to-end agentic-workflow coverage on their +repository. + +## Trademarks + +This skill is licensed under Apache 2.0. References to third-party IDE +and agent names (Claude Code, Cursor, GitHub Copilot, Codex, Continue, +Cline, Windsurf, Augment, Aider, Gemini CLI, Zed, RooCode, JetBrains +Junie, and others) are nominative and descriptive only — they identify +the tools the skill produces artifacts for. All such names remain the +trademarks of their respective owners. This skill is not affiliated with +or endorsed by any of them. + +## Reporting issues + +https://github.com/adobe/skills/issues diff --git a/plugins/aem/cloud-service/skills/aem-agentkit/SKILL.md b/plugins/aem/cloud-service/skills/aem-agentkit/SKILL.md new file mode 100644 index 00000000..c414bc84 --- /dev/null +++ b/plugins/aem/cloud-service/skills/aem-agentkit/SKILL.md @@ -0,0 +1,249 @@ +--- +name: aem-agentkit +description: | + [BETA] Bootstrap an AEM as a Cloud Service repository for agentic workflows + across Claude Code, Cursor, GitHub Copilot, Codex, Continue, Cline, Windsurf, + Augment, and any AGENTS.md-spec-compliant agent. Triggers: "set up agentic + context", "bootstrap aem-agentkit", "make this repo agent-ready", "agentkit". + Generates per-module AGENTS.md, codified context under .aem/context/, + project-scoped subagents, slash commands, rule files, Copilot instructions, + MCP placeholders, and guardrails — without modifying customer source. + Detects installed agent stacks silently. Defers root AGENTS.md to + ensure-agents-md when present. Deterministic operations (realpath, SHA-256 + canonical-body checksum, atomic write, Unicode sanitization, deny-list, + bounded walk) run through the helper in references/helpers.md. AEM as a + Cloud Service only; exits early on 6.5 LTS, AMS, on-premise. Beta — verify + outputs before production use. +license: Apache-2.0 +compatibility: AEM as a Cloud Service projects only (Java stack, Maven, Dispatcher). Not for AEM 6.5 LTS, AMS, or on-premise. +metadata: + status: beta + version: "1.0.0-beta" + aem_version: "Cloud Service" + complements: ensure-agents-md +--- + +# aem-agentkit — bootstrap for agentic workflows on AEM as a Cloud Service + +> **Beta Skill**: This skill is in beta and under active development. Results +> should be reviewed carefully before use in production. Report issues at +> https://github.com/adobe/skills/issues + +Writes per-module `AGENTS.md`, codified context under `.aem/context/`, and +tool-specific projections so coding agents work the repo with high +reliability and low hallucination — without modifying customer source. + +**Scope: AEM as a Cloud Service only.** The skill exits early on 6.5 LTS, +AMS, or on-premise layouts (signals: `pom.xml` declaring `uber-jar` `6.5.*` +classifiers; `dispatcher` legacy `conf/` only without `conf.d/`; +`.cloudmanager/` absent alongside `aem.dispatcher.module` references). + +## Relationship to `ensure-agents-md` + +| Skill | Owns | +|---|---| +| `ensure-agents-md` | Root `AGENTS.md` + `CLAUDE.md` | +| `aem-agentkit` | Per-module `AGENTS.md`, `.aem/context/`, tool-specific files | + +When root `AGENTS.md` is missing and `ensure-agents-md` is installed, this +skill defers to it as step 0 before continuing. + +## Trigger + +- User invokes by trigger phrase (see `description`). +- One of the owned slash commands fires (`/new-component`, + `/new-sling-model`, `/validate-dispatcher`, `/regen-context`, + `/agents-md-check` — see [references/per-tool-artifacts.md](./references/per-tool-artifacts.md)). +- Skip with one-line preamble notice when `_disable_agentkit` exists at + workspace root (`lstat`-by-name; symlink target never dereferenced; + contents ignored) or no root `pom.xml` is found within the documented + fallback set. Per-sub-project opt-out via the same file at a nested + AEM project root. Full collision behavior in + [references/collision-rules.md](./references/collision-rules.md). + +## IDE detection and selection + +The skill detects agentic toolchain signals from the filesystem and then +**asks the customer** which detected toolchains to materialize artifacts +for. The universal layer (`AGENTS.md` + `.aem/context/*`) is always +written; the tool-specific layer is opt-in per IDE. + +Detection signals are tightened to avoid false positives — having +`.github/*.yml` workflow files no longer counts as a Copilot signal, +and an empty `.claude/` directory (often left by IDE installers) no +longer fires. + +| Tool | Signal (must include the "content" half) | Artifacts (when selected) | +|---|---|---| +| Claude Code | `.claude/agents/` or `.claude/commands/` is non-empty | `.claude/agents/aem-*.md`, `.claude/commands/.md`, `.mcp.json` placeholder | +| Cursor | `.cursor/rules/` is non-empty or `.cursor/mcp.json` exists | `.cursor/rules/aem-*.mdc`, `.cursor/mcp.json` placeholder | +| GitHub Copilot | `.github/copilot-instructions.md` exists | `.github/instructions/aem-*.instructions.md` (+ `.github/copilot-instructions.md` only when missing) | +| Codex / Aider / native-AGENTS.md tools | always | (universal layer only — never IDE-specific files) | +| Continue.dev | `.continue/rules/` is non-empty | `.continue/rules/aem-*.md` | +| Cline | `.clinerules` exists OR `.vscode/extensions.json` lists `saoudrizwan.claude-dev` | `.clinerules` (when missing) | +| Windsurf | `.windsurfrules` exists OR `.codeium/` is non-empty | `.windsurfrules` (when missing) | +| Augment | `.augment/` exists OR `augment.md` exists | `augment.md` (when missing) | + +After detection, the skill prompts the customer with the detected +toolchains and the choices: **all**, **single (pick one)**, +**multi-select**, or **none** (universal layer only). The selection +is recorded under `decision: ide-targets` in +`.aem/agentkit-overrides.yml` so subsequent runs skip the prompt. +Prompt template in [`references/output-format.md`](./references/output-format.md) § 1.1. + +The prompt is **suppressed** (and the skill falls back to writing for +every detected toolchain — the original silent behavior) under any of: + +- CLI flag `--silent` on the invocation. +- Environment variable `AEM_AGENTKIT_SILENT=1` set in the shell. +- `.aem/agentkit-overrides.yml` already contains a + `decision: ide-targets` entry — that entry wins outright. + +These three escape hatches keep CI / scripted invocations fully +reproducible: a skill run in a non-interactive context with the +override file present makes no decisions of its own. + +When no IDE signal fires, the universal layer is still written and the +preamble lists which toolchain dirs the customer can create to layer in +tool-specific artifacts on a later run. + +## Hard guarantee — allow-list of paths the skill writes + +Every output sits under one of: + +- `/AGENTS.md` for each detected AEM module (recursive for nested monorepos) +- `.aem/context/` files: `components.json`, `osgi-services.json`, `conventions.md`, `avoid.md`, `glossary.md`, `test-patterns.md`, `aem-api-namespaces.md`, `README.md`, `.agentkit-manifest.json`, `.agentkit.lock` (manifest and lock are workspace-root only; the other files are mirrored per detected nested sub-project) +- Per-tool artifacts under `.claude/`, `.cursor/`, `.github/instructions/`, `.continue/`, plus single-file `.clinerules` / `.windsurfrules` / `augment.md` when their signal fires +- `.mcp.json` and `.cursor/mcp.json` placeholders (only when missing) + +Outside this list every pre-existing file is read-only. The skill's own +`.tmp` (atomic-write only, cleaned at startup when adjacent to a +marker-bearing target) and `.agentkit-new` (diff sidecar) are the +only other paths it touches. Root `AGENTS.md` / `CLAUDE.md` are owned by +`ensure-agents-md` and are never modified. + +The skill never reads anything matching the deny-list in +[references/privacy-and-sanitization.md](./references/privacy-and-sanitization.md) +§ 1, never embeds AEM 6.5 documentation URLs (the self-validation pass +rejects `/6.5/` and `experience-manager-65/`), and never names specific +MCP server packages in any AGENTS.md body. The skill prompts the +customer only for **IDE selection** (§ "IDE detection and selection") +— no prompts for content decisions, file overwrites, or path +resolution. + +## Generation order + +1–8: `.aem/context/components.json`, `osgi-services.json`, +`conventions.md`, `avoid.md`, `glossary.md`, `test-patterns.md`, +`aem-api-namespaces.md`, `README.md`. + +9: Per-module `AGENTS.md` (recursive — see [references/per-module-agents-md.md](./references/per-module-agents-md.md)). + +10: Tool-specific artifacts — see [references/per-tool-artifacts.md](./references/per-tool-artifacts.md). + +11: `.mcp.json` / `.cursor/mcp.json` placeholders — see [references/mcp-wiring.md](./references/mcp-wiring.md). + +12: `.aem/context/.agentkit-manifest.json` — see [references/manifest.md](./references/manifest.md). + +Then run the self-validation pass: every evidence pointer resolves; every +`slingModelFqcn` / `implFqcn` resolves; every per-module `AGENTS.md` +matches an existing directory; every marker checksum recomputes; every +URL is Cloud-Service-scoped; every sanitized string is strip-list clean; +every manifest entry's checksum matches on-disk. Exit `0` clean, `2` +completed-with-warnings, `1` hard failure. + +## Reference files + +| File | Purpose | +|---|---| +| [`per-module-agents-md.md`](./references/per-module-agents-md.md) | Per-module `AGENTS.md` rules, recursion, build-command resolution | +| [`codified-context.md`](./references/codified-context.md) | `.aem/context/*` schemas, discovery, output stability, determinism tiebreaker | +| [`per-tool-artifacts.md`](./references/per-tool-artifacts.md) | IDE detection, canonical role source, projection rules, size budgets | +| [`mcp-wiring.md`](./references/mcp-wiring.md) | `.mcp.json` / `.cursor/mcp.json` placeholder + validity definitions | +| [`guardrails.md`](./references/guardrails.md) | Canonical guardrail block and inter-skill index-mutation contract | +| [`module-catalog.md`](./references/module-catalog.md) | Module descriptions, frontend variants, add-on detection | +| [`collision-rules.md`](./references/collision-rules.md) | Pre-existing-state behavior table + marker check + `.agentkit-new` lifecycle | +| [`upgrade-and-migration.md`](./references/upgrade-and-migration.md) | Marker canonical-body bytes, version bumps, schema migration, static-reference handling | +| [`privacy-and-sanitization.md`](./references/privacy-and-sanitization.md) | Deny-list, symlink hardening, Unicode strip-list, casefold rule | +| [`output-format.md`](./references/output-format.md) | Preamble + summary + diagnostic templates with conditional rows | +| [`helpers.md`](./references/helpers.md) | Deterministic helper protocol, ops, version pinning | +| [`manifest.md`](./references/manifest.md) | Run-manifest schema, `/agents-md-check` consumer rules, overrides | + +## Deterministic helper + +Every operation that must be byte-exact (realpath + workspace boundary, +`O_NOFOLLOW` + TOCTOU re-check, SHA-256 canonical-body checksum, atomic +`.tmp` + `rename(2)`, Unicode sanitization, deny-list segment matching, +bounded file walk, advisory file lock) is performed by the helper at +[`bin/aem-agentkit-helper`](./bin/aem-agentkit-helper) (Python 3.10+, no +third-party deps). The skill compares `--version` against +`metadata.version` at startup and refuses to run on mismatch or absence. +Full protocol in [`references/helpers.md`](./references/helpers.md); +unit-test suite at [`tests/run-tests.sh`](./tests/run-tests.sh). + +## Concurrency, idempotency, modes + +- **Lock.** Workspace advisory lock at `.aem/context/.agentkit.lock` + (acquired through the helper); a second invocation exits `1` with a + clean diagnostic instead of racing on `.tmp` files. +- **Markers.** Markdown: first line is ``. JSON: top-level `"_generatedBy": "aem-agentkit"`, `"_skillVersion": "1.0.0-beta"`, `"schemaVersion": "1"`, `"_markerChecksum": ""` (plus `"_static": true` for static-reference files). The marker checksum covers the canonical body bytes only; `generatedAt` is excluded so identical content does not churn the file across runs. Marker spoofing (wrong / malformed / duplicated checksum) is treated as human-curated. Full byte-exact rules in [`references/upgrade-and-migration.md`](./references/upgrade-and-migration.md) § 1. +- **Modes.** `Default` runs the full order. `Refresh` (`/regen-context`) re-renders only `.aem/context/*`. `Check` (`/agents-md-check`) is read-only drift detection driven by the run manifest. + +## Communication + +The skill emits a one-line preamble before any writes, a deterministic +summary after the manifest is written (with `Heuristics`, `Warnings`, +`MCP placeholders to replace`, and `Manifest` rows always present), and a +one-line workspace-relative diagnostic on any error. Templates in +[`references/output-format.md`](./references/output-format.md). + +## Rules + +Every rule is enforced by the helper and / or the self-validation pass. +The references hold the byte-exact definitions; this section is a +checklist for review. + +- **Allow-list writes only** (this file § Hard guarantee). +- **Never overwrite** any pre-existing file lacking the skill's marker (see [`collision-rules.md`](./references/collision-rules.md)). +- **Never read** any path matching the privacy deny-list ([`privacy-and-sanitization.md`](./references/privacy-and-sanitization.md) § 1) — segment-by-segment match, ASCII-lowercase casefold, fail-closed on uncertainty. +- **Workspace boundary + symlink hardening** ([`privacy-and-sanitization.md`](./references/privacy-and-sanitization.md) § 1.2): realpath check, workspace-escape rejection, special-filesystem rejection (`/proc`, `/sys`, `/dev`, UNC paths), `O_NOFOLLOW` open with TOCTOU re-check, depth cap 32, global file cap 100,000, per-subtree cap 10,000. +- **Output stability** ([`codified-context.md`](./references/codified-context.md) § 2): JSON sorted-keys + 2-space indent + LF + final newline + UTF-8 no BOM; Markdown LF + final newline + no trailing whitespace; `generatedAt` is `YYYY-MM-DDTHH:MM:SSZ` and excluded from the checksum. +- **Determinism tiebreaker** ([`codified-context.md`](./references/codified-context.md) § 2): path → line number → pre-sanitization value → SHA-256 of pre-sanitization value. +- **Sanitize extracted strings** ([`privacy-and-sanitization.md`](./references/privacy-and-sanitization.md) § 2): NFC normalize, drop on strip-list match, 80-char cap, inline-code wrap with escalating fence. +- **Hallucination guard.** Emit a derived rule only when ≥ 3 evidence pointers exist; otherwise emit a TODO marker. +- **Customer-only discovery.** Never index Core Components or anything under `/libs`. +- **Sub-project resolution in role bodies** ([`per-tool-artifacts.md`](./references/per-tool-artifacts.md) § 2): role bodies walk up from the file under edit to the closest enclosing AEM project root before resolving `` or ``. +- **Slash-command input validation**: `` and `` against anchored regex before any shell or filesystem interpolation; `MVN_CMD` ∈ `{"mvn", "./mvnw"}` literally. +- **No inline mutation of `.aem/context/*.json`**: roles delegate to `/regen-context` so the marker checksum is recomputed by the helper, not by the agent. +- **Diagnostic-path scrubbing.** Workspace-relative paths only; never absolute, never `~/`. +- **Equivalence guarantee** ([`per-tool-artifacts.md`](./references/per-tool-artifacts.md) § 7): the canonical role-source body is byte-identical across IDE projections. + +## Example invocation + +``` +> bootstrap aem-agentkit +aem-agentkit: Bootstrapping agentic workflow context for this AEM as a Cloud Service repository. No source files will be modified. +… +aem-agentkit: complete + Universal layer: + Per-module AGENTS.md: 7 across [core, ui.apps, ui.frontend, dispatcher, it.tests, ui.tests, all] + Indexes: components.json (24), osgi-services.json (11) + Derived: conventions.md (7 rules, 1 TODO), avoid.md (3 entries), glossary.md (14 terms), test-patterns.md (4 rules) + Static refs: aem-api-namespaces.md, README.md + Tool-specific layer (detected: Claude): + Claude: 8 agents, 5 commands, mcp.json (new-placeholder) + Cursor: 0 rules, mcp.json (absent) + Copilot: 0 instructions, copilot-instructions.md (absent) + Continue: 0 rules + Cline: .clinerules (absent), .clinerules.aem-roles-extra.md (absent) + Windsurf: .windsurfrules (absent), .windsurfrules.aem-roles-extra.md (absent) + Augment: augment.md (absent), augment.md.aem-roles-extra.md (absent) + Heuristics (3): module-shape=leaf-module at core; frontend-variant=webpack at ui.frontend; ds-generation=R7 at core/.../MyService.java + TODO markers: 1 items pending human review + Warnings (0): none + MCP placeholders to replace: 3 (in .mcp.json) — agent will not connect until set + Manifest: .aem/context/.agentkit-manifest.json (24 entries, helper v1.0.0-beta) + Refresh: /regen-context + Drift: /agents-md-check + Exit code: 0 (clean) +``` diff --git a/plugins/aem/cloud-service/skills/aem-agentkit/bin/aem-agentkit-helper b/plugins/aem/cloud-service/skills/aem-agentkit/bin/aem-agentkit-helper new file mode 100755 index 00000000..1c7a94bd --- /dev/null +++ b/plugins/aem/cloud-service/skills/aem-agentkit/bin/aem-agentkit-helper @@ -0,0 +1,682 @@ +#!/usr/bin/env python3 +"""aem-agentkit-helper - deterministic helper for the aem-agentkit skill. + +Reference implementation. See references/helpers.md in the skill bundle +for the full operation spec. POSIX only (Linux, macOS); Windows is +rejected at startup because the symlink-hardening contract requires +O_NOFOLLOW / O_NOFOLLOW_ANY semantics that the Win32 API does not +expose in a portable form. + +Protocol +-------- +- `aem-agentkit-helper --version` prints VERSION and exits 0. +- `aem-agentkit-helper` reads JSON-line requests from + stdin until EOF, emits one JSON-line response per request to stdout. + Exit code 0 if every request returned ok=true, 1 otherwise. + +Every request is `{"op": "", ...}` with op-specific fields. Every +response is `{"ok": true, ...}` or `{"ok": false, "error": "..."}`. +""" + +import base64 +import ctypes +import ctypes.util +import errno +import fnmatch +import hashlib +import json +import os +import re +import sys +import unicodedata + +VERSION = "1.0.0-beta" + +# --------------------------------------------------------------------- # +# Platform gate # +# --------------------------------------------------------------------- # + +if sys.platform == "win32": + sys.stderr.write( + "aem-agentkit: platform lacks the syscall surface needed for the " + "symlink-hardening contract; aborting.\n" + ) + sys.exit(1) + +# --------------------------------------------------------------------- # +# Constants from privacy-and-sanitization.md # +# --------------------------------------------------------------------- # + +# Unicode code points to strip (privacy-and-sanitization.md § 2.1) +_STRIP_CODEPOINTS = ( + list(range(0x00, 0x09)) + list(range(0x0A, 0x20)) # control except \t + + [0x2028, 0x2029] # line/para separators + + [0x00AD, 0x180E] # zero-width + + list(range(0x200B, 0x2010)) # zero-width set U+200B..U+200F + + [0x2060, 0xFEFF, 0xFFFD] # word joiner, BOM, replacement + + [0x061C] # Arabic letter mark + + list(range(0x202A, 0x202F)) # bidi U+202A..U+202E + + list(range(0x2066, 0x206A)) # bidi U+2066..U+2069 +) +STRIP_SET = frozenset(_STRIP_CODEPOINTS) + +# Deny-list patterns applied per path segment, case-insensitive (ASCII casefold) +DENY_PATTERNS = [ + "env*.json", "secrets*", + ".env", ".env.*", "*.env", "*.env.*", + "credential*", "credentials*", "*creds*", "*cred", + "*secret*", "*secrets", "*password*", "*passwd*", "*token*", + "api-key*", "api_key*", "apikey*", + "auth.json", "auth-config*", "auth-tokens*", + "*.pem", "*.key", "*.p12", "*.pfx", "*.p8", "*.jks", "*.jceks", + "*.keystore", "*.truststore", "keystore", "truststore", "*.p7b", + "id_rsa*", "id_dsa*", "id_ecdsa*", "id_ed25519*", "*.ovpn", "*.netrc.gpg", + "*.key.json", "*-service-account*.json", "*-firebase-adminsdk-*.json", + "firebase.json", ".firebaserc", "aws-exports.js", "kubeconfig", + "profiles.yml", + ".npmrc", ".yarnrc", ".yarnrc.yml", ".pypirc", + ".dockercfg", "settings.xml", "settings-security.xml", + ".netrc", "_netrc", ".htpasswd", + "aio-config.json", "*-private.pem", "*ims*credentials*", "serviceuser*key*", + "*.tfvars", "*.tfstate", "*.tfstate.backup", + "*.gpg", "*.asc", "*.kdbx", "wallet.dat", "*.pgp", + "datasources.local.xml", "sshconfigs.xml", "websservers.xml", + "security*.xml", "sftp.json", "launch.local.json", "secrets.json", + "*.bak", "*.orig", "*.swp", "*.swo", ".#*", "*~", "*.rej", +] +DENY_PATTERNS_LC = [p.lower() for p in DENY_PATTERNS] + +# Directory names that prune the entire subtree at every depth +DENY_DIRS = frozenset({ + ".git", "target", "node_modules", "dist", "build", "out", + "crx-quickstart", ".idea", + ".terraform", ".gnupg", ".ssh", + ".aws", ".gcp", ".azure", ".kube", ".aio", ".adobe-aio", ".fbc", + ".password-store", ".aws-sam", ".m2", + ".databricks-cfg", ".snowflake", ".dbt", +}) + +# Special filesystems rejected even when the workspace lives inside them +REJECT_PREFIXES = ( + "/proc/", "/sys/", "/dev/", "/var/run/", "/run/", +) + +# Marker fields removed from a JSON body before checksum +JSON_MARKER_FIELDS = ( + "_generatedBy", "_skillVersion", "schemaVersion", + "_markerChecksum", "generatedAt", "_static", +) + +MAX_BYTES_CEILING = 16 * 1024 * 1024 +DEFAULT_MAX_FILES = 100_000 +DEFAULT_MAX_DEPTH = 32 +DEFAULT_MAX_FILES_PER_SUBTREE = 10_000 + +# --------------------------------------------------------------------- # +# Helpers # +# --------------------------------------------------------------------- # + + +def casefold_ascii(s: str) -> str: + """ASCII lowercase casefold (privacy-and-sanitization.md / helpers.md § 3). + + Bytes 0x41..0x5A -> 0x61..0x7A; every other byte unchanged. + """ + return "".join(c.lower() if "A" <= c <= "Z" else c for c in s) + + +def segment_denied(segment: str) -> str: + """Return the matching pattern name if the segment is denied, else "".""" + seg_lc = casefold_ascii(segment) + if seg_lc in DENY_DIRS: + return seg_lc + for pat in DENY_PATTERNS_LC: + if fnmatch.fnmatchcase(seg_lc, pat): + return pat + return "" + + +def _resolve_workspace(workspace: str) -> str: + ws_real = os.path.realpath(workspace) + if not os.path.isdir(ws_real): + raise ValueError("workspace is not a directory") + return ws_real + + +def _check_special_fs(realpath: str) -> str: + for prefix in REJECT_PREFIXES: + if realpath.startswith(prefix): + return prefix + return "" + + +def _fd_realpath(fd: int) -> str: + """Return the canonical path of an open file descriptor. + + Used for the TOCTOU re-check in op_open (helpers.md § 2.2). + """ + if sys.platform == "linux": + return os.readlink(f"/proc/self/fd/{fd}") + if sys.platform == "darwin": + # F_GETPATH on macOS = 50; returns the canonical path of fd into a buffer. + libc_name = ctypes.util.find_library("c") + if libc_name is None: + raise OSError("libc not available") + libc = ctypes.CDLL(libc_name, use_errno=True) + buf = ctypes.create_string_buffer(4096) + F_GETPATH = 50 + rc = libc.fcntl(fd, F_GETPATH, buf) + if rc == -1: + raise OSError(ctypes.get_errno(), "fcntl(F_GETPATH) failed") + return buf.value.decode("utf-8") + raise OSError("fd realpath unsupported on this platform") + + +def _validate_path(workspace: str, path: str) -> dict: + """Run the realpath gauntlet on `path` against `workspace`. Return dict.""" + try: + ws_real = _resolve_workspace(workspace) + except (OSError, ValueError) as e: + return {"ok": False, "error": f"workspace invalid: {e}"} + + try: + path_real = os.path.realpath(path, strict=True) + except OSError as e: + return {"ok": False, "error": f"realpath failed: {e.strerror or e}"} + + parts = path_real.split(os.sep) + if ".." in parts: + return {"ok": False, "error": "resolved path contains .."} + + if path_real != ws_real and not path_real.startswith(ws_real + os.sep): + return {"ok": False, "error": "path escapes workspace root"} + + bad_prefix = _check_special_fs(path_real) + if bad_prefix: + return {"ok": False, "error": f"path traverses rejected filesystem {bad_prefix}"} + + rel = "" if path_real == ws_real else os.path.relpath(path_real, ws_real) + if rel: + for seg in rel.split(os.sep): + denied = segment_denied(seg) + if denied: + return {"ok": False, "error": f"deny-list match on segment: {seg} (pattern: {denied})"} + + try: + is_symlink = os.path.islink(path) + except OSError: + is_symlink = False + is_dir = os.path.isdir(path_real) + + return { + "ok": True, + "realpath": path_real, + "workspaceRelative": rel.replace(os.sep, "/"), + "isSymlink": is_symlink, + "isDir": is_dir, + "workspaceRealpath": ws_real, + } + + +# --------------------------------------------------------------------- # +# Operations # +# --------------------------------------------------------------------- # + + +def op_realpath(req): + res = _validate_path(req["workspace"], req["path"]) + res.pop("workspaceRealpath", None) + return res + + +def op_match_deny(req): + res = _validate_path(req["workspace"], req["path"]) + if res["ok"]: + return {"ok": True, "denied": False, "matchedPattern": None, "matchedSegment": None} + # If validation failed because of deny-list, extract pattern and segment + err = res.get("error", "") + m = re.match(r"deny-list match on segment: (.+) \(pattern: (.+)\)$", err) + if m: + return {"ok": True, "denied": True, "matchedSegment": m.group(1), "matchedPattern": m.group(2)} + return res + + +def op_open(req): + workspace = req["workspace"] + path = req["path"] + max_bytes = min(int(req.get("maxBytes", MAX_BYTES_CEILING)), MAX_BYTES_CEILING) + + val = _validate_path(workspace, path) + if not val["ok"]: + return val + target = val["realpath"] + + flags = os.O_RDONLY | os.O_NOFOLLOW + if sys.platform == "darwin" and hasattr(os, "O_NOFOLLOW_ANY"): + flags |= os.O_NOFOLLOW_ANY + + try: + fd = os.open(path, flags) + except OSError as e: + return {"ok": False, "error": f"open failed: {e.strerror or e}"} + + try: + try: + fd_real = _fd_realpath(fd) + except OSError: + fd_real = None + if fd_real and fd_real != target: + return {"ok": False, "error": "TOCTOU mismatch: descriptor path differs from resolved path"} + + chunks = [] + remaining = max_bytes + 1 + while remaining > 0: + chunk = os.read(fd, min(remaining, 65536)) + if not chunk: + break + chunks.append(chunk) + remaining -= len(chunk) + data = b"".join(chunks) + if len(data) > max_bytes: + return {"ok": False, "error": f"file exceeds maxBytes ({max_bytes})"} + + return { + "ok": True, + "bytes": base64.b64encode(data).decode("ascii"), + "sha256": hashlib.sha256(data).hexdigest(), + } + finally: + os.close(fd) + + +def op_walk(req): + workspace = req["workspace"] + roots = req.get("roots", ["."]) + max_files = int(req.get("maxFiles", DEFAULT_MAX_FILES)) + max_depth = int(req.get("maxDepth", DEFAULT_MAX_DEPTH)) + per_subtree = int(req.get("maxFilesPerSubtree", DEFAULT_MAX_FILES_PER_SUBTREE)) + globs = req.get("globs", []) or [] + + try: + ws_real = _resolve_workspace(workspace) + except (OSError, ValueError) as e: + return {"ok": False, "error": f"workspace invalid: {e}"} + + files = [] + visited = set() + warnings = [] + truncated_subtrees = [] + truncated = False + + def matches_any_glob(rel_posix): + if not globs: + return True + return any(fnmatch.fnmatchcase(rel_posix, g) for g in globs) + + for root in roots: + if truncated: + break + root_path = root if os.path.isabs(root) else os.path.join(ws_real, root) + root_val = _validate_path(workspace, root_path) + if not root_val["ok"]: + warnings.append(f"root rejected: {root}: {root_val.get('error')}") + continue + root_real = root_val["realpath"] + subtree_count = 0 + subtree_label = root_val["workspaceRelative"] or "." + + stack = [(root_real, 0)] + while stack and not truncated: + current, depth = stack.pop() + if depth > max_depth: + warnings.append(f"depth cap reached at {os.path.relpath(current, ws_real)}") + continue + try: + entries = sorted(os.listdir(current)) + except OSError as e: + warnings.append(f"cannot list {os.path.relpath(current, ws_real)}: {e.strerror}") + continue + for name in entries: + seg_denied = segment_denied(name) + full = os.path.join(current, name) + if seg_denied: + warnings.append(f"deny-list segment match: {os.path.relpath(full, ws_real)} ({seg_denied})") + continue + try: + real = os.path.realpath(full, strict=True) + except OSError: + warnings.append(f"realpath failed for {os.path.relpath(full, ws_real)}") + continue + if real != ws_real and not real.startswith(ws_real + os.sep): + warnings.append(f"escape rejected: {os.path.relpath(full, ws_real)}") + continue + if _check_special_fs(real): + warnings.append(f"special-fs rejected: {os.path.relpath(full, ws_real)}") + continue + if real in visited: + continue + visited.add(real) + if os.path.isdir(real): + stack.append((real, depth + 1)) + continue + rel_posix = os.path.relpath(real, ws_real).replace(os.sep, "/") + if matches_any_glob(rel_posix): + files.append(rel_posix) + subtree_count += 1 + if subtree_count >= per_subtree: + truncated_subtrees.append(subtree_label) + warnings.append(f"per-subtree cap reached: {subtree_label}") + stack.clear() + break + if len(files) >= max_files: + truncated = True + truncated_subtrees.append(subtree_label) + warnings.append("global file-walk cap reached") + stack.clear() + break + + files.sort() + return { + "ok": True, + "files": files, + "truncated": truncated, + "truncatedSubtrees": sorted(set(truncated_subtrees)), + "warnings": warnings, + } + + +def op_sha256_canonical(req): + kind = req.get("kind") + try: + raw = base64.b64decode(req["bytes"], validate=True) + except Exception as e: + return {"ok": False, "error": f"bytes must be valid base64: {e}"} + + if raw.startswith(b"\xef\xbb\xbf"): + return {"ok": False, "error": "UTF-8 BOM not allowed"} + + if kind == "markdown": + nl = raw.find(b"\n") + if nl < 0: + return {"ok": False, "error": "markdown body missing a newline-terminated marker line"} + body = raw[nl + 1:] + return {"ok": True, "sha256": hashlib.sha256(body).hexdigest()} + + if kind == "json": + try: + obj = json.loads(raw.decode("utf-8")) + except (UnicodeDecodeError, json.JSONDecodeError) as e: + return {"ok": False, "error": f"json parse failed: {e}"} + if not isinstance(obj, dict): + return {"ok": False, "error": "json top-level must be an object"} + cleaned = {k: v for k, v in obj.items() if k not in JSON_MARKER_FIELDS} + emitted = json.dumps( + cleaned, sort_keys=True, indent=2, ensure_ascii=False, + separators=(",", ": "), + ).encode("utf-8") + b"\n" + return {"ok": True, "sha256": hashlib.sha256(emitted).hexdigest()} + + return {"ok": False, "error": f"unknown kind: {kind}"} + + +def op_write_atomic(req): + workspace = req["workspace"] + rel = req["path"] + try: + data = base64.b64decode(req["bytes"], validate=True) + except Exception as e: + return {"ok": False, "error": f"bytes must be valid base64: {e}"} + + try: + ws_real = _resolve_workspace(workspace) + except (OSError, ValueError) as e: + return {"ok": False, "error": f"workspace invalid: {e}"} + + if os.path.isabs(rel) or ".." in rel.split("/"): + return {"ok": False, "error": "path must be relative without .."} + + full = os.path.join(ws_real, *rel.split("/")) + parent = os.path.dirname(full) + if not parent: + return {"ok": False, "error": "path lacks a parent directory"} + try: + os.makedirs(parent, exist_ok=True) + except OSError as e: + return {"ok": False, "error": f"parent dir create failed: {e.strerror or e}"} + + parent_real = os.path.realpath(parent) + if parent_real != ws_real and not parent_real.startswith(ws_real + os.sep): + return {"ok": False, "error": "parent escapes workspace root"} + + tmp = full + ".tmp" + try: + fd = os.open(tmp, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644) + except FileExistsError: + return {"ok": False, "error": ".tmp already exists; aborting"} + except OSError as e: + return {"ok": False, "error": f"tmp open failed: {e.strerror or e}"} + + try: + os.write(fd, data) + os.fsync(fd) + finally: + os.close(fd) + + try: + os.rename(tmp, full) + except OSError as e: + try: + os.unlink(tmp) + except OSError: + pass + return {"ok": False, "error": f"rename failed: {e.strerror or e}"} + + try: + dfd = os.open(parent, os.O_RDONLY) + try: + os.fsync(dfd) + finally: + os.close(dfd) + except OSError: + pass + + return {"ok": True, "sha256": hashlib.sha256(data).hexdigest()} + + +def op_cleanup_tmp(req): + workspace = req["workspace"] + try: + ws_real = _resolve_workspace(workspace) + except (OSError, ValueError) as e: + return {"ok": False, "error": f"workspace invalid: {e}"} + + deleted = [] + for dirpath, dirnames, filenames in os.walk(ws_real): + # Prune deny dirs at every layer + dirnames[:] = [d for d in dirnames if d not in DENY_DIRS] + for name in filenames: + if not name.endswith(".tmp"): + continue + target_name = name[:-4] + target_path = os.path.join(dirpath, target_name) + if not os.path.exists(target_path): + continue + # Verify target carries a marker (Markdown or JSON) + try: + with open(target_path, "rb") as f: + head = f.read(4096) + except OSError: + continue + if (b" +--- +name: aem- +description: +model: sonnet +tools: Read, Glob, Grep, Edit, Write, Bash +--- + + +``` + +Plus slash commands at `.claude/commands/`: + +| File | Owns name | +|---|---| +| `new-component.md` | `/new-component ` | +| `new-sling-model.md` | `/new-sling-model ` | +| `validate-dispatcher.md` | `/validate-dispatcher` (only if `dispatcher/` exists) | +| `regen-context.md` | `/regen-context` | +| `agents-md-check.md` | `/agents-md-check` | + +**Slash-command pre-flight.** Before writing any of the above, the skill +scans `.claude/commands/` for files of the same name. A matching name +that is **not** marker-bearing (per [`collision-rules.md`](./collision-rules.md)) +is human-curated — usually owned by a sibling skill such as +`create-component`. The skill does **not** overwrite it; instead it +emits a `warningStubs` entry: `"slash-command name collision: / +is human-curated; aem-agentkit slash command not installed. Invoke +@aem- directly via the IDE's subagent invocation."` The Claude +projection still ships the role agents (`aem-component-author` etc.); +the customer can invoke them directly. The summary block surfaces one +line per collision with the alternate invocation so the customer is +never told a feature is missing without being told how to reach it. + +**Input-argument validation.** `` in `/new-component` must match +`^[a-z][a-z0-9-]{0,63}$`; `` in `/new-sling-model` must match the +FQCN regex documented in the template. `MVN_CMD` template variable is +restricted to the literal set `{"mvn", "./mvnw"}`; any other resolved +value emits a `warningStubs` entry and the build line is omitted from +the rendered command artifact. + +Plus MCP wiring at `.mcp.json` (see [`mcp-wiring.md`](./mcp-wiring.md)). + +### 3.2 Cursor — `.cursor/rules/aem-.mdc` + +```markdown + +--- +description: +globs: + - +alwaysApply: false +--- + + +``` + +Globs per role: + +| Role | `globs:` | +|---|---| +| component-author | `**/ui.apps/**`, `**/ui.apps.*/**` | +| sling-model-author | `**/src/main/java/**` | +| htl-author | `**/ui.apps*/**/*.html` | +| dispatcher-editor | `dispatcher/**` | +| osgi-config-author | `**/ui.config/**`, `**/ui.config.*/**`, `**/jcr_root/apps/*/config*/**` | +| integration-test-author | `**/it.tests/**` | +| ui-test-author | `**/ui.tests/**` | +| content-fragment-author | `**/conf/**/settings/dam/cfm/**`, `**/content/dam/**` | +| guardrails | `**/*` with `alwaysApply: true` | + +`htl-author` is intentionally scoped to `**/ui.apps*/**/*.html` (note the +trailing `*` after `ui.apps`) so it covers customer modules like +`ui.apps.commerce/` or `ui.apps.commons/` while still avoiding +`ui.frontend/dist/**`, `ui.tests/**`, and other non-HTL HTML in the +workspace. + +Plus MCP wiring at `.cursor/mcp.json`. + +### 3.3 GitHub Copilot — `.github/instructions/aem-.instructions.md` + +```markdown + +--- +applyTo: "" +--- + + +``` + +`applyTo` patterns mirror the Cursor `globs:` above. Guardrails use +`applyTo: "**/*"`. + +The Copilot custom-instructions spec accepts a single string with +comma-separated globs. When a role has multiple globs (e.g. +`osgi-config-author`, `content-fragment-author`), emit a single +`applyTo` line joining the globs with `,` (no surrounding spaces): + +```markdown +applyTo: "**/ui.config/**,**/ui.config.*/**,**/jcr_root/apps/*/config*/**" +``` + +Do **not** split into multiple `.instructions.md` files — the canonical +role source projects 1:1 to a single Copilot instruction file per role. + +If `.github/copilot-instructions.md` is missing **and** Copilot is detected, +write a minimal version: + +```markdown + +# Repository-wide Copilot instructions + +This repository follows the conventions documented in [`AGENTS.md`](../AGENTS.md) +and `.aem/context/`. Honor every guardrail in [`AGENTS.md`](../AGENTS.md) and +the scoped instructions in `.github/instructions/`. +``` + +If it already exists, the skill never touches it. + +### 3.4 Continue.dev — `.continue/rules/aem-.md` + +```markdown + +# aem- + + +``` + +Continue rules under `.continue/rules/` are always-on; no frontmatter +required. If Continue uses `.continue/config.json` for agent registration, +the skill does not modify it. + +### 3.5 Codex (OpenAI) + +No tool-specific files. Codex reads `AGENTS.md` (root + per-module) and +queries the indexes natively per the open standard. + +### 3.6 Cline (VS Code) — `.clinerules` + +Single Markdown file at the workspace root. Cline concatenates all rules +into its system prompt. + +```markdown + +# AEM as a Cloud Service — agent rules + +Read AGENTS.md, the relevant per-module AGENTS.md, and the indexes under +.aem/context/ before generating any code. Apply every rule under +"Agentic workflow guardrails" in AGENTS.md. + + + +--- + + + +--- + + + +(… all detected roles concatenated …) +``` + +A single file works for Cline because it ingests one rules document, not +per-file or per-glob rules. The same content blocks are reused from the +canonical role sources. When the budget in § 6 forces deferred roles, +the deferred bodies are inlined into the sibling +`.aem-roles-extra.md` so the customer keeps the full role set on +disk. + +### 3.7 Windsurf — `.windsurfrules` + +Same shape as `.clinerules`. Single file at the workspace root with all +detected roles concatenated. Deferred roles go into +`.windsurfrules.aem-roles-extra.md`. + +### 3.8 Aider + +No tool-specific files. Aider reads `AGENTS.md` natively. If the customer +maintains an `.aider.conf.yml`, the skill does not touch it. + +### 3.9 Augment Code + +Single file at `augment.md` (project root) — same concatenation pattern +as Cline / Windsurf. Created only when `.augment/` directory or existing +`augment.md` signal is detected. Deferred roles go into +`augment.md.aem-roles-extra.md`. + +## 4. Conditional generation + +| Role / artifact | Condition | +|---|---| +| component-author | Always (universal author role) | +| sling-model-author | Any module with `src/main/java/**` contains `@Model` classes | +| htl-author | `ui.apps` module present (any nesting level), including `ui.apps.*` siblings | +| dispatcher-editor | `dispatcher/` module present | +| osgi-config-author | `ui.config` module present (any nesting level), including `ui.config.*` siblings | +| integration-test-author | `it.tests/` module present | +| ui-test-author | `ui.tests/` module present | +| content-fragment-author | Content Fragment models present under `/conf/*/settings/dam/cfm/models/` | +| guardrails | Always (every IDE that is detected) | +| `/new-component` | `ui.apps` module present | +| `/new-sling-model` | Any module with `src/main/java/**` | +| `/validate-dispatcher` | `dispatcher/` module present | +| `/regen-context` | Always | +| `/agents-md-check` | Always | + +## 5. Index self-update rule (indexable roles only) + +Roles that author artifacts tracked by a `.aem/context/*.json` index end +with an `## Index self-update (mandatory final step)` section. The +section body is the role's instruction to call `/regen-context` after a +successful write so the index is recomputed and re-checksummed by the +skill (not by the agent inline). This is the **single shared protocol** +that any sibling skill (`create-component`, `best-practices`, `migration`, +or any future skill that touches `.aem/context/*.json`) MUST follow. +Agent-driven inline mutation of the index files is forbidden: the +agent cannot reliably compute SHA-256 over canonical bodies, so it +either succeeds (and the file becomes uncertified) or fails silently +(and the file looks human-curated to the next skill run, which then +treats it as a collision and starts producing `.agentkit-new` sidecars). + +| Role | Indexed by | Has the section | +|---|---|---| +| component-author | `.aem/context/components.json` | yes (delegates to `/regen-context`) | +| sling-model-author | `.aem/context/osgi-services.json` (`slingModels`) | yes (delegates to `/regen-context`) | +| htl-author | (covered by component-author when the HTL belongs to a new component) | no | +| dispatcher-editor | (dispatcher config is not indexed) | no | +| osgi-config-author | (PIDs are resolved against `osgi-services.json`, but the config files themselves are not indexed) | no | +| integration-test-author | (test files are not indexed) | no | +| ui-test-author | (test files are not indexed) | no | +| content-fragment-author | (CF instances are not indexed; CF models are read-only from the role's perspective) | no | +| guardrails | (no authoring) | no | + +The section body is identical across the two indexable roles, scoped to +that role's index file, and appears verbatim in every IDE projection +(Claude / Cursor / Copilot / Continue / Cline / Windsurf / Augment). + +Roles without the section still inherit the "Honor the indexes" rule from +the canonical guardrails block, so they will not bypass `/regen-context` +when the work they touch incidentally produces an indexable artifact (for +example, a new component HTL written by `htl-author` triggers an +`/regen-context` reminder from the guardrails block). + +## 6. Size budgets and deferred-role sidecar + +| Artifact | Soft | Hard | +|---|---|---| +| Claude subagent | 50 lines | 100 lines | +| Cursor `.mdc` rule | 50 lines | 100 lines | +| Copilot `.instructions.md` | 50 lines | 100 lines | +| Continue rule | 50 lines | 100 lines | +| Cline `.clinerules` (concatenated) | 300 lines | 600 lines | +| Windsurf `.windsurfrules` (concatenated) | 300 lines | 600 lines | +| Augment `augment.md` (concatenated) | 300 lines | 600 lines | +| Any slash command | 30 lines | 60 lines | + +When a concatenated single-file projection (Cline / Windsurf / Augment) +would exceed its hard budget, the skill keeps the guardrails role plus the +core roles (component-author, sling-model-author, htl-author, +dispatcher-editor) in full in the main file and writes the remaining role +bodies to a sibling `.aem-roles-extra.md` (e.g. +`.clinerules.aem-roles-extra.md`). The customer therefore always has every +role body on disk; nothing points back to the published skill bundle. A +one-line pointer at the bottom of the main file directs the agent to the +sidecar, and a `warningStubs` entry names the truncated roles. + +## 7. Self-validation + +After writing all tool-specific files: +- Every generated file carries the marker. +- The canonical role-source body appears verbatim across all tool projections (byte-identical inside each role across IDEs). +- No file contains marketing language; framing uses "agentic workflow" terminology only. +- Every URL is Cloud-Service-scoped (no `/6.5/`, no `experience-manager-65/`). +- Every sanitized customer string is free of every code point in [`privacy-and-sanitization.md`](./privacy-and-sanitization.md) § 2.1. diff --git a/plugins/aem/cloud-service/skills/aem-agentkit/references/privacy-and-sanitization.md b/plugins/aem/cloud-service/skills/aem-agentkit/references/privacy-and-sanitization.md new file mode 100644 index 00000000..df39cde3 --- /dev/null +++ b/plugins/aem/cloud-service/skills/aem-agentkit/references/privacy-and-sanitization.md @@ -0,0 +1,196 @@ +# Privacy deny-list and string sanitization + +> **Beta Skill:** Outputs must be reviewed before applying to production. + +This reference is the single source of truth for the skill's two +runtime safety contracts: which files the skill never reads, and how +extracted strings are sanitized before they land in a generated +artifact. [`SKILL.md`](../SKILL.md) § "What this skill never does" and +§ Rules summarize the contracts and link here for the exhaustive lists. +Every rule below is enforced by the deterministic helper documented in +[`helpers.md`](./helpers.md). + +## 1. Privacy deny-list + +Match is **case-insensitive on every platform** using the **ASCII +lowercase casefold** pinned in [`helpers.md`](./helpers.md) § 3 (so +`Credentials.json`, `SECRETS.txt`, and `.ENV` are denied without +depending on the platform's Unicode casefold). Globs use POSIX `/` +separators. + +Matching is applied to **every path segment**, not only the file's leaf +name: a directory whose name (or whose realpath segment) matches a deny +pattern prunes the entire subtree from the walk. A path is denied if +**any** segment matches **any** pattern below. + +**Fail closed:** when a path's classification is ambiguous, when +realpath resolution fails, when the resolved realpath contains `..`, +when an intermediate component is inaccessible (`EACCES`, +`ENOENT`-on-an-intermediate), or when the path crosses a rejected +special filesystem (see § 1.2), skip the path, emit a `warningStubs` +entry, and never read on uncertainty. + +### 1.1 Categories + +| Category | Patterns | +|---|---| +| Cloud Manager scoped | `.cloudmanager/env*.json`, `.cloudmanager/secrets*` (only `.cloudmanager/java-version` is read, with a 256-byte read cap and BOM strip) | +| Environment files | `.env`, `.env.*`, `**/*.env`, `**/*.env.*` | +| Generic credential / secret / token shapes | `**/credential*`, `**/credentials*`, `**/*creds*`, `**/*cred`, `**/*secret*`, `**/*secrets`, `**/*password*`, `**/*passwd*`, `**/*token*`, `**/api[-_]key*`, `**/apikey*`, `**/auth.json`, `**/auth-config*`, `**/auth-tokens*` | +| PKI / keystores | `**/*.pem`, `**/*.key`, `**/*.p12`, `**/*.pfx`, `**/*.p8`, `**/*.jks`, `**/*.jceks`, `**/*.keystore`, `**/*.truststore`, `**/keystore`, `**/truststore`, `**/*.p7b`, `**/*.crt` (private-key bundles), `**/*.csr` | +| SSH keys | `**/id_rsa*`, `**/id_dsa*`, `**/id_ecdsa*`, `**/id_ed25519*`, `**/.ssh/**`, `**/*.ovpn`, `**/.netrc.gpg` | +| Cloud SDK credentials | `**/.aws/**`, `**/aws-exports.js`, `**/.aws-sam/**`, `**/.gcp/**`, `**/*.key.json` (covers GCP service-account JSONs), `**/*-service-account*.json`, `**/*-firebase-adminsdk-*.json`, `**/firebase.json`, `**/.firebaserc`, `**/.azure/**`, `**/.kube/**`, `**/kubeconfig`, `**/.databricks-cfg`, `**/.snowflake/**`, `**/.dbt/profiles.yml` | +| Package registry / build secrets | `**/.npmrc`, `**/.yarnrc`, `**/.yarnrc.yml`, `**/.pypirc`, `**/.gem/credentials`, `**/.dockercfg`, `**/.docker/config.json`, `**/.m2/**/settings.xml`, `**/.m2/**/settings-security.xml` (denied by path alone to avoid the reading-to-classify bootstrap loop; project-local `pom.xml` and `settings.xml` outside `.m2/` are not denied), `**/.netrc`, `**/_netrc`, `**/.htpasswd`, `**/.config/composer/auth.json`, `**/composer-auth.json` | +| Adobe IO / IMS | `**/.adobe-aio*`, `**/.aio/**`, `**/aio-config.json`, `**/*-private.pem`, `**/*ims*credentials*`, `**/serviceuser*key*`, `**/.fbc/**`, `**/asset-compute-devtool/.env*` | +| IaC state / secret vars | `**/*.tfvars`, `**/*.tfstate`, `**/*.tfstate.backup`, `**/.terraform/**`, `**/*.pulumi.yaml` (with secrets), `**/*.sops.yaml` | +| Password managers | `**/.password-store/**`, `**/.config/op/**`, `**/.config/Bitwarden/**`, `**/.bitwardenrc` | +| PGP / encrypted archives | `**/*.gpg`, `**/*.asc`, `**/*.kdbx`, `**/wallet.dat`, `**/.gnupg/**`, `**/*.pgp` | +| IDE secret stores | `**/.idea/dataSources*.local.xml`, `**/.idea/sshConfigs.xml`, `**/.idea/webServers.xml`, `**/.idea/security*.xml`, `**/.vscode/sftp.json`, `**/.vscode/launch.local.json`, `**/.vscode/secrets*.json` | +| AEM SDK local state | `**/crx-quickstart/install/**`, `**/crx-quickstart/launchpad/config/**`, `**/crx-quickstart/repository/datastore/**`, `**/crx-quickstart/repository/version/**`, `**/crx-quickstart/repository/segmentstore/**` | +| Backup / swap artifacts | `**/*.bak`, `**/*.orig`, `**/*.swp`, `**/*.swo`, `**/.#*`, `**/*~`, `**/*.rej` | +| `.git/` (scoped exception) | Only `.git/HEAD` (top-of-tree branch) and `.git/refs/heads/*` (current SHA). `.git/config` is never read because it may contain `https://oauth2:@…` URLs. | + +In addition to the file patterns above, the walk **prunes** the +following directory names at every depth so they are never descended +into: `.git/`, `target/`, `node_modules/`, `dist/`, `build/`, `out/`, +`crx-quickstart/`, `.idea/`, `.vscode/` (except for the single +documented read of `.vscode/extensions.json`), `.terraform/`, +`.gnupg/`, `.ssh/`, `.aws/`, `.gcp/`, `.azure/`, `.kube/`, `.aio/`, +`.adobe-aio*/`, `.fbc/`, `.password-store/`, `.config/op/`, +`.config/Bitwarden/`, `.databricks-cfg/`, `.snowflake/`, `.dbt/`, +`.aws-sam/`, `.m2/`, `node_modules/`. This list is the source of +truth for the helper's `walk` operation; it composes with the +file-shaped patterns above so that a directory named `auth-tokens/` +prunes the whole subtree, not just its leaf file. + +### 1.2 Symlink hardening and workspace boundary + +Before opening any file: + +1. Resolve the **workspace root**'s canonical realpath once at startup + and cache the result for the lifetime of the run. On macOS this + resolves prefixes like `/var/folders → /private/var/folders` so a + workspace under one of these locations is compared correctly. +2. Resolve the candidate path's canonical realpath. +3. Reject when realpath resolution fails for any reason (broken + symlink, `EACCES` on an intermediate component, `ENOENT` on an + intermediate, returns a path containing `..`). Fail closed. +4. Reject if the realpath does not have the cached workspace realpath + as its prefix (workspace-escape rejection). +5. Reject if any path segment of the resolved realpath matches any + pattern in § 1.1 after ASCII lowercase casefold. +6. Reject if the resolved realpath traverses **any** of these special + filesystems, even when the workspace root happens to live under one + of these prefixes (the check looks at the realpath segments, not + the workspace's parent): + - `/proc/`, `/sys/`, `/dev/`, `/var/run/`, `/run/` on Linux / macOS. + - `\\?\` device paths, `\\server\share\` UNC roots, `\\.\pipe\`, + `\\.\Global*` on Windows. +7. Reject if the walk has already visited that realpath (visited-set + loop guard) so a symlink chain that resolves into a previously seen + subtree does not double-visit. +8. Open with `O_NOFOLLOW` on Linux, `O_NOFOLLOW_ANY` on macOS 11+ + (falling back to `openat2` with `RESOLVE_NO_SYMLINKS` on + Linux 5.6+), `FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS` + on Windows. Older platforms without the necessary flag are rejected + at startup with the diagnostic `aem-agentkit: platform lacks the + syscall surface needed for the symlink-hardening contract; aborting.` + The skill never silently relaxes this rule. +9. Re-resolve the opened descriptor's canonical path and reject if it + differs from the realpath resolved in step 2 — closes the TOCTOU + window. + +Hard depth cap: 32 directories from the workspace root. Hard global +file-walk cap: 100,000 files; per-immediate-child-of-root cap: 10,000 +files; on any cap, mark every affected index `truncated: true`, list +the offending subtrees in `truncatedSubtrees`, emit a `warningStubs` +entry, and downstream slash commands (`/new-component`, +`/new-sling-model`) refuse to proceed on a `truncated: true` index +until the customer either narrows the workspace or raises the cap via +`.aem/agentkit-overrides.yml`. Silent half-completion is the failure +mode being blocked. + +### 1.3 `_disable_agentkit` opt-out semantics + +The `_disable_agentkit` opt-out is checked by `lstat`-by-name at the +workspace root and at each candidate nested AEM sub-project root. The +inode named `_disable_agentkit` is the **signal regardless of what it +points at**; the skill never dereferences a symlink with this name. +Reasoning: if the deny-list inside § 1.2 later rejected the realpath, +the customer's opt-out intent would be silently disregarded. + +A regular file `_disable_agentkit` is constrained to `<= 1024 bytes`; +files larger than that are reported in `warningStubs` and **ignored** +(opt-out does not engage) to prevent an accidentally-committed large +binary from disabling the skill. A directory or empty file engages +opt-out immediately. Contents are ignored otherwise. + +## 2. String sanitization + +Any string extracted from customer source (evidence-pointer line +snippets, `cq:title` values, Content Fragment model titles, taxonomy +node names, Java package names) and baked into a generated Markdown +file passes the following sanitization, in order, executed by the +deterministic helper's `sanitize-string` operation (see +[`helpers.md`](./helpers.md) § 2.7): + +1. **NFC normalize.** Idempotent normalization so equivalent code + sequences hash identically. +2. **Drop on strip-list hit.** A string containing **any** code point + in § 2.1 is **dropped** in favor of a TODO marker — partial + sanitization is never returned. This guarantees no zero-width, + bidi, or format characters can survive into a generated artifact. +3. **Length cap.** 80 characters maximum. Truncate with `…` suffix. +4. **Inline-code wrap.** Wrap the sanitized value in backticks so it + cannot be parsed as instruction text by a downstream agent. When + the value already contains backticks, escalate to the next-longer + fence (` `` `, ` ``` `). +5. **Self-validate.** Re-scan the returned bytes for any strip-list + code point. Any survivor (which would indicate a helper bug) drops + the value. + +The self-validation pass after step 12 of the generation order +re-scans every output Markdown file end-to-end for strip-list code +points; any survivor aborts the manifest write. + +### 2.1 Code points to strip + +- **Control characters:** U+0000 through U+001F **except** `\t` (U+0009). +- **Line / paragraph separators that escape inline-code wrap:** U+2028, U+2029. +- **Zero-width / invisible:** U+00AD (soft hyphen), U+180E (Mongolian vowel separator), U+200B – U+200F (zero-width set), U+2060 (word joiner), U+FEFF (zero-width no-break space / BOM), U+FFFD (replacement character — drops on detection because it indicates upstream decode failure). +- **Bidirectional / directional overrides:** U+061C (Arabic letter mark), U+202A – U+202E, U+2066 – U+2069. + +## 3. Where these contracts apply + +- **Discovery scope** (`codified-context.md` § 1) — the deny-list is + checked on every file the walk would open, segment-by-segment, + pruning matching directories before descent. +- **Per-module AGENTS.md generation** (`per-module-agents-md.md` § 5) + — `.cloudmanager/java-version` is the only file inside `.cloudmanager/` + that may be read. The helper enforces a 256-byte read cap and BOM + strip; the value is regex-validated against `^(8|11|17|21|25)$` + against the first whitespace-trimmed line before being inlined. +- **Glossary / conventions / avoid / test-patterns extraction** + (`codified-context.md` § 5 – § 8) — every extracted value passes + the sanitization above before being written. +- **Error diagnostics** (`SKILL.md` § Rules "Diagnostic-path scrubbing") + — error paths are always workspace-relative; absolute paths or `~/` + are never emitted. +- **Slash-command input** (`per-tool-artifacts.md` § 3.1) — every + templated `` and `` argument passes an anchored regex + before any shell or filesystem interpolation. `MVN_CMD` is + restricted to `{"mvn", "./mvnw"}` literally. + +## 4. PII heuristics (glossary.md only) + +In addition to the sanitization above, glossary values are filtered +through a deterministic PII heuristic — see +[`codified-context.md`](./codified-context.md) § 7. Static regex set, no +LLM judgement, fail-closed TODO fallback on any match. The full regex +set is the single source of truth in codified-context.md; the +heuristic covers provider-prefixed tokens (`AKIA*`, `ghp_*`, `gho_*`, +`ghs_*`, `xoxb-*`, `xoxp-*`, `sk_live_*`, `sk_test_*`, `pat_*`, +`AIza*`, `EAACEdEose0cBA*`), JWTs (`eyJ` + base64url segments), base64 +blobs ≥ 40 chars, generic high-entropy tokens, IPv4 / IPv6 / IBAN / +postal / phone / email shapes, internal-domain URLs (`.corp.`, +`.internal.`, `.intranet.`), and human-name + date shapes. diff --git a/plugins/aem/cloud-service/skills/aem-agentkit/references/templates/AGENTS.module.analysis.md.template b/plugins/aem/cloud-service/skills/aem-agentkit/references/templates/AGENTS.module.analysis.md.template new file mode 100644 index 00000000..fc4db8af --- /dev/null +++ b/plugins/aem/cloud-service/skills/aem-agentkit/references/templates/AGENTS.module.analysis.md.template @@ -0,0 +1,22 @@ + +# {{MODULE_NAME}} + +Analysis / scripting / tooling module. Contains scripts, generators, or analysis utilities that run alongside the reactor build but do not ship application code. + +## Agentic workflow guardrails + +- This module's outputs are developer tools, not production code. Do not import its contents from production modules. +- Match the existing scripting style (bash / Groovy / Python — whichever is already present). + +## Common entry points + +{{ENTRY_POINTS}} + +## What to avoid in this module + +- Embedding production-only dependencies. +- Hard-coded paths outside the reactor root. + +## Build + +- `{{MVN_CMD}} -pl {{MODULE_NAME}} install` diff --git a/plugins/aem/cloud-service/skills/aem-agentkit/references/templates/AGENTS.module.code-quality.md.template b/plugins/aem/cloud-service/skills/aem-agentkit/references/templates/AGENTS.module.code-quality.md.template new file mode 100644 index 00000000..e60950f5 --- /dev/null +++ b/plugins/aem/cloud-service/skills/aem-agentkit/references/templates/AGENTS.module.code-quality.md.template @@ -0,0 +1,22 @@ + +# {{MODULE_NAME}} + +Code-quality / build-enforcement module. Carries `maven-checkstyle-plugin`, `maven-enforcer-plugin`, or similar build-time enforcement rules used by sibling modules. + +## Agentic workflow guardrails + +- This module ships rules, not application code. Do not add Sling Models, HTL, or content here. +- Update rules with care — they apply to the whole reactor. + +## Common entry points + +{{ENTRY_POINTS}} + +## What to avoid in this module + +- Adding application code (Java, HTL, content). +- Loosening enforcement rules to make a build pass; fix the offending module instead. + +## Build + +- `{{MVN_CMD}} -pl {{MODULE_NAME}} install` diff --git a/plugins/aem/cloud-service/skills/aem-agentkit/references/templates/AGENTS.module.core.md.template b/plugins/aem/cloud-service/skills/aem-agentkit/references/templates/AGENTS.module.core.md.template new file mode 100644 index 00000000..0276ef5d --- /dev/null +++ b/plugins/aem/cloud-service/skills/aem-agentkit/references/templates/AGENTS.module.core.md.template @@ -0,0 +1,36 @@ + +# core + +OSGi bundle. Backend services, Sling Models, business logic. Built with Maven, tested with JUnit and AEM Mocks. + +## Agentic workflow guardrails + +- Search the closest `.aem/context/osgi-services.json` before creating a service / model / servlet (closest = scoped sub-project copy when working inside a nested AEM project, root copy otherwise). +- Verify AEM class names in the Cloud Service Javadoc before importing. +- Use the project's logging style and DS annotations as derived in `.aem/context/conventions.md`. +- After adding any indexable artifact, run `/regen-context` so `.aem/context/osgi-services.json` is recomputed with a valid marker checksum. Do not mutate the JSON inline. + +## Common entry points + +{{ENTRY_POINTS}} + +## Module-local conventions + +{{CONVENTIONS}} + +## What to avoid in this module + +See `.aem/context/avoid.md` for the full list with evidence pointers and absolute Cloud Service documentation links. + +## Where to look + +- Services and models: `.aem/context/osgi-services.json` +- Conventions: `.aem/context/conventions.md` +- Test patterns: `.aem/context/test-patterns.md` + +## Build + +- Bundle-only build + deploy: `{{MVN_CMD}} clean install -pl core -PautoInstallBundle` +- Unit tests only: `{{MVN_CMD}} -pl core test` + +`{{MVN_CMD}}` is one of `mvn` / `./mvnw` (validated against this exact set; any other resolved value omits these build lines with a `warningStubs` entry). diff --git a/plugins/aem/cloud-service/skills/aem-agentkit/references/templates/AGENTS.module.dispatcher.md.template b/plugins/aem/cloud-service/skills/aem-agentkit/references/templates/AGENTS.module.dispatcher.md.template new file mode 100644 index 00000000..b772d138 --- /dev/null +++ b/plugins/aem/cloud-service/skills/aem-agentkit/references/templates/AGENTS.module.dispatcher.md.template @@ -0,0 +1,34 @@ + +# dispatcher + +Cloud-optimized Dispatcher configuration. Caching, security, virtual hosts. Validated locally by the Dispatcher SDK. + +Layout detected: **{{DISPATCHER_LAYOUT}}** (`{{DISPATCHER_LAYOUT_PATH}}`). + +## Agentic workflow guardrails + +- Never mutate immutable files in `dispatcher/src/conf.d/` (cloud layout). +- Customer changes go in `dispatcher/src/conf.dispatcher.d/`. +- Run `dispatcher/bin/validate.sh src` before every commit. + +## Common entry points + +{{ENTRY_POINTS}} + +## Module-local conventions + +{{CONVENTIONS}} + +## What to avoid in this module + +- Adding `allow` rules without a corresponding `deny` baseline. +- Editing under `conf.d/` (cloud layout — immutable). +- Bypassing the SDK validation step. + +## Validate + +```bash +cd dispatcher && ./bin/validate.sh src +``` + +The change is not complete until validation passes. diff --git a/plugins/aem/cloud-service/skills/aem-agentkit/references/templates/AGENTS.module.generic.md.template b/plugins/aem/cloud-service/skills/aem-agentkit/references/templates/AGENTS.module.generic.md.template new file mode 100644 index 00000000..59d9848f --- /dev/null +++ b/plugins/aem/cloud-service/skills/aem-agentkit/references/templates/AGENTS.module.generic.md.template @@ -0,0 +1,26 @@ + +# {{MODULE_NAME}} + +{{MODULE_DESCRIPTION}} + +## Agentic workflow guardrails + +- Honor the cross-cutting rules in the root `AGENTS.md`. +- Consult `.aem/context/conventions.md` before introducing new patterns. + +## Common entry points + +{{ENTRY_POINTS}} + +## Module-local conventions + +{{CONVENTIONS}} + +## What to avoid in this module + +{{AVOID_FOR_MODULE}} + +## Where to look + +- Cross-cutting conventions: `.aem/context/conventions.md` +- Indexes: `.aem/context/components.json`, `.aem/context/osgi-services.json` diff --git a/plugins/aem/cloud-service/skills/aem-agentkit/references/templates/AGENTS.module.it.tests.md.template b/plugins/aem/cloud-service/skills/aem-agentkit/references/templates/AGENTS.module.it.tests.md.template new file mode 100644 index 00000000..be305e2b --- /dev/null +++ b/plugins/aem/cloud-service/skills/aem-agentkit/references/templates/AGENTS.module.it.tests.md.template @@ -0,0 +1,32 @@ + +# it.tests + +Integration tests against a running AEM instance. AEM Testing clients. Executed by Cloud Manager during *Custom Functional Testing*. + +## Agentic workflow guardrails + +- Match the project's test client and assertion style derived in `.aem/context/test-patterns.md`. +- No hardcoded base URLs; resolve from the testing-client configuration. +- Every side-effecting test has a teardown. + +## Common entry points + +{{ENTRY_POINTS}} + +## Module-local conventions + +{{CONVENTIONS}} + +## What to avoid in this module + +- Admin-credential dependencies. Use configured test service users. +- Flaky waits. Use the testing-client's polling primitives. + +## Run + +- All: `{{MVN_CMD}} -pl it.tests verify -Pintegration-tests` +- One class: `{{MVN_CMD}} -pl it.tests verify -Pintegration-tests -Dit.test=` + +## Where to look + +- `.aem/context/test-patterns.md` diff --git a/plugins/aem/cloud-service/skills/aem-agentkit/references/templates/AGENTS.module.ui.apps.md.template b/plugins/aem/cloud-service/skills/aem-agentkit/references/templates/AGENTS.module.ui.apps.md.template new file mode 100644 index 00000000..290d5c15 --- /dev/null +++ b/plugins/aem/cloud-service/skills/aem-agentkit/references/templates/AGENTS.module.ui.apps.md.template @@ -0,0 +1,36 @@ + +# ui.apps + +FileVault content package. Application code: components, templates, client libraries, content structure. HTL is the scripting engine. + +## Agentic workflow guardrails + +- Search `.aem/context/components.json` before creating a new component (closest scoped copy when working in a nested sub-project). +- Never write under `/libs`; use `/apps//...` overlays where `` is resolved from the closest enclosing AEM project root (see `templates/roles/role.component-author.md` § "Resolve ``"). +- Honor the project's HTL conventions in `.aem/context/conventions.md`. +- After adding a component, run `/regen-context` so `.aem/context/components.json` is recomputed with a valid marker checksum. Do not mutate the JSON inline. + +## Common entry points + +{{ENTRY_POINTS}} + +## Module-local conventions + +{{CONVENTIONS}} + +## What to avoid in this module + +- HTL `data-sly-test` with redundant constant comparison (Cloud SDK lint warning). +- Hard-coded component groups; reuse the project's component-group naming. +- Mutating `/libs` paths. + +## Where to look + +- Components: `.aem/context/components.json` +- Conventions: `.aem/context/conventions.md` + +## Build + +- Content package build + deploy: `{{MVN_CMD}} clean install -pl ui.apps -PautoInstallPackage` + +`{{MVN_CMD}}` is one of `mvn` / `./mvnw` (validated against this exact set; any other value emits a `warningStubs` entry and this build line is omitted). diff --git a/plugins/aem/cloud-service/skills/aem-agentkit/references/templates/AGENTS.module.ui.frontend.md.template b/plugins/aem/cloud-service/skills/aem-agentkit/references/templates/AGENTS.module.ui.frontend.md.template new file mode 100644 index 00000000..5ecf55c6 --- /dev/null +++ b/plugins/aem/cloud-service/skills/aem-agentkit/references/templates/AGENTS.module.ui.frontend.md.template @@ -0,0 +1,29 @@ + +# ui.frontend + +{{FRONTEND_VARIANT_DESCRIPTION}} + +## Agentic workflow guardrails + +- Do not call `/libs/*` paths from the frontend. Use `/apps//*` (where `` is resolved from the closest enclosing AEM project root — see `templates/roles/role.component-author.md` for the resolution rule) or the JSON Model API. +- Reuse the project's webpack and TypeScript config; do not introduce a new build chain. +- Inline `