From 236689827a179aaf0fcced5ea16d6591ca5a5c7b Mon Sep 17 00:00:00 2001 From: Eric W Cochran Date: Tue, 22 Sep 2026 10:56:48 -0500 Subject: [PATCH 1/3] Refresh browser skill with runtime selection and resource cleanup --- AGENTS.md | 7 ++ docs/dev/evidence/browser-skill-sync.json | 32 ++++++ .../0004-2026-09-22-browser-skill-hygiene.md | 47 ++++++++ skills/dev-browser/SKILL.md | 104 +++++++++++++++--- 4 files changed, 173 insertions(+), 17 deletions(-) create mode 100644 docs/dev/evidence/browser-skill-sync.json create mode 100644 docs/dev/plans/0004-2026-09-22-browser-skill-hygiene.md diff --git a/AGENTS.md b/AGENTS.md index 79dada2..549d7b9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -65,3 +65,10 @@ python3 .agents/skills/repo-policy-selector/scripts/audit_planning_contract.py - python3 .agents/skills/repo-policy-selector/scripts/audit_planning_contract.py --repo-root . --active-only --json python3 .agents/skills/repo-policy-selector/scripts/select_policy.py --repo-root . --policy-root .agents/skills/repo-policy-selector/policy-library --json ``` + +## Browser runtime operations + +Before launching/attaching browsers or handing off browser work, read +[the maintained dev-browser skill](skills/dev-browser/SKILL.md). Agents choose +the appropriate existing runtime/profile and own tab, browser, daemon, and +resource cleanup. v1 remains explicit opt-in until an approved migration. diff --git a/docs/dev/evidence/browser-skill-sync.json b/docs/dev/evidence/browser-skill-sync.json new file mode 100644 index 0000000..7a5132d --- /dev/null +++ b/docs/dev/evidence/browser-skill-sync.json @@ -0,0 +1,32 @@ +{ + "date": "20260922T155648Z", + "backup": "/home/ecochran76/.dev-browser/backups/skill-hygiene-20260922T155648Z", + "source": "skills/dev-browser/SKILL.md", + "copies": [ + { + "target": "/home/ecochran76/.codex/shared/skills/dev-browser/SKILL.md", + "previousSha256": "56040c0fab85854d52cd91448a9da69e9868f2d27338d57c5b52dfdabbca47ae", + "sha256": "6dca7a17df9c8dbf236d1d8c58416ac6696f4a4970d1e446a7ebfd13ff1d46e9" + }, + { + "target": "/home/ecochran76/.agents/skills/dev-browser/SKILL.md", + "previousSha256": null, + "sha256": "6dca7a17df9c8dbf236d1d8c58416ac6696f4a4970d1e446a7ebfd13ff1d46e9" + }, + { + "target": "/home/ecochran76/.claude/skills/dev-browser/SKILL.md", + "previousSha256": null, + "sha256": "6dca7a17df9c8dbf236d1d8c58416ac6696f4a4970d1e446a7ebfd13ff1d46e9" + } + ], + "codexAliasVerified": true, + "runtimeChanged": false, + "runtimeCleanupPerformed": false, + "reason": "Skill maintenance only; no browser resources started.", + "embeddedInstaller": "Existing CLI embeds its older skill. Do not use its install-skill to refresh these copies until the CLI is rebuilt from this source.", + "policyLibrary": { + "installed": "v0.1.22", + "latestObserved": "v0.1.26", + "upgraded": false + } +} diff --git a/docs/dev/plans/0004-2026-09-22-browser-skill-hygiene.md b/docs/dev/plans/0004-2026-09-22-browser-skill-hygiene.md new file mode 100644 index 0000000..652aecb --- /dev/null +++ b/docs/dev/plans/0004-2026-09-22-browser-skill-hygiene.md @@ -0,0 +1,47 @@ +# Plan 0004: Refresh browser skill and resource ownership + +State: CLOSED + +## Current State + +Installed shared dev-browser skill lags the maintained fork skill and lacks +idle cleanup and ownership guidance. Current CLI confirms named profiles, +page lifecycle, daemon-wide idle policy, and global stop semantics. +Policy-selector is pinned at v0.1.22; GitHub latest is v0.1.26. Policy-library +upgrade is reported separately and is not part of this browser-skill correction. + +## Scope + +Owner: primary agent. Branch: chore/browser-skill-hygiene. Target: origin/main +through a PR. Update the canonical skill and AGENTS routing, sync installed +agent copies with backups, and verify byte identity and command guidance. +Serial work: inspect, adapt, sync, validate, commit/publish. No delegation. + +## Non-goals + +No runtime/binary/default-browser upgrade, live session termination, v1 promotion, +policy-library upgrade, unrelated service-skill replacement, or automatic merge. + +## Acceptance Criteria + +- Agents select task-appropriate installed runtime/profile without routine re-asking. +- Named browser versus tab semantics, resource budgets, and ownership are explicit. +- Cleanup covers success/failure/cancellation and includes OS-level verification. +- Shared/external browser ownership and daemon-wide idle/stop effects are preserved. +- Canonical and installed dev-browser skills match; original files are backed up. +- Policy checks and diff checks pass; publication status is explicit. + +## Definition of Done + +The updated local skill is active and source changes are reviewable. Record +sync receipt and PR; no browser execution test is required for prose-only changes. + +## Outcome + +Canonical skill and shared/Codex, Agents, and Claude copies now match; backups +and exact hashes are recorded in docs/dev/evidence/browser-skill-sync.json. +CLI help confirmed the documented options and tab APIs. The current embedded +install-skill payload remains older until the CLI is rebuilt; do not use it to +refresh these installed copies. Runtime/default browser/config were unchanged. +Policy and diff checks passed. Source publication is a review branch/PR, not +an automatic merge or v1 upgrade. diff --git a/skills/dev-browser/SKILL.md b/skills/dev-browser/SKILL.md index 1da5ab6..f876647 100644 --- a/skills/dev-browser/SKILL.md +++ b/skills/dev-browser/SKILL.md @@ -29,23 +29,95 @@ Do not use this skill when: - The task is generic automation unrelated to a browser - The user is asking for local filesystem or terminal work -## Runtime Model - -- Scripts run in a sandboxed QuickJS runtime, not Node.js -- Named pages from `browser.getPage("name")` persist between runs -- Default mode launches a separate managed Chromium profile -- `--connect` attaches to an already running Chrome with remote debugging enabled -- `--port` targets a specific CDP port during auto-discovery -- `--profile-path` reads `DevToolsActivePort` from a custom Chrome user-data/profile root -- In WSL, `--connect` can target Windows Chrome profiles under `/mnt/c/Users/.../AppData/Local/...` +## Runtime selection and ownership + +Agents are responsible for choosing the runtime and managing its resource use. +Make that choice from the task, required login/session, runtime health, and +available memory. Do not ask the user to choose routine implementation details. + +| Task need | Runtime/profile choice | +| --- | --- | +| Isolated browser work without an existing login session | Installed v0.2.x dev-browser, QuickJS/Playwright; one task-owned named browser, headless by default | +| Visible interaction or debugging | Same installed runtime, headed; do not change mode on a browser another task is using | +| Existing authenticated/user session | Attach to the exact authorized CDP endpoint/profile; list tabs before selecting an existing target ID | +| A managed agent-browser service/desktop session | Follow agent-browser / agent-browser-service ownership and profile guidance; reuse its route rather than launch a competing browser | +| v1/Puppeteer evaluation | Only when explicitly requested; isolated evaluation runtime and profiles, never an automatic fallback or default upgrade | + +On this WSL host, the validated managed-browser default remains native Linux +chromium-stealthcdp 150 via `~/.dev-browser/config.json` `executablePath`. +Chromium 153 is available but has known headless interaction failures; a higher +version is not proof of suitability. Use another artifact only for a bounded, +explicit compatibility task with its own verification. Preserve the configured +path and the shared Chromium alias. Do not reinstall or change the default +runtime to resolve an ordinary navigation failure. + +Scripts use QuickJS, not Node.js or v1's Puppeteer API. Named pages persist. +`--browser NAME` selects a separate browser/profile, not a tab. Avoid generating +fresh browser names on every call. `--profile-path` is a CDP discovery hint, +not an instruction to launch a new managed profile. Prefer an exact endpoint +for attachments when multiple sessions could be discovered. + +Before browser work, record the selected runtime, browser name/profile or +endpoint, existing tabs, and which resources this task creates versus borrows. +Reuse one task-owned browser across steps. For a shared session, record owned +tab IDs separately; never equate a matching URL or process name with ownership. +Use status/browser inventory only while intentionally operating the runtime: +`dev-browser status` and `dev-browser browsers` can start an absent daemon. + +## Tab and resource budget + +- Default to one active browser and one working tab per task. Allow up to three + task-owned tabs when comparison requires it; close a finished tab before + opening another. Raise that working budget only for a concrete task need + after checking available memory and other active work, and record the reason. +- Prefer `browser.newPage()` for disposable work; anonymous pages are cleaned + up when the script exits. Use `browser.getPage("task-purpose")` only when the + page needs to persist across steps. Inspect `browser.listPages()` before + creating another persistent page. +- Use finite script timeouts and serial browser operations by default. Do not + spawn more browsers or daemons because a request timed out; inspect and + reconcile the existing task-owned session first. Stop expanding work under + memory pressure and close expendable owned tabs before retrying. +- Close owned named pages with `await browser.closePage("task-purpose")` on + success, failure, or cancellation once they are no longer needed. Do not close + pre-existing user tabs or another task's tabs, or delete profiles/cookies. +- For unattended work, use `--idle-timeout 5m` when starting an exclusively + owned runtime or where that shared cleanup policy is already agreed. This + updates the daemon's policy for all managed browsers; it is not a per-task + lease. Do not silently override another task's retention policy, and do not + use `0` merely for convenience. Idle reaping preserves profiles, does not + reap running requests, and never closes attached external browsers. +- `dev-browser stop` is global: it stops the daemon and all its browser + connections. There is no `stop NAME` CLI in this version. Use it at task end + only when inventory establishes that the daemon has no other active owners + and all managed work is finished. Otherwise close this task's tabs, retain + shared connections needed by others, and explicitly report the remaining owner. +- Attachment does not transfer browser ownership. Never terminate the external + browser or its service/desktop to clean up a dev-browser task. Follow the + owning service's release/detach workflow; do not use broad `pkill`/`killall`. + +## Completion and handoff + +Capture required results, close expendable owned tabs, and stop an exclusively +owned idle runtime. Idle timeout is a backstop, not a substitute for cleanup. +After cleanup, inspect OS processes by recorded PID, parentage and profile/path; +verify owned browser/daemon processes actually exited. A CLI success or empty +browser list alone does not prove cleanup. Do not run `status` after stopping +just to verify it: that command can recreate the daemon. + +If work must remain open, record the exact browser/profile, owned tabs/PIDs, +reason, responsible task/owner, and expected expiry or next cleanup action. +Treat ambiguous ownership as a reason for bounded inspection, never blanket +termination. Finish with the selected runtime, resources closed, and anything +intentionally retained. Do not leave unused daemons, browsers, or tabs silently. ## Common Commands ```bash . "$HOME/.cargo/env" -dev-browser --headless <<'EOF' -const page = await browser.getPage("main"); +dev-browser --browser task-example --headless <<'EOF' +const page = await browser.newPage(); await page.goto("https://example.com", { waitUntil: "domcontentloaded" }); console.log(await page.title()); EOF @@ -54,7 +126,7 @@ EOF ```bash . "$HOME/.cargo/env" -dev-browser --connect <<'EOF' +dev-browser --browser task-attachment --connect <<'EOF' const tabs = await browser.listPages(); console.log(JSON.stringify(tabs, null, 2)); EOF @@ -63,7 +135,7 @@ EOF ```bash . "$HOME/.cargo/env" -dev-browser --connect --port 9333 <<'EOF' +dev-browser --browser task-attachment --connect --port 9333 <<'EOF' const tabs = await browser.listPages(); console.log(JSON.stringify(tabs, null, 2)); EOF @@ -72,7 +144,7 @@ EOF ```bash . "$HOME/.cargo/env" -dev-browser --connect --profile-path ~/.config/google-chrome-agent <<'EOF' +dev-browser --browser task-attachment --connect --profile-path ~/.config/google-chrome-agent <<'EOF' const tabs = await browser.listPages(); console.log(JSON.stringify(tabs, null, 2)); EOF @@ -81,7 +153,7 @@ EOF ```bash . "$HOME/.cargo/env" -dev-browser --connect --profile-path "/mnt/c/Users//AppData/Local/Google/Chrome/User Data" <<'EOF' +dev-browser --browser task-attachment --connect --profile-path "/mnt/c/Users//AppData/Local/Google/Chrome/User Data" <<'EOF' const tabs = await browser.listPages(); console.log(JSON.stringify(tabs, null, 2)); EOF @@ -93,5 +165,3 @@ EOF - Use persistent named pages to avoid re-navigation across turns - Use `--connect` only when the user wants to work inside an existing Chrome session - For command details and API reference, run `dev-browser --help` - -Named daemon-launched browsers persist by default. For unattended work, `--idle-timeout 5m` closes each launched browser after inactivity while preserving its profile and login state. The setting never closes Chrome attached with `--connect`; use `--idle-timeout 0` to disable configured cleanup. From af26d57d671d9f4246befa000ae1df920cef1bf0 Mon Sep 17 00:00:00 2001 From: Eric W Cochran Date: Tue, 22 Sep 2026 11:19:05 -0500 Subject: [PATCH 2/3] chore: upgrade policy bundle and verify embedded browser skill --- .agents/policy-selector-install.json | 16 +- .agents/skills/repo-policy-selector/SKILL.md | 24 +- .../policy-library/SCHEMA.md | 132 ++++- .../policy-library/catalog.yaml | 21 + .../modules/active-lane-coordination.md | 23 +- .../modules/architecture-guardrails.md | 28 +- .../collaborative-development-workflow.md | 102 ++++ .../modules/development-runtime-isolation.md | 45 ++ .../modules/forge-issue-reporting.md | 70 +++ .../modules/git-worktree-hygiene.md | 16 + .../modules/github-issue-operations.md | 47 ++ .../modules/gitlab-issue-operations.md | 50 ++ .../modules/goal-execution-governance.md | 24 +- .../model-selection-and-calibration.md | 39 ++ .../modules/multi-agent-reconciliation.md | 7 + .../modules/parallel-plan-design.md | 12 +- .../modules/planning-discipline.md | 12 + .../modules/subagent-runtime-governance.md | 2 + .../modules/subagent-workflow-optimization.md | 12 +- .../modules/validation-and-handoff.md | 8 + .../modules/work-item-traceability.md | 66 +++ .../forge-issue-targets.example.json | 47 ++ .../references/selection-workflow.md | 39 ++ .../release-manifest.json | 10 +- .../repo-policy-selector/releases/v0.1.23.md | 81 +++ .../repo-policy-selector/releases/v0.1.24.md | 71 +++ .../repo-policy-selector/releases/v0.1.25.md | 25 + .../repo-policy-selector/releases/v0.1.26.md | 97 ++++ .../scripts/audit_active_lanes.py | 60 ++- .../scripts/audit_planning_contract.py | 67 ++- .../scripts/preflight_forge_issue.py | 482 ++++++++++++++++++ .../scripts/select_policy.py | 142 ++++++ .../tests/test_audit_active_lanes.py | 118 ++++- .../tests/test_audit_planning_contract.py | 89 +++- ...llaborative_development_policy_contract.py | 56 ++ ...est_measurement_routing_policy_contract.py | 54 ++ ...t_multi_session_policy_harvest_contract.py | 84 +++ .../tests/test_preflight_forge_issue.py | 277 ++++++++++ .../tests/test_select_policy.py | 211 ++++++++ AGENTS.md | 1 + docs/dev/evidence/embedded-skill-upgrade.json | 43 ++ .../evidence/graphiti-upgrade-feedback.json | 9 + docs/dev/evidence/local-vitest-upgrade.json | 17 + .../0002-2026-09-22-policy-v0126-upgrade.md | 71 +++ ...09-22-policy-and-embedded-skill-upgrade.md | 49 ++ .../0002-policy-upgrade-management.md | 4 +- docs/dev/policies/0005-planning-discipline.md | 13 + .../dev/policies/0008-git-worktree-hygiene.md | 17 + .../policies/0014-validation-and-handoff.md | 9 + .../0017-model-selection-and-calibration.md | 48 ++ 50 files changed, 2966 insertions(+), 81 deletions(-) create mode 100644 .agents/skills/repo-policy-selector/policy-library/modules/collaborative-development-workflow.md create mode 100644 .agents/skills/repo-policy-selector/policy-library/modules/development-runtime-isolation.md create mode 100644 .agents/skills/repo-policy-selector/policy-library/modules/forge-issue-reporting.md create mode 100644 .agents/skills/repo-policy-selector/policy-library/modules/github-issue-operations.md create mode 100644 .agents/skills/repo-policy-selector/policy-library/modules/gitlab-issue-operations.md create mode 100644 .agents/skills/repo-policy-selector/policy-library/modules/model-selection-and-calibration.md create mode 100644 .agents/skills/repo-policy-selector/policy-library/modules/work-item-traceability.md create mode 100644 .agents/skills/repo-policy-selector/references/forge-issue-targets.example.json create mode 100644 .agents/skills/repo-policy-selector/releases/v0.1.23.md create mode 100644 .agents/skills/repo-policy-selector/releases/v0.1.24.md create mode 100644 .agents/skills/repo-policy-selector/releases/v0.1.25.md create mode 100644 .agents/skills/repo-policy-selector/releases/v0.1.26.md create mode 100644 .agents/skills/repo-policy-selector/scripts/preflight_forge_issue.py create mode 100644 .agents/skills/repo-policy-selector/tests/test_collaborative_development_policy_contract.py create mode 100644 .agents/skills/repo-policy-selector/tests/test_measurement_routing_policy_contract.py create mode 100644 .agents/skills/repo-policy-selector/tests/test_multi_session_policy_harvest_contract.py create mode 100644 .agents/skills/repo-policy-selector/tests/test_preflight_forge_issue.py create mode 100644 docs/dev/evidence/embedded-skill-upgrade.json create mode 100644 docs/dev/evidence/graphiti-upgrade-feedback.json create mode 100644 docs/dev/evidence/local-vitest-upgrade.json create mode 100644 docs/dev/notes/0002-2026-09-22-policy-v0126-upgrade.md create mode 100644 docs/dev/plans/0005-2026-09-22-policy-and-embedded-skill-upgrade.md create mode 100644 docs/dev/policies/0017-model-selection-and-calibration.md diff --git a/.agents/policy-selector-install.json b/.agents/policy-selector-install.json index d049096..8b4f4cb 100644 --- a/.agents/policy-selector-install.json +++ b/.agents/policy-selector-install.json @@ -2,22 +2,22 @@ "install_relpath": ".agents/skills/repo-policy-selector", "installed_bundle_release": { "bundle_name": "repo-policy-selector", - "bundle_version": "0.1.22", + "bundle_version": "0.1.26", "policy_library": { "catalog_path": "policy-library/catalog.yaml", - "content_sha256": "03d4cba5d1aaad7ca2805b847dd69083ebf4193b8812693ee7ea8d9079f56b42", + "content_sha256": "1129e37be16efbf571954f80d06ea7115eb8f244acbd327003abe37e2dadaa96", "relative_root": "policy-library", "schema_path": "policy-library/SCHEMA.md" }, - "release_ref": "v0.1.22", + "release_ref": "v0.1.26", "schema_version": 1, - "source_commit": "12a7f9fef466522e99be44d980c44a4ff056f540", - "source_ref": "12a7f9fef466522e99be44d980c44a4ff056f540", + "source_commit": "ead42f7d6932fedc9dea83111b234f954a6afa4d", + "source_ref": "ead42f7d6932fedc9dea83111b234f954a6afa4d", "source_repo_root": "/home/ecochran76/workspace.local/agent-policies", "source_tree_state": "clean-ref" }, - "installed_policy_root": "/home/ecochran76/workspace.local/dev-browser-policy-adoption/.agents/skills/repo-policy-selector/policy-library", - "installed_selector_root": "/home/ecochran76/workspace.local/dev-browser-policy-adoption/.agents/skills/repo-policy-selector", - "selector_root": "/home/ecochran76/.agents/skills/repo-policy-selector", + "installed_policy_root": "/home/ecochran76/workspace.local/dev-browser-skill-hygiene/.agents/skills/repo-policy-selector/policy-library", + "installed_selector_root": "/home/ecochran76/workspace.local/dev-browser-skill-hygiene/.agents/skills/repo-policy-selector", + "selector_root": "/tmp/dev-browser-policy-upgrade-v0126/repo-policy-selector", "source_type": "local-path" } diff --git a/.agents/skills/repo-policy-selector/SKILL.md b/.agents/skills/repo-policy-selector/SKILL.md index a59cccf..0ffb34c 100644 --- a/.agents/skills/repo-policy-selector/SKILL.md +++ b/.agents/skills/repo-policy-selector/SKILL.md @@ -38,16 +38,21 @@ Select the right reusable policy bundle for a repository, then adapt it into rep and unused baseline entries. Use `--force` only for pre-adoption assessment; active baselines never apply to full or forced audits. 10. If the repo adopts `goal-execution-governance`, run `scripts/audit_planning_contract.py --goal-only` and require concrete local bounds, execute-by-default continuation, action-specific approval gates, local replan before escalation, at-most-one risk-triggered drift-discovery pass, closed-world verification when review occurs, and minimal material-checkpoint fields. -11. If the repo adopts `active-lane-coordination`, refresh remote-tracking refs through its normal fetch policy and run `scripts/audit_active_lanes.py` with an explicit default ref. Prefer `--catalog-only` when the catalog is the complete authorized population; use repeated exact `--branch` selectors for bounded unregistered-lane discovery and reserve `--branch-prefix` for explicit broader surveys. Treat missing, unequal, or contradictory custody as a fail-closed planning input, not as permission for the auditor to mutate Git. -12. Validate that the recommended profile and modules exist in the installed library bundle before drafting changes. -13. Read the referenced policy modules from this policy library before drafting changes. -14. Decide whether the repo needs: +11. If the repo adopts `active-lane-coordination`, refresh remote-tracking refs through its normal fetch policy and run `scripts/audit_active_lanes.py` with an explicit default ref. Prefer `--catalog-only` when the catalog is the complete authorized population; use repeated exact `--branch` selectors for bounded unregistered-lane discovery and reserve `--branch-prefix` for explicit broader surveys. Treat missing, unequal, or contradictory custody as a fail-closed planning input, not as permission for the auditor to mutate Git. When `work-item-traceability` is adopted, migrate lane locators before setting catalog `work_item_tracking: required`. +12. If the repo adopts `forge-issue-reporting`, require an explicit target + registry and run `scripts/preflight_forge_issue.py` before any proposed + provider mutation. Treat its output as read-only capability and + configuration evidence, not write authority. Validate normalized labels + against exact provider labels and keep label creation separate. +13. Validate that the recommended profile and modules exist in the installed library bundle before drafting changes. +14. Read the referenced policy modules from this policy library before drafting changes. +15. Decide whether the repo needs: - a starter profile with minimal edits - a profile plus module overrides - a missing-modules patch when the repo already partially or mostly matches the selected profile - a migration-first adoption because plans, notes, or memories are cluttered - a custom composition because no single profile fits cleanly -15. Draft the repo-local policy patch or recommendation, keeping the adopted policy in `docs/dev/policies/` and using `AGENTS.md` as the wire-in entrypoint. +16. Draft the repo-local policy patch or recommendation, keeping the adopted policy in `docs/dev/policies/` and using `AGENTS.md` as the wire-in entrypoint. ## Required references @@ -81,6 +86,8 @@ python scripts/audit_active_lanes.py --repo-root /path/to/repo --default-ref ref python scripts/audit_active_lanes.py --repo-root /path/to/repo --default-ref refs/remotes/origin/main --catalog-only --json python scripts/audit_active_lanes.py --repo-root /path/to/repo --default-ref refs/remotes/origin/main --branch feature/lane-a --branch fix/lane-b --json python scripts/audit_active_lanes.py --repo-root /path/to/repo --default-ref refs/heads/main --catalog-path docs/dev/active-lanes.yaml --plans-dir docs/dev/plans +python scripts/preflight_forge_issue.py --registry /path/to/repo/docs/dev/forge-issue-targets.json --target --action create --label intent/defect --idempotency-key +python scripts/preflight_forge_issue.py --registry /path/to/repo/docs/dev/forge-issue-targets.json --target --action create --idempotency-key --snapshot /path/to/provider-snapshot.json ``` ## Guardrails @@ -94,4 +101,11 @@ python scripts/audit_active_lanes.py --repo-root /path/to/repo --default-ref ref - Downstream install should support a one-shot path that copies a pinned selector bundle into the target repo from either a reviewed git ref or a local bundle path and can draft the initial local policy set immediately. - Released selector bundles should carry a deterministic `release-manifest.json` next to the bundled `policy-library/`. - `audit_active_lanes.py` is read-only. It never fetches or performs Git, catalog, plan, worktree, branch, or remote mutations; a clean report never grants integration or cleanup authority. +- `preflight_forge_issue.py` is read-only. It may authenticate and query an + explicitly registered GitHub or GitLab target, but it never creates or + mutates issues, labels, assignments, milestones, projects, boards, or work + items. A clean result never grants mutation authority. +- Preflight JSON always reports `operator_authority_verified: false` and + `mutation_authorized: false`; the caller must establish those gates outside + the read-only tool for the exact requested action. - Treat the policy library as a source library, not the runtime source of truth for the target repo. diff --git a/.agents/skills/repo-policy-selector/policy-library/SCHEMA.md b/.agents/skills/repo-policy-selector/policy-library/SCHEMA.md index f81828e..b5d0c95 100644 --- a/.agents/skills/repo-policy-selector/policy-library/SCHEMA.md +++ b/.agents/skills/repo-policy-selector/policy-library/SCHEMA.md @@ -119,23 +119,33 @@ optional outside repositories with concurrent projects, branches, or worktrees. The catalog uses `schema_version: 1` and a `lanes` list. Every lane requires: ```yaml -- id: P42 - objective: Carrier reconciliation - plan: docs/dev/plans/0042-YYYY-MM-DD-carrier-reconciliation.md - plan_ref: refs/heads/feature/p42-carrier-reconciliation - branch: feature/p42-carrier-reconciliation - target: main - plan_state: OPEN - custody_state: ACTIVE_WORKTREE - checkpoint: - remote_ref: refs/remotes/origin/feature/p42-carrier-reconciliation - integration: merge - dependencies: [] - overlaps: [] - updated_at: YYYY-MM-DD +schema_version: 1 +work_item_tracking: required +lanes: + - id: P42 + objective: Carrier reconciliation + work_items: [owner/repo#42] + plan: docs/dev/plans/0042-YYYY-MM-DD-carrier-reconciliation.md + plan_ref: refs/heads/feature/p42-carrier-reconciliation + branch: feature/p42-carrier-reconciliation + target: main + plan_state: OPEN + custody_state: ACTIVE_WORKTREE + checkpoint: + remote_ref: refs/remotes/origin/feature/p42-carrier-reconciliation + integration: merge + dependencies: [] + overlaps: [] + updated_at: YYYY-MM-DD ``` -Optional fields include `reconciled_overlaps`, `validation_status`, +`work_item_tracking` is optional and accepts `optional` or `required`; omitted +catalogs behave as `optional` for backward compatibility. When it is +`required`, every lane needs a non-empty inline `work_items` list. Locators are +opaque, compact, non-secret identifiers; the auditor validates shape and +presence without querying a provider. + +Optional fields include `work_items`, `reconciled_overlaps`, `validation_status`, `validation_ref`, `integration_receipt`, `archive_ref`, `archive_remote_ref`, `blocker`, and `disposition`. Lists use inline YAML form in schema version 1 so the bundled dependency-free auditor can parse them deterministically. @@ -143,6 +153,8 @@ the bundled dependency-free auditor can parse them deterministically. Rules: - lane ids and branch ownership are unique +- `work_item_tracking: required` makes a non-empty `work_items` list mandatory + for every lane - plan states are `PLANNED`, `OPEN`, `BLOCKED`, `CLOSED`, or `CANCELLED` - custody states are `ACTIVE_WORKTREE`, `PAUSED_REF`, `INTEGRATION_READY`, `INTEGRATED`, `ARCHIVED`, or `DISCARD_APPROVED` @@ -164,6 +176,96 @@ Rules: `remote_ahead_of_local`, or `local_remote_diverged` when both tips exist but are unequal +## Forge Issue Target Contract + +Repos that adopt `forge-issue-reporting` should keep an explicit, non-secret +target registry at `docs/dev/forge-issue-targets.json` or a documented +equivalent. JSON is the canonical interchange format for the dependency-free +preflight; another source format may be used only when it deterministically +renders the same object. + +The registry uses `schema_version: 1` and a `targets` list: + +```json +{ + "schema_version": 1, + "targets": [ + { + "id": "odollo-github", + "forge": "github", + "host": "github.com", + "repository": "example/odollo", + "relationship": "owned", + "allowed_actions": ["read", "create", "comment", "apply_labels", "close"], + "security_route": "private_vulnerability_reporting", + "label_map": { + "intent/defect": {"provider_label": "bug"}, + "priority/high": {"provider_label": "priority: high"} + } + }, + { + "id": "litscout-gitlab", + "forge": "gitlab", + "host": "gitlab.example.com", + "repository": "research/tools/litscout", + "relationship": "permissioned", + "allowed_actions": ["read", "create", "comment", "apply_labels"], + "security_route": "confidential_issue", + "label_map": { + "intent/defect": {"provider_label": "type::bug"} + } + } + ] +} +``` + +Required target fields are `id`, `forge`, `host`, `repository`, +`relationship`, `allowed_actions`, `security_route`, and `label_map`. + +Rules: + +- target ids are unique and stable +- `forge` is `github` or `gitlab` +- `host` is explicit even for GitHub.com or GitLab.com +- GitHub repositories use `OWNER/REPO`; GitLab repositories allow + `GROUP/SUBGROUP/PROJECT` +- `relationship` is `owned` or `permissioned`; it expresses intended operating + scope and is not current provider-permission evidence +- allowed actions come from `read`, `create`, `comment`, `edit`, + `apply_labels`, `create_labels`, `assign`, `milestone`, `planning`, `close`, + `reopen`, and `transfer` +- normalized label keys are repo-local intent vocabulary; each maps to one + exact existing provider label +- applying labels and creating labels are separate actions; missing mapped + labels fail preflight and are not created during issue creation +- security routes are `private_vulnerability_reporting`, `security_policy`, + `private_contact`, `confidential_issue`, or `none`; `none` cannot authorize a + security report +- registries contain no credentials, tokens, vulnerability details, private + customer data, or inferred lists of every accessible repository +- registry membership, provider role, and operator authority remain separate + gates + +The read-only preflight snapshot schema is intentionally small: + +```json +{ + "forge": "github", + "host": "github.com", + "repository": "example/odollo", + "authenticated_actor": "reporter-login", + "effective_role": "TRIAGE", + "issues_enabled": true, + "archived": false, + "available_labels": ["bug", "priority: high"], + "duplicate_candidates": [] +} +``` + +Snapshots are evidence inputs for deterministic validation, not reusable +proof of current live capability. Live mutations require a fresh provider +readback. + ## Module Contract Modules live under `modules/*.md`. diff --git a/.agents/skills/repo-policy-selector/policy-library/catalog.yaml b/.agents/skills/repo-policy-selector/policy-library/catalog.yaml index 2096f74..3a5a99a 100644 --- a/.agents/skills/repo-policy-selector/policy-library/catalog.yaml +++ b/.agents/skills/repo-policy-selector/policy-library/catalog.yaml @@ -41,6 +41,21 @@ modules: - id: active-lane-coordination path: modules/active-lane-coordination.md tags: [git, planning, worktrees, coordination] + - id: work-item-traceability + path: modules/work-item-traceability.md + tags: [issues, planning, traceability, flow, governance] + - id: collaborative-development-workflow + path: modules/collaborative-development-workflow.md + tags: [collaboration, contributors, pull-requests, git, deployment] + - id: forge-issue-reporting + path: modules/forge-issue-reporting.md + tags: [issues, github, gitlab, reporting, governance] + - id: github-issue-operations + path: modules/github-issue-operations.md + tags: [github, issues, labels, permissions, security] + - id: gitlab-issue-operations + path: modules/gitlab-issue-operations.md + tags: [gitlab, glab, issues, labels, permissions] - id: commit-history-discipline path: modules/commit-history-discipline.md tags: [git, commits, history, review] @@ -80,6 +95,9 @@ modules: - id: subagent-workflow-optimization path: modules/subagent-workflow-optimization.md tags: [agents, delegation, subagents, optimization] + - id: model-selection-and-calibration + path: modules/model-selection-and-calibration.md + tags: [agents, models, calibration, allocation, evaluation] - id: subagent-runtime-governance path: modules/subagent-runtime-governance.md tags: [agents, subagents, runtime, governance] @@ -125,6 +143,9 @@ modules: - id: tenant-isolation-and-operator-state path: modules/tenant-isolation-and-operator-state.md tags: [tenant, runtime, isolation, operations] + - id: development-runtime-isolation + path: modules/development-runtime-isolation.md + tags: [runtime, development, isolation, environments, safety] - id: fieldwork-productization path: modules/fieldwork-productization.md tags: [fieldwork, productization, operations, migration] diff --git a/.agents/skills/repo-policy-selector/policy-library/modules/active-lane-coordination.md b/.agents/skills/repo-policy-selector/policy-library/modules/active-lane-coordination.md index f9d996b..7cee07b 100644 --- a/.agents/skills/repo-policy-selector/policy-library/modules/active-lane-coordination.md +++ b/.agents/skills/repo-policy-selector/policy-library/modules/active-lane-coordination.md @@ -14,16 +14,37 @@ tags: - Use this contract in repositories where several projects, agents, branches, or worktrees may remain active at once. Keep lighter repositories on proportional planning and Git policy without requiring a lane catalog. - Keep a compact machine-readable active-lane catalog on the canonical default branch, normally `docs/dev/active-lanes.yaml`. A documented equivalent path is allowed. - Treat the catalog as a discovery projection. A roadmap owns priority, a branch-local plan owns execution detail, a runbook owns chronological history, review tooling owns review state, and Git refs plus receipts prove custody and integration. -- Give each lane one stable id and one branch owner. Record its objective, plan path and source ref, branch, target, plan state, custody state, published checkpoint, remote ref, integration method, dependencies, overlaps, reconciliation date, and any blocker or disposition. +- Give each lane one stable id and one branch owner. Record its objective, work-item locators when work-item tracking is adopted, plan path and source ref, branch, target, plan state, custody state, published checkpoint, remote ref, integration method, dependencies, overlaps, reconciliation date, and any blocker or disposition. +- Give each substantive lane one accountable execution owner. When several lanes + depend on shared schemas, roadmaps, catalogs, or other integration surfaces, + name one coordination owner for those surfaces instead of allowing every lane + to edit them independently. A person or session may fill more than one role + when the portfolio is small and the ownership remains unambiguous. - Keep plan outcome state separate from Git custody state. Use a small plan vocabulary such as `PLANNED`, `OPEN`, `BLOCKED`, `CLOSED`, and `CANCELLED`, and a custody vocabulary such as `ACTIVE_WORKTREE`, `PAUSED_REF`, `INTEGRATION_READY`, `INTEGRATED`, `ARCHIVED`, and `DISCARD_APPROVED`. - Keep detailed plans with their topic branches. Expose deterministic metadata for lane, state, branch, target, integration method, dependencies, overlaps, and base or checkpoint evidence so an auditor can read it from an explicit ref without checkout. - Do not put absolute worktree paths, ephemeral agent identifiers, secrets, tenant data, or private runtime details in the shared catalog. Derive local worktree locations during reconciliation. +- Keep work-item locators tool-neutral and non-secret, for example a repository + issue number, a cross-repository `owner/repo#number`, or a stable governed + backlog identifier. Do not copy issue bodies or provider state into the lane + catalog. - Reconcile the catalog against current worktrees, bounded local and remote refs, branch-local plan metadata, checkpoint SHAs, target ancestry, receipts, dependencies, and overlap before planning, handoff, integration, or cleanup decisions. Prefer catalog-only discovery when the catalog is the complete authorized population; use exact repeated branch selectors for bounded unregistered-lane discovery. Prefix discovery is an explicit broader survey and should not be the default in repositories with large historical branch namespaces. - For active worktree custody, classify equal, local-ahead, remote-ahead, and diverged local/remote tips explicitly. Local-ahead, remote-ahead, and diverged state fail closed until the lane owner reconciles and publishes the intended checkpoint. +- Treat worktree presence as workstation-local evidence. When a cataloged + `ACTIVE_WORKTREE` lane has no local branch or checkout but its exact remote + ref and checkpoint agree, classify it as healthy remote-active custody; + require a local checkout only when evaluating local worktree claims. - Fetching is a caller-controlled operation. A lane auditor must remain read-only and must not fetch, merge, rebase, push, delete refs, remove worktrees, edit plans, or infer authority from a clean report. - Register normal work before parallel execution begins. An urgent lane may start first only when delay creates greater risk; register and publish its first recoverable checkpoint at the earliest safe boundary. +- After existing catalogs are migrated, enable required work-item validation so + every substantive active lane has at least one non-empty locator. Treat the + catalog link as traceability only; current tracker state still comes from the + governed tracker. - Do not silently resolve catalog conflicts. Duplicate lane ids, two lanes claiming one branch, missing custody, stale checkpoints, active local/remote mismatch, plan/catalog drift, and unresolved overlaps fail closed until reconciled. - Keep the catalog current through the repository's protected-default-branch workflow. A lane branch may propose its own registration, but it is not globally discoverable until that projection lands on the configured default ref. +- Reconcile worktree lifecycle with lane state. An integrated, archived, paused, + or handed-off branch does not justify an indefinitely registered checkout; + close an unneeded clean worktree after custody is verified and update the lane + projection without erasing the branch's disposition. ## Adoption Notes diff --git a/.agents/skills/repo-policy-selector/policy-library/modules/architecture-guardrails.md b/.agents/skills/repo-policy-selector/policy-library/modules/architecture-guardrails.md index 0af425a..b6dc90b 100644 --- a/.agents/skills/repo-policy-selector/policy-library/modules/architecture-guardrails.md +++ b/.agents/skills/repo-policy-selector/policy-library/modules/architecture-guardrails.md @@ -1,7 +1,7 @@ --- id: architecture-guardrails title: Architecture Guardrails -summary: Keep changes aligned with the live architecture and avoid unplanned surface expansion. +summary: Keep changes aligned with explicit boundaries and prevent unplanned repository and dependency sprawl. tags: - architecture - boundaries @@ -16,6 +16,32 @@ tags: - Prefer tightening semantics and ownership boundaries over widening the surface area opportunistically. - Keep provider-specific or deployment-specific heuristics at the narrowest layer that can own them cleanly. - When a change would blur current architecture boundaries, stop and update the governing plan before proceeding. +- Keep a discoverable current architecture map at the level needed to answer + which top-level areas exist, what each owns, and which dependency directions + are allowed. Prefer generated dependency evidence where practical, but keep + ownership and intent human-reviewable. +- Give each top-level package, service, application, or durable workflow one + primary responsibility and an explicit owner. New top-level areas require a + stated responsibility, dependency position, owner, and retirement condition; + directory creation alone is not an architecture decision. +- Record a short architecture decision record for changes that materially alter + system structure, key quality attributes, ownership, or a hard-to-reverse + dependency. Capture context, decision, alternatives, consequences, status, + and supersession; do not turn ordinary implementation detail into ADR churn. +- Keep accepted decision records immutable apart from factual corrections. + Supersede a changed decision with a linked successor so the reason for the + old structure remains recoverable. +- Prefer small, independently valid structural changes. Separate broad moves, + renames, dependency inversions, or mechanical refactors from behavior changes + when that makes review, rollback, and overlap reconciliation clearer. +- Treat repeated cross-boundary edits, circular dependencies, duplicate + responsibilities, import-layer violations, orphaned entrypoints, and large + change fan-out as structure-health signals. Diagnose ownership and boundary + causes before adding another facade, shared helper, or top-level directory. +- Define deprecation and deletion paths for superseded modules, flags, + workflows, and compatibility layers. A new abstraction is incomplete when it + leaves the old path active without an owner, consumer inventory, or retirement + disposition. ## Adoption Notes diff --git a/.agents/skills/repo-policy-selector/policy-library/modules/collaborative-development-workflow.md b/.agents/skills/repo-policy-selector/policy-library/modules/collaborative-development-workflow.md new file mode 100644 index 0000000..e0e5a58 --- /dev/null +++ b/.agents/skills/repo-policy-selector/policy-library/modules/collaborative-development-workflow.md @@ -0,0 +1,102 @@ +--- +id: collaborative-development-workflow +title: Collaborative Development Workflow +summary: Keep multi-user agent-assisted development visible in the shared forge while allowing accountable authors to self-check and merge routine work. +tags: + - collaboration + - contributors + - pull-requests + - git + - deployment +--- + +## Policy + +- Use this contract when more than one human contributor, or agents acting for + different humans, can change the same repository. The shared forge is the + coordination and source-custody system of record; chat and local agent plans + are supporting context, not substitutes for shared state. +- Configure one canonical remote integration branch for the repository. Normal + work starts from a freshly fetched readback of that ref, and only a merged + pull request may modify it. Do not push directly to the canonical branch. +- Give every substantive change one accountable human owner. An agent may plan, + implement, test, and self-review within delegated scope, but it does not + silently widen the objective, resolve a disagreement between humans, or + acquire deployment authority merely by performing the work. +- Before substantive implementation, search open work items and pull requests + for the problem and affected surface. Reuse an existing item when it already + represents the intent; otherwise create one in the repository's configured + tracker. Claim it with an accountable owner and an `in progress` state before + editing so another contributor or agent can discover the active lane. +- Keep the work-item projection concise. It must identify the objective, + accountable owner, current status, affected surface, risk or live effect, + branch, durable plan locator when one exists, known overlaps or dependencies, + and pull request when opened. Update material scope or ownership changes; + do not mirror the full implementation plan into tracker prose. +- Use one scoped branch per independently mergeable intent. Put simultaneous + work in separate physical checkouts or worktrees, and never let two people or + agent sessions edit the same checkout. Several small related edits may share + one work item and branch; this policy does not require an issue per commit. +- If discovery shows another active item, branch, or pull request touching the + same behavior, do not begin a competing implementation silently. Link the + overlap and let the accountable humans choose ownership, sequencing, or an + integration strategy. Unrelated work may continue. +- Push coherent checkpoints soon enough for shared discovery, recovery, and CI. + Before handoff or review, verify that the intended remote branch resolves to + the reported commit. A local branch, clean worktree, chat message, plan, or + live runtime does not prove shared custody. +- Every change to the canonical branch goes through a pull request linked to its + work item. Keep the description short: objective, affected surface, risk or + deployment effect, durable plan locator when applicable, validation evidence, + and unresolved overlap. Draft status may communicate incomplete work but does + not excuse stale remote custody or hidden scope. +- A second human review is optional unless another repository, security, + financial, legal, or live-effect rule explicitly requires it. The accountable + author may self-check and merge their own pull request after CI and applicable + checks pass. Self-check means inspecting the published diff, validation, base, + scope, and deployment effect; it is not represented as a separate GitHub + approval by the author. +- Close the work item only when its stated outcome is complete. Link the merged + pull request and record any deferred work separately. A closed issue, merged + pull request, successful test, deployment, or observed outcome proves only its + own boundary. +- Production deployment is allowed only from an exact commit on the configured + canonical remote branch. Fetch the remote immediately before release, resolve + the candidate SHA from the remote ref rather than a local branch, and verify + the commit entered the branch through a merged pull request. Normal deployment + uses the current verified remote tip; an authorized rollback may select a + previously merged commit on that branch with an incident or rollback record. +- Record the repository, canonical remote ref, exact commit, actor, target + environment, validation, authorization, and post-deploy readback. Deployment + automation must fail closed when the candidate is local-only, the remote + readback differs, pull-request provenance is absent, or required checks fail. +- Treat direct edits to a live CMS, commerce system, asset store, or other + database-backed surface as state changes that still require a reproducible + migration, export, or reconciliation artifact when the repository owns that + state. Do not let live state become the only surviving copy of a change. +- Run urgent fixes through the same accelerated issue, branch, pull-request, + merge, and canonical-branch deployment path. Urgency may shorten discussion + and validation to the safest relevant subset, but it does not authorize an + unpublished deployment. If the forge path is unavailable, apply separately + authorized operational mitigation and preserve evidence; reconcile source + before resuming normal deployment. +- Use one tracker as the canonical coordination ledger. Do not duplicate active + state in GitHub Issues and Jira unless a documented cross-system need and a + reliable ownership or synchronization rule justify the second system. + +## Adoption Notes + +For a small team, the minimum useful flow is: + +1. search or open the work item and claim it; +2. create a scoped branch in an isolated checkout; +3. publish the branch and link its pull request; +4. run CI and self-check the published diff; +5. merge without waiting for peer review when no separate approval applies; +6. deploy only the exact verified canonical remote commit. + +Use repository-local guidance for the canonical ref, contributor names, branch +format, issue states and labels, plan locations, CI requirements, deployment +commands, high-risk approval rules, and emergency contacts. Where provider +branch protection is unavailable, enforce the same contract through contributor +policy and a fail-closed deployment preflight until provider enforcement exists. diff --git a/.agents/skills/repo-policy-selector/policy-library/modules/development-runtime-isolation.md b/.agents/skills/repo-policy-selector/policy-library/modules/development-runtime-isolation.md new file mode 100644 index 0000000..c82dd8e --- /dev/null +++ b/.agents/skills/repo-policy-selector/policy-library/modules/development-runtime-isolation.md @@ -0,0 +1,45 @@ +--- +id: development-runtime-isolation +title: Development Runtime Isolation +summary: Isolate concurrent development runtimes from production and from one another, and keep live external effects separately governed. +tags: + - runtime + - development + - isolation + - environments + - safety +--- + +## Policy + +- Treat production, staging, and development runtimes as distinct environment + identities. A Git branch or local configuration change does not itself create + an isolated environment or authorize promotion between them. +- Give each concurrently executing development lane its own runtime identity + when it needs service execution. Isolate configuration roots, databases, + writable data, ports or sockets, logs, process or service identities, and + other mutable resources that could collide with another lane or production. +- Bind runtime identity to the lane and exact source checkpoint through a + readable health, status, or startup record. Do not claim isolation from naming + convention or intended configuration alone. +- Fail closed when resource separation cannot be proved. Do not inherit + production credentials, schedules, browser profiles, writable data, or other + effect-bearing state into a development runtime merely for convenience. +- Prefer provider-free fixtures and disposable development data for ordinary + validation. Treat authenticated provider calls, browser sessions, shared + hardware, rate limits, and other singleton resources as serialized unless an + explicit operating contract proves independent identities and effect + boundaries. +- Keep runtime provisioning and teardown exact and reversible. Teardown must + target only the named lane runtime and must verify that production and other + active lanes remain intact. +- Promote from lane evidence to integration and then through the repository's + staging and production gates. A passing local suite or healthy development + runtime proves neither staging acceptance nor production readiness. + +## Adoption Notes + +Use this module when multiple branches or agents may run services concurrently, +or when development execution could otherwise reuse production-like state. Keep +provider names, credential stores, ports, service commands, environment paths, +and deployment topology in repository-local guidance. diff --git a/.agents/skills/repo-policy-selector/policy-library/modules/forge-issue-reporting.md b/.agents/skills/repo-policy-selector/policy-library/modules/forge-issue-reporting.md new file mode 100644 index 0000000..7cb1d3c --- /dev/null +++ b/.agents/skills/repo-policy-selector/policy-library/modules/forge-issue-reporting.md @@ -0,0 +1,70 @@ +--- +id: forge-issue-reporting +title: Forge Issue Reporting +summary: Report work through exact, permission-aware forge targets with idempotency, governed metadata, and verified receipts. +tags: + - issues + - github + - gitlab + - reporting + - governance +--- + +## Policy + +- Use this module with `work-item-traceability`. The tracker owns intake, + discussion, priority, and dependencies; plans, lanes, review, Git, tests, + deploy readback, and receipts retain their separate authority. +- Resolve every operation to an explicit forge, hostname, and canonical + repository or project path. Do not infer an external write target solely from + the current directory, a default remote, a similarly named fork, or a prior + operation. +- Require all of these gates before a provider mutation: + - current operator authority for the exact action + - an allowlisted target and action + - compliance with the target's contribution, issue-template, security, and + disclosure rules + - current provider capability for the authenticated actor or app + - a postcondition that can be read back without repeating the mutation +- Treat authentication, repository ownership, organization membership, and a + provider role as capability evidence, not operator intent. Read or research + authority does not imply create, comment, edit, label, assign, close, reopen, + transfer, board, or project authority. +- Keep a non-secret repo-local target registry with the forge, host, canonical + path, owned-or-permissioned relationship, allowed actions, security route, + and normalized label mappings. Verify current provider state instead of + treating the registry as proof that access still exists. +- Classify a proposed report before creation, such as defect, feature, + operational incident, governance gap, or security report. Include bounded + expected-versus-observed evidence, impact, reproduction context, relevant + version or environment, and explicit uncertainty. Redact credentials, + private customer data, and unnecessary personal information. +- Map normalized label intent through the target registry to exact provider + labels. Unknown mappings and missing provider labels fail closed. Applying an + existing label, creating a label, changing label taxonomy, and applying a + group- or organization-scoped label are separate actions and authority gates. +- Preserve the target repository's labels and workflow vocabulary. Do not + silently substitute a similar label, create a missing label, or impose a + source repository's taxonomy on a permissioned target. +- Search for duplicates before creating an issue. Carry a stable, non-secret + idempotency marker in the proposed body or other supported metadata. After an + ambiguous provider response, search and read back the marker before retrying. +- Default permissioned targets to the least-invasive allowed behavior. Do not + assign maintainers, change labels or milestones, add planning metadata, + transfer, close, or reopen merely because the authenticated identity can. +- Treat security-sensitive content as a separate disclosure workflow. Use the + target's private vulnerability or security-reporting route and never publish + vulnerability details in a normal public issue as a fallback. +- Record a receipt for each mutation with authenticated actor, host, canonical + locator and URL, action, idempotency key, timestamp, relevant before-and-after + state, and post-write readback. A successful request without target readback + is not a verified effect. +- Keep issue closure distinct from implementation, validation, integration, + deployment, and cleanup. Use closing references only when the target workflow + permits them and the governing completion evidence will exist. + +## Adoption Notes + +Adopt the provider adapter for each forge in use. Keep provider commands, +target lists, label names, and operational routing in repo-local configuration +instead of copying them into this shared module. diff --git a/.agents/skills/repo-policy-selector/policy-library/modules/git-worktree-hygiene.md b/.agents/skills/repo-policy-selector/policy-library/modules/git-worktree-hygiene.md index 31e6bea..11784fc 100644 --- a/.agents/skills/repo-policy-selector/policy-library/modules/git-worktree-hygiene.md +++ b/.agents/skills/repo-policy-selector/policy-library/modules/git-worktree-hygiene.md @@ -13,13 +13,29 @@ tags: - Start branch-sensitive work by checking `git status`. - Inventory all registered worktrees with `git worktree list --porcelain` before creating, closing, pruning, or reassigning one; the current checkout alone is not the repository topology. +- Before creating a worktree, decide whether an existing clean checkout already + owns the intended branch and lane. Continue in the correct existing worktree + when it is safe; do not create duplicate checkouts merely to avoid orienting + to current custody. +- Create a new worktree only when the work needs an isolated branch, a separate + concurrent checkout, or continuity beyond the current session. Give it one + clear branch and purpose, and do not repurpose another active lane's checkout + by switching its branch or mixing in unrelated work. - Treat pre-existing dirty state as a real constraint. - Keep one bounded branch or worktree scope per execution slice or roadmap lane, consistent with the repo's documented integration model. - When parallel work is needed, prefer `git worktree` over a second full clone. - Do not call work merge-ready while the intended changes are still uncommitted. - Treat the worktree as a checkout, the branch or detached commit as local custody, and a verified remote or archive ref as shared custody. Removing a worktree does not preserve uncommitted changes and does not prove the commits remain discoverable. - Before removing a worktree, require a clean status, a named branch or explicitly preserved detached commit, an exact checkpoint SHA, and verified durable custody on the intended remote ref or on matching local and remote archive refs. +- Close a worktree promptly when its branch is integrated, its work is durably + handed off without needing the checkout, or its preserved branch is paused or + archived. Do not accumulate idle worktrees as informal reminders or confuse a + retained branch with a need to retain its checkout. - Normal closure uses `git worktree remove` without `--force`. Forced removal is exceptional recovery work: first inventory the exact path, preserve any recoverable diff and commit, establish a durable ref, record the reason, and verify the retained SHA. +- After removal, verify the exact path is absent from the registered worktree + inventory. Prune only stale administrative entries whose checkout absence and + branch custody have been established; pruning is not a substitute for closing + a live worktree deliberately. - Do not delete an unmerged branch merely because its worktree is gone. Prove integration, archival, or explicit discard approval separately. - If overlapping dirty work exists across branches or worktrees, open a reconciliation step rather than calling it a normal merge. - Keep branch scope narrow and avoid mixing unrelated lanes unless the active slice requires it. diff --git a/.agents/skills/repo-policy-selector/policy-library/modules/github-issue-operations.md b/.agents/skills/repo-policy-selector/policy-library/modules/github-issue-operations.md new file mode 100644 index 0000000..099667b --- /dev/null +++ b/.agents/skills/repo-policy-selector/policy-library/modules/github-issue-operations.md @@ -0,0 +1,47 @@ +--- +id: github-issue-operations +title: GitHub Issue Operations +summary: Apply forge issue-reporting controls to GitHub roles, repositories, issue forms, labels, and security routes. +tags: + - github + - issues + - labels + - permissions + - security +--- + +## Policy + +- Use this adapter with `forge-issue-reporting` and identify targets as an + explicit GitHub hostname plus `OWNER/REPO`. Distinguish an owned repository, + an owned fork, and a permissioned upstream before choosing the report target. +- Preflight the authenticated hostname and actor, canonical `nameWithOwner`, + viewer permission, archive state, Issues availability, fork parent, security + policy, issue templates, and relevant existing labels with read-only `gh` or + API calls. +- Evaluate the requested action against the current GitHub role and token or + app permissions. Opening an issue, applying an existing label, creating a + label, assigning, changing a milestone or Project, closing, and transferring + are distinct capabilities. +- Read the target's `CONTRIBUTING.md`, `SECURITY.md`, issue forms, issue-template + chooser, and contact links before drafting. Required target fields remain + required when the issue is created through an API rather than the web form. +- Resolve every normalized label to one exact existing repository label before + creation. A target may authorize application of existing labels without + authorizing label creation or taxonomy changes; never collapse those actions. +- Treat issue types, Projects, milestones, sub-issues, dependencies, and + assignees as optional GitHub extensions. Creation authority does not imply + authority to populate them. +- Use GitHub private vulnerability reporting or the repository's declared + private security route for vulnerability details. A public issue may ask for + a contact route only when the repository directs reporters to do so and must + not contain the vulnerability. +- Bind mutation receipts to the returned repository and issue number, canonical + URL, actor, and read-back state. On timeout or rate-limit ambiguity, search + for the idempotency marker before issuing another create request. + +## Adoption Notes + +Prefer explicit `--hostname` or host-qualified targeting for GitHub Enterprise. +Keep fine-grained token scopes least-privilege and do not print tokens in +diagnostic output. diff --git a/.agents/skills/repo-policy-selector/policy-library/modules/gitlab-issue-operations.md b/.agents/skills/repo-policy-selector/policy-library/modules/gitlab-issue-operations.md new file mode 100644 index 0000000..8eaafef --- /dev/null +++ b/.agents/skills/repo-policy-selector/policy-library/modules/gitlab-issue-operations.md @@ -0,0 +1,50 @@ +--- +id: gitlab-issue-operations +title: GitLab Issue Operations +summary: Apply forge issue-reporting controls to GitLab projects, roles, work items, labels, templates, and confidential reporting. +tags: + - gitlab + - glab + - issues + - labels + - permissions +--- + +## Policy + +- Use this adapter with `forge-issue-reporting` and identify targets as an + explicit GitLab hostname plus the full `GROUP/SUBGROUP/PROJECT` path. Preserve + both the project id and project-local issue IID in receipts when available. +- Preflight the authenticated hostname and actor, canonical + `path_with_namespace`, archive state, issue access level, effective + project-or-group role, project URL, and relevant labels with read-only `glab` + or API calls. Support GitLab.com and explicitly allowlisted self-managed + hosts without treating them as interchangeable. +- Evaluate each action against current GitLab permissions. Guest issue creation + does not imply authority to edit general issue metadata, manage boards or + milestones, create labels, move issues, or operate on another reporter's + issue. +- Read the target's contribution and security guidance plus + `.gitlab/issue_templates/` before drafting. Because `glab issue create + --template` loads from the local checkout, prove that the local template + belongs to the exact target revision or fetch the target template explicitly. +- Resolve normalized label intent to an exact existing project or group label. + Record the resolved scope, and do not create or modify group-scoped labels as + a side effect of reporting to one project. +- Treat boards, milestones, epics, tasks, work-item types, weights, time + tracking, linked issues, and linked merge requests as optional GitLab + extensions with separate authority. Do not force them into GitHub-equivalent + semantics. +- Treat `confidential` as an explicit visibility choice, not an automatic + security-disclosure guarantee. Follow the target's security workflow and + verify who can read the confidential issue before submitting sensitive + details. +- Bind mutation receipts to the returned project id, issue IID, canonical URL, + actor, and read-back state. On timeout or API ambiguity, search for the + idempotency marker before issuing another create request. + +## Adoption Notes + +Use an explicit `--hostname` for authentication and API discovery. Avoid +commands that display tokens, and URL-encode nested project paths for REST API +calls. diff --git a/.agents/skills/repo-policy-selector/policy-library/modules/goal-execution-governance.md b/.agents/skills/repo-policy-selector/policy-library/modules/goal-execution-governance.md index fe9a7b8..d425a5c 100644 --- a/.agents/skills/repo-policy-selector/policy-library/modules/goal-execution-governance.md +++ b/.agents/skills/repo-policy-selector/policy-library/modules/goal-execution-governance.md @@ -64,10 +64,20 @@ tags: Repo-local defaults may supply these values; an individual packet need not restate them, and missing packet metadata does not block a first safe attempt. Bounds prevent runaway work; they are not consumable approval tokens. When a - local bound is reached, first reassess, split the unit, change tactics, or - continue a different safe ready unit under the same authority. Escalate only - when no meaningful safe action remains or an exact action-specific gate is - reached. + local bound is reached, reassess, split the unit, change tactics, or continue + a different safe ready unit only within the remaining cumulative milestone + allowance. Escalate only when no meaningful safe action remains or an exact + action-specific gate is reached. +- Before the first material action in a multi-packet proof, reconcile the + remaining material allowance against every packet required for acceptance, + including baseline and candidate comparison. If the allowance cannot cover + the minimum complete proof, reframe before execution or record the scoped + block; do not spend the last attempt on a packet that cannot satisfy the + unchanged objective even if it passes. +- A failed sidecar, observer, maintenance check, or secondary verdict invalidates + completed primary evidence only when the plan records a causal path from that + failure to the evidence's identity, timing, correctness, or resource validity. + Otherwise retain the primary evidence and report the secondary axis separately. - Keep one primary orchestrator responsible for authority, the critical path, work-unit selection, integration, progress classification, and the final completion claim. @@ -108,6 +118,11 @@ tags: is complete, cancelled, or disproven. Repeated hardening or no-progress first requires a local tactic change or bounded reframe; it does not automatically require operator approval. +- Set an evidence deadline for the first artifact that can answer the goal's + primary question. When that deadline passes, simplify or split the packet + before adding more instrumentation, documentation, review, or governance. + Supporting work that does not improve the primary evidence by the configured + checkpoint is overhead and must not silently become the new objective. - Continue automatically whenever a useful in-scope action is available and no exact applicable gate blocks it. A recent checkpoint may support that choice, but creating another checkpoint is not a prerequisite for taking an obvious @@ -155,3 +170,4 @@ Recommended companion modules: - `subagent-workflow-optimization` - `validation-and-handoff` - `commit-and-push-cadence` +- `model-selection-and-calibration` diff --git a/.agents/skills/repo-policy-selector/policy-library/modules/model-selection-and-calibration.md b/.agents/skills/repo-policy-selector/policy-library/modules/model-selection-and-calibration.md new file mode 100644 index 0000000..90371f1 --- /dev/null +++ b/.agents/skills/repo-policy-selector/policy-library/modules/model-selection-and-calibration.md @@ -0,0 +1,39 @@ +--- +id: model-selection-and-calibration +title: Model Selection And Calibration +summary: Route agent work by demonstrated task fit and total accepted-outcome cost, then calibrate and revise defaults with bounded, comparable evidence. +tags: + - agents + - models + - calibration + - allocation + - evaluation +--- + +## Policy + +- Select model, reasoning effort, context scope, and delegation topology together to minimize total allocation consumed per accepted milestone while meeting required correctness, safety, and delivery-time constraints. Count orchestration, workers, failed attempts, evaluation, repair, and integration. Treat token counts and API-price estimates as labeled proxies when measured allocation is unavailable. +- Define the milestone before routing substantial work. It must describe usable behavior at its intended integration boundary and a stable acceptance check. Worker completion, prerequisite repair, passing unrelated tests, and document volume do not establish milestone completion. +- Keep a dated repo-local mapping from task tiers to available model and reasoning configurations. Start routine work on the calibrated economical default; use a cheaper tier for mechanical, readily verified work; route material policy design, architectural tradeoffs, or difficult consequential reasoning directly to a designated specialist tier when justified. Select reasoning effort separately. Model novelty and task length alone do not justify an upgrade. +- Reassess routing at non-trivial task start, material replanning, failed acceptance, conflicting evidence, and the configured no-progress interval. Reassessment is a brief primary-agent decision within the existing checkpoint; it does not itself require another model call. Distinguish reasoning limits from missing inputs, authority, unavailable tools, and environmental failure. +- Escalate only when stronger reasoning is likely to resolve a specific obstacle. Delegate the smallest useful decision or diagnostic task with evidence, attempted approaches, acceptance check, write scope, remaining budget, and stop condition. Return ordinary execution to its configured default when that task concludes. Record requested and runtime-reported effective model and effort; report unknown effective configuration explicitly. +- Model upgrades, reasoning changes, prompt edits, tool substitutions, successor plans, and worker replacement inherit cumulative milestone accounting. Reassessment intervals are not renewable budgets. Exhaustion cannot be bypassed by renaming an approach or opening another worker. +- Delegate when expected gains in expertise, independence, context isolation, or elapsed time justify setup and reconciliation cost. Do deterministic mechanical work with existing tools before purchasing model work for it. A compact specialist brief is preferred when full-history inheritance adds no value. The primary integrates returned evidence without repeating the worker's investigation. +- Route polling, hashing, schema checks, counter reconciliation, deterministic + test execution, and exact structured transformations to tools before any + model. When judgment-light work still needs a model, use the calibrated + economical tier with a narrow input packet, structured output, deterministic + verifier, attempt/time bound, and no authority to change scope or acceptance. +- Retain causality decisions, safety or authority changes, material experiment + design, architectural tradeoffs, and the final acceptance claim with the + primary or a justified specialist tier. Do not pay a stronger model to repeat + verified mechanical work returned by a cheaper worker. +- Calibrate complete workflows, not isolated responses. Before starting, freeze representative inputs, acceptance checks, baseline and candidate configurations, quality floor, sample size, retry allowance, resource ceiling, evaluator, and promotion/stop rules. Include failed and timed-out attempts, retain difficult regressions, and use held-out examples when tuning on earlier samples. +- Record the sample/date, workload identity, model and effort, context and tools, topology, accepted count and denominator, defects, interventions, elapsed time, cumulative agent effort, and measured allocation or labeled proxy. Keep elapsed wall time separate from summed worker effort. Do not attribute shared-account consumption to one configuration when concurrent use prevents attribution. +- Stop calibration at its predeclared sample or resource ceiling, or at a defined critical-quality failure. Small samples yield provisional routing only. Do not enlarge the experiment, weaken acceptance, or retry away failures to obtain a favorable result. +- Promote the least costly configuration that meets frozen quality and delivery requirements. An expensive configuration must show a task-relevant benefit that justifies its added consumption. Revert a regressed default promptly and retain specialist use only where justified. Recalibrate after material configuration changes or repeated observed failures with a bounded scheduled sample, not before every task. +- Deterministic audits establish wiring and record validity; they do not prove model quality, allocation savings, or runtime stopping. Repos that operate a controller must test aggregate counters and stop behavior at its real transition boundary. Policy-only adoption must identify calibration and runtime enforcement as unverified. + +## Adoption Notes + +Use this module for any repo where agents choose among model or reasoning configurations. Trivial one-step work needs no durable routing or calibration record. Keep provider names, prices, available efforts, configuration syntax, exact intervals, and calibration sample sizes in repo-local policy. diff --git a/.agents/skills/repo-policy-selector/policy-library/modules/multi-agent-reconciliation.md b/.agents/skills/repo-policy-selector/policy-library/modules/multi-agent-reconciliation.md index 1b081f7..35f4779 100644 --- a/.agents/skills/repo-policy-selector/policy-library/modules/multi-agent-reconciliation.md +++ b/.agents/skills/repo-policy-selector/policy-library/modules/multi-agent-reconciliation.md @@ -13,6 +13,10 @@ tags: - Treat overlapping agent changes as reconciliation work, not as normal silent merge cleanup. - Prefer disjoint write scopes before parallel execution, and record ownership when multiple agents are active. +- Declare shared integration surfaces before concurrent work and give each one a + single coordination owner. Other lanes may propose changes, but they must not + independently rewrite shared schemas, coordination catalogs, or equivalent + cross-lane authorities without explicit reconciliation. - Reconcile against the default-branch active-lane projection before assigning a new branch or worktree when the repository adopts that contract. Record dependencies and expected overlap before concurrent edits begin. - When integrating conflicting edits, inspect history directly rather than assuming the most recent edit is correct. - Use commit history, branch context, and `git blame` or equivalent file-history inspection when authorship and intent need to be reconstructed. @@ -22,6 +26,9 @@ tags: - If delegated outputs conflict, inspect logs or transcripts before deciding which result to keep. - Do not treat summarized announce messages as sufficient reconciliation evidence for high-risk changes. - Do not rewrite another agent's work without first understanding the intended change surface. +- Keep accountability with the lane owner even when agents or subagents perform + bounded work. Session topology is an implementation choice; ownership, + evidence, and reconciliation must remain clear across session replacement. - If a collision reveals weak lane boundaries, update the plan or policy so the same overlap is less likely next time. - Do not treat an agent session ending as Git closure. The responsible owner must leave a clean published checkpoint and an explicit custody or integration disposition before its worktree can be removed safely. diff --git a/.agents/skills/repo-policy-selector/policy-library/modules/parallel-plan-design.md b/.agents/skills/repo-policy-selector/policy-library/modules/parallel-plan-design.md index 1a07fdd..8b50ba3 100644 --- a/.agents/skills/repo-policy-selector/policy-library/modules/parallel-plan-design.md +++ b/.agents/skills/repo-policy-selector/policy-library/modules/parallel-plan-design.md @@ -15,10 +15,15 @@ tags: - Keep the critical path visible so parallel work does not hide the real blocker. - Prefer plan slices that minimize cross-lane file overlap and reconciliation cost. - Call out integration points explicitly when multiple lanes must converge before completion. +- Define shared schemas, interfaces, and other cross-lane contracts before + dependent implementations fan out. If the contract is still unsettled, keep + that decision on the critical path or assign it one coordination owner rather + than letting parallel lanes establish competing authorities. - Express non-trivial execution as inspectable work units and dependency edges, including fan-out, join, review, retry, and terminal transitions. A table or plan section is sufficient; a graph framework is not required. - Do not open parallel lanes just because tools allow delegation; open them only when the work can move independently. +- This restriction governs parallel fan-out. A sequential bounded specialist consultation may still be justified under `model-selection-and-calibration` when it can resolve a critical-path decision. - If a lane becomes coordination-heavy, collapse it back into the critical path or redefine the lane boundary. - Declare the intended active-agent concurrency before spawning many subagents or parallel workers. - Cap active subagents per plan lane unless the repo explicitly optimizes for `max-dev-speed` and has strong reconciliation rules. @@ -28,9 +33,10 @@ tags: - Put a semantic exit condition and a hard bound on every review, retry, repair, or agent-handoff edge that can cycle back to prior work. - Reaching a local loop bound ends or reframes that loop; it does not create a - user-approval gate by itself. Continue another safe in-scope route when one is - available, and escalate only when no meaningful route remains or an exact - action-specific boundary requires a user decision. + user-approval gate by itself. Continue another safe in-scope route only within + the remaining cumulative milestone allowance, and escalate only when no + meaningful route remains or an exact action-specific boundary requires a user + decision. - When a work unit cannot be bounded or has too many coupled write surfaces, return it for split/reframe before spawning workers. diff --git a/.agents/skills/repo-policy-selector/policy-library/modules/planning-discipline.md b/.agents/skills/repo-policy-selector/policy-library/modules/planning-discipline.md index e5b10d3..76ea739 100644 --- a/.agents/skills/repo-policy-selector/policy-library/modules/planning-discipline.md +++ b/.agents/skills/repo-policy-selector/policy-library/modules/planning-discipline.md @@ -31,6 +31,10 @@ tags: - `CLOSED` - `CANCELLED` - Multi-track repositories may also use `BLOCKED`. Keep this outcome state separate from Git custody such as active worktree, paused ref, integration-ready, integrated, archived, or discard-approved. +- Keep authoritative plan metadata in a bounded header before the first + second-level section. Auditors must not infer state or lane metadata from + narrative body text, and should exclude recognized plan indexes such as + `README.md` and `INDEX.md` from plan-file validation. - For any plan in an active state such as `OPEN`, require a short `Current State` section that says what already exists and what still remains. - Use bounded plan artifacts with explicit scope, non-goals, acceptance criteria, and definition of done. - A plan organizes execution; it does not grant, consume, or renew authority. @@ -53,6 +57,14 @@ tags: change the outcome, scope, cost, or safety envelope. - Give each active execution packet one bounded outcome, owner, expected write surface, required inputs, validation evidence, and terminal condition. +- For measurement or optimization work, obtain and durably record the smallest + usable baseline before expanding into broad matrices, soak, maintenance, or + secondary acceptance checks. Order the packet so an early stop can still + answer the primary measurement question; breadth may follow after that + minimum evidence exists. +- Do not make independent acceptance axes one all-or-nothing packet by default. + State which checks govern measurement validity, correctness, resources, + maintenance, and evidence integrity, and how each result affects the others. - When active work lives off the default branch, keep execution detail in the branch-local plan and publish only a compact active-lane projection to the default branch. Plan closure does not by itself authorize branch deletion or worktree removal. - When a task is large enough to plan, explicitly separate: - parallelizable low-conflict tracks diff --git a/.agents/skills/repo-policy-selector/policy-library/modules/subagent-runtime-governance.md b/.agents/skills/repo-policy-selector/policy-library/modules/subagent-runtime-governance.md index 9bc7b5b..c5838ff 100644 --- a/.agents/skills/repo-policy-selector/policy-library/modules/subagent-runtime-governance.md +++ b/.agents/skills/repo-policy-selector/policy-library/modules/subagent-runtime-governance.md @@ -22,6 +22,7 @@ tags: - start and finish timestamps - runtime status - token, model, and cost metadata when available +- For model-routed work, preserve requested and runtime-reported effective model and reasoning effort, any fallback or escalation, and aggregate parent accounting when the runtime can expose it. - Define the expected announce or completion payload shape, including status, result, notes, and retrieval path for deeper inspection. - Make subagent tool access explicit. - Deny session-management, system, destructive, credential, and live-operation tools by default unless the subagent role requires them. @@ -36,6 +37,7 @@ tags: - Require timeouts or watchdog expectations for long-running subagent work. - Treat transcript cleanup, archive, or deletion as a retention decision rather than incidental cleanup. - Make cost and model defaults explicit for spawned work so low-risk sidecar work does not silently consume high-cost reasoning. +- Apply `model-selection-and-calibration` for task-tier selection and calibration; this module governs only runtime implementation and provenance. - Document known runtime limitations, such as best-effort announce delivery, process restarts, shared gateway resources, or missing context injection. - Keep runtime-specific command names, config syntax, and deployment assumptions repo-local unless they generalize across multiple subagent runtimes. diff --git a/.agents/skills/repo-policy-selector/policy-library/modules/subagent-workflow-optimization.md b/.agents/skills/repo-policy-selector/policy-library/modules/subagent-workflow-optimization.md index b0d4121..fa3bbd7 100644 --- a/.agents/skills/repo-policy-selector/policy-library/modules/subagent-workflow-optimization.md +++ b/.agents/skills/repo-policy-selector/policy-library/modules/subagent-workflow-optimization.md @@ -27,8 +27,13 @@ tags: - When delegation occurs, leave a durable receipt for consequential work: record the bounded lane, available agent/run/session handle, terminal status, evidence returned, and the primary agent's reconciliation decision. -- Keep urgent blocking work local when the next action depends directly on the answer. +- Keep urgent blocking work local when the next action depends directly on the answer, except for a bounded specialist consultation whose demonstrated capability is more likely to resolve that exact obstacle than local continuation. - Give delegated work explicit ownership, expected output, and write scope. +- Prefer economical workers for bounded inventory, normalization, receipt + drafting, manifest/counter checks, focused test triage, and other readily + verified transformations. Give them minimal context, a structured return + contract, deterministic checks, and no authority to alter goal scope, + acceptance, safety controls, or material budgets. - Prefer subagents for independent sidecar work, verification, or implementation slices with disjoint write sets. - Do not spawn parallel work that duplicates context loading or repeats the same exploration without a clear benefit. - Reuse prior agent context when the task is a continuation of the same bounded thread. @@ -51,6 +56,9 @@ tags: replacement, plan revisions, and successor packets so review discovery does not restart accidentally. - Keep final integration responsibility with the primary agent even when subagents perform part of the work. +- Integrate a verified worker result directly. Repeating its full investigation + with the primary defeats the routing decision; re-open only failed checks, + missing evidence, or consequential judgments reserved to the primary. - Be explicit about whether the repo optimizes for wall-clock speed, token efficiency, or a balance of the two. - Treat spawned subagents as asynchronous runtime artifacts, not just informal delegation. - Record the subagent run id, session id, transcript path, or equivalent handle when the runtime provides one. @@ -63,7 +71,7 @@ tags: - Set explicit timeout expectations for long-running, slow-tool, or uncertain delegated work. - Give each subagent a stop condition and require it to return partial evidence rather than self-extending into adjacent work when the bound is reached. -- Use lower-cost or lower-reasoning models for bounded sidecar work only when the quality risk is low; keep synthesis, architecture, and final integration on an appropriately capable model. +- Use `model-selection-and-calibration` for model, reasoning, escalation, and calibration decisions. Architecture decisions may use a bounded specialist; the primary retains integration and acceptance. - Treat subagent cleanup and transcript retention as deliberate choices when later evidence or reconciliation may matter. ## Adoption Notes diff --git a/.agents/skills/repo-policy-selector/policy-library/modules/validation-and-handoff.md b/.agents/skills/repo-policy-selector/policy-library/modules/validation-and-handoff.md index 8598d22..e3d2465 100644 --- a/.agents/skills/repo-policy-selector/policy-library/modules/validation-and-handoff.md +++ b/.agents/skills/repo-policy-selector/policy-library/modules/validation-and-handoff.md @@ -45,6 +45,14 @@ tags: specification or acceptance contract. Do not let a pass on one axis mask a failure on the other, and do not let the separation bypass primary-agent evidence review and disposition. +- For experiments and multi-axis acceptance, record verdicts independently for + the primary measurement, correctness, resources, maintenance, and evidence + integrity when those axes apply. Preserve every completed sample with its + identity and conditions even when the overall packet later stops. +- Define an invalidation map before execution: each stop predicate names the + samples and verdicts it can invalidate and the causal reason. A later failure + may mark the packet incomplete without erasing unaffected valid samples. + Never promote partial evidence into a complete acceptance claim. - Separate review modes. Use at most one broad fresh-context `drift_discovery` pass when observed drift, consequence, or uncertainty justifies it. After adjudication, use `closed_world` remediation diff --git a/.agents/skills/repo-policy-selector/policy-library/modules/work-item-traceability.md b/.agents/skills/repo-policy-selector/policy-library/modules/work-item-traceability.md new file mode 100644 index 0000000..33e1799 --- /dev/null +++ b/.agents/skills/repo-policy-selector/policy-library/modules/work-item-traceability.md @@ -0,0 +1,66 @@ +--- +id: work-item-traceability +title: Work Item Traceability +summary: Connect intake and priority to plans, active lanes, delivery changes, and evidence without confusing their authority. +tags: + - issues + - planning + - traceability + - flow + - governance +--- + +## Policy + +- Use the issue tracker or governed repo-local backlog as the intake, + discussion, prioritization, and dependency surface. Do not make it the sole + authority for detailed execution, Git custody, review, or completion proof. +- Keep authority roles explicit: + - roadmap or portfolio view: initiative priority and sequencing; + - work item: problem or outcome, owner, state, priority, and dependencies; + - bounded plan: execution scope, non-goals, acceptance criteria, and next action; + - active-lane catalog: concurrent branch and custody projection; + - review system: review state and findings; + - Git, tests, deployment readback, and receipts: implementation and completion evidence. +- Give every actionable work item a stable locator, one bounded outcome, + acceptance evidence, an owner or owning lane, current state, and explicit + blockers or dependencies. Preserve the locator across plans, branches, + changes, handoffs, and completion receipts. +- Use a small workflow vocabulary with explicit local mappings, normally + `TRIAGE`, `READY`, `IN_PROGRESS`, `BLOCKED`, `DONE`, and `CANCELLED`. Do not + infer implementation or integration from a label, assignee, comment, or + closed issue alone. +- Keep unrefined ideas in `TRIAGE`. Move an item to `READY` only when its + outcome and acceptance signal are understandable. Move it to `IN_PROGRESS` + only when an owner has accepted it and any substantive implementation lane is + discoverable. +- Set and periodically review an explicit work-in-process limit for substantive + `IN_PROGRESS` items or active lanes. When the limit is reached, finish, + unblock, split, cancel, or pause existing work before starting another lane; + do not raise the limit merely to hide contention. +- Split oversized work into outcome-oriented child items or plans and record + blocking relationships. Avoid parallel children whose expected write + surfaces substantially overlap unless reconciliation is part of the plan. +- Link delivery changes to their governing work item. Use automatic closing + only when integration into the intended target really satisfies the item; + otherwise use a non-closing reference and close from verified completion + evidence. +- Close an item only when its acceptance evidence is recorded and its + implementation, integration, rollout, cleanup, or non-code disposition is + truthful for the item type. Reopen or create a linked corrective item when + rollback, regression, or missing evidence invalidates that claim. +- Triage stale, duplicate, blocked, and abandoned items on a documented + cadence. Merge or link duplicates, retain decision context, and cancel work + explicitly rather than leaving an indefinitely active shadow backlog. + +## Adoption Notes + +Use this module when a repository has enough concurrent or deferred work that +chat, branch names, and plan files no longer provide a reliable intake and +priority surface. The contract is tracker-neutral: GitHub Issues and Projects, +another governed tracker, or a repo-local backlog may supply the work-item +locator. + +When `active-lane-coordination` is also adopted, record one or more work-item +locators in each substantive lane and enable required work-item validation in +the lane catalog after existing entries are migrated. diff --git a/.agents/skills/repo-policy-selector/references/forge-issue-targets.example.json b/.agents/skills/repo-policy-selector/references/forge-issue-targets.example.json new file mode 100644 index 0000000..fccd0e6 --- /dev/null +++ b/.agents/skills/repo-policy-selector/references/forge-issue-targets.example.json @@ -0,0 +1,47 @@ +{ + "schema_version": 1, + "targets": [ + { + "id": "owned-github-example", + "forge": "github", + "host": "github.com", + "repository": "example/owned-repository", + "relationship": "owned", + "allowed_actions": [ + "read", + "create", + "comment", + "apply_labels", + "close" + ], + "security_route": "private_vulnerability_reporting", + "label_map": { + "intent/defect": { + "provider_label": "bug" + }, + "priority/high": { + "provider_label": "priority: high" + } + } + }, + { + "id": "permissioned-gitlab-example", + "forge": "gitlab", + "host": "gitlab.example.com", + "repository": "group/subgroup/permissioned-project", + "relationship": "permissioned", + "allowed_actions": [ + "read", + "create", + "comment", + "apply_labels" + ], + "security_route": "confidential_issue", + "label_map": { + "intent/defect": { + "provider_label": "type::bug" + } + } + } + ] +} diff --git a/.agents/skills/repo-policy-selector/references/selection-workflow.md b/.agents/skills/repo-policy-selector/references/selection-workflow.md index 2276887..083140b 100644 --- a/.agents/skills/repo-policy-selector/references/selection-workflow.md +++ b/.agents/skills/repo-policy-selector/references/selection-workflow.md @@ -32,6 +32,11 @@ Read the target repo's: - roadmap / runbook / progress files if present - `docs/dev/plans/`, `docs/dev/notes/`, and `docs/dev/memories/` when present - obvious repo-shape signals such as `package.json`, `pyproject.toml`, `tests/`, `docs/dev/` +- issue, work-item, backlog, dependency, WIP-limit, and provider-neutral + traceability language +- GitHub or GitLab issue-reporting language, explicit owned or permissioned + targets, `gh` or `glab` usage, target registries, label mappings, and + provider-specific security routes Extract existing policy surfaces before recommending adoption changes. That extraction should inventory current policy-bearing files and classify them against the installed templates as: @@ -78,6 +83,40 @@ Treat `AGENTS.md` as a policy-loading contract, not just a static pointer: - whether the repo uses an installed durable graph-memory system and needs explicit read/write/cleanup discipline in addition to notes and memories - whether the repo has an indexed codegraph or `../codegraph` workflow that agents should consult before source-code edits or architecture analysis - whether the repo produces local artifacts, reports, review packets, rendered documents, or local builds that should be surfaced through a preview or approval service for human review +- whether the repo reports issues across GitHub, GitLab, enterprise or + self-managed hosts, owned forks, or permissioned upstreams +- whether multiple human contributors or agents acting for different people + share branches, worktrees, pull requests, reviews, or deployment authority + +For collaborative development, require both contributor signals and workflow +signals so incidental uses of words such as "collaborate" do not add team +ceremony to a single-maintainer repository. When selected, compose +`collaborative-development-workflow` with `work-item-traceability`, the complete +Git worktree/commit/branch/push set, and `validation-and-handoff`. Add +`active-lane-coordination` separately when the repository actually has several +concurrent off-main lanes. + +Add `development-runtime-isolation` when repository policy or operating docs +explicitly describe isolated development services, per-lane runtimes, or +concurrent lane execution that could share mutable runtime resources. Do not add +it merely because a repository has a local development command or mentions a +generic development environment. + +For forge issue reporting, prioritize signals such as: + +- GitHub Issues, GitLab Issues, `gh issue`, `glab issue`, or cross-forge + reporting language +- owned or permissioned repository targets and fork/upstream distinctions +- issue forms, templates, contribution guidance, or security-reporting routes +- label taxonomies, normalized label mappings, project or group labels, and + rules for label creation versus application +- idempotency markers, duplicate searches, ambiguous-write retries, and + provider receipts + +Recommend `forge-issue-reporting` plus `work-item-traceability` whenever these +signals are present. Add `github-issue-operations` only for GitHub signals and +`gitlab-issue-operations` only for GitLab or `glab` signals. Do not add either +adapter to a profile merely because a Git remote happens to use that provider. For course workspaces, prioritize operational signals over generic document-folder shape: - LMS config such as `canvas-cli.yml` diff --git a/.agents/skills/repo-policy-selector/release-manifest.json b/.agents/skills/repo-policy-selector/release-manifest.json index ae34c0b..224f8ce 100644 --- a/.agents/skills/repo-policy-selector/release-manifest.json +++ b/.agents/skills/repo-policy-selector/release-manifest.json @@ -1,16 +1,16 @@ { "bundle_name": "repo-policy-selector", - "bundle_version": "0.1.22", + "bundle_version": "0.1.26", "policy_library": { "catalog_path": "policy-library/catalog.yaml", - "content_sha256": "03d4cba5d1aaad7ca2805b847dd69083ebf4193b8812693ee7ea8d9079f56b42", + "content_sha256": "1129e37be16efbf571954f80d06ea7115eb8f244acbd327003abe37e2dadaa96", "relative_root": "policy-library", "schema_path": "policy-library/SCHEMA.md" }, - "release_ref": "v0.1.22", + "release_ref": "v0.1.26", "schema_version": 1, - "source_commit": "12a7f9fef466522e99be44d980c44a4ff056f540", - "source_ref": "12a7f9fef466522e99be44d980c44a4ff056f540", + "source_commit": "ead42f7d6932fedc9dea83111b234f954a6afa4d", + "source_ref": "ead42f7d6932fedc9dea83111b234f954a6afa4d", "source_repo_root": "/home/ecochran76/workspace.local/agent-policies", "source_tree_state": "clean-ref" } diff --git a/.agents/skills/repo-policy-selector/releases/v0.1.23.md b/.agents/skills/repo-policy-selector/releases/v0.1.23.md new file mode 100644 index 0000000..aed14c0 --- /dev/null +++ b/.agents/skills/repo-policy-selector/releases/v0.1.23.md @@ -0,0 +1,81 @@ +# Selector Release v0.1.23 + +- Bundle version: `0.1.23` +- Source commit: `94b6df499f7887097d0ccfa3664cc140e1b0f65b` +- Source ref: `94b6df499f7887097d0ccfa3664cc140e1b0f65b` +- Previous release: `v0.1.22` + +## Summary + +- `scripts`: 3 changed +- `modules`: 12 changed +- `profiles`: 4 changed +- `docs`: 19 changed +- `other`: 4 changed + +## Commits + +- `94b6df4` plan selector v0.1.23 targeted deployment +- `8f302da` docs: close forge issue reporting plan +- `8817e9d` Add forge issue reporting governance +- `2d12c7e` Add work-item and structure governance +- `a0f0955` docs: close selector v0.1.22 rollout + +## Changed Paths + +### Scripts + +- `repo-policy-selector/scripts/audit_active_lanes.py` +- `repo-policy-selector/scripts/preflight_forge_issue.py` +- `repo-policy-selector/scripts/select_policy.py` + +### Modules + +- `modules/active-lane-coordination.md` +- `modules/architecture-guardrails.md` +- `modules/forge-issue-reporting.md` +- `modules/github-issue-operations.md` +- `modules/gitlab-issue-operations.md` +- `modules/work-item-traceability.md` +- `repo-policy-selector/policy-library/modules/active-lane-coordination.md` +- `repo-policy-selector/policy-library/modules/architecture-guardrails.md` +- `repo-policy-selector/policy-library/modules/forge-issue-reporting.md` +- `repo-policy-selector/policy-library/modules/github-issue-operations.md` +- `repo-policy-selector/policy-library/modules/gitlab-issue-operations.md` +- `repo-policy-selector/policy-library/modules/work-item-traceability.md` + +### Profiles + +- `profiles/operations-platform.yaml` +- `profiles/repo-product-engineering.yaml` +- `repo-policy-selector/policy-library/profiles/operations-platform.yaml` +- `repo-policy-selector/policy-library/profiles/repo-product-engineering.yaml` + +### Docs + +- `ADOPTION.md` +- `AGENTS.md` +- `README.md` +- `SCHEMA.md` +- `catalog.yaml` +- `docs/dev/notes/0024-2026-08-29-parallel-lane-and-structure-practices.md` +- `docs/dev/notes/0024-2026-08-29-selector-v0-1-22-release-and-fleet-rollout.md` +- `docs/dev/notes/0025-2026-08-31-forge-issue-reporting-research.md` +- `docs/dev/plans/0023-2026-08-29-selector-v0-1-22-release-and-fleet-rollout.md` +- `docs/dev/plans/0024-2026-08-29-work-item-and-structure-governance.md` +- `docs/dev/plans/0025-2026-08-31-forge-issue-reporting.md` +- `docs/dev/plans/0026-2026-08-31-selector-v0-1-23-forge-reporting-deployment.md` +- `docs/dev/policies/0021-active-lane-coordination.md` +- `docs/dev/policies/0023-work-item-traceability.md` +- `docs/dev/policies/0024-forge-issue-reporting.md` +- `repo-policy-selector/SKILL.md` +- `repo-policy-selector/policy-library/SCHEMA.md` +- `repo-policy-selector/policy-library/catalog.yaml` +- `repo-policy-selector/references/selection-workflow.md` + +### Other + +- `repo-policy-selector/references/forge-issue-targets.example.json` +- `repo-policy-selector/tests/test_audit_active_lanes.py` +- `repo-policy-selector/tests/test_preflight_forge_issue.py` +- `repo-policy-selector/tests/test_select_policy.py` diff --git a/.agents/skills/repo-policy-selector/releases/v0.1.24.md b/.agents/skills/repo-policy-selector/releases/v0.1.24.md new file mode 100644 index 0000000..32c9ae9 --- /dev/null +++ b/.agents/skills/repo-policy-selector/releases/v0.1.24.md @@ -0,0 +1,71 @@ +# Selector Release v0.1.24 + +- Bundle version: `0.1.24` +- Source commit: `9caf5708e5aa48cc0cf6264baa7065fc335d193e` +- Source ref: `9caf5708e5aa48cc0cf6264baa7065fc335d193e` +- Previous release: `v0.1.23` + +## Summary + +- `modules`: 10 changed +- `profiles`: 16 changed +- `docs`: 9 changed +- `other`: 3 changed + +## Commits + +- `9caf570` policy: make model routing runtime adaptable +- `4e7ec07` policy: add model selection calibration +- `6d34ed8` docs: close selector v0.1.23 deployment + +## Changed Paths + +### Modules + +- `modules/goal-execution-governance.md` +- `modules/model-selection-and-calibration.md` +- `modules/parallel-plan-design.md` +- `modules/subagent-runtime-governance.md` +- `modules/subagent-workflow-optimization.md` +- `repo-policy-selector/policy-library/modules/goal-execution-governance.md` +- `repo-policy-selector/policy-library/modules/model-selection-and-calibration.md` +- `repo-policy-selector/policy-library/modules/parallel-plan-design.md` +- `repo-policy-selector/policy-library/modules/subagent-runtime-governance.md` +- `repo-policy-selector/policy-library/modules/subagent-workflow-optimization.md` + +### Profiles + +- `profiles/course-workspace.yaml` +- `profiles/operations-platform.yaml` +- `profiles/repo-product-engineering.yaml` +- `profiles/seminal-workspace.yaml` +- `profiles/skill-repo-maintainer.yaml` +- `profiles/standalone-library.yaml` +- `profiles/website-maintenance.yaml` +- `profiles/writing-project.yaml` +- `repo-policy-selector/policy-library/profiles/course-workspace.yaml` +- `repo-policy-selector/policy-library/profiles/operations-platform.yaml` +- `repo-policy-selector/policy-library/profiles/repo-product-engineering.yaml` +- `repo-policy-selector/policy-library/profiles/seminal-workspace.yaml` +- `repo-policy-selector/policy-library/profiles/skill-repo-maintainer.yaml` +- `repo-policy-selector/policy-library/profiles/standalone-library.yaml` +- `repo-policy-selector/policy-library/profiles/website-maintenance.yaml` +- `repo-policy-selector/policy-library/profiles/writing-project.yaml` + +### Docs + +- `AGENTS.md` +- `catalog.yaml` +- `docs/dev/plans/0026-2026-08-31-selector-v0-1-23-forge-reporting-deployment.md` +- `docs/dev/policies/0009-parallel-plan-design.md` +- `docs/dev/policies/0014-goal-execution-governance.md` +- `docs/dev/policies/0019-subagent-workflow-optimization.md` +- `docs/dev/policies/0022-code-testing-discipline.md` +- `docs/dev/policies/0025-model-selection-and-calibration.md` +- `repo-policy-selector/policy-library/catalog.yaml` + +### Other + +- `.codex/skills/repo-policy-harvester/scripts/harvest_policy.py` +- `.codex/skills/repo-policy-harvester/tests/test_harvest_policy.py` +- `repo-policy-selector/tests/test_select_policy.py` diff --git a/.agents/skills/repo-policy-selector/releases/v0.1.25.md b/.agents/skills/repo-policy-selector/releases/v0.1.25.md new file mode 100644 index 0000000..342e3ef --- /dev/null +++ b/.agents/skills/repo-policy-selector/releases/v0.1.25.md @@ -0,0 +1,25 @@ +# Selector Release v0.1.25 + +- Bundle version: `0.1.25` +- Source commit: `b22b1e9f6ca2e1733a3decd4f68e95660aa9b0bb` +- Source ref: `b22b1e9f6ca2e1733a3decd4f68e95660aa9b0bb` +- Previous release: `v0.1.24` + +## Summary + +- `scripts`: 1 changed +- `other`: 1 changed + +## Commits + +- `b22b1e9` fix: install referenced model policy + +## Changed Paths + +### Scripts + +- `repo-policy-selector/scripts/select_policy.py` + +### Other + +- `repo-policy-selector/tests/test_select_policy.py` diff --git a/.agents/skills/repo-policy-selector/releases/v0.1.26.md b/.agents/skills/repo-policy-selector/releases/v0.1.26.md new file mode 100644 index 0000000..9f70822 --- /dev/null +++ b/.agents/skills/repo-policy-selector/releases/v0.1.26.md @@ -0,0 +1,97 @@ +# Selector Release v0.1.26 + +- Bundle version: `0.1.26` +- Source commit: `ead42f7d6932fedc9dea83111b234f954a6afa4d` +- Source ref: `ead42f7d6932fedc9dea83111b234f954a6afa4d` +- Previous release: `v0.1.25` + +## Summary + +- `scripts`: 3 changed +- `modules`: 22 changed +- `profiles`: 2 changed +- `docs`: 21 changed +- `other`: 8 changed + +## Commits + +- `ead42f7` Plan selector v0.1.26 fleet rollout +- `a61f625` Repair planning and active-lane audits (#3) +- `57f67b8` Harvest Last30 multi-session coordination policy (#2) +- `8f4273d` Add shared multi-user development policy (#1) +- `6f97e8c` docs: close measurement routing policy plan +- `57ed245` policy: require measurement-first evidence +- `d34a969` docs: record model policy deployment + +## Changed Paths + +### Scripts + +- `repo-policy-selector/scripts/audit_active_lanes.py` +- `repo-policy-selector/scripts/audit_planning_contract.py` +- `repo-policy-selector/scripts/select_policy.py` + +### Modules + +- `modules/active-lane-coordination.md` +- `modules/collaborative-development-workflow.md` +- `modules/development-runtime-isolation.md` +- `modules/git-worktree-hygiene.md` +- `modules/goal-execution-governance.md` +- `modules/model-selection-and-calibration.md` +- `modules/multi-agent-reconciliation.md` +- `modules/parallel-plan-design.md` +- `modules/planning-discipline.md` +- `modules/subagent-workflow-optimization.md` +- `modules/validation-and-handoff.md` +- `repo-policy-selector/policy-library/modules/active-lane-coordination.md` +- `repo-policy-selector/policy-library/modules/collaborative-development-workflow.md` +- `repo-policy-selector/policy-library/modules/development-runtime-isolation.md` +- `repo-policy-selector/policy-library/modules/git-worktree-hygiene.md` +- `repo-policy-selector/policy-library/modules/goal-execution-governance.md` +- `repo-policy-selector/policy-library/modules/model-selection-and-calibration.md` +- `repo-policy-selector/policy-library/modules/multi-agent-reconciliation.md` +- `repo-policy-selector/policy-library/modules/parallel-plan-design.md` +- `repo-policy-selector/policy-library/modules/planning-discipline.md` +- `repo-policy-selector/policy-library/modules/subagent-workflow-optimization.md` +- `repo-policy-selector/policy-library/modules/validation-and-handoff.md` + +### Profiles + +- `profiles/operations-platform.yaml` +- `repo-policy-selector/policy-library/profiles/operations-platform.yaml` + +### Docs + +- `catalog.yaml` +- `docs/dev/notes/0026-2026-08-31-planning-and-lane-drift-remediation-handoff.md` +- `docs/dev/notes/0027-2026-09-07-model-selection-policy-deployment.md` +- `docs/dev/notes/0028-2026-09-11-soylei-collaborative-development-adoption.md` +- `docs/dev/notes/0029-2026-09-11-proportional-soylei-collaboration.md` +- `docs/dev/notes/0030-2026-09-11-soylei-shared-github-collaboration-policy.md` +- `docs/dev/notes/0031-2026-09-13-last30-multi-session-policy-harvest.md` +- `docs/dev/plans/0027-2026-09-08-measurement-first-and-economical-routing-policy.md` +- `docs/dev/plans/0028-2026-09-11-collaborative-development-workflow.md` +- `docs/dev/plans/0029-2026-09-11-proportional-collaboration-policy.md` +- `docs/dev/plans/0030-2026-09-11-shared-github-collaboration-policy.md` +- `docs/dev/plans/0031-2026-09-13-last30-multi-session-policy-harvest.md` +- `docs/dev/plans/0032-2026-09-13-planning-and-lane-auditor-repair.md` +- `docs/dev/plans/0033-2026-09-13-selector-v0-1-26-release-and-fleet-rollout.md` +- `docs/dev/policies/0004-git-worktree-hygiene.md` +- `docs/dev/policies/0009-parallel-plan-design.md` +- `docs/dev/policies/0015-planning-discipline.md` +- `docs/dev/policies/0018-multi-agent-reconciliation.md` +- `docs/dev/policies/0021-active-lane-coordination.md` +- `repo-policy-selector/policy-library/catalog.yaml` +- `repo-policy-selector/references/selection-workflow.md` + +### Other + +- `.codex/skills/repo-policy-harvester/scripts/harvest_policy.py` +- `.codex/skills/repo-policy-harvester/tests/test_harvest_policy.py` +- `repo-policy-selector/tests/test_audit_active_lanes.py` +- `repo-policy-selector/tests/test_audit_planning_contract.py` +- `repo-policy-selector/tests/test_collaborative_development_policy_contract.py` +- `repo-policy-selector/tests/test_measurement_routing_policy_contract.py` +- `repo-policy-selector/tests/test_multi_session_policy_harvest_contract.py` +- `repo-policy-selector/tests/test_select_policy.py` diff --git a/.agents/skills/repo-policy-selector/scripts/audit_active_lanes.py b/.agents/skills/repo-policy-selector/scripts/audit_active_lanes.py index e20e721..5f09f76 100644 --- a/.agents/skills/repo-policy-selector/scripts/audit_active_lanes.py +++ b/.agents/skills/repo-policy-selector/scripts/audit_active_lanes.py @@ -117,12 +117,23 @@ def read_ref_file(repo: Path, ref: str, path: str) -> str | None: return result.stdout if result.returncode == 0 else None +def resolve_read_ref(repo: Path, value: str, *, remote: str) -> str | None: + candidates = [value] + if value.startswith("refs/heads/"): + candidates.append(f"refs/remotes/{remote}/{value.removeprefix('refs/heads/')}") + elif not value.startswith("refs/"): + candidates.extend((f"refs/heads/{value}", f"refs/remotes/{remote}/{value}")) + return next((candidate for candidate in candidates if ref_tip(repo, candidate)), None) + + def plan_metadata(text: str) -> dict[str, str]: metadata: dict[str, str] = {} + header = re.split(r"(?m)^##\s+", text, maxsplit=1)[0] for key in ("state", "lane", "branch", "target", "integration"): - match = re.search(rf"(?im)^{key}\s*:\s*(.+?)\s*$", text) + match = re.search(rf"(?im)^{key}\s*:\s*(.+?)\s*$", header) if match: - metadata[key] = match.group(1).strip() + value = match.group(1).strip().strip("`") + metadata[key] = value.upper() if key in {"state", "lane"} else value return metadata @@ -275,6 +286,9 @@ def audit_repo( } if catalog.get("schema_version") != 1: problems.append("catalog schema_version must be 1") + work_item_tracking = str(catalog.get("work_item_tracking", "optional")) + if work_item_tracking not in {"optional", "required"}: + problems.append("catalog work_item_tracking must be optional or required") lanes_value = catalog.get("lanes") if not isinstance(lanes_value, list): lanes_value = [] @@ -331,6 +345,20 @@ def audit_repo( for dependency in dependencies: if dependency not in known_lane_ids: problems.append(f"{lane_id}: unknown dependency lane: {dependency}") + work_items = lane.get("work_items", []) + if not isinstance(work_items, list): + problems.append(f"{lane_id}: work_items must be a list") + else: + if work_item_tracking == "required" and not work_items: + problems.append(f"{lane_id}: work_items must not be empty when tracking is required") + for locator in work_items: + if ( + not isinstance(locator, str) + or not locator + or len(locator) > 200 + or any(character.isspace() for character in locator) + ): + problems.append(f"{lane_id}: invalid work-item locator: {locator}") if lane.get("plan_state") not in allowed_plan_states: problems.append(f"{lane_id}: invalid plan_state: {lane.get('plan_state')}") if lane.get("custody_state") not in allowed_custody_states: @@ -381,7 +409,8 @@ def audit_repo( if plan_state in {"PLANNED", "OPEN", "BLOCKED"}: plan_ref = str(catalog_lane.get("plan_ref", "")) plan_path = str(catalog_lane.get("plan", "")) - plan_body = read_ref_file(repo, plan_ref, plan_path) if plan_ref and plan_path else None + resolved_plan_ref = resolve_read_ref(repo, plan_ref, remote=remote) if plan_ref else None + plan_body = read_ref_file(repo, resolved_plan_ref, plan_path) if resolved_plan_ref and plan_path else None if plan_body is None: findings.append("plan_catalog_drift") problems.append(f"{lane_id}: registered plan is not readable at plan_ref") @@ -431,7 +460,14 @@ def audit_repo( if custody_state == "ACTIVE_WORKTREE" and ref_relation == "diverged": findings.append("local_remote_diverged") problems.append(f"{lane_id}: active local and remote custody have diverged") - if custody_state == "ACTIVE_WORKTREE" and not lane_worktrees: + if ( + custody_state == "ACTIVE_WORKTREE" + and not lane_worktrees + and ref_relation == "remote_only" + and checkpoint == remote_tip + ): + findings.append("remote_active") + elif custody_state == "ACTIVE_WORKTREE" and not lane_worktrees: if "registered_but_missing" not in findings: findings.append("registered_but_missing") problems.append(f"{lane_id}: ACTIVE_WORKTREE lane has no assigned worktree") @@ -466,9 +502,14 @@ def audit_repo( for overlap in unreconciled_overlaps: problems.append(f"{lane_id}: declared overlap lacks disposition: {overlap}") target = str(catalog_lane.get("target", "")) - target_ref = target if target.startswith("refs/") else f"refs/heads/{target}" - target_tip = ref_tip(repo, target_ref) if target else None - integrated_into_target = is_ancestor(repo, local_tip, target_tip) + target_ref = ( + default_ref + if target and default_ref.rsplit("/", 1)[-1] == target.rsplit("/", 1)[-1] + else resolve_read_ref(repo, target, remote=remote) + ) + target_tip = ref_tip(repo, target_ref) if target_ref else None + branch_tip = local_tip or remote_tip + integrated_into_target = is_ancestor(repo, branch_tip, target_tip) integration_receipt = str(catalog_lane.get("integration_receipt", "")) receipt_verified = bool( integration_receipt @@ -477,8 +518,8 @@ def audit_repo( ) readiness_evidenced = ( custody_state == "INTEGRATION_READY" - and local_tip - and local_tip == remote_tip == checkpoint + and branch_tip + and branch_tip == remote_tip == checkpoint and catalog_lane.get("validation_status") == "passed" and catalog_lane.get("validation_ref") == checkpoint and not any(item.get("status") for item in lane_worktrees) @@ -613,6 +654,7 @@ def audit_repo( else "prefixes" ), "selected_branches": list(selected_branches), + "work_item_tracking": work_item_tracking, "lanes": lanes, "problems": problems, "ok": not problems, diff --git a/.agents/skills/repo-policy-selector/scripts/audit_planning_contract.py b/.agents/skills/repo-policy-selector/scripts/audit_planning_contract.py index a128395..8bf553c 100644 --- a/.agents/skills/repo-policy-selector/scripts/audit_planning_contract.py +++ b/.agents/skills/repo-policy-selector/scripts/audit_planning_contract.py @@ -10,13 +10,16 @@ from pathlib import Path -ROADMAP_HEADING_RE = re.compile(r"^##\s+P\d{2}\s+\|\s+.+$") +ROADMAP_HEADING_RE = re.compile(r"^##\s+P\d+\s+\|\s+.+$") ROADMAP_LANE_HEADING_PREFIX_RE = re.compile(r"^##\s+P\d+") -RUNBOOK_TURN_RE = re.compile(r"^##\s+Turn\s+\d+\s+\|\s+\d{4}-\d{2}-\d{2}$") +RUNBOOK_TURN_RE = re.compile(r"^##\s+Turn\b.+\b\d{4}-\d{2}-\d{2}$", re.IGNORECASE) RUNBOOK_TURN_HEADING_PREFIX_RE = re.compile(r"^##\s+Turn\b", re.IGNORECASE) PLAN_FILE_RE = re.compile(r"^\d{4}-\d{4}-\d{2}-\d{2}-[a-z0-9-]+\.md$") -PLAN_STATE_RE = re.compile(r"(?im)^(?:state|status)\s*:\s*(PLANNED|OPEN|CLOSED|CANCELLED)\s*$") -ROADMAP_LANE_RE = re.compile(r"(?im)^(?:roadmap|lane|phase)\s*:\s*(P\d{2})\b") +PLAN_INDEX_NAMES = {"INDEX.MD", "README.MD"} +PLAN_STATE_RE = re.compile( + r"(?im)^(?:state|status)\s*:\s*`?(PLANNED|OPEN|BLOCKED|CLOSED|CANCELLED)`?\s*$" +) +ROADMAP_LANE_RE = re.compile(r"(?im)^(?:roadmap|lane|phase)\s*:\s*`?(P\d+)`?\s*$") CURRENT_STATE_RE = re.compile(r"(?im)^##\s+Current State\s*$|^(?:current state)\s*:", re.MULTILINE) GOAL_BOUND_PATTERNS = { "max_work_unit_attempts": re.compile(r"(?im)^max_work_unit_attempts\s*:\s*[1-9]\d*\s*$"), @@ -63,6 +66,11 @@ def read_text(path: Path) -> str: return "" +def plan_header(text: str) -> str: + """Return metadata before the first second-level plan section.""" + return re.split(r"(?m)^##\s+", text, maxsplit=1)[0] + + def split_roadmap_sections(roadmap_text: str) -> dict[str, str]: sections: dict[str, str] = {} current_lane: str | None = None @@ -72,7 +80,7 @@ def split_roadmap_sections(roadmap_text: str) -> dict[str, str]: if current_lane is not None: sections[current_lane] = "\n".join(current_lines).strip() current_lines = [line] - match = re.match(r"^##\s+(P\d{2})\s+\|", line) + match = re.match(r"^##\s+(P\d+)\s+\|", line) current_lane = match.group(1) if match else None elif current_lane is not None: current_lines.append(line) @@ -124,15 +132,29 @@ def audit_goal_execution_contract(root: Path) -> dict: } +def planning_dev_root(root: Path) -> str: + agents_text = read_text(root / "AGENTS.md") or read_text(root / "AGENT.MD") + if re.search(r"(? tuple[dict[str, bool], dict[str, bool]]: - policy_dir = root / "docs" / "dev" / "policies" + policy_dirs = (root / "docs/dev/policies", root / "doc/dev/policies") available = { - "planning_discipline": bool(list(policy_dir.glob("*planning-discipline.md"))) if policy_dir.exists() else False, - "roadmap_runbook_governance": bool(list(policy_dir.glob("*roadmap-runbook-governance.md"))) if policy_dir.exists() else False, + "planning_discipline": any(directory.exists() and list(directory.glob("*planning-discipline.md")) for directory in policy_dirs), + "roadmap_runbook_governance": any(directory.exists() and list(directory.glob("*roadmap-runbook-governance.md")) for directory in policy_dirs), } agents_text = read_text(root / "AGENTS.md") or read_text(root / "AGENT.MD") policy_wired = bool( - re.search(r"docs/dev/policies|docs/dev/agent-policies", agents_text, re.IGNORECASE) + re.search(r"docs?/dev/(?:policies|agent-policies)", agents_text, re.IGNORECASE) and re.search(r"\b(?:read|follow|policy entry|policy loading)\b", agents_text, re.IGNORECASE) ) adopted = {name: bool(present and policy_wired) for name, present in available.items()} @@ -176,9 +198,10 @@ def audit_repo( active_only: bool = False, force: bool = False, ) -> dict: + default_dev_root = planning_dev_root(root) roadmap = resolve_repo_path(root, roadmap_path, "ROADMAP.md") runbook = resolve_repo_path(root, runbook_path, "RUNBOOK.md") - plans_dir = resolve_repo_path(root, plans_dir_path, "docs/dev/plans") + plans_dir = resolve_repo_path(root, plans_dir_path, f"{default_dev_root}/plans") available_contracts, contracts = planning_contracts(root) planning_applicable = contracts["planning_discipline"] or contracts["roadmap_runbook_governance"] roadmap_applicable = contracts["roadmap_runbook_governance"] or force @@ -222,7 +245,7 @@ def audit_repo( ] bad_headings = [line for line in roadmap_headings if not ROADMAP_HEADING_RE.match(line)] if roadmap_applicable and roadmap_text and bad_headings: - problems.append("ROADMAP.md has top-level headings that do not match '## P## | Title'") + problems.append("ROADMAP.md has lane headings that do not match '## P | Title'") report["roadmap_headings"] = roadmap_headings roadmap_sections = split_roadmap_sections(roadmap_text) open_roadmap_lanes = [ @@ -241,11 +264,13 @@ def audit_repo( ] bad_turns = [line for line in runbook_turns if not RUNBOOK_TURN_RE.match(line)] if roadmap_applicable and runbook_text and bad_turns: - problems.append("RUNBOOK.md has headings that do not match '## Turn N | YYYY-MM-DD'") + problems.append("RUNBOOK.md has Turn headings without a terminal YYYY-MM-DD date") report["runbook_turns"] = runbook_turns if plans_dir.exists(): for plan_path in sorted(plans_dir.glob("*.md")): + if plan_path.name.upper() in PLAN_INDEX_NAMES: + continue entry = { "file": plan_path.name, "path": str(plan_path), @@ -259,23 +284,25 @@ def audit_repo( "wired_in_runbook": False, } text = read_text(plan_path) - state_match = PLAN_STATE_RE.search(text) - lane_match = ROADMAP_LANE_RE.search(text) - if active_only and not state_match: + header = plan_header(text) + state_match = PLAN_STATE_RE.search(header) + lane_match = ROADMAP_LANE_RE.search(header) + if active_only and not state_match and not entry["filename_ok"]: excluded = report["excluded_unclassified_plans"] assert isinstance(excluded, list) excluded.append(plan_path.name) continue - if active_only and state_match and state_match.group(1) not in {"PLANNED", "OPEN"}: + state = state_match.group(1).upper() if state_match else None + if active_only and state is not None and state not in {"PLANNED", "OPEN", "BLOCKED"}: excluded = report["excluded_closed_plans"] assert isinstance(excluded, list) excluded.append(plan_path.name) continue if state_match: - entry["state"] = state_match.group(1) + entry["state"] = state entry["state_ok"] = True if lane_match: - entry["lane_id"] = lane_match.group(1) + entry["lane_id"] = lane_match.group(1).upper() entry["lane_ok"] = True entry["current_state_ok"] = bool(CURRENT_STATE_RE.search(text)) entry["wired_in_roadmap"] = plan_path.name in roadmap_text @@ -297,7 +324,7 @@ def audit_repo( cast_list.append(entry) plans = report["plans"] assert isinstance(plans, list) - actionable_states = {"PLANNED", "OPEN"} + actionable_states = {"PLANNED", "OPEN", "BLOCKED"} for lane_id in open_roadmap_lanes if roadmap_applicable else []: if not any( plan.get("lane_id") == lane_id and plan.get("state") in actionable_states @@ -306,7 +333,7 @@ def audit_repo( ): problems.append(f"OPEN roadmap lane missing actionable plan coverage: {lane_id}") - baseline_path = root / "docs/dev/planning-audit-baseline.json" + baseline_path = root / default_dev_root / "planning-audit-baseline.json" accepted_baseline_findings: list[str] = [] unused_baseline_findings: list[str] = [] if active_only and baseline_path.exists(): diff --git a/.agents/skills/repo-policy-selector/scripts/preflight_forge_issue.py b/.agents/skills/repo-policy-selector/scripts/preflight_forge_issue.py new file mode 100644 index 0000000..0bc371c --- /dev/null +++ b/.agents/skills/repo-policy-selector/scripts/preflight_forge_issue.py @@ -0,0 +1,482 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +import os +import shutil +import subprocess +import sys +import urllib.parse +from pathlib import Path +from typing import Any + + +ACTIONS = { + "read", + "create", + "comment", + "edit", + "apply_labels", + "create_labels", + "assign", + "milestone", + "planning", + "close", + "reopen", + "transfer", +} +RELATIONSHIPS = {"owned", "permissioned"} +SECURITY_ROUTES = { + "private_vulnerability_reporting", + "security_policy", + "private_contact", + "confidential_issue", + "none", +} +GITHUB_ROLES = {"NONE": 0, "READ": 10, "TRIAGE": 20, "WRITE": 30, "MAINTAIN": 40, "ADMIN": 50} +GITLAB_ROLES = { + "NO_ACCESS": 0, + "MINIMAL_ACCESS": 5, + "GUEST": 10, + "PLANNER": 15, + "REPORTER": 20, + "SECURITY_MANAGER": 25, + "DEVELOPER": 30, + "MAINTAINER": 40, + "OWNER": 50, +} +GITLAB_ACCESS_NAMES = {value: key for key, value in GITLAB_ROLES.items()} +PROVIDER_TIMEOUT_SECONDS = 20 + +MINIMUM_ROLE = { + "github": { + "read": "READ", + "create": "READ", + "comment": "READ", + "edit": "TRIAGE", + "apply_labels": "TRIAGE", + "create_labels": "WRITE", + "assign": "TRIAGE", + "milestone": "TRIAGE", + "planning": "WRITE", + "close": "TRIAGE", + "reopen": "TRIAGE", + "transfer": "WRITE", + }, + "gitlab": { + "read": "GUEST", + "create": "GUEST", + "comment": "GUEST", + "edit": "PLANNER", + "apply_labels": "PLANNER", + "create_labels": "MAINTAINER", + "assign": "PLANNER", + "milestone": "PLANNER", + "planning": "PLANNER", + "close": "PLANNER", + "reopen": "PLANNER", + "transfer": "MAINTAINER", + }, +} + + +class PreflightError(RuntimeError): + pass + + +def read_json(path: Path) -> dict[str, Any]: + try: + value = json.loads(path.read_text(encoding="utf-8")) + except FileNotFoundError as exc: + raise PreflightError(f"file not found: {path}") from exc + except json.JSONDecodeError as exc: + raise PreflightError(f"invalid JSON in {path}: {exc}") from exc + if not isinstance(value, dict): + raise PreflightError(f"expected a JSON object in {path}") + return value + + +def validate_registry(registry: dict[str, Any]) -> list[str]: + problems: list[str] = [] + if registry.get("schema_version") != 1: + problems.append("registry schema_version must be 1") + targets = registry.get("targets") + if not isinstance(targets, list): + return problems + ["registry targets must be a list"] + seen: set[str] = set() + for index, target in enumerate(targets): + prefix = f"targets[{index}]" + if not isinstance(target, dict): + problems.append(f"{prefix} must be an object") + continue + target_id = target.get("id") + if not isinstance(target_id, str) or not target_id.strip(): + problems.append(f"{prefix}.id must be a non-empty string") + elif target_id in seen: + problems.append(f"duplicate target id: {target_id}") + else: + seen.add(target_id) + forge = target.get("forge") + if forge not in {"github", "gitlab"}: + problems.append(f"{prefix}.forge must be github or gitlab") + for field in ("host", "repository"): + if not isinstance(target.get(field), str) or not target[field].strip(): + problems.append(f"{prefix}.{field} must be a non-empty string") + repository = target.get("repository", "") + minimum_parts = 2 if forge == "github" else 2 + if isinstance(repository, str) and len([part for part in repository.split("/") if part]) < minimum_parts: + problems.append(f"{prefix}.repository must include a namespace and repository") + if forge == "github" and isinstance(repository, str) and len(repository.split("/")) != 2: + problems.append(f"{prefix}.repository must use OWNER/REPO for GitHub") + if target.get("relationship") not in RELATIONSHIPS: + problems.append(f"{prefix}.relationship must be owned or permissioned") + allowed = target.get("allowed_actions") + if not isinstance(allowed, list) or any(action not in ACTIONS for action in allowed): + problems.append(f"{prefix}.allowed_actions contains an unsupported action") + elif len(allowed) != len(set(allowed)): + problems.append(f"{prefix}.allowed_actions contains duplicates") + if target.get("security_route") not in SECURITY_ROUTES: + problems.append(f"{prefix}.security_route is unsupported") + elif forge == "github" and target.get("security_route") == "confidential_issue": + problems.append(f"{prefix}.security_route confidential_issue is GitLab-specific") + elif forge == "gitlab" and target.get("security_route") == "private_vulnerability_reporting": + problems.append(f"{prefix}.security_route private_vulnerability_reporting is GitHub-specific") + label_map = target.get("label_map") + if not isinstance(label_map, dict): + problems.append(f"{prefix}.label_map must be an object") + else: + provider_labels: set[str] = set() + for intent, mapping in label_map.items(): + if not isinstance(intent, str) or not intent.strip(): + problems.append(f"{prefix}.label_map has an empty intent key") + continue + if not isinstance(mapping, dict) or not isinstance(mapping.get("provider_label"), str) or not mapping["provider_label"].strip(): + problems.append(f"{prefix}.label_map[{intent}] needs provider_label") + continue + provider_label = mapping["provider_label"] + if provider_label in provider_labels: + problems.append(f"{prefix}.label_map maps more than one intent to {provider_label}") + provider_labels.add(provider_label) + return problems + + +def select_target(registry: dict[str, Any], target_id: str) -> dict[str, Any]: + matches = [target for target in registry.get("targets", []) if target.get("id") == target_id] + if len(matches) != 1: + raise PreflightError(f"target id must resolve exactly once: {target_id}") + return matches[0] + + +def run_json(command: list[str], *, env: dict[str, str] | None = None) -> Any: + try: + result = subprocess.run( + command, + text=True, + capture_output=True, + env=env, + check=False, + timeout=PROVIDER_TIMEOUT_SECONDS, + ) + except subprocess.TimeoutExpired as exc: + raise PreflightError( + f"read-only provider command timed out after {PROVIDER_TIMEOUT_SECONDS}s: {command[0]}" + ) from exc + if result.returncode != 0: + detail = result.stderr.strip().splitlines()[-1] if result.stderr.strip() else "command failed" + raise PreflightError(f"read-only provider command failed: {command[0]}: {detail}") + try: + return json.loads(result.stdout) + except json.JSONDecodeError as exc: + raise PreflightError(f"provider returned invalid JSON from {command[0]}") from exc + + +def label_names(value: Any) -> list[str]: + if isinstance(value, dict): + value = value.get("nodes", []) + if not isinstance(value, list): + return [] + names: list[str] = [] + for item in value: + if isinstance(item, str): + names.append(item) + elif isinstance(item, dict) and isinstance(item.get("name"), str): + names.append(item["name"]) + return sorted(set(names)) + + +def inspect_github(target: dict[str, Any], idempotency_key: str | None) -> dict[str, Any]: + if shutil.which("gh") is None: + raise PreflightError("gh is not installed") + host = target["host"] + repository = target["repository"] + env = os.environ.copy() + env["GH_HOST"] = host + actor = run_json(["gh", "api", "--hostname", host, "user"], env=env).get("login") + project = run_json( + [ + "gh", + "repo", + "view", + repository, + "--json", + "nameWithOwner,viewerPermission,hasIssuesEnabled,isArchived,isFork,url,labels", + ], + env=env, + ) + duplicates: list[dict[str, Any]] = [] + if idempotency_key: + found = run_json( + [ + "gh", + "issue", + "list", + "--repo", + repository, + "--state", + "all", + "--search", + idempotency_key, + "--limit", + "20", + "--json", + "number,title,url,state", + ], + env=env, + ) + duplicates = found if isinstance(found, list) else [] + return { + "forge": "github", + "host": host, + "repository": project.get("nameWithOwner"), + "authenticated_actor": actor, + "effective_role": project.get("viewerPermission", "NONE"), + "issues_enabled": bool(project.get("hasIssuesEnabled")), + "archived": bool(project.get("isArchived")), + "available_labels": label_names(project.get("labels")), + "duplicate_candidates": duplicates, + "url": project.get("url"), + "is_fork": bool(project.get("isFork")), + } + + +def gitlab_effective_role(project: dict[str, Any]) -> str: + permissions = project.get("permissions") if isinstance(project.get("permissions"), dict) else {} + levels = [] + for key in ("project_access", "group_access"): + value = permissions.get(key) + if isinstance(value, dict) and isinstance(value.get("access_level"), int): + levels.append(value["access_level"]) + level = max(levels, default=0) + return GITLAB_ACCESS_NAMES.get(level, f"CUSTOM_{level}") + + +def inspect_gitlab(target: dict[str, Any], idempotency_key: str | None) -> dict[str, Any]: + if shutil.which("glab") is None: + raise PreflightError("glab is not installed") + host = target["host"] + repository = target["repository"] + encoded = urllib.parse.quote(repository, safe="") + actor = run_json(["glab", "api", "--hostname", host, "user"]).get("username") + project = run_json(["glab", "api", "--hostname", host, f"projects/{encoded}"]) + labels = run_json( + ["glab", "api", "--hostname", host, f"projects/{encoded}/labels?per_page=100"] + ) + duplicates: list[dict[str, Any]] = [] + if idempotency_key: + query = urllib.parse.urlencode( + {"scope": "all", "state": "all", "search": idempotency_key, "in": "description", "per_page": 20} + ) + found = run_json( + ["glab", "api", "--hostname", host, f"projects/{encoded}/issues?{query}"] + ) + duplicates = found if isinstance(found, list) else [] + return { + "forge": "gitlab", + "host": host, + "repository": project.get("path_with_namespace"), + "project_id": project.get("id"), + "authenticated_actor": actor, + "effective_role": gitlab_effective_role(project), + "issues_enabled": ( + project.get("issues_access_level") != "disabled" + if project.get("issues_access_level") is not None + else bool(project.get("issues_enabled")) + ), + "archived": bool(project.get("archived")), + "available_labels": label_names(labels), + "duplicate_candidates": duplicates, + "url": project.get("web_url"), + } + + +def inspect_live(target: dict[str, Any], idempotency_key: str | None) -> dict[str, Any]: + if target["forge"] == "github": + return inspect_github(target, idempotency_key) + return inspect_gitlab(target, idempotency_key) + + +def role_level(forge: str, role: str) -> int: + normalized = role.upper().replace(" ", "_").replace("-", "_") + if normalized.startswith("CUSTOM_"): + try: + return int(normalized.removeprefix("CUSTOM_")) + except ValueError: + return 0 + roles = GITHUB_ROLES if forge == "github" else GITLAB_ROLES + return roles.get(normalized, 0) + + +def evaluate( + target: dict[str, Any], + snapshot: dict[str, Any], + *, + action: str, + label_intents: list[str], + report_kind: str, + idempotency_key: str | None, +) -> dict[str, Any]: + problems: list[str] = [] + warnings: list[str] = [] + required_actions = [action] + if label_intents and "apply_labels" not in required_actions: + required_actions.append("apply_labels") + + for field in ("forge", "host"): + if snapshot.get(field) != target.get(field): + problems.append(f"target drift: snapshot {field} does not match registry") + expected_repo = str(target.get("repository", "")) + observed_repo = str(snapshot.get("repository", "")) + matches = expected_repo.lower() == observed_repo.lower() if target["forge"] == "github" else expected_repo == observed_repo + if not matches: + problems.append("target drift: snapshot repository does not match registry") + if snapshot.get("archived") is not False: + problems.append("target repository is archived or archive state is unknown") + if snapshot.get("issues_enabled") is not True: + problems.append("target issue surface is disabled or unknown") + if not snapshot.get("authenticated_actor"): + problems.append("authenticated actor is missing") + + allowed = set(target.get("allowed_actions", [])) + for required in required_actions: + if required not in allowed: + problems.append(f"action is not allowlisted: {required}") + + forge = target["forge"] + current_role = str(snapshot.get("effective_role", "")) + current_level = role_level(forge, current_role) + for required in required_actions: + minimum_name = MINIMUM_ROLE[forge][required] + if forge == "gitlab" and action == "create" and required == "apply_labels": + # GitLab Guests may set metadata while creating their own issue, + # even though later metadata mutation requires Planner. + minimum_name = "GUEST" + if current_level < role_level(forge, minimum_name): + problems.append( + f"insufficient provider role for {required}: {current_role or 'UNKNOWN'} < {minimum_name}" + ) + + if action == "create" and not idempotency_key: + problems.append("create requires a stable idempotency key") + duplicates = snapshot.get("duplicate_candidates", []) + if action == "create" and isinstance(duplicates, list) and duplicates: + problems.append("duplicate candidates found for the idempotency key") + + if report_kind == "security" and target.get("security_route") == "none": + problems.append("security report has no approved private route") + if report_kind != "security" and target.get("security_route") == "confidential_issue": + warnings.append("confidential_issue is configured but confidentiality still requires explicit use") + + available_labels = set(snapshot.get("available_labels", [])) + mappings = target.get("label_map", {}) + resolved_labels: list[str] = [] + label_resolution: list[dict[str, str]] = [] + for intent in label_intents: + mapping = mappings.get(intent) + if not isinstance(mapping, dict): + problems.append(f"unknown normalized label intent: {intent}") + continue + provider_label = mapping.get("provider_label") + if provider_label not in available_labels: + problems.append(f"mapped provider label does not exist: {provider_label}") + continue + resolved_labels.append(provider_label) + label_resolution.append({"intent": intent, "provider_label": provider_label}) + + if target.get("relationship") == "permissioned" and action in { + "create_labels", + "assign", + "milestone", + "planning", + "transfer", + }: + warnings.append("permissioned-target metadata action requires explicit least-invasive review") + + return { + "ok": not problems, + "target_id": target["id"], + "forge": forge, + "host": target["host"], + "repository": target["repository"], + "relationship": target["relationship"], + "security_route": target["security_route"], + "action": action, + "required_actions": required_actions, + "authenticated_actor": snapshot.get("authenticated_actor"), + "effective_role": current_role or None, + "resolved_labels": resolved_labels, + "label_resolution": label_resolution, + "idempotency_key": idempotency_key, + "duplicate_candidates": duplicates if isinstance(duplicates, list) else [], + "problems": problems, + "warnings": warnings, + "live_provider_read": snapshot.get("source") == "live", + "operator_authority_verified": False, + "mutation_authorized": False, + "assessment_scope": "target registry, provider capability, duplicate, security-route, and label preflight only", + } + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Read-only GitHub/GitLab issue-action preflight") + parser.add_argument("--registry", type=Path, required=True) + parser.add_argument("--target", required=True, help="target id from the registry") + parser.add_argument("--action", choices=sorted(ACTIONS), required=True) + parser.add_argument("--label", action="append", default=[], dest="labels", help="normalized label intent") + parser.add_argument( + "--report-kind", + choices=["defect", "feature", "operations", "governance", "security"], + default="defect", + ) + parser.add_argument("--idempotency-key") + parser.add_argument("--snapshot", type=Path, help="provider-free JSON snapshot instead of live discovery") + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + args = parse_args(argv) + try: + registry = read_json(args.registry) + registry_problems = validate_registry(registry) + if registry_problems: + result = {"ok": False, "problems": registry_problems, "warnings": []} + else: + target = select_target(registry, args.target) + snapshot = read_json(args.snapshot) if args.snapshot else inspect_live(target, args.idempotency_key) + snapshot["source"] = "fixture" if args.snapshot else "live" + result = evaluate( + target, + snapshot, + action=args.action, + label_intents=args.labels, + report_kind=args.report_kind, + idempotency_key=args.idempotency_key, + ) + except PreflightError as exc: + result = {"ok": False, "problems": [str(exc)], "warnings": []} + print(json.dumps(result, indent=2, sort_keys=True)) + return 0 if result.get("ok") else 2 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.agents/skills/repo-policy-selector/scripts/select_policy.py b/.agents/skills/repo-policy-selector/scripts/select_policy.py index b6b6885..7c5ba9a 100755 --- a/.agents/skills/repo-policy-selector/scripts/select_policy.py +++ b/.agents/skills/repo-policy-selector/scripts/select_policy.py @@ -607,6 +607,7 @@ def semantic_module_matches( "codegraph-usage", "graph-backed-memory-usage", "memory-service-runtime-governance", + "model-selection-and-calibration", "policy-adoption-feedback-loop", "policy-harvest-loop", "preview-artifact-review", @@ -676,6 +677,23 @@ def infer_repo_local_policy_findings( "planning-discipline": ["plan", "planning", "bounded plan", "definition of done"], "goal-execution-governance": ["/goal", "goal execution", "long-running goal", "goal checkpoint", "goal-compatible"], "roadmap-runbook-governance": ["roadmap", "runbook", "progress", "current working set"], + "work-item-traceability": ["issue tracking", "work item", "work-item", "backlog", "wip limit"], + "collaborative-development-workflow": [ + "multi-user development", + "collaborative development", + "multiple contributors", + "pull request workflow", + "peer review", + ], + "forge-issue-reporting": [ + "forge issue reporting", + "owned repository issue", + "permissioned repository issue", + "issue target registry", + "idempotency marker", + ], + "github-issue-operations": ["github issues", "gh issue", "github issue forms", "viewerpermission"], + "gitlab-issue-operations": ["gitlab issues", "glab issue", "gitlab work items", "confidential issue"], "git-worktree-hygiene": ["worktree", "worktrees"], "commit-history-discipline": ["commit", "atomic", "history"], "branch-and-integration-strategy": ["branch", "rebase", "merge", "integration"], @@ -686,6 +704,12 @@ def infer_repo_local_policy_findings( "runtime-vs-product-boundary": ["runtime home", "outside the repo", "user-scoped runtime", "product repo"], "runtime-state-governance": ["version controlled", "runtime state", "redaction", "pruning"], "tenant-isolation-and-operator-state": ["tenant", "tenant-scoped", "one profile per tenant", "state isolation"], + "development-runtime-isolation": [ + "isolated development runtime", + "per-lane runtime", + "lane runtime", + "development runtime isolation", + ], "fieldwork-productization": ["fieldwork", "keep as product", "refactor before keep", "archive as note only"], "monolith-extraction-discipline": ["monolith", "monolithic", "strong trunk", "oversized cli"], "policy-management": ["docs/dev/policies", "policy library", "agents.md"], @@ -1259,6 +1283,87 @@ def detect_signals(repo_root: Path) -> dict: "parallel worktree", ] ), + "mentions_development_runtime_isolation": any( + phrase in combined or phrase in semantic_text + for phrase in [ + "isolated development runtime", + "isolated dev runtime", + "per-lane runtime", + "per lane runtime", + "lane runtime", + "development runtime isolation", + ] + ), + "mentions_work_item_tracking": any( + phrase in combined or phrase in semantic_text + for phrase in [ + "issue tracking", + "issue tracker", + "work item", + "work-item", + "backlog governance", + "wip limit", + "work in process limit", + "issue dependency", + ] + ), + "mentions_collaborative_development": any( + phrase in combined or phrase in semantic_text + for phrase in [ + "multi-user development", + "multi user development", + "collaborative development", + "multiple contributors", + "more than one contributor", + "human collaborators", + "team development", + ] + ) and any( + phrase in combined or phrase in semantic_text + for phrase in [ + "pull request", + "feature branch", + "worktree", + "shared repository", + "shared repo", + "peer review", + ] + ), + "mentions_forge_issue_reporting": any( + phrase in combined or phrase in semantic_text + for phrase in [ + "forge issue reporting", + "owned repository issue", + "permissioned repository issue", + "owned repo issue", + "permissioned repo issue", + "issue target registry", + "cross-forge issue", + "cross forge issue", + ] + ), + "mentions_github_issue_operations": any( + phrase in combined or phrase in semantic_text + for phrase in [ + "github issues", + "github issue", + "gh issue", + "github project issue", + "github issue form", + "github issue template", + ] + ), + "mentions_gitlab_issue_operations": any( + phrase in combined or phrase in semantic_text + for phrase in [ + "gitlab issues", + "gitlab issue", + "glab issue", + "gitlab work item", + "gitlab confidential issue", + "gitlab issue template", + ] + ), "mentions_closeout": "closeout" in text or "best recommendation" in text, "mentions_policy_harvest": "policy" in semantic_text and "harvest" in semantic_text, "mentions_policy_library": any( @@ -2205,6 +2310,43 @@ def choose_profile(signals: dict, installed_library: dict[str, Any]) -> tuple[st if signals["mentions_active_lane_coordination"] and "active-lane-coordination" not in modules: modules.append("active-lane-coordination") reasons.append("repo language indicates concurrent off-main lanes need default-branch discovery") + if signals["mentions_development_runtime_isolation"] and "development-runtime-isolation" not in modules: + modules.append("development-runtime-isolation") + reasons.append("repo language indicates concurrent development services need lane and production isolation") + if signals["mentions_work_item_tracking"] and "work-item-traceability" not in modules: + modules.append("work-item-traceability") + reasons.append("repo language indicates issue or backlog state needs traceability to plans and delivery evidence") + if signals["mentions_collaborative_development"]: + collaboration_modules = ( + "work-item-traceability", + "git-worktree-hygiene", + "commit-history-discipline", + "branch-and-integration-strategy", + "commit-and-push-cadence", + "validation-and-handoff", + "collaborative-development-workflow", + ) + for module_id in collaboration_modules: + if module_id not in modules: + modules.append(module_id) + reasons.append("repo language indicates multiple human contributors need proportional shared-custody and deployment coordination") + forge_reporting = ( + signals["mentions_forge_issue_reporting"] + or signals["mentions_github_issue_operations"] + or signals["mentions_gitlab_issue_operations"] + ) + if forge_reporting and "work-item-traceability" not in modules: + modules.append("work-item-traceability") + reasons.append("provider issue reporting still needs traceability to plans and delivery evidence") + if forge_reporting and "forge-issue-reporting" not in modules: + modules.append("forge-issue-reporting") + reasons.append("repo language indicates permission-aware reporting to an owned or permissioned forge target") + if signals["mentions_github_issue_operations"] and "github-issue-operations" not in modules: + modules.append("github-issue-operations") + reasons.append("repo language explicitly references GitHub issue operations") + if signals["mentions_gitlab_issue_operations"] and "gitlab-issue-operations" not in modules: + modules.append("gitlab-issue-operations") + reasons.append("repo language explicitly references GitLab or glab issue operations") if signals["mentions_upstream_fork"] and "upstream-fork-maintenance" not in modules: modules.append("upstream-fork-maintenance") reasons.append("repo signals indicate private or local work layered on a non-owned upstream") diff --git a/.agents/skills/repo-policy-selector/tests/test_audit_active_lanes.py b/.agents/skills/repo-policy-selector/tests/test_audit_active_lanes.py index e02e08c..f4092b3 100644 --- a/.agents/skills/repo-policy-selector/tests/test_audit_active_lanes.py +++ b/.agents/skills/repo-policy-selector/tests/test_audit_active_lanes.py @@ -50,11 +50,11 @@ def make_registered_active_repo(self) -> tuple[Path, tempfile.TemporaryDirectory worktree / plan_path, """# Plan 0042 | Carrier Reconciliation -State: OPEN -Lane: P42 -Branch: feature/p42-carrier-reconciliation -Target: main -Integration: merge +State: `open` +Lane: `p42` +Branch: `feature/p42-carrier-reconciliation` +Target: `main` +Integration: `merge` ## Current State @@ -126,6 +126,49 @@ def test_registered_pushed_lane_with_worktree_is_active(self) -> None: self.assertEqual(lane["checkpoint"], lane["local_tip"]) self.assertEqual(len(lane["worktrees"]), 1) + def test_required_work_item_tracking_fails_when_lane_has_no_locator(self) -> None: + repo, temporary = self.make_registered_active_repo() + self.addCleanup(temporary.cleanup) + catalog = repo / "docs/dev/active-lanes.yaml" + catalog.write_text( + catalog.read_text(encoding="utf-8").replace( + "schema_version: 1", "schema_version: 1\nwork_item_tracking: required" + ), + encoding="utf-8", + ) + self.git(repo, "add", "docs/dev/active-lanes.yaml") + self.git(repo, "commit", "-q", "-m", "Require work-item tracking") + + result = self.run_audit(repo) + + self.assertEqual(result.returncode, 1) + report = json.loads(result.stdout) + self.assertEqual(report["work_item_tracking"], "required") + self.assertIn( + "P42: work_items must not be empty when tracking is required", + report["problems"], + ) + + def test_required_work_item_tracking_accepts_compact_locator(self) -> None: + repo, temporary = self.make_registered_active_repo() + self.addCleanup(temporary.cleanup) + catalog = repo / "docs/dev/active-lanes.yaml" + catalog.write_text( + catalog.read_text(encoding="utf-8") + .replace("schema_version: 1", "schema_version: 1\nwork_item_tracking: required") + .replace(" dependencies: []", " work_items: [owner/repo#42]\n dependencies: []"), + encoding="utf-8", + ) + self.git(repo, "add", "docs/dev/active-lanes.yaml") + self.git(repo, "commit", "-q", "-m", "Link lane work item") + + result = self.run_audit(repo) + + self.assertEqual(result.returncode, 0, result.stderr) + report = json.loads(result.stdout) + self.assertEqual(report["work_item_tracking"], "required") + self.assertEqual(report["lanes"][0]["work_items"], ["owner/repo#42"]) + def test_duplicate_lane_ids_fail_closed(self) -> None: repo, temporary = self.make_registered_active_repo() self.addCleanup(temporary.cleanup) @@ -320,6 +363,71 @@ def test_active_worktree_registration_without_worktree_is_missing(self) -> None: self.assertEqual(lane["worktrees"], []) self.assertIn("P42: ACTIVE_WORKTREE lane has no assigned worktree", report["problems"]) + def test_remote_only_active_lane_is_portable_across_workstations(self) -> None: + repo, temporary = self.make_registered_active_repo() + self.addCleanup(temporary.cleanup) + worktree = Path(self.git(repo, "worktree", "list", "--porcelain").split("worktree ")[2].splitlines()[0]) + self.git(repo, "worktree", "remove", str(worktree)) + self.git(repo, "branch", "-D", "feature/p42-carrier-reconciliation") + catalog = repo / "docs/dev/active-lanes.yaml" + catalog.write_text( + catalog.read_text(encoding="utf-8").replace( + "plan_ref: refs/heads/feature/p42-carrier-reconciliation", + "plan_ref: feature/p42-carrier-reconciliation", + ), + encoding="utf-8", + ) + self.git(repo, "add", "docs/dev/active-lanes.yaml") + self.git(repo, "commit", "-q", "-m", "Use workstation-neutral plan ref") + + result = self.run_audit(repo) + + self.assertEqual(result.returncode, 0, result.stdout) + report = json.loads(result.stdout) + lane = report["lanes"][0] + self.assertEqual(lane["local_remote_relation"], "remote_only") + self.assertEqual(lane["findings"], ["remote_active"]) + + def test_target_uses_explicit_remote_default_ref(self) -> None: + repo, temporary = self.make_registered_active_repo() + self.addCleanup(temporary.cleanup) + writer = repo.parent / "integrator" + remote = self.git(repo, "remote", "get-url", "origin") + subprocess.run(["git", "clone", "-q", "-b", "main", remote, str(writer)], check=True) + self.git(writer, "config", "user.email", "test@example.com") + self.git(writer, "config", "user.name", "Test") + self.git(writer, "merge", "-q", "--no-ff", "-m", "Integrate lane", "origin/feature/p42-carrier-reconciliation") + catalog = writer / "docs/dev/active-lanes.yaml" + catalog.write_text( + catalog.read_text(encoding="utf-8") + .replace("plan_state: OPEN", "plan_state: CLOSED") + .replace("custody_state: ACTIVE_WORKTREE", "custody_state: INTEGRATED"), + encoding="utf-8", + ) + self.git(writer, "add", "docs/dev/active-lanes.yaml") + self.git(writer, "commit", "-q", "-m", "Record lane integration") + self.git(writer, "push", "-q", "origin", "main") + self.git(repo, "fetch", "-q", "origin") + + result = subprocess.run( + [ + sys.executable, + str(SCRIPT), + "--repo-root", + str(repo), + "--default-ref", + "refs/remotes/origin/main", + "--json", + ], + capture_output=True, + text=True, + ) + + self.assertEqual(result.returncode, 0, result.stdout) + lane = json.loads(result.stdout)["lanes"][0] + self.assertTrue(lane["integrated_into_target"]) + self.assertIn("integrated_cleanup_pending", lane["findings"]) + def test_pushed_branch_without_worktree_can_be_paused(self) -> None: repo, temporary = self.make_registered_active_repo() self.addCleanup(temporary.cleanup) diff --git a/.agents/skills/repo-policy-selector/tests/test_audit_planning_contract.py b/.agents/skills/repo-policy-selector/tests/test_audit_planning_contract.py index bfea3a0..ef7f3ed 100644 --- a/.agents/skills/repo-policy-selector/tests/test_audit_planning_contract.py +++ b/.agents/skills/repo-policy-selector/tests/test_audit_planning_contract.py @@ -190,6 +190,71 @@ def test_planning_only_does_not_require_roadmap_runbook_or_lane(self): self.assertTrue(report["applicable"]) self.assertTrue(report["ok"], report["problems"]) + def test_active_only_recognizes_normalized_blocked_state_and_wide_lane(self): + root = self.make_repo(("planning-discipline",)) + plans = root / "docs/dev/plans" + plans.mkdir(parents=True) + (plans / "0001-2026-07-20-blocked.md").write_text( + "State: `blocked`\nLane: `p0118`\n## Current State\nWaiting.\n", + encoding="utf-8", + ) + + report = self.audit.audit_repo(root, active_only=True) + + self.assertTrue(report["ok"], report["problems"]) + self.assertEqual(report["plans"][0]["state"], "BLOCKED") + self.assertEqual(report["plans"][0]["lane_id"], "P0118") + self.assertEqual(report["excluded_unclassified_plans"], []) + + def test_active_only_rejects_state_found_only_in_plan_body(self): + root = self.make_repo(("planning-discipline",)) + plans = root / "docs/dev/plans" + plans.mkdir(parents=True) + (plans / "0001-2026-07-20-ambiguous.md").write_text( + "# Plan\n\n## Discussion\nState: OPEN\n", + encoding="utf-8", + ) + + report = self.audit.audit_repo(root, active_only=True) + + self.assertFalse(report["ok"]) + self.assertIn( + "plan missing deterministic state: 0001-2026-07-20-ambiguous.md", + report["problems"], + ) + + def test_recognized_plan_indexes_are_not_audited_as_plans(self): + root = self.make_repo(("planning-discipline",)) + plans = root / "docs/dev/plans" + plans.mkdir(parents=True) + (plans / "README.md").write_text("# Plan index\n", encoding="utf-8") + (plans / "INDEX.md").write_text("# Plan index\n", encoding="utf-8") + + report = self.audit.audit_repo(root) + + self.assertTrue(report["ok"], report["problems"]) + self.assertEqual(report["plans"], []) + + def test_doc_dev_layout_is_detected_without_path_overrides(self): + root = self.make_repo(("planning-discipline",)) + (root / "docs").rename(root / "doc") + (root / "AGENTS.md").write_text( + "Read and follow the files under doc/dev/policies.\n", + encoding="utf-8", + ) + plans = root / "doc/dev/plans" + plans.mkdir(parents=True) + (plans / "0001-2026-07-20-work.md").write_text( + "State: CLOSED\n", + encoding="utf-8", + ) + + report = self.audit.audit_repo(root) + + self.assertTrue(report["applicable"]) + self.assertTrue(report["ok"], report["problems"]) + self.assertEqual([item["file"] for item in report["plans"]], ["0001-2026-07-20-work.md"]) + def test_roadmap_contract_requires_wiring_but_allows_non_turn_sections(self): root = self.make_repo(("planning-discipline", "roadmap-runbook-governance")) plans = root / "docs/dev/plans" @@ -209,6 +274,28 @@ def test_roadmap_contract_requires_wiring_but_allows_non_turn_sections(self): self.assertTrue(report["ok"], report["problems"]) + def test_roadmap_contract_accepts_wide_lane_and_dated_turn_dialect(self): + root = self.make_repo(("planning-discipline", "roadmap-runbook-governance")) + plans = root / "docs/dev/plans" + plans.mkdir(parents=True) + plan_name = "0001-2026-07-20-work.md" + (plans / plan_name).write_text( + "State: OPEN\nLane: P0118\n## Current State\nReady.\n", + encoding="utf-8", + ) + (root / "ROADMAP.md").write_text( + f"# Roadmap\n\n## P0118 | Work\nState: OPEN\nCurrent State: Ready\n{plan_name}\n", + encoding="utf-8", + ) + (root / "RUNBOOK.md").write_text( + f"# Runbook\n\n## Turn 001 - 2026-07-20\n{plan_name}\n", + encoding="utf-8", + ) + + report = self.audit.audit_repo(root) + + self.assertTrue(report["ok"], report["problems"]) + def test_active_only_excludes_closed_and_unclassified_legacy_plans(self): root = self.make_repo(("planning-discipline",)) plans = root / "docs/dev/plans" @@ -317,4 +404,4 @@ def test_active_only_still_rejects_malformed_turn_headings(self): report = self.audit.audit_repo(root, active_only=True) self.assertFalse(report["ok"]) - self.assertTrue(any("RUNBOOK.md has headings" in item for item in report["problems"])) + self.assertTrue(any("RUNBOOK.md has Turn headings" in item for item in report["problems"])) diff --git a/.agents/skills/repo-policy-selector/tests/test_collaborative_development_policy_contract.py b/.agents/skills/repo-policy-selector/tests/test_collaborative_development_policy_contract.py new file mode 100644 index 0000000..d33d88d --- /dev/null +++ b/.agents/skills/repo-policy-selector/tests/test_collaborative_development_policy_contract.py @@ -0,0 +1,56 @@ +from pathlib import Path +import unittest + + +class CollaborativeDevelopmentPolicyContractTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.repo_root = Path(__file__).resolve().parents[2] + cls.bundle_root = cls.repo_root / "repo-policy-selector" / "policy-library" + + def module_text(self) -> str: + return ( + self.repo_root / "modules" / "collaborative-development-workflow.md" + ).read_text(encoding="utf-8") + + def test_collaboration_contract_has_required_boundaries(self) -> None: + text = " ".join(self.module_text().split()) + + for required in ( + "accountable human owner", + "shared forge is the coordination and source-custody system of record", + "only a merged pull request may modify it", + "Do not push directly to the canonical branch", + "search open work items and pull requests", + "Claim it with an accountable owner", + "never let two people or agent sessions edit the same checkout", + "pull request linked to its work item", + "may self-check and merge their own pull request", + "Production deployment is allowed only from an exact commit", + "verify the commit entered the branch through a merged pull request", + "Deployment automation must fail closed", + "urgent fixes through the same accelerated issue", + "Do not duplicate active state in GitHub Issues and Jira", + ): + self.assertIn(required, text) + + for blanket_gate in ( + "one required peer review", + "someone other than the author", + "a second human review is required", + "one-item WIP limit", + ): + self.assertNotIn(blanket_gate, text) + + def test_source_module_matches_selector_bundle(self) -> None: + source = self.repo_root / "modules" / "collaborative-development-workflow.md" + bundled = ( + self.bundle_root + / "modules" + / "collaborative-development-workflow.md" + ) + self.assertEqual(source.read_bytes(), bundled.read_bytes()) + + +if __name__ == "__main__": + unittest.main() diff --git a/.agents/skills/repo-policy-selector/tests/test_measurement_routing_policy_contract.py b/.agents/skills/repo-policy-selector/tests/test_measurement_routing_policy_contract.py new file mode 100644 index 0000000..c1bb729 --- /dev/null +++ b/.agents/skills/repo-policy-selector/tests/test_measurement_routing_policy_contract.py @@ -0,0 +1,54 @@ +from pathlib import Path +import unittest + + +class MeasurementRoutingPolicyContractTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.repo_root = Path(__file__).resolve().parents[2] + cls.bundle_root = cls.repo_root / "repo-policy-selector" / "policy-library" + + def module_text(self, module_id: str) -> str: + return (self.repo_root / "modules" / f"{module_id}.md").read_text(encoding="utf-8") + + def normalized_module_text(self, module_id: str) -> str: + return " ".join(self.module_text(module_id).split()) + + def test_measurement_first_and_causal_invalidation_contract(self) -> None: + planning = self.normalized_module_text("planning-discipline") + goal = self.normalized_module_text("goal-execution-governance") + validation = self.normalized_module_text("validation-and-handoff") + + self.assertIn("smallest usable baseline", planning) + self.assertIn("independent acceptance axes", planning) + self.assertIn("every packet required for acceptance", goal) + self.assertIn("causal path", goal) + self.assertIn("evidence deadline", goal) + self.assertIn("Preserve every completed sample", validation) + self.assertIn("invalidation map", validation) + + def test_economical_worker_contract(self) -> None: + model = self.normalized_module_text("model-selection-and-calibration") + subagent = self.normalized_module_text("subagent-workflow-optimization") + + self.assertIn("to tools before any model", model) + self.assertIn("calibrated economical tier", model) + self.assertIn("final acceptance claim", model) + self.assertIn("Prefer economical workers", subagent) + self.assertIn("Repeating its full investigation", subagent) + + def test_changed_source_modules_match_selector_bundle(self) -> None: + for module_id in ( + "planning-discipline", + "goal-execution-governance", + "validation-and-handoff", + "model-selection-and-calibration", + "subagent-workflow-optimization", + ): + source = self.repo_root / "modules" / f"{module_id}.md" + bundled = self.bundle_root / "modules" / f"{module_id}.md" + self.assertEqual(source.read_bytes(), bundled.read_bytes(), module_id) + + +if __name__ == "__main__": + unittest.main() diff --git a/.agents/skills/repo-policy-selector/tests/test_multi_session_policy_harvest_contract.py b/.agents/skills/repo-policy-selector/tests/test_multi_session_policy_harvest_contract.py new file mode 100644 index 0000000..71bd817 --- /dev/null +++ b/.agents/skills/repo-policy-selector/tests/test_multi_session_policy_harvest_contract.py @@ -0,0 +1,84 @@ +from pathlib import Path +import unittest + + +class MultiSessionPolicyHarvestContractTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.repo_root = Path(__file__).resolve().parents[2] + cls.bundle_root = cls.repo_root / "repo-policy-selector" / "policy-library" + + def module_text(self, module_id: str) -> str: + return (self.repo_root / "modules" / f"{module_id}.md").read_text( + encoding="utf-8" + ) + + def test_worktree_lifecycle_is_explicit(self) -> None: + text = " ".join(self.module_text("git-worktree-hygiene").split()) + for required in ( + "whether an existing clean checkout already owns the intended branch and lane", + "Create a new worktree only when", + "do not repurpose another active lane's checkout", + "Close a worktree promptly", + "Do not accumulate idle worktrees", + "verify the exact path is absent", + ): + self.assertIn(required, text) + + def test_coordination_contracts_are_split_across_existing_modules(self) -> None: + active_lane = " ".join(self.module_text("active-lane-coordination").split()) + reconciliation = " ".join( + self.module_text("multi-agent-reconciliation").split() + ) + parallel = " ".join(self.module_text("parallel-plan-design").split()) + + self.assertIn("one accountable execution owner", active_lane) + self.assertIn("one coordination owner", active_lane) + self.assertIn("single coordination owner", reconciliation) + self.assertIn("Session topology is an implementation choice", reconciliation) + self.assertIn("before dependent implementations fan out", parallel) + + def test_development_runtime_isolation_has_required_boundaries(self) -> None: + text = " ".join(self.module_text("development-runtime-isolation").split()) + for required in ( + "production, staging, and development runtimes", + "each concurrently executing development lane", + "Bind runtime identity to the lane and exact source checkpoint", + "Do not inherit production credentials", + "singleton resources as serialized", + "Teardown must target only the named lane runtime", + ): + self.assertIn(required, text) + + def test_source_modules_match_selector_bundle(self) -> None: + for module_id in ( + "active-lane-coordination", + "development-runtime-isolation", + "git-worktree-hygiene", + "multi-agent-reconciliation", + "parallel-plan-design", + ): + source = self.repo_root / "modules" / f"{module_id}.md" + bundled = self.bundle_root / "modules" / f"{module_id}.md" + self.assertEqual(source.read_bytes(), bundled.read_bytes(), module_id) + + def test_new_module_is_cataloged_and_profiled(self) -> None: + source_catalog = (self.repo_root / "catalog.yaml").read_text(encoding="utf-8") + bundled_catalog = (self.bundle_root / "catalog.yaml").read_text( + encoding="utf-8" + ) + source_profile = ( + self.repo_root / "profiles" / "operations-platform.yaml" + ).read_text(encoding="utf-8") + bundled_profile = ( + self.bundle_root / "profiles" / "operations-platform.yaml" + ).read_text(encoding="utf-8") + + self.assertIn("id: development-runtime-isolation", source_catalog) + self.assertEqual(source_catalog, bundled_catalog) + self.assertIn("- development-runtime-isolation", source_profile) + self.assertEqual(source_profile, bundled_profile) + + +if __name__ == "__main__": + unittest.main() diff --git a/.agents/skills/repo-policy-selector/tests/test_preflight_forge_issue.py b/.agents/skills/repo-policy-selector/tests/test_preflight_forge_issue.py new file mode 100644 index 0000000..e705238 --- /dev/null +++ b/.agents/skills/repo-policy-selector/tests/test_preflight_forge_issue.py @@ -0,0 +1,277 @@ +import importlib.util +import io +import json +import tempfile +import unittest +from contextlib import redirect_stdout +from pathlib import Path +from unittest.mock import patch + + +SCRIPT_PATH = Path(__file__).resolve().parents[1] / "scripts" / "preflight_forge_issue.py" + + +def load_module(): + spec = importlib.util.spec_from_file_location("preflight_forge_issue", SCRIPT_PATH) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +class ForgeIssuePreflightTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.module = load_module() + + def github_target(self): + return { + "id": "odollo", + "forge": "github", + "host": "github.com", + "repository": "example/odollo", + "relationship": "owned", + "allowed_actions": ["read", "create", "comment", "apply_labels", "close"], + "security_route": "private_vulnerability_reporting", + "label_map": { + "intent/defect": {"provider_label": "bug"}, + "priority/high": {"provider_label": "priority: high"}, + }, + } + + def github_snapshot(self): + return { + "forge": "github", + "host": "github.com", + "repository": "Example/Odollo", + "authenticated_actor": "operator", + "effective_role": "TRIAGE", + "issues_enabled": True, + "archived": False, + "available_labels": ["bug", "priority: high"], + "duplicate_candidates": [], + "source": "fixture", + } + + def test_owned_github_create_with_governed_labels_passes(self): + result = self.module.evaluate( + self.github_target(), + self.github_snapshot(), + action="create", + label_intents=["intent/defect", "priority/high"], + report_kind="defect", + idempotency_key="odollo:duplicate-order:v1", + ) + + self.assertTrue(result["ok"], result) + self.assertEqual(result["resolved_labels"], ["bug", "priority: high"]) + self.assertEqual( + result["label_resolution"], + [ + {"intent": "intent/defect", "provider_label": "bug"}, + {"intent": "priority/high", "provider_label": "priority: high"}, + ], + ) + self.assertEqual(result["required_actions"], ["create", "apply_labels"]) + self.assertFalse(result["operator_authority_verified"]) + self.assertFalse(result["mutation_authorized"]) + + def test_unknown_label_intent_fails_closed(self): + result = self.module.evaluate( + self.github_target(), + self.github_snapshot(), + action="create", + label_intents=["priority/emergency"], + report_kind="defect", + idempotency_key="odollo:duplicate-order:v1", + ) + + self.assertFalse(result["ok"]) + self.assertIn("unknown normalized label intent: priority/emergency", result["problems"]) + + def test_missing_provider_label_is_not_created_during_preflight(self): + snapshot = self.github_snapshot() + snapshot["available_labels"] = ["bug"] + result = self.module.evaluate( + self.github_target(), + snapshot, + action="create", + label_intents=["priority/high"], + report_kind="defect", + idempotency_key="odollo:duplicate-order:v1", + ) + + self.assertFalse(result["ok"]) + self.assertIn("mapped provider label does not exist: priority: high", result["problems"]) + + def test_label_application_requires_separate_allowlist_and_role(self): + target = self.github_target() + target["allowed_actions"] = ["read", "create"] + snapshot = self.github_snapshot() + snapshot["effective_role"] = "READ" + result = self.module.evaluate( + target, + snapshot, + action="create", + label_intents=["intent/defect"], + report_kind="defect", + idempotency_key="odollo:duplicate-order:v1", + ) + + self.assertFalse(result["ok"]) + self.assertIn("action is not allowlisted: apply_labels", result["problems"]) + self.assertIn("insufficient provider role for apply_labels: READ < TRIAGE", result["problems"]) + + def test_duplicate_candidate_blocks_create(self): + snapshot = self.github_snapshot() + snapshot["duplicate_candidates"] = [{"number": 42, "title": "Existing"}] + result = self.module.evaluate( + self.github_target(), + snapshot, + action="create", + label_intents=[], + report_kind="defect", + idempotency_key="odollo:duplicate-order:v1", + ) + + self.assertFalse(result["ok"]) + self.assertIn("duplicate candidates found for the idempotency key", result["problems"]) + + def test_target_drift_archive_and_disabled_issues_fail_closed(self): + snapshot = self.github_snapshot() + snapshot["host"] = "github.example.com" + snapshot["repository"] = "example/other" + snapshot["archived"] = True + snapshot["issues_enabled"] = False + result = self.module.evaluate( + self.github_target(), + snapshot, + action="read", + label_intents=[], + report_kind="governance", + idempotency_key=None, + ) + + self.assertFalse(result["ok"]) + self.assertIn("target drift: snapshot host does not match registry", result["problems"]) + self.assertIn("target drift: snapshot repository does not match registry", result["problems"]) + self.assertIn("target repository is archived or archive state is unknown", result["problems"]) + self.assertIn("target issue surface is disabled or unknown", result["problems"]) + + def test_gitlab_nested_namespace_guest_can_create_with_existing_label(self): + target = { + "id": "litscout", + "forge": "gitlab", + "host": "gitlab.example.com", + "repository": "research/tools/litscout", + "relationship": "permissioned", + "allowed_actions": ["read", "create", "comment", "apply_labels"], + "security_route": "confidential_issue", + "label_map": {"intent/defect": {"provider_label": "type::bug"}}, + } + snapshot = { + "forge": "gitlab", + "host": "gitlab.example.com", + "repository": "research/tools/litscout", + "authenticated_actor": "operator", + "effective_role": "GUEST", + "issues_enabled": True, + "archived": False, + "available_labels": ["type::bug"], + "duplicate_candidates": [], + "source": "fixture", + } + result = self.module.evaluate( + target, + snapshot, + action="create", + label_intents=["intent/defect"], + report_kind="defect", + idempotency_key="litscout:parser:v1", + ) + + self.assertTrue(result["ok"], result) + + later_mutation = self.module.evaluate( + target, + snapshot, + action="apply_labels", + label_intents=["intent/defect"], + report_kind="defect", + idempotency_key=None, + ) + self.assertFalse(later_mutation["ok"]) + self.assertIn( + "insufficient provider role for apply_labels: GUEST < PLANNER", + later_mutation["problems"], + ) + + def test_security_report_requires_private_route(self): + target = self.github_target() + target["security_route"] = "none" + result = self.module.evaluate( + target, + self.github_snapshot(), + action="create", + label_intents=[], + report_kind="security", + idempotency_key="odollo:security:v1", + ) + + self.assertFalse(result["ok"]) + self.assertIn("security report has no approved private route", result["problems"]) + + def test_registry_rejects_duplicate_ids_and_duplicate_provider_labels(self): + target = self.github_target() + duplicate = dict(target) + duplicate["label_map"] = { + "intent/defect": {"provider_label": "bug"}, + "intent/regression": {"provider_label": "bug"}, + } + problems = self.module.validate_registry({"schema_version": 1, "targets": [target, duplicate]}) + + self.assertIn("duplicate target id: odollo", problems) + self.assertTrue(any("maps more than one intent to bug" in problem for problem in problems)) + + def test_provider_api_timeout_fails_closed(self): + expired = self.module.subprocess.TimeoutExpired(cmd=["glab"], timeout=20) + with patch.object(self.module.subprocess, "run", side_effect=expired): + with self.assertRaisesRegex(self.module.PreflightError, "provider command timed out"): + self.module.run_json(["glab", "api", "user"]) + + def test_cli_snapshot_mode_is_provider_free(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + registry_path = root / "targets.json" + snapshot_path = root / "snapshot.json" + registry_path.write_text( + json.dumps({"schema_version": 1, "targets": [self.github_target()]}), + encoding="utf-8", + ) + snapshot_path.write_text(json.dumps(self.github_snapshot()), encoding="utf-8") + + output = io.StringIO() + with redirect_stdout(output): + exit_code = self.module.main( + [ + "--registry", + str(registry_path), + "--target", + "odollo", + "--action", + "create", + "--label", + "intent/defect", + "--idempotency-key", + "odollo:duplicate-order:v1", + "--snapshot", + str(snapshot_path), + ] + ) + + self.assertEqual(exit_code, 0) + self.assertTrue(json.loads(output.getvalue())["ok"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/.agents/skills/repo-policy-selector/tests/test_select_policy.py b/.agents/skills/repo-policy-selector/tests/test_select_policy.py index 9ff6835..463b1de 100644 --- a/.agents/skills/repo-policy-selector/tests/test_select_policy.py +++ b/.agents/skills/repo-policy-selector/tests/test_select_policy.py @@ -321,6 +321,36 @@ def test_harvest_policy_does_not_semantically_adopt_feedback_or_preview_policy(s self.assertNotIn("policy-adoption-feedback-loop", semantic_matches) self.assertNotIn("preview-artifact-review", semantic_matches) + def test_model_selection_reference_does_not_replace_its_local_policy(self): + repo_root = self.make_repo() + policy_dir = repo_root / "docs" / "dev" / "policies" + policy_dir.mkdir(parents=True, exist_ok=True) + (policy_dir / "0002-subagent-runtime-governance.md").write_text( + "# Policy | Subagent Runtime Governance\n\n" + "## Policy\n\n" + "- Apply `model-selection-and-calibration` for task-tier selection.\n", + encoding="utf-8", + ) + + installed_library = self.select_policy.enumerate_policy_library(self.policy_root) + surfaces = self.select_policy.extract_existing_policy_surfaces(repo_root) + semantic_matches = self.select_policy.semantic_module_matches(surfaces, installed_library) + coverage = self.select_policy.policy_adoption_coverage( + surfaces, + ["model-selection-and-calibration"], + installed_library, + ) + plan = self.select_policy.build_install_plan( + repo_root, + coverage["missing_recommended_modules"], + coverage, + installed_library, + ) + + self.assertNotIn("model-selection-and-calibration", semantic_matches) + self.assertEqual(coverage["missing_recommended_modules"], ["model-selection-and-calibration"]) + self.assertEqual(plan[0]["action"], "install-new") + def test_memory_consumer_language_does_not_imply_runtime_operations(self): repo_root = self.make_repo( agents_text=""" @@ -454,6 +484,148 @@ def test_active_lane_coordination_is_limited_to_multi_track_profiles(self): modules = self.select_policy.base_modules_for_profile(profile_id, installed_library) self.assertNotIn("active-lane-coordination", modules, profile_id) + def test_work_item_traceability_defaults_to_complex_profiles(self): + installed_library = self.select_policy.enumerate_policy_library(self.policy_root) + for profile_id in ["repo-product-engineering", "operations-platform"]: + modules = self.select_policy.base_modules_for_profile(profile_id, installed_library) + self.assertIn("work-item-traceability", modules, profile_id) + for profile_id in ["writing-project", "standalone-library"]: + modules = self.select_policy.base_modules_for_profile(profile_id, installed_library) + self.assertNotIn("work-item-traceability", modules, profile_id) + + def test_issue_tracking_language_selects_work_item_traceability(self): + repo_root = self.make_repo( + readme_text=""" + # Grant Proposal + + The issue tracker is the backlog and uses WIP limits and issue dependencies. + """ + ) + installed_library = self.select_policy.enumerate_policy_library(self.policy_root) + signals = self.select_policy.detect_signals(repo_root) + purpose, _subtype, _execution_bias, profile, modules, reasons = self.select_policy.choose_profile( + signals, installed_library + ) + + self.assertEqual((purpose, profile), ("writing-project", "writing-project")) + self.assertTrue(signals["mentions_work_item_tracking"]) + self.assertIn("work-item-traceability", modules, reasons) + + def test_collaborative_development_selects_complete_workflow(self): + repo_root = self.make_repo( + readme_text=""" + # Shared Website Repository + + Multiple contributors use feature branches and isolated worktrees. + Every change follows a pull request workflow with peer review before + deployment. + """ + ) + installed_library = self.select_policy.enumerate_policy_library(self.policy_root) + signals = self.select_policy.detect_signals(repo_root) + _purpose, _subtype, _execution_bias, _profile, modules, reasons = self.select_policy.choose_profile( + signals, installed_library + ) + + self.assertTrue(signals["mentions_collaborative_development"]) + for module_id in ( + "work-item-traceability", + "git-worktree-hygiene", + "commit-history-discipline", + "branch-and-integration-strategy", + "commit-and-push-cadence", + "validation-and-handoff", + "collaborative-development-workflow", + ): + self.assertIn(module_id, modules, reasons) + + def test_incidental_collaboration_word_does_not_select_workflow(self): + repo_root = self.make_repo( + readme_text=""" + # Library + + This package collaborates with an external rendering service. + """ + ) + + signals = self.select_policy.detect_signals(repo_root) + + self.assertFalse(signals["mentions_collaborative_development"]) + + def test_github_issue_reporting_selects_core_and_github_adapter(self): + repo_root = self.make_repo( + readme_text=""" + # Maintainer Tool + + Report findings through GitHub Issues on an owned repository. + Use gh issue only after checking the GitHub issue form and labels. + """ + ) + installed_library = self.select_policy.enumerate_policy_library(self.policy_root) + signals = self.select_policy.detect_signals(repo_root) + _purpose, _subtype, _execution_bias, _profile, modules, reasons = self.select_policy.choose_profile( + signals, installed_library + ) + + self.assertTrue(signals["mentions_github_issue_operations"]) + self.assertIn("work-item-traceability", modules, reasons) + self.assertIn("forge-issue-reporting", modules, reasons) + self.assertIn("github-issue-operations", modules, reasons) + self.assertNotIn("gitlab-issue-operations", modules, reasons) + + def test_gitlab_issue_reporting_selects_core_and_gitlab_adapter(self): + repo_root = self.make_repo( + readme_text=""" + # Maintainer Tool + + Use glab issue creation for a permissioned repo issue on a + self-managed GitLab host. Respect GitLab issue templates and labels. + """ + ) + installed_library = self.select_policy.enumerate_policy_library(self.policy_root) + signals = self.select_policy.detect_signals(repo_root) + _purpose, _subtype, _execution_bias, _profile, modules, reasons = self.select_policy.choose_profile( + signals, installed_library + ) + + self.assertTrue(signals["mentions_forge_issue_reporting"]) + self.assertTrue(signals["mentions_gitlab_issue_operations"]) + self.assertIn("work-item-traceability", modules, reasons) + self.assertIn("forge-issue-reporting", modules, reasons) + self.assertIn("gitlab-issue-operations", modules, reasons) + self.assertNotIn("github-issue-operations", modules, reasons) + + def test_cross_forge_language_selects_both_adapters(self): + repo_root = self.make_repo( + readme_text=""" + # Cross-forge Reporter + + This tool supports GitHub Issues with gh issue and GitLab Issues + with glab issue, using one issue target registry. + """ + ) + installed_library = self.select_policy.enumerate_policy_library(self.policy_root) + signals = self.select_policy.detect_signals(repo_root) + _purpose, _subtype, _execution_bias, _profile, modules, reasons = self.select_policy.choose_profile( + signals, installed_library + ) + + for module_id in ( + "work-item-traceability", + "forge-issue-reporting", + "github-issue-operations", + "gitlab-issue-operations", + ): + self.assertIn(module_id, modules, reasons) + + def test_forge_issue_modules_are_conditional_not_profile_defaults(self): + installed_library = self.select_policy.enumerate_policy_library(self.policy_root) + for profile_id in installed_library["profile_ids"]: + modules = self.select_policy.base_modules_for_profile(profile_id, installed_library) + self.assertNotIn("forge-issue-reporting", modules, profile_id) + self.assertNotIn("github-issue-operations", modules, profile_id) + self.assertNotIn("gitlab-issue-operations", modules, profile_id) + def test_codegraph_policy_maps_to_shared_codegraph_module(self): repo_root = self.make_repo( agents_text=""" @@ -645,6 +817,45 @@ def test_subagent_runtime_signal_is_specific_to_runtime_lifecycle(self): self.assertTrue(runtime_signals["mentions_subagents"]) self.assertTrue(runtime_signals["mentions_subagent_runtime"]) + def test_development_runtime_isolation_requires_explicit_lane_runtime_signal(self): + generic_repo = self.make_repo( + agents_text=""" + # Product Repo + + Run the local development server before submitting changes. + """, + ) + isolated_repo = self.make_repo( + agents_text=""" + # Multi-Lane Product Repo + + Each active branch that executes the service needs an isolated + development runtime with per-lane runtime state, ports, and data. + """, + ) + + generic_signals = self.select_policy.detect_signals(generic_repo) + isolated_signals = self.select_policy.detect_signals(isolated_repo) + installed_library = self.select_policy.enumerate_policy_library(self.policy_root) + _purpose, _subtype, _bias, _profile, modules, reasons = ( + self.select_policy.choose_profile(isolated_signals, installed_library) + ) + + self.assertFalse(generic_signals["mentions_development_runtime_isolation"]) + self.assertTrue(isolated_signals["mentions_development_runtime_isolation"]) + self.assertIn("development-runtime-isolation", modules, reasons) + + def test_every_profile_adopts_model_selection_and_calibration(self): + installed_library = self.select_policy.enumerate_policy_library(self.policy_root) + + self.assertIn("model-selection-and-calibration", installed_library["module_ids"]) + for profile_id, profile in installed_library["parsed_profiles"].items(): + self.assertIn( + "model-selection-and-calibration", + profile.get("modules", []), + profile_id, + ) + if __name__ == "__main__": unittest.main() diff --git a/AGENTS.md b/AGENTS.md index 549d7b9..1f6c7a4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -51,6 +51,7 @@ library is source material, not an additional set of active instructions. | Validating, reviewing, or handing off work | [0014-validation-and-handoff](docs/dev/policies/0014-validation-and-handoff.md) | | Fetching or integrating upstream changes | [0015-upstream-fork-maintenance](docs/dev/policies/0015-upstream-fork-maintenance.md) | | Managing issues, opening/reviewing/merging PRs | [0016-pull-request-and-issue-management](docs/dev/policies/0016-pull-request-and-issue-management.md) | +| Routing substantial agent work or reassessing model/effort | [0017-model-selection-and-calibration](docs/dev/policies/0017-model-selection-and-calibration.md) | Use `docs/dev/plans/` for bounded plans and `docs/dev/notes/` for dated feedback. The adoption-time branch inventory is [workstreams](docs/dev/workstreams.md). diff --git a/docs/dev/evidence/embedded-skill-upgrade.json b/docs/dev/evidence/embedded-skill-upgrade.json new file mode 100644 index 0000000..c05317e --- /dev/null +++ b/docs/dev/evidence/embedded-skill-upgrade.json @@ -0,0 +1,43 @@ +{ + "date": "20260922T161519Z", + "version": "0.2.9", + "backup": "/home/ecochran76/.dev-browser/backups/embedded-skill-20260922T161519Z", + "previousBinarySha256": "8ec4899b452e741c1dc848c05a4c848110370999004ef7fe51549a8e1498a56c", + "binarySha256": "04c73cb93e9a932b212b5671b95386aa0459af95ba8052e0ea16624429ef3691", + "skillSha256": "6dca7a17df9c8dbf236d1d8c58416ac6696f4a4970d1e446a7ebfd13ff1d46e9", + "installedSkillTargets": [ + "/home/ecochran76/.claude/skills/dev-browser/SKILL.md", + "/home/ecochran76/.agents/skills/dev-browser/SKILL.md", + "/home/ecochran76/.codex/skills/dev-browser/SKILL.md" + ], + "installerOutput": [ + "dev-browser skill is already installed at ~/.claude/skills/dev-browser/SKILL.md", + "dev-browser skill is already installed at ~/.agents/skills/dev-browser/SKILL.md", + "dev-browser skill is already installed at ~/.codex/skills/dev-browser/SKILL.md" + ], + "before": { + "daemonPid": "77326", + "daemonAlive": true, + "daemonSha256": "a824ab2457b7319acb39d49ed4f75aef20bbf62a9a850e10b2d40a79ac75b395", + "sandboxSha256": "ebd093d662faa8883b640860e60071b8f4c6f37f03ee3ebbeca572194560a7c8", + "configSha256": "99770ab7efafceb493324ed17ba2d2b7668e5692cf977651a5b112f688692d6c" + }, + "after": { + "daemonPid": "77326", + "daemonAlive": true, + "daemonSha256": "a824ab2457b7319acb39d49ed4f75aef20bbf62a9a850e10b2d40a79ac75b395", + "sandboxSha256": "ebd093d662faa8883b640860e60071b8f4c6f37f03ee3ebbeca572194560a7c8", + "configSha256": "99770ab7efafceb493324ed17ba2d2b7668e5692cf977651a5b112f688692d6c" + }, + "daemonPayloadChanged": false, + "browserConfigChanged": false, + "daemonRestarted": false, + "validation": [ + "daemon bundles rebuilt; hashes match installed payloads", + "TypeScript noEmit passed", + "cargo release build passed", + "cargo test: 12 passed", + "embedded skill exact bytes verified", + "install-skill all three targets passed" + ] +} diff --git a/docs/dev/evidence/graphiti-upgrade-feedback.json b/docs/dev/evidence/graphiti-upgrade-feedback.json new file mode 100644 index 0000000..d98ba03 --- /dev/null +++ b/docs/dev/evidence/graphiti-upgrade-feedback.json @@ -0,0 +1,9 @@ +{ + "jobId": "87999c8e-f81e-4124-9958-931e2de1c3e8", + "status": "timed_out", + "error": "Processing timed out after 90.0 seconds", + "groupId": "dev_browser_main", + "episodeUuid": null, + "exactMetadataReadback": "No matching episodes found", + "retryAttempted": false +} diff --git a/docs/dev/evidence/local-vitest-upgrade.json b/docs/dev/evidence/local-vitest-upgrade.json new file mode 100644 index 0000000..c2293c4 --- /dev/null +++ b/docs/dev/evidence/local-vitest-upgrade.json @@ -0,0 +1,17 @@ +{ + "result": "173 test assertions passed; 19 suites passed; 2 suites failed afterAll teardown hooks at 180 seconds", + "failedSuites": [ + "sandbox/__tests__/cua.test.ts navigation", + "sandbox/__tests__/playwright-api.test.ts navigation" + ], + "elapsedSeconds": 191.53, + "termination": "Suite exited before proposed SIGTERM; no signals sent", + "temporaryProfiles": [ + "/tmp/dev-browser-playwright-api-drJCsq", + "/tmp/dev-browser-cua-xmLaLp" + ], + "remainingLiveProfileProcessIds": [], + "observedTestRootAndWorkerPidsGone": true, + "existingDaemonAlive": true, + "runtimeChanges": "none; configured Chromium 150 used by test harness" +} diff --git a/docs/dev/notes/0002-2026-09-22-policy-v0126-upgrade.md b/docs/dev/notes/0002-2026-09-22-policy-v0126-upgrade.md new file mode 100644 index 0000000..a01e786 --- /dev/null +++ b/docs/dev/notes/0002-2026-09-22-policy-v0126-upgrade.md @@ -0,0 +1,71 @@ +# Policy v0.1.26 upgrade and embedded-skill refresh + +Reviewed 2026-09-22. Plan 0005; issue #5; PR #6. + +## Provenance and decisions + +The pinned repo-policy-selector moves from v0.1.22 to v0.1.26. Release tag +checkout: 3e5fd3e06e4133754e02b9a4df03f20476dba2a7; manifest source: +ead42f7d6932fedc9dea83111b234f954a6afa4d. Library SHA256: +1129e37be16efbf571954f80d06ea7115eb8f244acbd327003abe37e2dadaa96. + +Retain the standalone-library profile with fork-specific composition. Merge +planning-discipline (0005), git-worktree-hygiene (0008), and the additionally +retained validation-and-handoff (0014) in place. Adopt model-selection-and- +calibration as 0017 with a provisional inherited-session routing map, tool-first +mechanical work, and no independent authority to spawn agents. The other +retained identities remain unchanged, apart from the explicit source pin in +0002. AGENTS.md wires each of the 17 active policies exactly once. + +New forge/GitHub/GitLab issue and work-item modules remain deferred: local 0016 +already governs this fork's issue/PR lifecycle and we are not expanding to +other forges. Collaborative runtime/lane/goal modules remain deferred for this +bounded serial maintenance task. Nothing is retired. Graphiti routing, issue/PR +review gates, upstream integration, and browser ownership contracts stay local. + +## Experience and reproducibility + +Release notes v0.1.23 through v0.1.26 scoped review before installation. Existing +worktree reuse and identity-preserving updates worked cleanly. The upgrade-plan +helper needs an explicit policy root pointing to the upstream checkout when +comparing refs; omitting it initially produced false retire-review actions. +That output was discarded. The corrected standalone-library plan identified +0005, 0008, and new 0017; retained 0014 was reviewed separately because it is +outside that starter profile. Future upgrades must inspect the retained set, +not just the profile's suggested modules. No upstream issue was sent. + +```sh +python3 /tmp/dev-browser-policy-upgrade-v0126/repo-policy-selector/scripts/manage_policy.py --repo-root . --policy-root /tmp/dev-browser-policy-upgrade-v0126 upgrade-plan --profile standalone-library --policy-git-root /tmp/dev-browser-policy-upgrade-v0126 --baseline-ref v0.1.22 --current-ref v0.1.26 --json +python3 scripts/check-repo-policy.py +``` + +## Independent acceptance evidence + +- Policy installation/wiring: pinned manifest and installation record; full and + active planning audits pass with no baseline exceptions. +- CLI/skill correctness: v0.2.9 rebuilt with current skill; Rust 12 tests and + TypeScript check pass; installer verifies all three installed skill targets. + Exact hashes and backup path are in ../evidence/embedded-skill-upgrade.json. +- Runtime/resources: daemon and sandbox bundle hashes match installed payloads; + daemon PID 77326 stayed live, configuration hash unchanged. The full local Vitest run passed all 173 assertions but two navigation suites + failed 180-second afterAll teardown hooks with configured Chromium 150. + It exited before cleanup signals were sent; fresh process reads verified + workers and both temporary browser profiles gone + (../evidence/local-vitest-upgrade.json). CI uses its isolated Playwright + browser. This local teardown failure remains a compatibility limitation. + +- Model calibration/allocation savings/runtime enforcement: unverified, not + inferred from documentation or deterministic audit success. +- Integration: PR #6 carries the source change; CI and maintainer review remain + separate from local installation and are required before merge. + +Graphiti discovery assessment: skip renewed historical investigation because +current release refs, manifests, local policies, and receipts settle this scope. +The feedback mirror is tracked separately; local evidence remains authoritative. +Historical Plan 0004 and its receipt retain the earlier embedded-skill gap; +Plan 0005 and the new receipt supersede that installation state. + +Graphiti doctor was healthy. Exact metadata preflight found no existing feedback. +The compact mirror job 87999c8e-f81e-4124-9958-931e2de1c3e8 timed out after 90 +seconds; exact metadata readback still found no episode. Persistence is not +claimed. No retry or operational repair was attempted; repo feedback is durable. diff --git a/docs/dev/plans/0005-2026-09-22-policy-and-embedded-skill-upgrade.md b/docs/dev/plans/0005-2026-09-22-policy-and-embedded-skill-upgrade.md new file mode 100644 index 0000000..1f78f16 --- /dev/null +++ b/docs/dev/plans/0005-2026-09-22-policy-and-embedded-skill-upgrade.md @@ -0,0 +1,49 @@ +# Plan 0005: Upgrade pinned policy and embedded browser skill + +State: CLOSED +Owner: primary agent +Branch: chore/browser-skill-hygiene +Issue: #5 +PR: #6 + +## Current State + +Plan 0004 refreshed the skill files but the installed v0.2.9 CLI still embeds +the older skill. The selector bundle is pinned to v0.1.22. User requests both +remaining updates under the repository policy. Reuse the existing worktree. + +## Scope + +Review v0.1.26 against the retained modules, merge applicable changes without +losing local contracts, rebuild/install v0.2.9 with the current embedded skill, +and publish through PR #6. Use serial tools and the primary agent. + +## Non-goals + +No v1 migration, browser upgrade, daemon restart, session cleanup, unrelated +policy expansion, model calibration, or service-skill replacement. + +## Acceptance Criteria + +- One active file and AGENTS link per adopted policy identity; audits pass. +- Current installed CLI and all skill targets agree on the canonical skill. +- Installed daemon payload and browser configuration remain unchanged. +- Source changes and installation evidence are committed and published. +- CI and required maintainer review gate integration; no self-approved merge. + +## Definition of Done + +Record exact source pins, test results, installation hashes, and remaining +review/integration state. Close the implementation plan after publication; +keep the issue open until integration is verified. + +## Outcome + +Policy bundle v0.1.26 is installed and 17 active policies are uniquely wired; +full/active planning audits and 22 planning-auditor tests pass. TypeScript and +12 Rust tests pass; the v0.2.9 release CLI is installed with the current skill. +Installer round-trip and unchanged daemon/config hashes are recorded in +../evidence/embedded-skill-upgrade.json. Local browser tests passed 173 assertions +but two teardown hooks timed out; test processes exited and their absence was +verified. CI and maintainer review remain PR #6 integration gates. Graphiti +feedback mirroring timed out and is not claimed persisted. See dated note 0002. diff --git a/docs/dev/policies/0002-policy-upgrade-management.md b/docs/dev/policies/0002-policy-upgrade-management.md index e672c86..a0f03bc 100644 --- a/docs/dev/policies/0002-policy-upgrade-management.md +++ b/docs/dev/policies/0002-policy-upgrade-management.md @@ -48,7 +48,7 @@ Repo-type guidance: ## Fork-specific contract -Reviewed bundle: `v0.1.22`, source commit -`12a7f9fef466522e99be44d980c44a4ff056f540`. Review updates during release or +Reviewed bundle: `v0.1.26`, source commit +`ead42f7d6932fedc9dea83111b234f954a6afa4d`. Review updates during release or upstream-sync maintenance, using the installed manifest and explicit policy-root. Preserve the local PR/issue contract and fork overrides during upgrades. diff --git a/docs/dev/policies/0005-planning-discipline.md b/docs/dev/policies/0005-planning-discipline.md index cd7ed58..aa864ce 100644 --- a/docs/dev/policies/0005-planning-discipline.md +++ b/docs/dev/policies/0005-planning-discipline.md @@ -22,6 +22,10 @@ - `CLOSED` - `CANCELLED` - Multi-track repositories may also use `BLOCKED`. Keep this outcome state separate from Git custody such as active worktree, paused ref, integration-ready, integrated, archived, or discard-approved. +- Keep authoritative plan metadata in a bounded header before the first + second-level section. Auditors must not infer state or lane metadata from + narrative body text, and should exclude recognized plan indexes such as + `README.md` and `INDEX.md` from plan-file validation. - For any plan in an active state such as `OPEN`, require a short `Current State` section that says what already exists and what still remains. - Use bounded plan artifacts with explicit scope, non-goals, acceptance criteria, and definition of done. - A plan organizes execution; it does not grant, consume, or renew authority. @@ -44,6 +48,14 @@ change the outcome, scope, cost, or safety envelope. - Give each active execution packet one bounded outcome, owner, expected write surface, required inputs, validation evidence, and terminal condition. +- For measurement or optimization work, obtain and durably record the smallest + usable baseline before expanding into broad matrices, soak, maintenance, or + secondary acceptance checks. Order the packet so an early stop can still + answer the primary measurement question; breadth may follow after that + minimum evidence exists. +- Do not make independent acceptance axes one all-or-nothing packet by default. + State which checks govern measurement validity, correctness, resources, + maintenance, and evidence integrity, and how each result affects the others. - When active work lives off the default branch, keep execution detail in the branch-local plan and publish only a compact active-lane projection to the default branch. Plan closure does not by itself authorize branch deletion or worktree removal. - When a task is large enough to plan, explicitly separate: - parallelizable low-conflict tracks @@ -72,6 +84,7 @@ forced audits, and do not let one accepted finding suppress a new one. - Absence of a plans directory is not itself an active-scope defect. Continue to require the configured directory during full or forced structural audits. + ## Adoption Notes Use this module as a baseline in every starter profile. A lightweight repo may diff --git a/docs/dev/policies/0008-git-worktree-hygiene.md b/docs/dev/policies/0008-git-worktree-hygiene.md index e48a36a..58410d5 100644 --- a/docs/dev/policies/0008-git-worktree-hygiene.md +++ b/docs/dev/policies/0008-git-worktree-hygiene.md @@ -4,16 +4,33 @@ - Start branch-sensitive work by checking `git status`. - Inventory all registered worktrees with `git worktree list --porcelain` before creating, closing, pruning, or reassigning one; the current checkout alone is not the repository topology. +- Before creating a worktree, decide whether an existing clean checkout already + owns the intended branch and lane. Continue in the correct existing worktree + when it is safe; do not create duplicate checkouts merely to avoid orienting + to current custody. +- Create a new worktree only when the work needs an isolated branch, a separate + concurrent checkout, or continuity beyond the current session. Give it one + clear branch and purpose, and do not repurpose another active lane's checkout + by switching its branch or mixing in unrelated work. - Treat pre-existing dirty state as a real constraint. - Keep one bounded branch or worktree scope per execution slice or roadmap lane, consistent with the repo's documented integration model. - When parallel work is needed, prefer `git worktree` over a second full clone. - Do not call work merge-ready while the intended changes are still uncommitted. - Treat the worktree as a checkout, the branch or detached commit as local custody, and a verified remote or archive ref as shared custody. Removing a worktree does not preserve uncommitted changes and does not prove the commits remain discoverable. - Before removing a worktree, require a clean status, a named branch or explicitly preserved detached commit, an exact checkpoint SHA, and verified durable custody on the intended remote ref or on matching local and remote archive refs. +- Close a worktree promptly when its branch is integrated, its work is durably + handed off without needing the checkout, or its preserved branch is paused or + archived. Do not accumulate idle worktrees as informal reminders or confuse a + retained branch with a need to retain its checkout. - Normal closure uses `git worktree remove` without `--force`. Forced removal is exceptional recovery work: first inventory the exact path, preserve any recoverable diff and commit, establish a durable ref, record the reason, and verify the retained SHA. +- After removal, verify the exact path is absent from the registered worktree + inventory. Prune only stale administrative entries whose checkout absence and + branch custody have been established; pruning is not a substitute for closing + a live worktree deliberately. - Do not delete an unmerged branch merely because its worktree is gone. Prove integration, archival, or explicit discard approval separately. - If overlapping dirty work exists across branches or worktrees, open a reconciliation step rather than calling it a normal merge. - Keep branch scope narrow and avoid mixing unrelated lanes unless the active slice requires it. + ## Adoption Notes Use this module in repos where multiple lanes, multiple worktrees, or parallel agents regularly overlap. diff --git a/docs/dev/policies/0014-validation-and-handoff.md b/docs/dev/policies/0014-validation-and-handoff.md index 29091fb..2ae5c01 100644 --- a/docs/dev/policies/0014-validation-and-handoff.md +++ b/docs/dev/policies/0014-validation-and-handoff.md @@ -36,6 +36,14 @@ specification or acceptance contract. Do not let a pass on one axis mask a failure on the other, and do not let the separation bypass primary-agent evidence review and disposition. +- For experiments and multi-axis acceptance, record verdicts independently for + the primary measurement, correctness, resources, maintenance, and evidence + integrity when those axes apply. Preserve every completed sample with its + identity and conditions even when the overall packet later stops. +- Define an invalidation map before execution: each stop predicate names the + samples and verdicts it can invalidate and the causal reason. A later failure + may mark the packet incomplete without erasing unaffected valid samples. + Never promote partial evidence into a complete acceptance claim. - Separate review modes. Use at most one broad fresh-context `drift_discovery` pass when observed drift, consequence, or uncertainty justifies it. After adjudication, use `closed_world` remediation @@ -56,6 +64,7 @@ - Treat fail-closed gates as successful policy execution when they prevent an unsafe or disproven change from integrating. Report the blocked outcome and evidence instead of grading effectiveness only by shipped changes. + ## Adoption Notes Use this module when the repo: diff --git a/docs/dev/policies/0017-model-selection-and-calibration.md b/docs/dev/policies/0017-model-selection-and-calibration.md new file mode 100644 index 0000000..f85b8f1 --- /dev/null +++ b/docs/dev/policies/0017-model-selection-and-calibration.md @@ -0,0 +1,48 @@ +# Policy | Model Selection And Calibration + +## Policy + +- Select model, reasoning effort, context scope, and delegation topology together to minimize total allocation consumed per accepted milestone while meeting required correctness, safety, and delivery-time constraints. Count orchestration, workers, failed attempts, evaluation, repair, and integration. Treat token counts and API-price estimates as labeled proxies when measured allocation is unavailable. +- Define the milestone before routing substantial work. It must describe usable behavior at its intended integration boundary and a stable acceptance check. Worker completion, prerequisite repair, passing unrelated tests, and document volume do not establish milestone completion. +- Keep a dated repo-local mapping from task tiers to available model and reasoning configurations. Start routine work on the calibrated economical default; use a cheaper tier for mechanical, readily verified work; route material policy design, architectural tradeoffs, or difficult consequential reasoning directly to a designated specialist tier when justified. Select reasoning effort separately. Model novelty and task length alone do not justify an upgrade. +- Reassess routing at non-trivial task start, material replanning, failed acceptance, conflicting evidence, and the configured no-progress interval. Reassessment is a brief primary-agent decision within the existing checkpoint; it does not itself require another model call. Distinguish reasoning limits from missing inputs, authority, unavailable tools, and environmental failure. +- Escalate only when stronger reasoning is likely to resolve a specific obstacle. Delegate the smallest useful decision or diagnostic task with evidence, attempted approaches, acceptance check, write scope, remaining budget, and stop condition. Return ordinary execution to its configured default when that task concludes. Record requested and runtime-reported effective model and effort; report unknown effective configuration explicitly. +- Model upgrades, reasoning changes, prompt edits, tool substitutions, successor plans, and worker replacement inherit cumulative milestone accounting. Reassessment intervals are not renewable budgets. Exhaustion cannot be bypassed by renaming an approach or opening another worker. +- Delegate when expected gains in expertise, independence, context isolation, or elapsed time justify setup and reconciliation cost. Do deterministic mechanical work with existing tools before purchasing model work for it. A compact specialist brief is preferred when full-history inheritance adds no value. The primary integrates returned evidence without repeating the worker's investigation. +- Route polling, hashing, schema checks, counter reconciliation, deterministic + test execution, and exact structured transformations to tools before any + model. When judgment-light work still needs a model, use the calibrated + economical tier with a narrow input packet, structured output, deterministic + verifier, attempt/time bound, and no authority to change scope or acceptance. +- Retain causality decisions, safety or authority changes, material experiment + design, architectural tradeoffs, and the final acceptance claim with the + primary or a justified specialist tier. Do not pay a stronger model to repeat + verified mechanical work returned by a cheaper worker. +- Calibrate complete workflows, not isolated responses. Before starting, freeze representative inputs, acceptance checks, baseline and candidate configurations, quality floor, sample size, retry allowance, resource ceiling, evaluator, and promotion/stop rules. Include failed and timed-out attempts, retain difficult regressions, and use held-out examples when tuning on earlier samples. +- Record the sample/date, workload identity, model and effort, context and tools, topology, accepted count and denominator, defects, interventions, elapsed time, cumulative agent effort, and measured allocation or labeled proxy. Keep elapsed wall time separate from summed worker effort. Do not attribute shared-account consumption to one configuration when concurrent use prevents attribution. +- Stop calibration at its predeclared sample or resource ceiling, or at a defined critical-quality failure. Small samples yield provisional routing only. Do not enlarge the experiment, weaken acceptance, or retry away failures to obtain a favorable result. +- Promote the least costly configuration that meets frozen quality and delivery requirements. An expensive configuration must show a task-relevant benefit that justifies its added consumption. Revert a regressed default promptly and retain specialist use only where justified. Recalibrate after material configuration changes or repeated observed failures with a bounded scheduled sample, not before every task. +- Deterministic audits establish wiring and record validity; they do not prove model quality, allocation savings, or runtime stopping. Repos that operate a controller must test aggregate counters and stop behavior at its real transition boundary. Policy-only adoption must identify calibration and runtime enforcement as unverified. + +## Adoption Notes + +Use this module for any repo where agents choose among model or reasoning configurations. Trivial one-step work needs no durable routing or calibration record. Keep provider names, prices, available efforts, configuration syntax, exact intervals, and calibration sample sizes in repo-local policy. + +## Fork-specific contract + +Provisional routing map reviewed 2026-09-22: + +| Task | Configuration | +| --- | --- | +| Hashing, polling, exact transforms, tests, ref checks | Deterministic local tools; no additional model call | +| Ordinary edits and bounded investigation | Primary session's inherited model and reasoning effort | +| Material architecture, policy interpretation, final acceptance | Primary session; reassess the specific obstacle before requesting a different configuration | + +Session and operator restrictions govern delegation. This policy does not +independently authorize spawning agents or changing model configuration. +Reassess at task start, material replanning, and failed acceptance; after two +consecutive attempts without new evidence, stop repetition and diagnose the +missing input, authority, environment, or reasoning limitation. This is not a +renewable retry budget. No calibration experiment is authorized by this map. +Model-quality calibration, allocation savings, and runtime enforcement are +unverified; do not describe this provisional mapping as measured optimization. From a24fb52b6236e288dedb3837fa4816a447c62eee Mon Sep 17 00:00:00 2001 From: Eric W Cochran Date: Tue, 22 Sep 2026 12:04:09 -0500 Subject: [PATCH 3/3] fix: close navigation test server connections during teardown --- daemon/src/sandbox/__tests__/cua.test.ts | 14 +---- .../sandbox/__tests__/playwright-api.test.ts | 14 +---- daemon/src/test-cleanup.test.ts | 51 ++++++++++++++++- daemon/src/test-cleanup.ts | 14 +++++ .../navigation-teardown-profile-census.json | 38 +++++++++++++ .../navigation-teardown-validation.json | 32 +++++++++++ ...0003-2026-09-22-navigation-teardown-fix.md | 52 +++++++++++++++++ .../0006-2026-09-22-navigation-teardown.md | 56 +++++++++++++++++++ 8 files changed, 246 insertions(+), 25 deletions(-) create mode 100644 docs/dev/evidence/navigation-teardown-profile-census.json create mode 100644 docs/dev/evidence/navigation-teardown-validation.json create mode 100644 docs/dev/notes/0003-2026-09-22-navigation-teardown-fix.md create mode 100644 docs/dev/plans/0006-2026-09-22-navigation-teardown.md diff --git a/daemon/src/sandbox/__tests__/cua.test.ts b/daemon/src/sandbox/__tests__/cua.test.ts index 08f14d8..659e577 100644 --- a/daemon/src/sandbox/__tests__/cua.test.ts +++ b/daemon/src/sandbox/__tests__/cua.test.ts @@ -9,7 +9,7 @@ import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { BrowserManager } from "../../browser-manager.js"; import { DEV_BROWSER_TMP_DIR } from "../../temp-files.js"; -import { removeDirectoryWithRetries } from "../../test-cleanup.js"; +import { closeTestHttpServer, removeDirectoryWithRetries } from "../../test-cleanup.js"; import { QuickJSSandbox } from "../quickjs-sandbox.js"; import { ensureSandboxClientBundle } from "./bundle-test-helpers.js"; @@ -324,17 +324,7 @@ async function createNavigationServer(): Promise { return { baseUrl: `http://127.0.0.1:${port}`, - close: async () => { - await new Promise((resolve, reject) => { - server.close((error) => { - if (error) { - reject(error); - return; - } - resolve(); - }); - }); - }, + close: () => closeTestHttpServer(server), }; } diff --git a/daemon/src/sandbox/__tests__/playwright-api.test.ts b/daemon/src/sandbox/__tests__/playwright-api.test.ts index 8ba9221..f4fc131 100644 --- a/daemon/src/sandbox/__tests__/playwright-api.test.ts +++ b/daemon/src/sandbox/__tests__/playwright-api.test.ts @@ -8,7 +8,7 @@ import path from "node:path"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { BrowserManager } from "../../browser-manager.js"; -import { removeDirectoryWithRetries } from "../../test-cleanup.js"; +import { closeTestHttpServer, removeDirectoryWithRetries } from "../../test-cleanup.js"; import { QuickJSSandbox } from "../quickjs-sandbox.js"; import { ensureSandboxClientBundle } from "./bundle-test-helpers.js"; @@ -302,17 +302,7 @@ async function createNavigationServer(): Promise { return { baseUrl: `http://127.0.0.1:${port}`, - close: async () => { - await new Promise((resolve, reject) => { - server.close((error) => { - if (error) { - reject(error); - return; - } - resolve(); - }); - }); - }, + close: () => closeTestHttpServer(server), }; } diff --git a/daemon/src/test-cleanup.test.ts b/daemon/src/test-cleanup.test.ts index 21559a6..3cb4f9f 100644 --- a/daemon/src/test-cleanup.test.ts +++ b/daemon/src/test-cleanup.test.ts @@ -1,8 +1,11 @@ +import { once } from "node:events"; +import { createServer } from "node:http"; +import { createConnection, type AddressInfo } from "node:net"; import type { rm } from "node:fs/promises"; import { describe, expect, it, vi } from "vitest"; -import { removeDirectoryWithRetries } from "./test-cleanup.js"; +import { closeTestHttpServer, removeDirectoryWithRetries } from "./test-cleanup.js"; function createFsError(code: string): NodeJS.ErrnoException { const error = new Error(`rm failed with ${code}`) as NodeJS.ErrnoException; @@ -62,3 +65,49 @@ describe("removeDirectoryWithRetries", () => { expect(sleep).toHaveBeenCalledTimes(2); }); }); + +describe("closeTestHttpServer", () => { + it.each(["preconnected", "unfinished request"])( + "closes a %s client without waiting for browser shutdown", + async (state) => { + const server = createServer(() => { + // An intentionally unfinished response models an in-flight browser request. + }); + server.listen(0, "127.0.0.1"); + await once(server, "listening"); + const accepted = once(server, "connection"); + const client = createConnection({ + host: "127.0.0.1", + port: (server.address() as AddressInfo).port, + }); + const connected = once(client, "connect"); + client.resume(); + let deadline: ReturnType | undefined; + try { + await accepted; + await connected; + if (state === "unfinished request") { + const requested = once(server, "request"); + client.write("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"); + await requested; + } + const disconnected = once(client, "close"); + await Promise.race([ + Promise.all([closeTestHttpServer(server), disconnected]), + new Promise((_, reject) => { + deadline = setTimeout(() => reject(new Error("HTTP teardown did not settle")), 1_000); + }), + ]); + expect(server.listening).toBe(false); + expect(client.destroyed).toBe(true); + } finally { + clearTimeout(deadline); + client.destroy(); + // Release all test resources even when the regression is deliberately reproduced. + const closed = new Promise((resolve) => server.close(() => resolve())); + server.closeAllConnections(); + await closed; + } + } + ); +}); diff --git a/daemon/src/test-cleanup.ts b/daemon/src/test-cleanup.ts index 46a756e..a381a86 100644 --- a/daemon/src/test-cleanup.ts +++ b/daemon/src/test-cleanup.ts @@ -1,3 +1,4 @@ +import type { Server } from "node:http"; import { rm } from "node:fs/promises"; import { setTimeout as delay } from "node:timers/promises"; @@ -44,3 +45,16 @@ export async function removeDirectoryWithRetries( } } } + +/** Close an exclusively owned HTTP test server, including unfinished connections. */ +export async function closeTestHttpServer(server: Server): Promise { + await new Promise((resolve, reject) => { + server.close((error) => { + if (error) reject(error); + else resolve(); + }); + // Stop accepting first, then release preconnects and unfinished requests. + // Waiting for graceful close here can block the browser shutdown that follows. + server.closeAllConnections(); + }); +} diff --git a/docs/dev/evidence/navigation-teardown-profile-census.json b/docs/dev/evidence/navigation-teardown-profile-census.json new file mode 100644 index 0000000..19e2714 --- /dev/null +++ b/docs/dev/evidence/navigation-teardown-profile-census.json @@ -0,0 +1,38 @@ +{ + "command": "vitest run cua.test.ts playwright-api.test.ts --maxWorkers 1", + "exitCode": 0, + "elapsedSeconds": 18.16, + "sampledAggregatePeakRssBytes": 3289092096, + "sampleIntervalSeconds": 0.2, + "observedProcessCount": 125, + "ownedBrowserProfiles": [ + "--user-data-dir=/tmp/dev-browser-cua-OBQtly/browsers/cua-navigation/chromium-profile", + "--user-data-dir=/tmp/dev-browser-playwright-api-1aSlTH/browsers/playwright-form-waiting/chromium-profile", + "--user-data-dir=/tmp/dev-browser-playwright-api-1aSlTH/browsers/playwright-locators/chromium-profile" + ], + "remainingOwnedLivePids": [], + "remainingOwnedProfilePids": [], + "runtimeBefore": { + "config.json": "99770ab7efafceb493324ed17ba2d2b7668e5692cf977651a5b112f688692d6c", + "daemon.mjs": "a824ab2457b7319acb39d49ed4f75aef20bbf62a9a850e10b2d40a79ac75b395", + "sandbox-client.js": "ebd093d662faa8883b640860e60071b8f4c6f37f03ee3ebbeca572194560a7c8", + "daemon.pid": "05748197a73be2b6924a9414b63ac72553d671f25e0b8e0568a8d4bc56870a39" + }, + "runtimeAfter": { + "config.json": "99770ab7efafceb493324ed17ba2d2b7668e5692cf977651a5b112f688692d6c", + "daemon.mjs": "a824ab2457b7319acb39d49ed4f75aef20bbf62a9a850e10b2d40a79ac75b395", + "sandbox-client.js": "ebd093d662faa8883b640860e60071b8f4c6f37f03ee3ebbeca572194560a7c8", + "daemon.pid": "05748197a73be2b6924a9414b63ac72553d671f25e0b8e0568a8d4bc56870a39" + }, + "existingDaemonPid": 77326, + "existingDaemonSameProcess": true, + "browser": "configured chromium-stealthcdp 150; config unchanged", + "limits": { + "workers": 1, + "wallSeconds": 900, + "targetRssBytes": 8589934592 + }, + "freshAllAffectedProfilePrefixPids": [], + "testsPassed": 32, + "testFilesPassed": 2 +} diff --git a/docs/dev/evidence/navigation-teardown-validation.json b/docs/dev/evidence/navigation-teardown-validation.json new file mode 100644 index 0000000..aa5f78e --- /dev/null +++ b/docs/dev/evidence/navigation-teardown-validation.json @@ -0,0 +1,32 @@ +{ + "command": "vitest run --maxWorkers 2", + "exitCode": 0, + "elapsedSeconds": 27.63, + "sampledAggregatePeakRssBytes": 4546093056, + "sampleIntervalSeconds": 0.2, + "observedProcessCount": 332, + "remainingOwnedLivePids": [], + "runtimeBefore": { + "config.json": "99770ab7efafceb493324ed17ba2d2b7668e5692cf977651a5b112f688692d6c", + "daemon.mjs": "a824ab2457b7319acb39d49ed4f75aef20bbf62a9a850e10b2d40a79ac75b395", + "sandbox-client.js": "ebd093d662faa8883b640860e60071b8f4c6f37f03ee3ebbeca572194560a7c8", + "daemon.pid": "05748197a73be2b6924a9414b63ac72553d671f25e0b8e0568a8d4bc56870a39" + }, + "runtimeAfter": { + "config.json": "99770ab7efafceb493324ed17ba2d2b7668e5692cf977651a5b112f688692d6c", + "daemon.mjs": "a824ab2457b7319acb39d49ed4f75aef20bbf62a9a850e10b2d40a79ac75b395", + "sandbox-client.js": "ebd093d662faa8883b640860e60071b8f4c6f37f03ee3ebbeca572194560a7c8", + "daemon.pid": "05748197a73be2b6924a9414b63ac72553d671f25e0b8e0568a8d4bc56870a39" + }, + "existingDaemonPid": 77326, + "existingDaemonSameProcess": true, + "browser": "configured chromium-stealthcdp 150; config unchanged", + "limits": { + "workers": 2, + "wallSeconds": 900, + "targetRssBytes": 8589934592 + }, + "profileCensus": "Unavailable in this first monitor: incorrect cmdline delimiter; PID/start-time census remains valid. Follow-up focused profile census recorded separately.", + "testFilesPassed": 21, + "testsPassed": 175 +} diff --git a/docs/dev/notes/0003-2026-09-22-navigation-teardown-fix.md b/docs/dev/notes/0003-2026-09-22-navigation-teardown-fix.md new file mode 100644 index 0000000..4a5c0e4 --- /dev/null +++ b/docs/dev/notes/0003-2026-09-22-navigation-teardown-fix.md @@ -0,0 +1,52 @@ +# Navigation teardown diagnosis and repair + +Plan 0006; issue #5; PR #6. Base: af26d57d671d9f4246befa000ae1df920cef1bf0. + +## Proven cause and scope + +Both navigation fixtures awaited `server.close()` before stopping their browser. +The instrumented reproduction reached `sandbox done; HTTP close begin` in both +suites, then timed out. In CUA, HTTP close completed only after the enclosing +suite's fallback browser shutdown. This is an HTTP test-fixture shutdown +ordering problem, not evidence that the installed browser daemon cannot stop. + +The existing 180-second failure is preserved in local-vitest-upgrade.json. +Temporary diagnostics reduced only the two hook limits to 10 seconds and +selected navigation tests: five assertions passed, both teardown hooks failed, +27 unrelated tests were excluded by the explicit name filter, duration 27.11s. +Instrumentation and diagnostic limits were removed before the final patch. + +A shared test-only `closeTestHttpServer` starts server close, then calls +`closeAllConnections` so exclusively owned preconnected and unfinished HTTP +connections cannot hold shutdown open. It still awaits the close callback and +propagates errors. Both navigation fixtures use it. No production browser, +daemon, sandbox implementation, timeout, or installed binary changed. + +## Regression and acceptance + +A socket-level regression covers clients that connect without sending a request +and clients with unfinished requests. Both fail with the original graceful-only +close (`HTTP teardown did not settle` at one second), and both pass after the +fix. Client sockets and the server are released in finally even on failure. +An initial regression-fixture connect-listener ordering bug was corrected before +this before/after comparison; it was not a product failure. + +- Socket/cleanup tests: 5 passed, 0.305s (2 new regression cases). +- Complete affected suites: 32 passed, 17.83s with configured Chromium 150. +- Full local suite: 21 suites, 175 tests passed, 26.95s (27.63s monitored wall). +- TypeScript, changed-file formatting, and Cargo build passed. +- Full run sampled aggregate descendant RSS peaked at 4,546,093,056 bytes with + two workers, below the 8 GiB target. PID/start-time census found no surviving + owned live processes. Daemon PID 77326 and config/payload hashes stayed equal. +- The first monitor's cmdline delimiter prevented profile collection; it does + not support a profile-level claim. That limitation is preserved in the full + receipt; a corrected focused monitor supplies the separate profile census. + The focused run passed 32 tests in 18.16s monitored wall; a final scan across + all affected temporary profile prefixes found zero browser processes. + +Receipts: ../evidence/navigation-teardown-validation.json and +../evidence/navigation-teardown-profile-census.json. Original failures remain +historical evidence; this is a verified code repair, not a pass-on-retry claim. +No test skips or enlarged timeouts are retained. No daemon restart or Chromium +configuration change was needed. CI and maintainer review remain integration +checks for the new PR head. diff --git a/docs/dev/plans/0006-2026-09-22-navigation-teardown.md b/docs/dev/plans/0006-2026-09-22-navigation-teardown.md new file mode 100644 index 0000000..365839e --- /dev/null +++ b/docs/dev/plans/0006-2026-09-22-navigation-teardown.md @@ -0,0 +1,56 @@ +# Plan 0006: Resolve local navigation teardown timeouts + +State: CLOSED +Owner: primary agent +Branch: chore/browser-skill-hygiene +Issue: #5 +PR: #6 + +## Current State + +The prior local full suite passed 173 assertions but two navigation afterAll +hooks timed out at 180 seconds. The exact stalled operation is unknown. CI +passed with a different browser environment; that does not resolve this failure. + +## Scope + +Serial critical path: instrument the two cleanup hooks, reproduce with configured +Chromium 150 in isolated test profiles, repair the proven cause, validate focused +and full tests, and publish evidence in PR #6. Reuse the clean existing worktree. +Primary agent owns diagnosis and integration. No delegated work. + +## Non-goals + +No v1 migration, Chromium/config upgrade, shared browser termination, timeout +inflation, skipped tests, or unrelated cleanup. No automatic merge without review. + +## Acceptance Criteria + +- Identify the stalled cleanup step with direct evidence. +- A regression check detects the old behavior and passes with the fix. +- Both affected suites and full daemon validation pass on configured Chromium 150. +- No test-owned browser/worker processes survive; existing daemon/config unchanged. +- Publish exact head and CI evidence, retaining the original failed run. + +## Definition of Done + +Commit and push the bounded fix and evidence; reconcile PR/issue status. Focused +checks target two minutes (diagnostic timeout reproduction may use the existing +180-second hook bound once); full checks budget 15 minutes, one suite at a time, +at most four workers. Measure observed duration and record any breach. + +Graphiti assessment: skip, because current failing logs and exact source are +sufficient; prior policy mirror timeouts do not block diagnosis. Routing uses +primary inherited model and deterministic tools; no calibration work. + +## Outcome + +Diagnosed HTTP fixture close waiting for connections while browser shutdown was +queued behind it. Added test-only forced connection cleanup after stopping the +listener; both socket regressions detect old behavior and pass with the repair. +Affected suites pass (32); full suite passes (175/21 suites), TypeScript, +formatting, Cargo build, and policy audits pass. Full monitored wall time 27.63s, +peak sampled aggregate RSS 4.55 GB. Fresh PID/start-time and affected profile +prefix checks show no test leftovers; daemon/config/payload unchanged. See note +0003 and navigation-teardown evidence receipts. Publish through existing PR #6; +CI/review remain integration gates. No production installation needed.