diff --git a/agent-system/README.md b/agent-system/README.md new file mode 100644 index 0000000..7ad0985 --- /dev/null +++ b/agent-system/README.md @@ -0,0 +1,14 @@ +# Agent System + +This folder contains the technical assets for the multi-role workflow. + +Structure: +- `contracts/`: JSON schemas for role outputs +- `templates/`: starter JSON payloads +- `prompts/`: role-specific prompt baselines +- `checks/`: guard catalog, quality gates, and review checklists +- `runs/`: optional runtime artifacts (can stay untracked) + +References: +- Planner standard: `tools/codex-skills/starterkit-planner/SKILL.md` +- Guardrails: `tools/codex-skills/core-guardrails/SKILL.md` diff --git a/agent-system/checks/acceptance-checklist.md b/agent-system/checks/acceptance-checklist.md new file mode 100644 index 0000000..3dc64a7 --- /dev/null +++ b/agent-system/checks/acceptance-checklist.md @@ -0,0 +1,25 @@ +# Acceptance Checklist + +Use this checklist for the acceptance reviewer. + +## Scope +- all in-scope items from `plan.json` are implemented +- out-of-scope items are not silently introduced + +## Behavior +- each `SC-*` success criterion from `plan.json` is verifiably met +- negative and edge paths behave as defined + +## Regressions +- existing flows still work after change +- no critical UX break on key user paths + +## Tests +- every test listed in `plan.json tests[]` is implemented and passes (QG-001 green) +- new Service/Gateway logic has at least one test covering the happy path and one failure/edge case (GR-TEST-001) +- no new untestable code patterns introduced (GR-TEST-002) + +## Evidence +- each criterion has explicit pass/fail evidence +- each criterion check references the exact `SC-*` ID +- fail findings include exact missing behavior diff --git a/agent-system/checks/guard-catalog.json b/agent-system/checks/guard-catalog.json new file mode 100644 index 0000000..24d99d3 --- /dev/null +++ b/agent-system/checks/guard-catalog.json @@ -0,0 +1,272 @@ +{ + "version": "1.0.0", + "source_skill": "tools/codex-skills/core-guardrails/SKILL.md", + "guards": [ + { + "id": "GR-CORE-001", + "area": "core", + "title": "Layering boundaries", + "requirement": "MUST keep pages orchestration-only and business logic in services.", + "source": "tools/codex-skills/core-guardrails/references/boundaries-core.md" + }, + { + "id": "GR-CORE-002", + "area": "core", + "title": "No direct service instantiation in pages", + "requirement": "MUST NOT instantiate Service, Gateway or Repository directly in pages.", + "source": "tools/codex-skills/core-guardrails/references/boundaries-core.md" + }, + { + "id": "GR-CORE-003", + "area": "core", + "title": "Request input contract", + "requirement": "MUST read input via requestInput in pages and avoid superglobals for request data.", + "source": "tools/codex-skills/core-guardrails/references/boundaries-core.md" + }, + { + "id": "GR-CORE-004", + "area": "core", + "title": "API json body contract", + "requirement": "MUST NOT use ApiResponse::readJsonBody in pages/api/v1.", + "source": "tools/codex-skills/core-guardrails/references/boundaries-core.md" + }, + { + "id": "GR-CORE-005", + "area": "security", + "title": "Server-side authz", + "requirement": "MUST enforce authorization server-side.", + "source": "tools/codex-skills/core-guardrails/references/boundaries-core.md" + }, + { + "id": "GR-CORE-006", + "area": "security", + "title": "Server-side tenant scope", + "requirement": "MUST enforce tenant scope server-side.", + "source": "tools/codex-skills/core-guardrails/references/boundaries-core.md" + }, + { + "id": "GR-CORE-007", + "area": "api", + "title": "OpenAPI sync", + "requirement": "MUST update OpenAPI in same merge when API changes.", + "source": "tools/codex-skills/core-guardrails/references/boundaries-core.md" + }, + { + "id": "GR-UI-001", + "area": "ui", + "title": "List initialization standard", + "requirement": "MUST initialize lists via initStandardListPage.", + "source": "tools/codex-skills/core-guardrails/references/boundaries-ui.md" + }, + { + "id": "GR-UI-002", + "area": "ui", + "title": "No legacy filter toggle markup", + "requirement": "MUST NOT introduce legacy filter toggle attributes.", + "source": "tools/codex-skills/core-guardrails/references/boundaries-ui.md" + }, + { + "id": "GR-UI-003", + "area": "ui", + "title": "Row interaction standard", + "requirement": "MUST preserve row action column plus enter and dblclick fallback behavior.", + "source": "tools/codex-skills/core-guardrails/references/boundaries-ui.md" + }, + { + "id": "GR-UI-004", + "area": "ui", + "title": "Page size standard", + "requirement": "MUST preserve page size options and URL/localStorage/default precedence.", + "source": "tools/codex-skills/core-guardrails/references/boundaries-ui.md" + }, + { + "id": "GR-UI-005", + "area": "ui", + "title": "Detail page standard", + "requirement": "MUST use standard detail page contracts for dirty-state and actions.", + "source": "tools/codex-skills/core-guardrails/references/boundaries-ui.md" + }, + { + "id": "GR-UI-006", + "area": "ui", + "title": "No inline confirm", + "requirement": "MUST NOT use inline confirm handlers on standardized detail flows.", + "source": "tools/codex-skills/core-guardrails/references/boundaries-ui.md" + }, + { + "id": "GR-UI-007", + "area": "ui", + "title": "Shared partial usage", + "requirement": "MUST use shared list and detail partials for standardized UI blocks.", + "source": "tools/codex-skills/core-guardrails/references/boundaries-ui.md" + }, + { + "id": "GR-UI-016", + "area": "ui", + "title": "Template structure discipline for new features", + "requirement": "New pages MUST use the established layout system (templates/default.phtml or a named layout variant). UI blocks that appear in two or more pages MUST be extracted as a partial under templates/partials/ rather than duplicated inline. New partials MUST follow the naming convention app-{context}-{purpose}.phtml. Page-specific rendering belongs in pages/*.phtml, not in templates/. MUST NOT create ad-hoc layout HTML inside pages that bypasses the layout system.", + "source": "tools/codex-skills/core-guardrails/references/boundaries-ui.md" + }, + { + "id": "GR-UI-008", + "area": "ui", + "title": "Template RBAC contract", + "requirement": "MUST use viewAuth in templates and avoid can calls.", + "source": "tools/codex-skills/core-guardrails/references/boundaries-ui.md" + }, + { + "id": "GR-UI-009", + "area": "ui", + "title": "No a11y regression", + "requirement": "MUST NOT introduce regressions in accessibility basics (labels, roles, keyboard focus) on touched flows.", + "source": "tools/codex-skills/core-guardrails/references/boundaries-ui.md" + }, + { + "id": "GR-CORE-008", + "area": "core", + "title": "No direct superglobal access in pages", + "requirement": "MUST NOT use $_SESSION, $_SERVER, $_COOKIE, $_ENV directly in pages; use framework abstractions (app(SessionStoreInterface::class), RequestContext, requestInput()) instead.", + "source": "tools/codex-skills/core-guardrails/references/boundaries-core.md" + }, + { + "id": "GR-SEC-001", + "area": "security", + "title": "CSRF verification on POST endpoints", + "requirement": "MUST verify CSRF token on every POST endpoint in pages before processing the request body.", + "source": "tools/codex-skills/core-guardrails/references/boundaries-core.md" + }, + { + "id": "GR-SEC-002", + "area": "security", + "title": "No unredacted PII or secrets in logs and audit metadata", + "requirement": "MUST NOT write PII (email, names, UUIDs) or secrets (tokens, passwords) to logs or audit metadata without prior redaction.", + "source": "tools/codex-skills/core-guardrails/references/boundaries-core.md" + }, + { + "id": "GR-SEC-003", + "area": "security", + "title": "SQL via Repository with prepared statements only", + "requirement": "MUST NOT build SQL strings with user-controlled input; all queries MUST go through Repository classes using prepared statements.", + "source": "tools/codex-skills/core-guardrails/references/boundaries-core.md" + }, + { + "id": "GR-LANG-001", + "area": "lang", + "title": "PSR-4 namespace alignment", + "requirement": "MUST align PHP namespace with directory path (MintyPHP\\ → lib/, MintyPHP\\Tests\\ → tests/); violations are caught by QG-003.", + "source": "tools/codex-skills/core-guardrails/references/boundaries-core.md" + }, + { + "id": "GR-LANG-002", + "area": "lang", + "title": "PHP style compliance", + "requirement": "MUST pass composer cs:check without diff (php-cs-fixer dry-run); fix with composer cs:fix before merge.", + "source": "tools/codex-skills/starterkit-php-style-ci/SKILL.md" + }, + { + "id": "GR-TEST-001", + "area": "test", + "title": "Tests ship with new logic", + "requirement": "New or changed business logic in lib/Service/ MUST be accompanied by PHPUnit tests in the same merge covering the happy path and at least one failure or edge case. No new public method in a Service or Gateway without a test.", + "source": "tools/codex-skills/core-guardrails/references/boundaries-core.md" + }, + { + "id": "GR-TEST-002", + "area": "test", + "title": "Code must be unit-testable", + "requirement": "New code MUST use constructor injection for all dependencies. MUST NOT introduce static side-effects in constructors, hidden global state, or direct DB/HTTP calls outside Repository/Gateway abstractions — patterns that make unit testing without mocks impossible.", + "source": "tools/codex-skills/core-guardrails/references/boundaries-core.md" + }, + { + "id": "GR-CORE-010", + "area": "core", + "title": "DB schema changes for existing installs must be idempotent SQL scripts in db/updates/", + "requirement": "Any schema or data change for existing installations MUST be shipped as an idempotent SQL script in db/updates/ (e.g. using IF NOT EXISTS, ON DUPLICATE KEY, or guarded ALTER TABLE). MUST NOT modify db/init/init.sql only — existing containers will not re-run init.", + "source": "tools/codex-skills/core-guardrails/references/boundaries-core.md" + }, + { + "id": "GR-CORE-011", + "area": "core", + "title": "New settings belong in the DB settings table, not in config files", + "requirement": "New application settings MUST be stored in the settings DB table as the single source of truth. config/settings.php is a partial hot-path cache only and MUST NOT be used as primary storage for new settings. Security, integration, and feature-flag settings MUST go through the settings gateway layer.", + "source": "tools/codex-skills/core-guardrails/references/boundaries-core.md" + }, + { + "id": "GR-CORE-012", + "area": "core", + "title": "POST-Redirect-GET after successful mutating actions", + "requirement": "Actions that successfully mutate state (create, update, delete) MUST respond with a redirect (PRG pattern) rather than rendering a response directly. This prevents double-submit on browser reload. Flash messages MUST be set before the redirect via Flash::set().", + "source": "tools/codex-skills/core-guardrails/references/boundaries-core.md" + }, + { + "id": "GR-CORE-009", + "area": "core", + "title": "Repository list queries must use RepoQuery::sanitizeLimitOffset", + "requirement": "New Repository methods that return lists MUST use RepoQuery::sanitizeLimitOffset() to enforce LIMIT and OFFSET. Unbounded SELECT queries without LIMIT on potentially large tables are forbidden. The default maxLimit of 100 MUST NOT be raised without explicit justification reviewed and approved.", + "source": "tools/codex-skills/core-guardrails/references/boundaries-core.md" + }, + { + "id": "GR-SEC-006", + "area": "security", + "title": "User-uploaded files stored in storage/, never in web/", + "requirement": "Files uploaded by users MUST be stored under storage/ (e.g. storage/users/{uuid}/, storage/tenants/{uuid}/, storage/imports/) and MUST NOT be written to web/ or any other publicly served directory. Storage paths MUST use non-guessable identifiers (UUID v4 or equivalent) to prevent enumeration. Files MUST be served through a PHP controller that enforces authorization, not directly by the web server. MUST validate file type (MIME + extension allowlist) before storing.", + "source": "tools/codex-skills/core-guardrails/references/boundaries-core.md" + }, + { + "id": "GR-SEC-005", + "area": "security", + "title": "Encryption only via lib/Support/Crypto.php", + "requirement": "Any encryption or decryption of sensitive data MUST use the Crypto class (lib/Support/Crypto.php) which provides AES-256-GCM via APP_CRYPTO_KEY. MUST NOT use raw openssl_encrypt/openssl_decrypt, base64 encoding as a substitute for encryption, or any other hand-rolled crypto primitives.", + "source": "tools/codex-skills/core-guardrails/references/boundaries-core.md" + }, + { + "id": "GR-SEC-004", + "area": "security", + "title": "No external CDN or remote asset loading", + "requirement": "MUST NOT load JS, CSS, fonts, or any other assets from external CDNs (Cloudflare, jsDelivr, unpkg, Google Fonts, etc.) or remote URLs in templates, pages, or layout files. If a third-party library is required it MUST be vendored locally in the repository (e.g. web/js/vendor/ or web/css/vendor/) and served via assetVersion(). No @@ -53,6 +75,7 @@ if ($bufferTitle !== '') { + diff --git a/templates/partials/app-confirm-dialog.phtml b/templates/partials/app-confirm-dialog.phtml new file mode 100644 index 0000000..efeb1fb --- /dev/null +++ b/templates/partials/app-confirm-dialog.phtml @@ -0,0 +1,22 @@ + +
+
+

+ +
+

+ + +
+
diff --git a/templates/partials/app-details-aside-actions.phtml b/templates/partials/app-details-aside-actions.phtml index 49a88a3..5a8cd87 100644 --- a/templates/partials/app-details-aside-actions.phtml +++ b/templates/partials/app-details-aside-actions.phtml @@ -32,6 +32,11 @@ if (!$asideActions) { $itemMethod = strtoupper((string) ($item['method'] ?? 'POST')); $itemAction = (string) ($item['action'] ?? ''); $itemConfirm = (string) ($item['confirm'] ?? ''); + $itemConfirmTitle = (string) ($item['confirmTitle'] ?? ''); + $itemConfirmConfirmLabel = (string) ($item['confirmConfirmLabel'] ?? ''); + $itemConfirmCancelLabel = (string) ($item['confirmCancelLabel'] ?? ''); + $itemConfirmVariant = strtolower(trim((string) ($item['confirmVariant'] ?? ''))); + $itemConfirmFocus = strtolower(trim((string) ($item['confirmFocus'] ?? ''))); $itemButtonClass = (string) ($item['class'] ?? 'secondary outline small'); $itemNeedsCsrf = !array_key_exists('csrf', $item) ? true : !empty($item['csrf']); $itemTarget = (string) ($item['target'] ?? ''); @@ -45,6 +50,11 @@ if (!$asideActions) { action="" target="" data-detail-confirm-message="" + data-detail-confirm-title="" + data-detail-confirm-confirm-label="" + data-detail-confirm-cancel-label="" + data-detail-confirm-variant="" + data-detail-confirm-focus="" data-detail-action-kind="" data-detail-submit-lock="" > diff --git a/templates/partials/app-details-titlebar.phtml b/templates/partials/app-details-titlebar.phtml index 5229667..6d10b48 100644 --- a/templates/partials/app-details-titlebar.phtml +++ b/templates/partials/app-details-titlebar.phtml @@ -55,6 +55,11 @@ $menuItems = is_array($titlebar['menuItems'] ?? null) ? $titlebar['menuItems'] : $itemMethod = strtoupper((string) ($item['method'] ?? 'POST')); $itemAction = (string) ($item['action'] ?? ''); $itemConfirm = (string) ($item['confirm'] ?? ''); + $itemConfirmTitle = (string) ($item['confirmTitle'] ?? ''); + $itemConfirmConfirmLabel = (string) ($item['confirmConfirmLabel'] ?? ''); + $itemConfirmCancelLabel = (string) ($item['confirmCancelLabel'] ?? ''); + $itemConfirmVariant = strtolower(trim((string) ($item['confirmVariant'] ?? ''))); + $itemConfirmFocus = strtolower(trim((string) ($item['confirmFocus'] ?? ''))); $itemButtonClass = (string) ($item['class'] ?? 'transparent'); $itemNeedsCsrf = !array_key_exists('csrf', $item) ? true : !empty($item['csrf']); $itemTarget = (string) ($item['target'] ?? ''); @@ -68,6 +73,11 @@ $menuItems = is_array($titlebar['menuItems'] ?? null) ? $titlebar['menuItems'] : action="" target="" data-detail-confirm-message="" + data-detail-confirm-title="" + data-detail-confirm-confirm-label="" + data-detail-confirm-cancel-label="" + data-detail-confirm-variant="" + data-detail-confirm-focus="" data-detail-action-kind="" data-detail-submit-lock="" > @@ -118,6 +128,11 @@ $menuItems = is_array($titlebar['menuItems'] ?? null) ? $titlebar['menuItems'] : $itemName = (string) ($item['name'] ?? ''); $itemValue = (string) ($item['value'] ?? ''); $itemConfirm = (string) ($item['confirm'] ?? ''); + $itemConfirmTitle = (string) ($item['confirmTitle'] ?? ''); + $itemConfirmConfirmLabel = (string) ($item['confirmConfirmLabel'] ?? ''); + $itemConfirmCancelLabel = (string) ($item['confirmCancelLabel'] ?? ''); + $itemConfirmVariant = strtolower(trim((string) ($item['confirmVariant'] ?? ''))); + $itemConfirmFocus = strtolower(trim((string) ($item['confirmFocus'] ?? ''))); $itemDetailActionKind = strtolower(trim((string) ($item['detailActionKind'] ?? ''))); $itemDetailSubmitLock = strtolower(trim((string) ($item['detailSubmitLock'] ?? ''))); $itemDisabled = !empty($item['disabled']); @@ -144,6 +159,11 @@ $menuItems = is_array($titlebar['menuItems'] ?? null) ? $titlebar['menuItems'] : name="" value="" data-detail-confirm-message="" + data-detail-confirm-title="" + data-detail-confirm-confirm-label="" + data-detail-confirm-cancel-label="" + data-detail-confirm-variant="" + data-detail-confirm-focus="" data-detail-action-kind="" data-detail-submit-lock="" data-detail-save-primary="1" diff --git a/templates/partials/app-footer.phtml b/templates/partials/app-footer.phtml index a1c0dee..02063d5 100644 --- a/templates/partials/app-footer.phtml +++ b/templates/partials/app-footer.phtml @@ -1,5 +1,5 @@