diff --git a/AGENTS.md b/AGENTS.md index 27dad95..d940e05 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,31 +1,88 @@ -# AI Optimizer contributor instructions +# AI Optimizer agent instructions AI Optimizer is a public, macOS-only diagnostic and narrowly owned maintenance -CLI for Claude Code, Codex, MCP, skills, and coding workspaces. - -## Safety invariants - -- `doctor` and `scan` are read-only. -- Never print command arguments, environment values, credentials, MCP endpoint - values, or workspace file contents. -- Mutating commands may write only AI Optimizer-owned configuration, reports, - release installation paths, and launchd label - `io.github.nyldn.ai-optimizer.daily`. -- Never silently edit or upgrade Claude Code, Codex, MCP servers, skills, - companion tools, or repositories. -- Keep runtime code compatible with `/usr/bin/ruby` 2.6 and standard library - only. -- Every lifecycle change needs an isolated install/uninstall or - schedule/unschedule round-trip test. - -## Verification - -Run all of these before claiming completion: - -```sh -/usr/bin/ruby -w test/test_all.rb -scripts/check-syntax.sh -scripts/build-release.sh -test/install_test.sh -gitleaks git --redact --exit-code 1 . -``` +CLI for Claude Code, Codex, MCP, skills, and coding workspaces. Codex reads this +file automatically. Claude Code receives the same contract through `CLAUDE.md`. + +## Choose the workflow + +- Use the **Operator workflow** when the user asks to inspect, configure, + optimize, maintain, or repair their AI coding environment. +- Use the **Development workflow** when the user asks to change AI Optimizer + itself. +- If both apply, gather operator evidence first, then develop against a failing + test without mixing host-specific data into the repository. + +## Operator workflow + +1. From this repository root, run the read-only handshake: + + ```sh + ./bin/ai-optimizer agent-context --json + ``` + + Add `--workspace-root PATH` only when the user identifies a different + workspace container. Do not run `setup`, scheduling, package-manager, or + repair commands merely to gather context. + +2. Read `overall_status`, `prioritized_actions`, and both embedded reports. + Treat priorities as: + + - `P0`: required product/platform failure or unknown state; + - `P1`: non-required failure or unknown state; + - `P2`: warning that needs review. + +3. Preserve the user's authorization boundary: + + - For inspect, explain, review, or status requests, remain read-only. + - When the user explicitly asks for fixes, investigate each finding at its + owning source and make the smallest reversible change. + - Ask before deleting data, creating credentials, authenticating remote MCP + services, enabling schedules, upgrading unrelated software, or changing + another repository's behavior. + +4. Never print or commit credentials, environment values, MCP endpoints, + command arguments from user sessions, workspace names, or workspace file + contents. AI Optimizer reports are deliberately redacted; do not weaken that + boundary while investigating. + +5. Verify repairs with the affected tool's native doctor or focused test, then + re-run: + + ```sh + ./bin/ai-optimizer agent-context --json + ``` + + Report exact evidence, remaining findings, changed paths, and rollback + instructions. Do not claim the environment is healthy solely because a + command exited successfully. + +## Development workflow + +1. Read `README.md`, `docs/architecture.md`, and the relevant tests. +2. Preserve these invariants: + + - `doctor`, `scan`, and `agent-context` are read-only. + - Mutating commands may write only AI Optimizer-owned configuration, + reports, release installation paths, and launchd label + `io.github.nyldn.ai-optimizer.daily`. + - Never silently edit or upgrade Claude Code, Codex, MCP servers, skills, + companion tools, or repositories. + - Keep runtime code compatible with `/usr/bin/ruby` 2.6 and the standard + library only. + - Every lifecycle change needs an isolated install/uninstall or + schedule/unschedule round-trip test. + +3. Write a failing regression test before implementation. +4. Run the smallest relevant test while iterating. +5. Before claiming completion, run: + + ```sh + /usr/bin/ruby -w test/test_all.rb + scripts/check-syntax.sh + scripts/build-release.sh + test/install_test.sh + gitleaks git --redact --exit-code 1 . + ``` + +See `docs/agent-workflow.md` for the handshake schema and integration examples. diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ff37de..0f5ff7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes are documented here. AI Optimizer follows semantic versioning. +## 0.1.8 - 2026-08-23 + +- Add a read-only `agent-context` handshake with embedded doctor and workspace + reports, deduplicated priorities, authorization boundaries, and completion + gates for coding agents. +- Give Codex and Claude Code one shared repository-root operating contract, + including distinct environment-operator and product-development workflows. +- Document and test the complete agent onboarding and verification loop. + ## 0.1.7 - 2026-08-23 - Warn (and therefore fail `--strict`) when a linked skill directory cannot be diff --git a/CLAUDE.md b/CLAUDE.md index b76d9ee..30cbc6e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,5 +1,14 @@ # AI Optimizer -Read `AGENTS.md` first. Keep diagnostics observational, structured, redacted, -and deterministic. Product writes must be explicit, contained, reversible, and -covered by lifecycle tests. +@AGENTS.md + +Follow `AGENTS.md` as the shared Codex/Claude contract. For any environment +assessment, begin with the read-only repository handshake: + +```sh +./bin/ai-optimizer agent-context --json +``` + +Do not infer repair authority from a diagnostic finding. If the user asked for +changes, make the smallest reversible fix, verify it with the owning tool, and +re-run the handshake before reporting completion. diff --git a/README.md b/README.md index 6dc1e0d..0dc56e7 100644 --- a/README.md +++ b/README.md @@ -27,13 +27,16 @@ skills, repositories, or launchd unless you explicitly add `--schedule`. - A `scan` that summarizes Git workspace coverage without reporting repository names or file contents. - The same stable findings in readable text or one clean JSON document. +- A shared `agent-context` handshake that tells Codex or Claude Code what to + inspect, how to prioritize findings, where mutation authority stops, and how + to verify repairs. - Opt-in evening diagnostics through one product-owned user launch agent. - No telemetry and no autonomous code repair. Example: ```text -AI Optimizer 0.1.7 +AI Optimizer 0.1.8 [PASS] system.macos - macOS is supported [PASS] tools.claude.present - Claude Code is available @@ -53,6 +56,7 @@ automation. ai-optimizer setup [--workspace-root PATH] [--schedule] ai-optimizer doctor [--json] [--strict] ai-optimizer scan [--json] [--strict] [--workspace-root PATH] +ai-optimizer agent-context [--json] [--strict] [--workspace-root PATH] ai-optimizer report [--json] ai-optimizer schedule [--hour H] [--minute M] ai-optimizer schedule status @@ -63,6 +67,21 @@ ai-optimizer version The default workspace root is `~/git` when it exists, otherwise the current directory. +## Use with Codex or Claude Code + +Clone or open this repository, then start either agent from its root. Codex +automatically reads `AGENTS.md`; Claude Code loads `CLAUDE.md`, which imports +the same operating contract. Both are directed to begin with: + +```sh +./bin/ai-optimizer agent-context --json +``` + +The handshake combines current doctor and workspace evidence with deduplicated +priorities, remediation, safety rules, and completion checks. It is read-only. +See [Working with Codex and Claude Code](docs/agent-workflow.md) for the schema, +recommended prompt, and repair loop. + ## Evening maintenance Scheduling is opt-in: @@ -114,7 +133,7 @@ ai-optimizer unschedule ## Privacy and mutation boundary -`doctor` and `scan` are read-only. Reports contain status identifiers, +`doctor`, `scan`, and `agent-context` are read-only. Reports contain status identifiers, counts, tool versions, and remediation. They do not contain environment values, command arguments, MCP endpoints, tokens, workspace names, or workspace file contents. @@ -138,7 +157,7 @@ The direct path verifies the installer before it runs, then the installer verifies the release archive before changing live paths: ```sh -VERSION=0.1.7 +VERSION=0.1.8 curl -fLO "https://github.com/nyldn/ai-optimizer/releases/download/v$VERSION/install.sh" curl -fLO "https://github.com/nyldn/ai-optimizer/releases/download/v$VERSION/install.sh.sha256" shasum -a 256 -c install.sh.sha256 diff --git a/VERSION b/VERSION index 1180819..699c6c6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.7 +0.1.8 diff --git a/docs/agent-workflow.md b/docs/agent-workflow.md new file mode 100644 index 0000000..18d1c40 --- /dev/null +++ b/docs/agent-workflow.md @@ -0,0 +1,76 @@ +# Working with Codex and Claude Code + +AI Optimizer gives coding agents a deterministic, read-only starting point. +The repository's `AGENTS.md` and `CLAUDE.md` route both Codex and Claude Code to +the same command: + +```sh +./bin/ai-optimizer agent-context --json +``` + +Use the installed command instead when you are not working from a checkout: + +```sh +ai-optimizer agent-context --json +``` + +## What the handshake returns + +The single JSON document contains: + +- `overall_status`: `healthy`, `review`, or `action_required`; +- `agent_contract`: the read-only boundary, workflow, prohibited behavior, and + completion gates; +- `prioritized_actions`: deduplicated `P0`, `P1`, and `P2` findings with their + affected tools and existing remediation; +- `reports.doctor`: current macOS, tool, MCP, skill, and product findings; +- `reports.scan`: the same health evidence plus privacy-preserving workspace + coverage. + +The command does not write configuration, start services, install packages, +authenticate providers, or edit repositories. Its exit behavior matches +`doctor` and `scan`: required failures exit 1, while warnings exit 1 only with +`--strict`. + +## Recommended agent loop + +```text +read repository instructions + ↓ +run agent-context --json + ↓ +inspect prioritized evidence + ↓ +stay read-only or apply an explicitly requested, reversible repair + ↓ +run focused native verification + ↓ +run agent-context --json again and report remaining findings +``` + +This deliberately separates diagnosis from mutation. An LLM can understand +what to inspect and how to verify it without receiving blanket authority to +rewrite the host. + +## Starting a session + +From the repository root, start either agent normally: + +```sh +codex +``` + +or: + +```sh +claude +``` + +A useful first request is: + +> Assess this Mac's AI coding environment. Follow the repository agent +> contract, fix only safe and reversible issues I authorized, and report +> anything that needs my decision. + +Codex automatically discovers `AGENTS.md`; Claude Code loads `CLAUDE.md`, which +imports the same contract. diff --git a/docs/architecture.md b/docs/architecture.md index 80ff373..44266e6 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -11,6 +11,7 @@ flowchart LR D --> E[Finding objects] E --> F[Human report] E --> G[JSON report] + G --> L[Agent context handshake] H[Explicit setup or schedule] --> I[Owned config and direct-install launchd state] J[Explicit brew services start] --> K[Homebrew-managed launchd state] ``` @@ -31,6 +32,14 @@ Statuses are `pass`, `warn`, `fail`, `info`, `skip`, and `unknown`. Required `fail` or `unknown` findings exit 1. A top-level internal failure exits 3. +## Agent context + +`agent-context` runs the doctor and workspace scanner over one read-only check +context. It embeds both redacted reports in a single JSON document, deduplicates +actionable findings, assigns deterministic P0/P1/P2 priorities, and includes a +static authorization and verification contract for Codex and Claude Code. It +does not infer repair commands or introduce another mutation path. + ## Ownership Configuration and reports live under diff --git a/docs/privacy.md b/docs/privacy.md index 1ed8d4e..c2f77ef 100644 --- a/docs/privacy.md +++ b/docs/privacy.md @@ -1,6 +1,6 @@ # Privacy -AI Optimizer 0.1.7 has no telemetry and sends no diagnostic data anywhere. +AI Optimizer 0.1.8 has no telemetry and sends no diagnostic data anywhere. ## Read @@ -27,7 +27,7 @@ finding. ## Write -`doctor` and `scan` do not write. `setup`, `schedule`, and `unschedule` +`doctor`, `scan`, and `agent-context` do not write. `setup`, `schedule`, and `unschedule` write or remove only AI Optimizer-owned files and the documented launchd label. Scheduled maintenance writes one local run receipt after the execution-time evening guard. Configuration, receipts, and scheduler logs use owner-only diff --git a/lib/ai_optimizer.rb b/lib/ai_optimizer.rb index a32a73f..6a129f2 100644 --- a/lib/ai_optimizer.rb +++ b/lib/ai_optimizer.rb @@ -9,6 +9,7 @@ require_relative "ai_optimizer/config" require_relative "ai_optimizer/checks" require_relative "ai_optimizer/doctor" +require_relative "ai_optimizer/agent_context" require_relative "ai_optimizer/scheduler" require_relative "ai_optimizer/maintenance" require_relative "ai_optimizer/cli" diff --git a/lib/ai_optimizer/agent_context.rb b/lib/ai_optimizer/agent_context.rb new file mode 100644 index 0000000..d0ae8f4 --- /dev/null +++ b/lib/ai_optimizer/agent_context.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +module AIOptimizer + class AgentContext + WORKFLOW = [ + "Read the diagnostic evidence before changing configuration or code.", + "Investigate one prioritized finding at its owning source.", + "Make the smallest reversible repair only when the user asked for changes.", + "Re-run agent-context after any approved repair and include focused verification." + ].freeze + + NEVER = [ + "Do not expose credentials, environment values, MCP endpoints, or workspace contents.", + "Do not silently install, upgrade, remove, or reconfigure third-party tools.", + "Do not treat a warning as permission to mutate the host." + ].freeze + + PRIORITY_ORDER = { "P0" => 0, "P1" => 1, "P2" => 2 }.freeze + + attr_reader :doctor_report, :scan_report, :generated_at, :version + + def initialize(doctor_report:, scan_report:, generated_at: Time.now.utc, version: VERSION) + @doctor_report = doctor_report + @scan_report = scan_report + @generated_at = generated_at.utc + @version = version + end + + def to_h + { + schema_version: 1, + product: "ai-optimizer", + version: version, + generated_at: generated_at.iso8601, + mode: "read_only_advisor", + overall_status: overall_status, + agent_contract: { + diagnostics_are_read_only: true, + repairs_are_automatic: false, + workflow: WORKFLOW, + never: NEVER, + completion_gate: [ + "Verify the original finding no longer reproduces.", + "Run the relevant native tool doctor or product test.", + "Re-run agent-context and report remaining warnings or failures." + ] + }, + prioritized_actions: prioritized_actions, + reports: { + doctor: doctor_report.to_h, + scan: scan_report.to_h + } + } + end + + def to_json(*_args) + JSON.generate(to_h) + end + + def to_text + lines = [ + "AI Optimizer agent context #{version}", + "Mode: read-only advisor", + "Overall: #{overall_status}", + "", + "Agent workflow:" + ] + WORKFLOW.each_with_index { |step, index| lines << "#{index + 1}. #{step}" } + lines << "" + lines << "Safety boundary:" + NEVER.each { |rule| lines << "- #{rule}" } + lines << "" + lines << "Prioritized actions:" + if prioritized_actions.empty? + lines << "- None. Preserve the current state." + else + prioritized_actions.each do |action| + lines << "[#{action.fetch(:priority)}] #{action.fetch(:finding_id)} - #{action.fetch(:message)}" + remediation = action[:remediation] + lines << " Next: #{remediation}" if remediation && !remediation.empty? + end + end + lines.join("\n") + "\n" + end + + def exit_code(strict: false) + [doctor_report.exit_code(strict: strict), scan_report.exit_code(strict: strict)].max + end + + private + + def overall_status + return "action_required" if prioritized_actions.any? { |action| action.fetch(:priority) == "P0" } + return "review" unless prioritized_actions.empty? + + "healthy" + end + + def prioritized_actions + @prioritized_actions ||= begin + entries = [] + { "doctor" => doctor_report, "scan" => scan_report }.each do |source, report| + report.findings.each do |finding| + priority = priority_for(finding) + entries << [finding, priority, source] if priority + end + end + + entries.group_by { |finding, _priority, _source| finding.id }.map do |_id, matches| + selected = matches.min_by { |_finding, priority, _source| PRIORITY_ORDER.fetch(priority) } + finding = selected[0] + { + priority: selected[1], + finding_id: finding.id, + status: finding.status, + message: finding.message, + remediation: finding.remediation, + required: finding.required, + affects: finding.affects, + source_reports: matches.map { |_item, _priority, source| source }.uniq.sort + } + end.sort_by { |action| [PRIORITY_ORDER.fetch(action.fetch(:priority)), action.fetch(:finding_id)] } + end + end + + def priority_for(finding) + return "P0" if finding.required && %w[fail unknown].include?(finding.status) + return "P1" if %w[fail unknown].include?(finding.status) + return "P2" if finding.status == "warn" + + nil + end + end +end diff --git a/lib/ai_optimizer/cli.rb b/lib/ai_optimizer/cli.rb index fbb39da..c9a3c4e 100644 --- a/lib/ai_optimizer/cli.rb +++ b/lib/ai_optimizer/cli.rb @@ -13,6 +13,7 @@ class CLI ai-optimizer setup [--workspace-root PATH] [--schedule] ai-optimizer doctor [--json] [--strict] ai-optimizer scan [--json] [--strict] [--workspace-root PATH] + ai-optimizer agent-context [--json] [--strict] [--workspace-root PATH] ai-optimizer report [--json] ai-optimizer schedule [--hour H] [--minute M] [--force-outside-window] ai-optimizer schedule status @@ -20,7 +21,7 @@ class CLI ai-optimizer version ai-optimizer help - doctor and scan are read-only. Scheduling is opt-in and owns only: + doctor, scan, and agent-context are read-only. Scheduling is opt-in and owns only: #{Scheduler::LABEL} HELP @@ -55,6 +56,8 @@ def run(argv) run_diagnostic(args, Doctor) when "scan" run_diagnostic(args, Scanner) + when "agent-context" + run_agent_context(args) when "setup" run_setup(args) when "schedule" @@ -118,6 +121,25 @@ def run_setup(args) 0 end + def run_agent_context(args) + options = { json: false, strict: false, workspace_root: nil } + parser = OptionParser.new do |opts| + opts.on("--json") { options[:json] = true } + opts.on("--strict") { options[:strict] = true } + opts.on("--workspace-root PATH") { |path| options[:workspace_root] = path } + end + parser.parse!(args) + require_no_args(args) + + context = build_context(options[:workspace_root]) + agent_context = AgentContext.new( + doctor_report: Doctor.new(context).run, + scan_report: Scanner.new(context).run + ) + @stdout.write(options[:json] ? agent_context.to_json + "\n" : agent_context.to_text) + agent_context.exit_code(strict: options[:strict]) + end + def run_schedule(args) if args.first == "status" args.shift diff --git a/test/agent_context_test.rb b/test/agent_context_test.rb new file mode 100644 index 0000000..27f139f --- /dev/null +++ b/test/agent_context_test.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +class AgentContextTest < Minitest::Test + NOW = Time.utc(2026, 8, 23, 6, 0, 0) + + def test_builds_a_read_only_agent_contract_with_prioritized_unique_actions + warning = AIOptimizer::Finding.new( + id: "tools.codex.present", + category: "tools", + status: "warn", + message: "Codex needs attention", + remediation: "Inspect the preferred Codex installation.", + affects: ["codex"] + ) + required_failure = AIOptimizer::Finding.new( + id: "product.config", + category: "product", + status: "fail", + message: "Configuration is invalid", + remediation: "Repair AI Optimizer configuration.", + required: true, + affects: ["ai-optimizer"] + ) + passing = AIOptimizer::Finding.new( + id: "system.macos", + category: "system", + status: "pass", + message: "macOS is supported" + ) + doctor = AIOptimizer::Report.new([warning, passing], generated_at: NOW) + scan = AIOptimizer::Report.new([warning, required_failure], generated_at: NOW) + + payload = AIOptimizer::AgentContext.new( + doctor_report: doctor, + scan_report: scan, + generated_at: NOW + ).to_h + + assert_equal "read_only_advisor", payload.fetch(:mode) + assert_equal "action_required", payload.fetch(:overall_status) + assert_equal true, payload.fetch(:agent_contract).fetch(:diagnostics_are_read_only) + assert_equal false, payload.fetch(:agent_contract).fetch(:repairs_are_automatic) + assert_equal %w[P0 P2], payload.fetch(:prioritized_actions).map { |action| action.fetch(:priority) } + assert_equal 1, payload.fetch(:prioritized_actions).count { |action| action.fetch(:finding_id) == warning.id } + refute payload.fetch(:prioritized_actions).any? { |action| action.fetch(:finding_id) == passing.id } + assert_equal doctor.to_h, payload.fetch(:reports).fetch(:doctor) + assert_equal scan.to_h, payload.fetch(:reports).fetch(:scan) + end + + def test_text_output_tells_an_agent_how_to_continue + report = AIOptimizer::Report.new([], generated_at: NOW) + context = AIOptimizer::AgentContext.new( + doctor_report: report, + scan_report: report, + generated_at: NOW + ) + + text = context.to_text + + assert_includes text, "Mode: read-only advisor" + assert_includes text, "Overall: healthy" + assert_includes text, "Do not expose credentials" + assert_includes text, "Re-run agent-context after any approved repair" + end +end diff --git a/test/cli_test.rb b/test/cli_test.rb index 3a827af..5b27b7e 100644 --- a/test/cli_test.rb +++ b/test/cli_test.rb @@ -18,6 +18,7 @@ def test_version_and_help assert status.success?, stderr assert_includes stdout, "doctor" assert_includes stdout, "schedule" + assert_includes stdout, "doctor, scan, and agent-context are read-only" end def test_doctor_json_is_one_document @@ -30,6 +31,22 @@ def test_doctor_json_is_one_document end end + def test_agent_context_json_is_one_actionable_document + in_tmpdir do |dir| + stdout, stderr, status = run_cli( + "agent-context", "--json", "--workspace-root", dir, + env: { "AI_OPTIMIZER_DATA_DIR" => File.join(dir, "data") } + ) + assert_includes [0, 1], status.exitstatus, stderr + payload = JSON.parse(stdout) + assert_equal "read_only_advisor", payload.fetch("mode") + assert payload.fetch("agent_contract").fetch("workflow").any? + assert payload.fetch("reports").fetch("doctor").fetch("findings").is_a?(Array) + assert payload.fetch("reports").fetch("scan").fetch("findings").is_a?(Array) + assert_empty stderr + end + end + def test_usage_error_exits_two _stdout, stderr, status = run_cli("doctor", "--not-a-real-flag") assert_equal 2, status.exitstatus diff --git a/test/script_test.rb b/test/script_test.rb index 413859b..cbf7b8b 100644 --- a/test/script_test.rb +++ b/test/script_test.rb @@ -12,4 +12,17 @@ def test_scripts_and_workflows_have_no_patch_continuation_artifacts refute_match(/\s\+\s{2,}/, File.read(path), "patch marker artifact in #{path}") end end + + def test_codex_and_claude_entrypoints_share_the_agent_context_handshake + agents = File.read(File.join(ROOT, "AGENTS.md")) + claude = File.read(File.join(ROOT, "CLAUDE.md")) + + assert_includes agents, "./bin/ai-optimizer agent-context --json" + assert_includes agents, "Operator workflow" + assert_includes agents, "Development workflow" + assert_includes claude, "AGENTS.md" + assert_includes claude, "./bin/ai-optimizer agent-context --json" + assert_includes File.read(File.join(ROOT, "docs", "architecture.md")), "Agent context" + assert_includes File.read(File.join(ROOT, "docs", "privacy.md")), "`doctor`, `scan`, and `agent-context` do not write" + end end