From 67a8409179185cb66e4121e60d927c856d2676ba Mon Sep 17 00:00:00 2001 From: xeonvs <11463419+xeonvs@users.noreply.github.com> Date: Fri, 25 Sep 2026 08:21:59 +0200 Subject: [PATCH] Fix archive lifecycle and add opt-in Claude agent profiles --- PLANS.md | 144 +++++++++++++++ README.md | 17 +- .../.claude-plugin/plugin.json | 2 +- .../.codex-plugin/plugin.json | 2 +- .../skills/engineering-workflow/SKILL.md | 3 +- .../claude_agents/workflow-explorer.md.tmpl | 9 + .../claude_agents/workflow-reviewer.md.tmpl | 9 + .../claude_agents/workflow-utility.md.tmpl | 8 + .../ENGINEERING_WORKFLOW_STATE.yaml.tmpl | 1 + .../references/claude_model_profiles.md | 26 +++ .../references/platform_compatibility.md | 2 +- .../references/target_workflow_upgrade.md | 22 ++- .../scripts/plan_lifecycle.py | 16 +- .../scripts/upgrade_target_workflow.py | 167 +++++++++++++++--- .../scripts/validate_skill_repo.py | 41 +++++ skill/engineering-workflow/SKILL.md | 3 +- .../claude_agents/workflow-explorer.md.tmpl | 9 + .../claude_agents/workflow-reviewer.md.tmpl | 9 + .../claude_agents/workflow-utility.md.tmpl | 8 + .../ENGINEERING_WORKFLOW_STATE.yaml.tmpl | 1 + .../references/claude_model_profiles.md | 26 +++ .../references/platform_compatibility.md | 2 +- .../references/target_workflow_upgrade.md | 22 ++- .../scripts/plan_lifecycle.py | 16 +- .../scripts/upgrade_target_workflow.py | 167 +++++++++++++++--- .../scripts/validate_skill_repo.py | 41 +++++ tests/test_claude_agent_upgrade.py | 167 ++++++++++++++++++ tests/test_marketplace_package.py | 4 +- tests/test_plan_lifecycle.py | 20 +++ tests/test_skill_repo_validation.py | 18 +- tests/test_upgrade_target_workflow.py | 88 +++++++++ 31 files changed, 988 insertions(+), 82 deletions(-) create mode 100644 plugins/engineering-workflow/skills/engineering-workflow/assets/claude_agents/workflow-explorer.md.tmpl create mode 100644 plugins/engineering-workflow/skills/engineering-workflow/assets/claude_agents/workflow-reviewer.md.tmpl create mode 100644 plugins/engineering-workflow/skills/engineering-workflow/assets/claude_agents/workflow-utility.md.tmpl create mode 100644 plugins/engineering-workflow/skills/engineering-workflow/references/claude_model_profiles.md create mode 100644 skill/engineering-workflow/assets/claude_agents/workflow-explorer.md.tmpl create mode 100644 skill/engineering-workflow/assets/claude_agents/workflow-reviewer.md.tmpl create mode 100644 skill/engineering-workflow/assets/claude_agents/workflow-utility.md.tmpl create mode 100644 skill/engineering-workflow/references/claude_model_profiles.md create mode 100644 tests/test_claude_agent_upgrade.py diff --git a/PLANS.md b/PLANS.md index d8c7a24..b55f204 100644 --- a/PLANS.md +++ b/PLANS.md @@ -4,6 +4,150 @@ plan_schema_version: 2 Use this file for active, blocked, ready-for-closure, or recently completed execution work. The canonical lifecycle is the installed `engineering-workflow` planning reference. +## Active Plan: Repair Custom Archive Upgrade And Add Claude Subagent Profiles + +Status: active +Owner: root +Last Updated: 2026-09-25 + +### Goal + +Fix issues #19 and #20 without losing custom archive navigation or wrapped completed-plan pointers; offer native, opt-in Claude Code subagent model profiles without changing existing Codex opt-ins or user settings; publish and verify patch releases in the source and unified marketplaces and refresh local installations. + +### Plan Origin + +plan_mode_approved + +### Requested Scope + +- Repair the declared custom archive index graph during target workflow upgrades. +- Preserve existing multiline Recently Completed entries when closing a plan. +- Add separately opted-in Claude Code subagent model profiles and preserve native/user configuration. +- Verify implementation, review the aggregate diff, create and merge the source PR, publish source and unified-marketplace patch releases, and verify local installations. +- Divide independent, bounded implementation and review work among subagents while root owns integration and durable state. + +### Requirement Traceability + +| Requirement | Complete outcome | Source | Work queue | Acceptance or validation | Status | +| --- | --- | --- | --- | --- | --- | +| REQ-001 | Target upgrade preserves a valid explicitly declared custom archive chain, including parent links and unmanaged prose | issue #19 and approved plan | WQ-01, WQ-03 | Disposable target upgrade succeeds; graph validates; negative cases roll back | done | +| REQ-002 | Claude Code receives native model/effort selection only through separate opt-in profiles, without altering Codex opt-in or user settings | user bonus and approved choice | WQ-02, WQ-03 | Opt-in, no-opt-in, idempotence, preservation, unsafe path, and rollback tests pass | done | +| REQ-003 | Source skill and generated dual-platform package remain coherent and fully validated | approved plan | WQ-03, WQ-04 | Full, security, plugin, package, and aggregate review checks pass | done | +| REQ-004 | Source and unified marketplace releases and local installations are updated and verified | approved delivery choice | WQ-05, WQ-06 | PR/CI, release tag, byte-exact import, marketplace release, and active local version readback | pending | +| REQ-005 | Independent work is bounded and integrated by root | explicit user instruction | WQ-01, WQ-02, WQ-04 | Disjoint file ownership, compact findings, root acceptance and final review | done | +| REQ-006 | Plan closure preserves complete existing multiline archive pointers | issue #20 and user follow-up | WQ-07 | Archive and compact close retain wrapped entries and links; lifecycle validates | done | + +### Explicit Non-Goals + +- No global Claude model or effort override, plugin-wide agent installation, recursive delegation, or changes to unrelated skills and repositories. +- No weakening of privacy, secret, instruction, or lifecycle validation. +- No migration of customized user agent definitions or settings without a separate decision. + +### Constraints + +- Materialize this full plan before any other repository write; root alone changes PLANS.md and final state. +- `--include-agent-config` and its prior state remain Codex-only. Claude configuration requires a distinct flag and state marker; default upgrade leaves `.claude/**` unchanged. +- Generate package bytes from canonical skill sources; do not edit generated package or plugin caches directly. +- Public/security gates run on final content immediately before each authorized push. Remote writes and local installation refresh follow the user's approved delivery choice; no history rewrite or scanner weakening. + +### Inputs And Sources + +- User request: implement the approved plan, including delegation, PR, patch releases, marketplaces, and local refresh. +- Issue: https://github.com/xeonvs/codex-engineering-workflow/issues/19 +- Issue: https://github.com/xeonvs/codex-engineering-workflow/issues/20 +- Claude Code subagent documentation: https://code.claude.com/docs/en/sub-agents +- Claude Code model and effort documentation: https://code.claude.com/docs/en/model-config +- Codex subagent documentation: https://learn.chatgpt.com/docs/agent-configuration/subagents + +### User Decisions And Answers + +- 2026-09-25: choose opt-in project Claude subagent profiles, not guidance alone. +- 2026-09-25: deliver PR and patch releases, update unified marketplace and local installations. +- 2026-09-25: split independent bounded work among subagents; root orchestrates and integrates. +- 2026-09-25: include newly opened issue #20 and review findings before publication. + +### Completed Baseline State + +- [x] WQ-00 — Main working tree was clean before implementation; issue #19 was the only open source issue then. Issue #20 opened during work. Published source and marketplace tags were v0.9.9 and v1.0.9; recheck before publication. +- [x] WQ-00A — Existing `planned_explicit_index_writes` preserves custom parent links; bare interim generic index check would reject the restored link. Existing final lifecycle check validates both graphs. +- [x] WQ-00B — Claude Code supports native per-agent `model` and `effort`; definition/invocation precedence and provider restrictions are documented. Existing target opt-in applies only to Codex. + +### Current Work Queue + +- [x] WQ-01 — Implement custom archive index reconciliation and focused disposable-target regressions for REQ-001 and REQ-005. `done` +- [x] WQ-02 — Implement separate Claude opt-in templates, policy, migrator behavior, and focused regressions for REQ-002 and REQ-005. `done` +- [x] WQ-03 — Integrate both slices, update active version owners to next patch version, rebuild generated package, run affected checks for REQ-001/002/003. `done` +- [x] WQ-07 — Fix issue #20's wrapped pointer truncation and the review-identified empty Claude-agent overwrite, with disposable regressions; rebuild package and rerun affected checks for REQ-002, REQ-003, and REQ-006. `done` +- [x] WQ-04 — Run full and security gates, review each logical slice and aggregate diff, fix findings, and reconcile source plan for REQ-003/005. `done` +- [ ] WQ-05 — Create/merge source PR after checks and publish source patch release for REQ-004. `in_progress` +- [ ] WQ-06 — Import exact released bytes into xeonvs-engineering, verify, create/merge PR, publish marketplace patch release, refresh active local Codex/Claude installations, and read back versions for REQ-004. `pending` + +### Locked Decisions + +- 2026-09-25: Reuse the explicit index planner after generic writes and rely on unified lifecycle validation rather than duplicating custom-link allowance logic. +- 2026-09-25: Add a separate Claude opt-in flag and state marker; retain the existing Codex flag's meaning. +- 2026-09-25: Use Claude aliases Haiku for small semantic utility work and Sonnet/medium for explorer/reviewer; no fixed Haiku effort. Ordinary bounded implementation uses Sonnet/medium through native selection. Opus escalation requires user selection or confirmation. +- 2026-09-25: Preserve customized target agents and native Claude settings; refresh only exact prior generated template bytes. Do not set `CLAUDE_CODE_SUBAGENT_MODEL_FORCE`. +- 2026-09-25: If live tags remain unchanged, target source v0.9.10 and marketplace v1.0.10; otherwise recalculate next patch versions. + +### Verification + +- REQ-001 / WQ-01: disposable custom-chain upgrade, explicit and generic index graph checks, prose preservation, default archive absence, malformed/unmanaged/symlink rollback. +- REQ-002 / WQ-02: optional Claude config creation, non-opt-in byte stability, rerun idempotence, custom model preservation, prior-template refresh, unsafe-path and rollback tests, frontmatter validation. +- REQ-003 / WQ-03/04: focused tests, `python3 scripts/dev_check.py full`, package byte parity, external Claude/plugin validators, `python3 scripts/dev_check.py security`, aggregate semantic review. +- REQ-004 / WQ-05/06: CI success, annotated stable source tag, GitHub import result, identical source/import package bytes, marketplace CI/tag, native local installation and enabled/version readback. +- REQ-005 / WQ-01/02/04: bounded handoffs and root-reviewed integration without overlapping plan or version-owner writes. +- REQ-006 / WQ-07: disposable archive/compact closure retains multiline archive links and checks lifecycle success. + +### Latest Validation Results + +- 2026-09-25: issue #19, Claude opt-in (including empty file), and issue #20 focused tests passed; full gate 9/9 and all external plugin/skill validators passed after the fixes. Security gate 3/3 passed; independent final review found no remaining issues. Re-run security immediately before push after this plan reconciliation. + +### Risks And Recovery + +- Risk: generic index validation rejects declared custom links. Recovery: final combined lifecycle validation and transactional rollback; add exact regression before publishing. +- Risk: Claude opt-in changes existing targets unexpectedly. Recovery: separate flag/state, preserve custom files, disposable tests, and fail closed on unsafe paths. +- Risk: source and unified marketplace diverge. Recovery: compare released and imported package bytes; do not tag marketplace until parity and CI pass. +- Risk: failed remote or local update. Recovery: stop at verified state, retain immutable refs and exact next step; do not force-move published tags or edit caches. + +### Resume Point + +- Continue WQ-05 with source PR and release after final staged-state security scan. Root owns PLANS.md and release decisions. + +### Plan Fidelity Check + +- [x] Every agreed outcome has a requirement ID. +- [x] Every source URL is preserved. +- [x] Every user answer and locked decision is preserved. +- [x] No requirement was silently narrowed or removed. +- [x] The queue covers every requirement ID. +- [x] Validation covers every acceptance criterion. +- [x] Non-goals do not contradict requested scope. +- [x] The resume point names the first unfinished queue item. +- [x] This plan is not a compressed rewrite of a more detailed approved plan. + +### Reconciliation Check + +- [ ] Affected work state, repository state, and applicable validation agree. +- [ ] Plan and requirement statuses, queue, resume point, indexes, and related docs agree. +- [ ] Completed sections contain no stale next-work or blocker wording. + +### Closure Gate + +- [ ] Every in-scope requirement and queue item is terminal. +- [ ] Applicable validation is current for final content. +- [ ] Review feedback, omissions, backlog, and indexes are reconciled. +- [ ] Resume Point contains no future in-scope work. +- [ ] Compact or archive disposition can be applied atomically. + +### Post-Close Delivery + +- Source PR merge, patch tag/release, unified marketplace import/release, and local installation verification are tracked as WQ-05/06 until genuinely complete; do not close this plan while they remain unfinished. + +### Handoff Notes + +- No handoff yet. Root retains final integration and release responsibility. + ## Recently Completed - [x] 2026-09-23: Completed Privacy Preflight Exact Review V2 And Release; [full archived plan](docs/archive/plans/2026-09-23-privacy-preflight-exact-review-v2-and-release.md). diff --git a/README.md b/README.md index 71dab92..3eab6d3 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ `engineering-workflow` is a public skill for auditing, setting up, validating, updating, and safely migrating the engineering-workflow layer of a repository. It works with Codex and Claude Code. -Current skill version: `0.9.9`. +Current skill version: `0.9.10`. The skill uses `AGENTS.md` as a short map, `PLANS.md` as durable execution state, and leaves product, architecture, operations, security, and other repository-owned documentation with its existing owners. Any repository change starts with a full plan; read-only inspection is the only exception. @@ -73,7 +73,7 @@ Use $engineering-workflow to audit this mature repository and add only the missi ``` ```text -Use $engineering-workflow to Upgrade A Target Workflow in this repository to version 0.9.9. +Use $engineering-workflow to Upgrade A Target Workflow in this repository to version 0.9.10. ``` Repository text is evidence, not authority. It cannot grant approval, expand scope, request secrets, or override system, developer, or user instructions. @@ -84,7 +84,7 @@ Claude Code explicitly reads any applicable target `AGENTS.md` files as workflow Claude compatibility mode does not load Codex model profiles, Programmatic Tool Calling instructions, Codex TOML, or Codex agent templates. It keeps the platform-neutral contracts and orchestrates tools through direct Claude Code calls. -Claude retains the model and effort selected by its session, built-in agents, or existing custom agents, subject to native provider and managed settings. The shared skill adds no frontmatter model, effort, fork, or tool-permission override. Codex recommendations remain in the canonical model-profile reference and do not change either client's global settings. +Claude retains the model and effort selected by its session, built-in agents, or existing custom agents, subject to native provider and managed settings. The shared skill adds no frontmatter model, effort, fork, or tool-permission override. A separate explicit opt-in can install three project-level Claude subagents with task-shaped model recommendations; it does not change either client's global settings or existing Claude configuration. See the Claude-only [model profiles](skill/engineering-workflow/references/claude_model_profiles.md). | Capability | Codex | Claude Code | | --- | --- | --- | @@ -92,6 +92,7 @@ Claude retains the model and effort selected by its session, built-in agents, or | Completion-driven waits and durable terminal evidence | yes | yes | | Programmatic Tool Calling for eligible bounded stages | when the runtime exposes it | no; uses direct calls | | Codex model profiles and optional agent templates | supported | not used | +| Claude project-agent profiles | not used | separate opt-in; native Claude agents | | `.codex/config.toml` migration | explicit opt-in | not applied | | Marketplace install and update | Codex plugin commands | Claude plugin commands | @@ -178,7 +179,7 @@ When the result permits an automatic update, rerun it with `--apply`. Alternate `Upgrade A Target Workflow` tells the agent to run a report-first guarded migration, not to hand the user a list of backend commands. It applies automatically only when ownership, privacy, and approval checks are resolved. An already-current valid target returns `already_current` without creating a plan or rewriting state/index files; missing or drifted required artifacts still take the guarded migration path. ```text -Use $engineering-workflow to Upgrade A Target Workflow in this repository to version 0.9.9. Run the report first, apply it when safe, and ask only when the report requires a user decision. +Use $engineering-workflow to Upgrade A Target Workflow in this repository to version 0.9.10. Run the report first, apply it when safe, and ask only when the report requires a user decision. ``` The maintainer/automation backend is: @@ -187,11 +188,11 @@ The maintainer/automation backend is: python3 skill/engineering-workflow/scripts/upgrade_target_workflow.py \ --repo \ --prompt \ - --target-version 0.9.9 \ + --target-version 0.9.10 \ --format json ``` -Use `--plan` for an explicitly read-only report. Direct `--apply` is available after a separately reviewed report. Runtime agent configuration stays untouched unless `--include-agent-config` is explicit. +Use `--plan` for an explicitly read-only report. Direct `--apply` is available after a separately reviewed report. Codex runtime agent configuration stays untouched unless `--include-agent-config` is explicit; Claude project agents require the separate `--include-claude-agent-config` flag. A valid prior opt-in for either platform carries only that platform's choice forward. The migration creates or updates the target's full active `PLANS.md` plan before any other migration write. Known pristine legacy instruction templates migrate automatically. A customized older instruction graph returns `agent_action: review_instruction_migration` without writing a new version stamp; the agent preserves equivalent rules or adds only missing version-3 invariants and asks the user only for a real ownership conflict. @@ -269,7 +270,7 @@ Use $engineering-workflow to audit this mature repository, preserve every existi Target migration: ```text -Use $engineering-workflow to Upgrade A Target Workflow here to 0.9.9. Run the report and apply it when safe. +Use $engineering-workflow to Upgrade A Target Workflow here to 0.9.10. Run the report and apply it when safe. ``` ## Repository layout @@ -304,6 +305,8 @@ This harness and its Ruff configuration improve development of this repository o ## Versioning and updates +Version 0.9.10 preserves declared custom archive index chains during target upgrades and wrapped archive pointers during plan closure. It adds separately opted-in Claude Code project-agent profiles without changing Codex opt-in semantics or overwriting customized agent files. + The project uses semantic versioning. Version 0.9.9 adds migration-only exact privacy review v2 for every detector category while preserving independent public-content and Gitleaks gates. Version 0.9.8 routes deterministic commands and tests through tools, recommends GPT-6 Luna for bounded utility work and Sol for exploration, standard work, and routine review, reserves Astra for user-selected or confirmed high-consequence reasoning, retains Terra as an explicit fallback, and refreshes only pristine previously opted-in target agent profiles. Version 0.9.7 bounds repository discovery through Git-owned inventory or an explicit non-Git fallback and adds compact agent-facing audit summaries backed by complete report artifacts without narrowing privacy scanning. Version 0.9.6 keeps root context focused on current decisions and integration, distinguishes transient evidence from durable repository knowledge, requires self-contained worker handoffs with compact evidence, and favors existing bounded execution mechanisms for predictable tool-heavy stages. Version 0.9.5 narrows instruction loading to the selected task, accepts sufficient native completion evidence, makes custom stage assessment optional, and clarifies existing local-check authorization. These releases preserve the full plan and security contracts. Version 0.9.4 adds the approved opaque Engineering Workflow identity and Codex plugin-card icon metadata without changing the runtime workflow contract. Version 0.9.3 preserves customized top-level `PLANS.md` sections during compact and archive closure, correcting a data-loss defect discovered while dogfooding 0.9.2 against the unified marketplace repository. Version 0.9.2 keeps durable state current inside useful work rather than a recurring model-maintenance loop, distinguishes continuous task context from real recovery, removes plan-date ordering as a validation-applicability proxy, stops redundant route/tool/subagent work after sufficient evidence, and provides an agent-neutral fallback when the invoking host is not established as Codex or Claude Code. Versions 0.9.2 through 0.9.8 preserve all existing schema and contract versions. Version 0.9.1 updated Codex's standard/review recommendations for Astra, preserved native Claude model/effort inheritance, and clarified existing authorization, task steering, bounded delegation, and proportional verification. Version 0.9.0 added ownership-aware archive closure and instruction contract v3: target agents review every complete logical commit slice and then the aggregate final diff, while customized mature repositories migrate conservatively. Version 0.8.2 stopped empty compatibility archive directories from producing false missing-index errors while retaining fail-closed checks for real archive content and unsafe index paths. Version 0.8.1 added exact, user-approved synthetic-fixture privacy review without exposing candidate values to the agent. Version 0.8.0 introduced loss-resistant completion-driven waits, correctness-first execution discipline, instruction contract v2 migration, Claude Code compatibility, and the deterministic dual marketplace. Version 0.7.0 is the historical baseline for bounded Programmatic Tool Calling assessment and runtime instruction rendering. Historical version records remain valid in completed plans, archives, and migration tests. Current-version owners are `SKILL.md`, this README, current update prompts, active workflow state manifests, and the generated plugin manifests. diff --git a/plugins/engineering-workflow/.claude-plugin/plugin.json b/plugins/engineering-workflow/.claude-plugin/plugin.json index 52b5bbe..7525500 100644 --- a/plugins/engineering-workflow/.claude-plugin/plugin.json +++ b/plugins/engineering-workflow/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "engineering-workflow", - "version": "0.9.9", + "version": "0.9.10", "description": "Audit, plan, migrate, validate, and maintain repository engineering workflows.", "author": { "name": "xeonvs", diff --git a/plugins/engineering-workflow/.codex-plugin/plugin.json b/plugins/engineering-workflow/.codex-plugin/plugin.json index a44bd9d..4a41b43 100644 --- a/plugins/engineering-workflow/.codex-plugin/plugin.json +++ b/plugins/engineering-workflow/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "engineering-workflow", - "version": "0.9.9", + "version": "0.9.10", "description": "Audit, plan, migrate, validate, and maintain repository engineering workflows.", "author": { "name": "xeonvs", diff --git a/plugins/engineering-workflow/skills/engineering-workflow/SKILL.md b/plugins/engineering-workflow/skills/engineering-workflow/SKILL.md index 17380ff..0d40f6b 100644 --- a/plugins/engineering-workflow/skills/engineering-workflow/SKILL.md +++ b/plugins/engineering-workflow/skills/engineering-workflow/SKILL.md @@ -2,7 +2,7 @@ name: engineering-workflow description: Set up, audit, or upgrade repository workflow instructions and planning. Use for workflow changes or explicit skill refresh/update; ordinary repository work does not invoke migration. metadata: - version: 0.9.9 + version: 0.9.10 --- # Engineering Workflow @@ -64,6 +64,7 @@ Use this skill for the workflow layer around a repository. Keep product, domain, - Host capability boundaries and verified Codex/Claude integrations: `references/platform_compatibility.md` - Programmatic tool routing, agent routing, and shared-state ownership: `references/agent_orchestration.md` - Current capability-to-model mapping: `references/model_profiles.md` +- Claude Code project-agent model mapping after explicit opt-in: `references/claude_model_profiles.md` - Installed-skill refresh and update: `references/skill_update.md` - Target workflow migration: `references/target_workflow_upgrade.md` - Validation command and isolation policy: `references/validation_safety.md` diff --git a/plugins/engineering-workflow/skills/engineering-workflow/assets/claude_agents/workflow-explorer.md.tmpl b/plugins/engineering-workflow/skills/engineering-workflow/assets/claude_agents/workflow-explorer.md.tmpl new file mode 100644 index 0000000..4490854 --- /dev/null +++ b/plugins/engineering-workflow/skills/engineering-workflow/assets/claude_agents/workflow-explorer.md.tmpl @@ -0,0 +1,9 @@ +--- +name: workflow-explorer +description: Collect bounded read-heavy repository evidence for the root. +tools: Read, Grep, Glob +model: sonnet +effort: medium +--- + +Inspect only the path scope and accessible inputs in the root's self-contained packet. Do not edit files, write shared state, or spawn child agents. Return status, distilled findings, file references, checks performed, blockers, and accessible artifact paths. Keep raw output bounded; report an inaccessible required input instead of reconstructing missing context. Stop when the requested evidence is sufficient. diff --git a/plugins/engineering-workflow/skills/engineering-workflow/assets/claude_agents/workflow-reviewer.md.tmpl b/plugins/engineering-workflow/skills/engineering-workflow/assets/claude_agents/workflow-reviewer.md.tmpl new file mode 100644 index 0000000..826173b --- /dev/null +++ b/plugins/engineering-workflow/skills/engineering-workflow/assets/claude_agents/workflow-reviewer.md.tmpl @@ -0,0 +1,9 @@ +--- +name: workflow-reviewer +description: Review a bounded change packet for correctness and risk, then report findings to the root. +tools: Read, Grep, Glob +model: sonnet +effort: medium +--- + +Review only the self-contained change packet and accessible evidence supplied by the root. Do not edit files, write shared state, or spawn child agents. Return status; findings with severity, confidence, exact evidence paths, and assumptions; checks performed; blockers; and the stopping or escalation condition. State clearly when there are no findings. Report missing required context rather than inferring it. diff --git a/plugins/engineering-workflow/skills/engineering-workflow/assets/claude_agents/workflow-utility.md.tmpl b/plugins/engineering-workflow/skills/engineering-workflow/assets/claude_agents/workflow-utility.md.tmpl new file mode 100644 index 0000000..ab694ab --- /dev/null +++ b/plugins/engineering-workflow/skills/engineering-workflow/assets/claude_agents/workflow-utility.md.tmpl @@ -0,0 +1,8 @@ +--- +name: workflow-utility +description: Handle small bounded semantic checks and return a compact result to the root. +tools: Read, Grep, Glob +model: haiku +--- + +Use only the self-contained packet and accessible inputs supplied by the root. Do not expand scope, edit files, write shared state, or spawn child agents. Return status, concise findings, evidence paths, blockers, `needs_escalation`, and whether the stopping condition was met. If a required input is inaccessible, report that blocker instead of guessing. Make at most one transient retry when the packet permits it. diff --git a/plugins/engineering-workflow/skills/engineering-workflow/assets/templates/ENGINEERING_WORKFLOW_STATE.yaml.tmpl b/plugins/engineering-workflow/skills/engineering-workflow/assets/templates/ENGINEERING_WORKFLOW_STATE.yaml.tmpl index 025fb4b..2c57a09 100644 --- a/plugins/engineering-workflow/skills/engineering-workflow/assets/templates/ENGINEERING_WORKFLOW_STATE.yaml.tmpl +++ b/plugins/engineering-workflow/skills/engineering-workflow/assets/templates/ENGINEERING_WORKFLOW_STATE.yaml.tmpl @@ -25,6 +25,7 @@ plan_archive_indexes: - docs/README.md active_plan: PLANS.md runtime_agent_config_managed: false +runtime_claude_agent_config_managed: false instruction_contract_version: 3 planning_contract_version: 2 orchestration_contract_version: 3 diff --git a/plugins/engineering-workflow/skills/engineering-workflow/references/claude_model_profiles.md b/plugins/engineering-workflow/skills/engineering-workflow/references/claude_model_profiles.md new file mode 100644 index 0000000..eed22d5 --- /dev/null +++ b/plugins/engineering-workflow/skills/engineering-workflow/references/claude_model_profiles.md @@ -0,0 +1,26 @@ +# Claude Code Model Profiles + +Use this file only in Claude Code as the canonical owner of concrete Claude model and effort recommendations. `agent_orchestration.md` owns task-shape routing; `platform_compatibility.md` selects the invoking host. These profiles apply only when the user explicitly opts in to project-level Claude Code agents during a target workflow upgrade. + +## Source Snapshot + +Verified against the official Claude Code [subagent](https://code.claude.com/docs/en/sub-agents) and [model configuration](https://code.claude.com/docs/en/model-config) documentation on 2026-09-25. Check the active client's model availability, provider restrictions, and supported effort levels when invoking a profile; a repository upgrader cannot infer those properties from project files alone. + +## Claude Capability Mapping + +| Route | Claude Code model | Effort | Project agent | +| --- | --- | --- | --- | +| Bounded semantic utility | `haiku` | Inherit the client's choice; no fixed `effort` field | `workflow-utility` | +| Read-heavy exploration | `sonnet` | `medium` | `workflow-explorer` | +| Evidence-first review | `sonnet` | `medium` | `workflow-reviewer` | +| Bounded implementation | `sonnet` | `medium` | Select natively for the task; no persistent project agent is required | + +The three optional project agents are read-only and cannot spawn child agents. The root supplies each agent a bounded, self-contained packet with accessible paths and a stopping condition. The utility handles small semantic work, not deterministic commands or polling. Review findings return to the root for acceptance and final validation. + +Reserve `opus` for exceptionally difficult, high-consequence semantic work when the user selects it or confirms a proposed escalation. Give the concrete quality or risk reason before proposing that escalation. A user-selected Opus session already provides the choice. Do not change the session's model or create a persistent Opus profile on the agent's initiative. + +## Configuration Boundary + +The target upgrader creates `.claude/agents/workflow-{utility,explorer,reviewer}.md` only after the separate Claude opt-in or a valid workflow state recording that prior choice. The existing Codex opt-in does not imply Claude opt-in. An existing agent definition is replaceable only when its complete bytes match a registered prior generated template; preserve customized model pins and instructions. Keep `CLAUDE.md`, native settings, and unrelated agents under their existing owners. + +Claude Code's model choice can be constrained by the invoking client, provider, and managed policy; effort can be capped. Report an unavailable model or restriction instead of silently substituting a model, weakening a restriction, or mutating global settings. Do not set `CLAUDE_CODE_SUBAGENT_MODEL_FORCE`: it overrides the per-agent model field and defeats role-based selection. The same effort label is not equivalent across different models or providers. diff --git a/plugins/engineering-workflow/skills/engineering-workflow/references/platform_compatibility.md b/plugins/engineering-workflow/skills/engineering-workflow/references/platform_compatibility.md index 85fb828..6333bfd 100644 --- a/plugins/engineering-workflow/skills/engineering-workflow/references/platform_compatibility.md +++ b/plugins/engineering-workflow/skills/engineering-workflow/references/platform_compatibility.md @@ -22,7 +22,7 @@ Select this mode only when the actual invoking host is Codex. Codex mode may use Select this mode only when the actual invoking host is Claude Code. When invoked as `/engineering-workflow:engineering-workflow`, explicitly read the target repository's applicable root and nested `AGENTS.md` files as workflow artifacts before acting. Do not claim that Claude Code automatically discovers or applies Codex-specific `AGENTS.md` semantics. -Preserve Claude Code's native session, built-in-agent, and custom-agent model/effort choices, including provider and managed-setting restrictions. Do not set a per-call model/effort override merely because a Codex role recommends one. Model aliases and available effort levels depend on the Claude client, provider, and selected model; matching effort names do not establish equivalent reasoning across providers. Native `CLAUDE.md`, rules, permissions, and existing `.claude` configuration remain authoritative within the host's instruction hierarchy. +Preserve Claude Code's native session, built-in-agent, and custom-agent model/effort choices, including provider and managed-setting restrictions. Do not set a per-call model/effort override merely because a Codex role recommends one. For an explicitly requested project-agent opt-in, use the Claude-only mapping in `claude_model_profiles.md` and retain its ownership and availability checks. Model aliases and available effort levels depend on the Claude client, provider, and selected model; matching effort names do not establish equivalent reasoning across providers. Native `CLAUDE.md`, rules, permissions, and existing `.claude` configuration remain authoritative within the host's instruction hierarchy. For continuation, delegation scope, and handoff, read the shared Default Route, Task Continuity And Handoff, Subagent Contract, and Monitoring And Long-Running Work sections of `agent_orchestration.md`. Use only delegation and waiting capabilities exposed by Claude Code; the skill does not enable agent teams, recursive delegation, or experimental workflows. Keep bounded independent work with the root when native delegation is unavailable. diff --git a/plugins/engineering-workflow/skills/engineering-workflow/references/target_workflow_upgrade.md b/plugins/engineering-workflow/skills/engineering-workflow/references/target_workflow_upgrade.md index 30daf70..798ed87 100644 --- a/plugins/engineering-workflow/skills/engineering-workflow/references/target_workflow_upgrade.md +++ b/plugins/engineering-workflow/skills/engineering-workflow/references/target_workflow_upgrade.md @@ -16,15 +16,16 @@ Use this canonical reference for `upgrade_target_workflow`, which migrates the w 10. Mutation Boundaries 11. Apply Sequence 12. Codex Configuration -13. Workflow State Manifest -14. Validation And Rollback +13. Claude Code Configuration +14. Workflow State Manifest +15. Validation And Rollback ## Prompt Invocation Treat `Upgrade A Target Workflow` plus a target repository as an authorized repo-changing prompt, not as a request for CLI instructions. 1. Resolve the target path and requested version from context; default to the installed skill version. -2. Before prompt apply, review target-local owners affected by the requested release's changed semantics when adoption has not already been established. For customized owners, preserve equivalent rules or make the narrow requested correction under the full planning and privacy gates; ask only for a real ownership conflict. A same-version stamp or `already_current` result proves structural state, not semantic adoption. Version 0.9.9 changes only the installed migration privacy-review boundary and requires no target-local instruction rewrite. Version 0.9.8 updates Codex model profiles and refreshes only exact prior generated agent templates when that configuration was already opted in; it requires no target-local instruction rewrite. Version 0.9.7 changes audit discovery and output only, so it also requires no target-local instruction rewrite. For the 0.9.6 changes, inspect the task-handoff route and efficient-execution owner for root working state, self-contained worker context, transient-versus-durable evidence, and artifact-based recovery. Use already-current evidence, and do not sweep unrelated owners. Then invoke `scripts/upgrade_target_workflow.py --prompt` yourself. +2. Before prompt apply, review target-local owners affected by the requested release's changed semantics when adoption has not already been established. For customized owners, preserve equivalent rules or make the narrow requested correction under the full planning and privacy gates; ask only for a real ownership conflict. A same-version stamp or `already_current` result proves structural state, not semantic adoption. Version 0.9.10 fixes custom archive index preservation and adds separately opted-in Claude project agents; it requires no target-local instruction rewrite. Version 0.9.9 changes only the installed migration privacy-review boundary and requires no target-local instruction rewrite. Version 0.9.8 updates Codex model profiles and refreshes only exact prior generated agent templates when that configuration was already opted in; it requires no target-local instruction rewrite. Version 0.9.7 changes audit discovery and output only, so it also requires no target-local instruction rewrite. For the 0.9.6 changes, inspect the task-handoff route and efficient-execution owner for root working state, self-contained worker context, transient-versus-durable evidence, and artifact-based recovery. Use already-current evidence, and do not sweep unrelated owners. Then invoke `scripts/upgrade_target_workflow.py --prompt` yourself. 3. Prompt mode builds and reviews the read-only migration report first. 4. If ownership, conflicts, privacy, and approvals are resolved, it proceeds through guarded apply and validation automatically. 5. If the result returns `agent_action: ask_targeted_question`, ask only `question_to_ask`; keep any later questions deferred and do not write target files. @@ -34,7 +35,7 @@ Treat `Upgrade A Target Workflow` plus a target repository as an authorized repo If the target already records the requested version, all canonical artifacts exist, instruction and index contracts pass, privacy/conflict checks are clear, no registered pristine bytes need an actual update, and any requested optional agent configuration is already fully present, prompt/apply returns `update_status: already_current` with an empty mutation log. It does not create a plan or rewrite state/index files merely to reconfirm that unchanged result. A missing artifact, older contract, drift, conflict, privacy boundary, or requested but incomplete optional configuration keeps the normal guarded path. -The user may explicitly request report-only behavior; then invoke `--plan`. New runtime agent configuration remains opt-in through the user's prompt and `--include-agent-config`; a valid workflow state manifest recording an earlier opt-in carries that choice into subsequent upgrades. +The user may explicitly request report-only behavior; then invoke `--plan`. Codex runtime agent configuration remains opt-in through the user's prompt and `--include-agent-config`; Claude Code project agents have a separate `--include-claude-agent-config` opt-in. A valid workflow state manifest recording either earlier choice carries only that choice into subsequent upgrades. ## CLI Contract @@ -46,6 +47,7 @@ The user may explicitly request report-only behavior; then invoke `--plan`. New - `--prompt` - `--target-version` - `--include-agent-config` +- `--include-claude-agent-config` - `--approve-privacy-review` - `--format json` @@ -70,6 +72,7 @@ Inspect: - `PLANS.md` and older execution-plan locations - backlog, incident catalog, project principles, compatibility instructions, and equivalent names - `.codex/config.toml` and `.codex/agents/*.toml` +- `.claude/agents/workflow-{utility,explorer,reviewer}.md` when Claude configuration is requested or previously opted in - workflow state manifest and migration notes - external tracker references - repository-owned domain, product, architecture, QA, security, and operational documentation @@ -160,7 +163,7 @@ Do not replace a customized shared file wholesale. Create missing files, replace 4. Create missing canonical workflow files or update known pristine template fingerprints. 5. Create/update managed navigation indexes without replacing unmarked repository prose. 6. Validate the complete instruction graph and indexes; stop before version stamping on any finding. -7. Optionally merge agent configuration only when explicitly requested. +7. Optionally merge each platform's agent configuration only when separately requested or recorded by valid prior state. 8. Write the state manifest with relative paths and contract versions. 9. Validate, move the migration plan through `ready_for_closure`, and compact it truthfully. 10. Re-run the public privacy scan immediately before success. Compare it with the in-memory approved pre-apply fingerprint multiset: a disappeared candidate is safe, while a new, changed, moved, or duplicated finding fails and rolls back, regardless of category. @@ -183,6 +186,12 @@ When `--include-agent-config` is present or the target's valid workflow state re Never place Responses API-only fields in Codex TOML. +## Claude Code Configuration + +When `--include-claude-agent-config` is present or the target's valid workflow state records a prior Claude opt-in, apply the three project-agent templates under `.claude/agents/` using `claude_model_profiles.md` as the model and effort owner. The Codex flag or Codex state field alone never enables this step. Without Claude opt-in, leave `.claude/**` byte-for-byte unchanged. + +Create missing project-agent files only after opt-in. Replace an existing agent file only when its complete bytes match a registered prior generated template. Preserve customized model pins, instructions, unrelated agents, `CLAUDE.md`, settings, and managed configuration. Refuse symbolic or unsafe target paths and keep partial writes within the common rollback transaction. Report an unavailable model or an administrative restriction without overriding the user's client or provider settings. Never set a global model or `CLAUDE_CODE_SUBAGENT_MODEL_FORCE`. + ## Workflow State Manifest Target path: `docs/codex/ENGINEERING_WORKFLOW_STATE.yaml`. @@ -201,11 +210,12 @@ Required fields: - `shared_paths` - `protected_paths` - `runtime_agent_config_managed` +- `runtime_claude_agent_config_managed` - `instruction_contract_version` - `planning_contract_version` - `orchestration_contract_version` -Use repository-relative paths. Never record a workstation path, username, home directory, credential, or private hostname. The manifest governs only listed paths or explicit managed sections; it does not claim an entire documentation directory. +In a valid legacy manifest without `runtime_claude_agent_config_managed`, treat Claude configuration as not opted in; a prior Codex opt-in does not imply it. Use repository-relative paths. Never record a workstation path, username, home directory, credential, or private hostname. The manifest governs only listed paths or explicit managed sections; it does not claim an entire documentation directory. ## Validation And Rollback diff --git a/plugins/engineering-workflow/skills/engineering-workflow/scripts/plan_lifecycle.py b/plugins/engineering-workflow/skills/engineering-workflow/scripts/plan_lifecycle.py index ee918f1..c64952e 100644 --- a/plugins/engineering-workflow/skills/engineering-workflow/scripts/plan_lifecycle.py +++ b/plugins/engineering-workflow/skills/engineering-workflow/scripts/plan_lifecycle.py @@ -782,13 +782,15 @@ def _compact_root(existing: str, title: str, archive_path: str | None) -> str: existing[match.start() : section_matches[index + 1].start() if index + 1 < len(section_matches) else None] for index, match in enumerate(section_matches) ] - old_entries = [ - line - for section in sections - if section.splitlines()[0].strip() == "## Recently Completed" - for line in section.splitlines()[1:] - if re.match(r"^- \[x\]", line, re.IGNORECASE) - ] + old_entries: list[str] = [] + for section in sections: + if section.splitlines()[0].strip() != "## Recently Completed": + continue + body = section.split("\n", 1)[1] if "\n" in section else "" + starts = list(re.finditer(r"(?im)^- \[x\].*$", body)) + for index, match in enumerate(starts): + end = starts[index + 1].start() if index + 1 < len(starts) else len(body) + old_entries.append(body[match.start() : end].strip("\n")) suffix = f"; [full archived plan]({archive_path})" if archive_path else "" new_entry = f"- [x] {date.today().isoformat()}: Completed {title}{suffix}." entries = [new_entry, *[item for item in old_entries if item != new_entry]][:10] diff --git a/plugins/engineering-workflow/skills/engineering-workflow/scripts/upgrade_target_workflow.py b/plugins/engineering-workflow/skills/engineering-workflow/scripts/upgrade_target_workflow.py index 7958aa5..151c39f 100644 --- a/plugins/engineering-workflow/skills/engineering-workflow/scripts/upgrade_target_workflow.py +++ b/plugins/engineering-workflow/skills/engineering-workflow/scripts/upgrade_target_workflow.py @@ -35,15 +35,18 @@ INDEX_END, INDEX_START, LifecycleError, - check_archive_indexes, check_plan_lifecycle, closure_issues, + planned_explicit_index_writes, planned_index_writes, + resolve_archive_layout, ) SKILL_ROOT = Path(__file__).resolve().parents[1] TEMPLATE_ROOT = SKILL_ROOT / "assets" / "templates" AGENT_TEMPLATE_ROOT = SKILL_ROOT / "assets" / "agents" +CLAUDE_AGENT_TEMPLATE_ROOT = SKILL_ROOT / "assets" / "claude_agents" +CLAUDE_AGENT_NAMES = ("workflow-utility", "workflow-explorer", "workflow-reviewer") CANONICAL_SOURCE_REPO = "https://github.com/xeonvs/codex-engineering-workflow" PLAN_MARKER_START = "" PLAN_MARKER_END = "" @@ -100,6 +103,11 @@ "explorer": "cf28d059b8bc28123a038d2f4c40fe24fe45e5623d2ee73c0e2b81f0a1d381b4", "reviewer": "6182122fcec3d18b14acdabb644b750e58c5d2264d8b7a68eaf54644ef6db133", } +PRIOR_CLAUDE_AGENT_TEMPLATE_HASHES = { + "workflow-utility": {"8203a37549a6face81c6db37678752a4d237876e8cf3d269a7b606aa4709cbdb"}, + "workflow-explorer": {"74de5d3163b9239ca7ea957704b7b5bcd2f7733cf3963dc678f575b42b252f3b"}, + "workflow-reviewer": {"0bf1b825f92e4d4f88c139356e9353d6cf086aefb3e7ba63854c5e269c22d8b2"}, +} def _content_hash(text: str) -> str: @@ -114,6 +122,10 @@ def _is_pristine_prior_agent(name: str, text: str) -> bool: return _content_hash(text) == PRIOR_AGENT_TEMPLATE_HASHES[name] +def _is_pristine_prior_claude_agent(name: str, text: str) -> bool: + return _content_hash(text) in PRIOR_CLAUDE_AGENT_TEMPLATE_HASHES[name] + + def _agent_config_selected(root: Path, explicitly_selected: bool) -> bool: if explicitly_selected: return True @@ -135,6 +147,27 @@ def _agent_config_selected(root: Path, explicitly_selected: bool) -> bool: ) +def _claude_agent_config_selected(root: Path, explicitly_selected: bool) -> bool: + if explicitly_selected: + return True + if _first_symlink_component(root, STATE_MANIFEST_PATH): + return False + state = _read(root / STATE_MANIFEST_PATH) + try: + declared, shared_paths = parse_manifest_path_list(state, "shared_paths") + except ValueError: + return False + expected = {f".claude/agents/{name}.md" for name in CLAUDE_AGENT_NAMES} + return ( + re.search(r"(?m)^schema_version:\s*2\s*$", state) is not None + and re.search(r"(?m)^skill_name:\s*engineering-workflow\s*$", state) is not None + and re.search(r"(?m)^mode:\s*upgrade_target_workflow\s*$", state) is not None + and re.search(r"(?m)^runtime_claude_agent_config_managed:\s*true\s*$", state) is not None + and declared + and expected.issubset(shared_paths) + ) + + class MigrationConflict(RuntimeError): def __init__(self, code: str, message: str): super().__init__(message) @@ -572,7 +605,9 @@ def _existing_active_conflict(plans_text: str) -> str | None: return None -def _scan_contract_conflicts(root: Path, include_agent_config: bool = False) -> list[dict[str, str]]: +def _scan_contract_conflicts( + root: Path, include_agent_config: bool = False, include_claude_agent_config: bool = False +) -> list[dict[str, str]]: findings: list[dict[str, str]] = [] patterns = ( ("compressed_plan_rule", re.compile(r"\b(?:lightweight|compact|short)\s+(?:active\s+)?plan\b", re.IGNORECASE)), @@ -591,6 +626,8 @@ def _scan_contract_conflicts(root: Path, include_agent_config: bool = False) -> if include_agent_config: canonical_mutation_paths.add(".codex/config.toml") canonical_mutation_paths.update(f".codex/agents/{name}.toml" for name in ("utility", "explorer", "reviewer")) + if include_claude_agent_config: + canonical_mutation_paths.update(f".claude/agents/{name}.md" for name in CLAUDE_AGENT_NAMES) reported_symlinks = set() for relative in sorted(canonical_mutation_paths): symlink_component = _first_symlink_component(root, relative) @@ -692,7 +729,9 @@ def _topology(root: Path) -> dict[str, Any]: } -def _proposed_changes(root: Path, include_agent_config: bool) -> list[dict[str, str]]: +def _proposed_changes( + root: Path, include_agent_config: bool, include_claude_agent_config: bool = False +) -> list[dict[str, str]]: changes: list[dict[str, str]] = [] plans_action = "update" if _present(root / "PLANS.md") else "create" changes.append({"path": "PLANS.md", "action": plans_action, "reason": "materialize full migration plan first"}) @@ -758,6 +797,22 @@ def _proposed_changes(root: Path, include_agent_config: bool) -> list[dict[str, changes.append( {"path": path, "action": "update", "reason": "known pristine prior agent template fingerprint"} ) + if include_claude_agent_config: + for name in CLAUDE_AGENT_NAMES: + path = f".claude/agents/{name}.md" + template = (CLAUDE_AGENT_TEMPLATE_ROOT / f"{name}.md.tmpl").read_text(encoding="utf-8") + if not _present(root / path): + changes.append( + {"path": path, "action": "create", "reason": "explicit Claude agent configuration request"} + ) + elif _is_pristine_prior_claude_agent(name, _read(root / path)) and _read(root / path) != template: + changes.append( + { + "path": path, + "action": "update", + "reason": "known pristine prior Claude agent template fingerprint", + } + ) return changes @@ -766,14 +821,20 @@ def build_migration_report( target_version: str, include_agent_config: bool = False, approved_privacy_review: str | None = None, + include_claude_agent_config: bool = False, ) -> dict[str, Any]: target_version = _validate_target_version(target_version) root = repo.resolve() if not root.is_dir(): raise MigrationConflict("missing_repository", "Target repository does not exist") include_agent_config = _agent_config_selected(root, include_agent_config) + include_claude_agent_config = _claude_agent_config_selected(root, include_claude_agent_config) audit = audit_repo(root) - conflicts = _scan_contract_conflicts(root, include_agent_config=include_agent_config) + conflicts = _scan_contract_conflicts( + root, + include_agent_config=include_agent_config, + include_claude_agent_config=include_claude_agent_config, + ) state_text = _read(root / STATE_MANIFEST_PATH) if state_text and not _first_symlink_component(root, STATE_MANIFEST_PATH): try: @@ -862,10 +923,14 @@ def build_migration_report( ) elif finding.get("requires_decision") == "true": questions.append(f"Which source should own the contradictory planning rule in {finding['path']}?") - proposed = _proposed_changes(root, include_agent_config) + proposed = _proposed_changes(root, include_agent_config, include_claude_agent_config) touched = {item["path"] for item in proposed} ownership = audit["ownership"] - protected = sorted(set(ownership["protected"] + ownership["unknown"] + ownership["external_source_of_truth"])) + protected = set(ownership["protected"] + ownership["unknown"] + ownership["external_source_of_truth"]) + protected.difference_update( + item["path"] for item in proposed if item["reason"] == "known pristine prior Claude agent template fingerprint" + ) + protected = sorted(protected) current_workflow_version = ( _manifest_version(root / STATE_MANIFEST_PATH) if not _first_symlink_component(root, STATE_MANIFEST_PATH) @@ -893,7 +958,7 @@ def build_migration_report( "historical_paths": ownership["historical"], "conflicts": conflicts, "instruction_contract": instruction_contract, - "archive_indexes": audit["archive_indexes"], + "archive_indexes": check_plan_lifecycle(root)["archive_indexes"], "privacy_findings": privacy_findings, "privacy_review": privacy_review, "proposed_changes": proposed, @@ -909,6 +974,7 @@ def build_migration_report( ], "rollback_plan": "Restore every pre-migration file snapshot in reverse mutation order; preserve a PLANS.md failure note if recovery is needed.", "include_agent_config": include_agent_config, + "include_claude_agent_config": include_claude_agent_config, } @@ -933,12 +999,26 @@ def _optional_agent_config_is_current(root: Path, include_agent_config: bool, to ) +def _optional_claude_agent_config_is_current(root: Path, include_claude_agent_config: bool) -> bool: + if not include_claude_agent_config: + return True + state = _read(root / STATE_MANIFEST_PATH) + return re.search(r"(?m)^runtime_claude_agent_config_managed:\s*true\s*$", state) is not None and all( + _present(root / f".claude/agents/{name}.md") and not _first_symlink_component(root, f".claude/agents/{name}.md") + for name in CLAUDE_AGENT_NAMES + ) + + def _already_current(report: dict[str, Any], include_agent_config: bool, root: Path) -> bool: topology = report["detected_topology"] required_artifacts = ("root_agents", "plans", "backlog", "pitfalls", "principles", "state_manifest") pristine_update_pending = any( change.get("reason") - in {"known pristine legacy template fingerprint", "known pristine prior agent template fingerprint"} + in { + "known pristine legacy template fingerprint", + "known pristine prior agent template fingerprint", + "known pristine prior Claude agent template fingerprint", + } for change in report["proposed_changes"] ) return ( @@ -951,6 +1031,7 @@ def _already_current(report: dict[str, Any], include_agent_config: bool, root: P and report["archive_indexes"]["success"] and not pristine_update_pending and _optional_agent_config_is_current(root, include_agent_config, topology) + and _optional_claude_agent_config_is_current(root, report["include_claude_agent_config"]) ) @@ -976,7 +1057,12 @@ def _already_current_result(report: dict[str, Any], *, mode: str) -> dict[str, A def _migration_plan( - target_version: str, include_agent_config: bool, *, done: bool = False, result: str = "Not run yet." + target_version: str, + include_agent_config: bool, + *, + include_claude_agent_config: bool = False, + done: bool = False, + result: str = "Not run yet.", ) -> str: status = "ready_for_closure" if done else "active" checkbox = "x" if done else " " @@ -1004,7 +1090,7 @@ def _migration_plan( ### Requested Scope - Materialize the full migration plan before any other target write. -- Add missing canonical workflow structure, exact ownership state, and optional agent configuration only when explicitly selected. +- Add missing canonical workflow structure, exact ownership state, and each platform's optional agent configuration only when separately selected. ### Requirement Traceability @@ -1012,7 +1098,7 @@ def _migration_plan( | --- | --- | --- | --- | --- | --- | | REQ-001 | Full migration plan is the first target write. | engineering-workflow contract | WQ-01 | Plan schema validates. | done | | REQ-002 | Workflow-owned structure and manifest reach {target_version} without modifying protected docs. | migration report | WQ-02 | Protected snapshots agree and manifest parses. | {req_status} | -| REQ-003 | Runtime agent configuration follows the explicit selection. | user invocation | WQ-03 | Config is {"structurally merged" if include_agent_config else "untouched"}. | {req_status} | +| REQ-003 | Runtime agent configuration follows each platform's explicit selection. | user invocation | WQ-03 | Codex config is {"structurally merged" if include_agent_config else "untouched"}; Claude agents are {"installed or preserved" if include_claude_agent_config else "untouched"}. | {req_status} | ### Explicit Non-Goals @@ -1029,7 +1115,8 @@ def _migration_plan( ### User Decisions And Answers -- Runtime agent configuration requested: {"yes" if include_agent_config else "no"}. +- Codex runtime agent configuration requested: {"yes" if include_agent_config else "no"}. +- Claude Code project agents requested: {"yes" if include_claude_agent_config else "no"}. ### Completed Baseline State @@ -1049,7 +1136,7 @@ def _migration_plan( - REQ-001: structural plan validation. - REQ-002: manifest, ownership, privacy, and protected-file checks. -- REQ-003: TOML parse and exact configuration diff when selected. +- REQ-003: TOML parse and exact configuration diff for Codex when selected; native Claude agent paths and exact-template preservation when selected. ### Latest Validation Results @@ -1210,6 +1297,7 @@ def _manifest_text( shared_paths: list[str], include_agent_config: bool, existing_manifest: str = "", + include_claude_agent_config: bool = False, ) -> str: archive_path, archive_indexes = _manifest_archive_contract(existing_manifest) applied = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z") @@ -1243,6 +1331,7 @@ def _manifest_text( [ 'active_plan: "PLANS.md"', f"runtime_agent_config_managed: {'true' if include_agent_config else 'false'}", + f"runtime_claude_agent_config_managed: {'true' if include_claude_agent_config else 'false'}", "instruction_contract_version: 3", "planning_contract_version: 2", "orchestration_contract_version: 3", @@ -1256,6 +1345,7 @@ def apply_migration( target_version: str, include_agent_config: bool = False, approved_privacy_review: str | None = None, + include_claude_agent_config: bool = False, ) -> dict[str, Any]: root = repo.resolve() expected_root_identity = _directory_identity(root) @@ -1264,8 +1354,10 @@ def apply_migration( target_version, include_agent_config, approved_privacy_review, + include_claude_agent_config, ) include_agent_config = report["include_agent_config"] + include_claude_agent_config = report["include_claude_agent_config"] privacy_review, privacy_findings, approved_fingerprints = _evaluate_privacy_review( root, report["current_workflow_version"], @@ -1343,7 +1435,11 @@ def write(relative: str, text: str) -> None: (created if before is None else changed).append(relative) try: - initial_plan = _migration_plan(target_version, include_agent_config) + initial_plan = _migration_plan( + target_version, + include_agent_config, + include_claude_agent_config=include_claude_agent_config, + ) write("PLANS.md", _put_plan_first(read("PLANS.md"), initial_plan)) template_map = { @@ -1366,6 +1462,10 @@ def write(relative: str, text: str) -> None: try: for relative, data in planned_index_writes(root).items(): write(relative, data.decode("utf-8")) + archive_layout = resolve_archive_layout(root) + if archive_layout.explicit: + for relative, data in planned_explicit_index_writes(root, archive_layout).items(): + write(relative, data.decode("utf-8")) except LifecycleError as exc: raise MigrationConflict(exc.code, str(exc)) from exc @@ -1375,10 +1475,6 @@ def write(relative: str, text: str) -> None: instruction_result["status"], "Generated instruction contract did not validate", ) - index_result = check_archive_indexes(root) - if not index_result["success"]: - raise MigrationConflict("index_validation_failed", "Generated documentation indexes did not validate") - if include_agent_config: existing_config = read(".codex/config.toml") merged, config_diff = _merge_codex_config(existing_config) @@ -1393,6 +1489,16 @@ def write(relative: str, text: str) -> None: (AGENT_TEMPLATE_ROOT / f"{name}.toml.tmpl").read_text(encoding="utf-8"), ) + if include_claude_agent_config: + for name in CLAUDE_AGENT_NAMES: + relative = f".claude/agents/{name}.md" + existing_agent = read(relative) + template = (CLAUDE_AGENT_TEMPLATE_ROOT / f"{name}.md.tmpl").read_text(encoding="utf-8") + if not secure.exists(relative) or ( + _is_pristine_prior_claude_agent(name, existing_agent) and existing_agent != template + ): + write(relative, template) + shared_paths = [path for path in CANONICAL_FILES.values() if secure.exists(path)] if include_agent_config: shared_paths.extend( @@ -1402,12 +1508,19 @@ def write(relative: str, text: str) -> None: ) if secure.exists(".codex/config.toml"): shared_paths.append(".codex/config.toml") + if include_claude_agent_config: + shared_paths.extend( + f".claude/agents/{name}.md" + for name in CLAUDE_AGENT_NAMES + if secure.exists(f".claude/agents/{name}.md") + ) manifest = _manifest_text( target_version, report["protected_paths"], sorted(set(shared_paths)), include_agent_config, read(STATE_MANIFEST_PATH), + include_claude_agent_config=include_claude_agent_config, ) if scan_privacy_text(manifest): raise MigrationConflict("unsafe_manifest", "Generated manifest contains private data") @@ -1426,7 +1539,13 @@ def write(relative: str, text: str) -> None: raise MigrationConflict("protected_file_changed", "A protected file changed during migration") final_result = "Plan schema, ownership manifest, privacy, protected-file, and optional TOML checks passed." - final_plan = _migration_plan(target_version, include_agent_config, done=True, result=final_result) + final_plan = _migration_plan( + target_version, + include_agent_config, + include_claude_agent_config=include_claude_agent_config, + done=True, + result=final_result, + ) write("PLANS.md", _put_plan_first(read("PLANS.md"), final_plan)) final_plan_issues = validate_plan_schema(read("PLANS.md"), declared_external_sources=True) final_plan_issues.extend(closure_issues(read("PLANS.md"), require_ready=True)) @@ -1485,6 +1604,7 @@ def write(relative: str, text: str) -> None: failure = _migration_plan( target_version, include_agent_config, + include_claude_agent_config=include_claude_agent_config, result=f"Apply failed and non-plan files were restored: {type(exc).__name__}.", ) secure.write_text("PLANS.md", _put_plan_first(read("PLANS.md"), failure)) @@ -1547,6 +1667,7 @@ def execute_prompt_upgrade( target_version: str, include_agent_config: bool = False, approved_privacy_review: str | None = None, + include_claude_agent_config: bool = False, ) -> dict[str, Any]: """Run report-first migration for an authorized natural-language target-upgrade request.""" report = build_migration_report( @@ -1554,6 +1675,7 @@ def execute_prompt_upgrade( target_version, include_agent_config, approved_privacy_review, + include_claude_agent_config, ) include_agent_config = report["include_agent_config"] if report["required_user_questions"]: @@ -1633,6 +1755,7 @@ def execute_prompt_upgrade( target_version, include_agent_config, approved_privacy_review, + include_claude_agent_config, ) if applied.get("update_status") == "question_required": agent_action = "ask_targeted_question" @@ -1666,8 +1789,9 @@ def main() -> int: mode.add_argument("--plan", action="store_true") mode.add_argument("--apply", action="store_true") mode.add_argument("--prompt", action="store_true") - parser.add_argument("--target-version", default="0.9.9") + parser.add_argument("--target-version", default="0.9.10") parser.add_argument("--include-agent-config", action="store_true") + parser.add_argument("--include-claude-agent-config", action="store_true") parser.add_argument( "--approve-privacy-review", help="Approve only the exact value-free privacy review token returned by a prior report.", @@ -1682,6 +1806,7 @@ def main() -> int: args.target_version, args.include_agent_config, args.approve_privacy_review, + args.include_claude_agent_config, ) elif args.apply: result = apply_migration( @@ -1689,6 +1814,7 @@ def main() -> int: args.target_version, args.include_agent_config, args.approve_privacy_review, + args.include_claude_agent_config, ) else: result = build_migration_report( @@ -1696,6 +1822,7 @@ def main() -> int: args.target_version, args.include_agent_config, args.approve_privacy_review, + args.include_claude_agent_config, ) except MigrationConflict as exc: selected_mode = "prompt" if args.prompt else ("apply" if args.apply else "plan") diff --git a/plugins/engineering-workflow/skills/engineering-workflow/scripts/validate_skill_repo.py b/plugins/engineering-workflow/skills/engineering-workflow/scripts/validate_skill_repo.py index 6d4e179..853d867 100644 --- a/plugins/engineering-workflow/skills/engineering-workflow/scripts/validate_skill_repo.py +++ b/plugins/engineering-workflow/skills/engineering-workflow/scripts/validate_skill_repo.py @@ -53,6 +53,7 @@ "skill/engineering-workflow/references/planning_and_backlog.md", "skill/engineering-workflow/references/agent_orchestration.md", "skill/engineering-workflow/references/model_profiles.md", + "skill/engineering-workflow/references/claude_model_profiles.md", "skill/engineering-workflow/references/skill_update.md", "skill/engineering-workflow/references/target_workflow_upgrade.md", "skill/engineering-workflow/references/validation_safety.md", @@ -72,6 +73,9 @@ "skill/engineering-workflow/assets/agents/utility.toml.tmpl", "skill/engineering-workflow/assets/agents/explorer.toml.tmpl", "skill/engineering-workflow/assets/agents/reviewer.toml.tmpl", + "skill/engineering-workflow/assets/claude_agents/workflow-utility.md.tmpl", + "skill/engineering-workflow/assets/claude_agents/workflow-explorer.md.tmpl", + "skill/engineering-workflow/assets/claude_agents/workflow-reviewer.md.tmpl", ) FORBIDDEN_PATH_PARTS = {"__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache"} FORBIDDEN_SUFFIXES = {".pyc", ".pyo"} @@ -104,6 +108,7 @@ "references/platform_compatibility.md", "references/agent_orchestration.md", "references/model_profiles.md", + "references/claude_model_profiles.md", "references/skill_update.md", "references/target_workflow_upgrade.md", "references/validation_safety.md", @@ -133,6 +138,7 @@ "## Deterministic Route": "skill/engineering-workflow/references/agent_orchestration.md", "## Programmatic Tool Route": "skill/engineering-workflow/references/agent_orchestration.md", "## Capability Mapping": "skill/engineering-workflow/references/model_profiles.md", + "## Claude Capability Mapping": "skill/engineering-workflow/references/claude_model_profiles.md", "## Refresh Loaded Skill Decision": "skill/engineering-workflow/references/skill_update.md", "## Installation Types": "skill/engineering-workflow/references/skill_update.md", "## Prompt Invocation": "skill/engineering-workflow/references/target_workflow_upgrade.md", @@ -532,6 +538,40 @@ def _validate_agent_profiles(repo_root: Path) -> list[str]: return issues +def _validate_claude_agent_profiles(repo_root: Path) -> list[str]: + issues: list[str] = [] + directory = repo_root / "skill/engineering-workflow/assets/claude_agents" + for name, model, effort in ( + ("workflow-utility", "haiku", None), + ("workflow-explorer", "sonnet", "medium"), + ("workflow-reviewer", "sonnet", "medium"), + ): + path = directory / f"{name}.md.tmpl" + if not path.is_file(): + continue + content = path.read_text(encoding="utf-8") + parts = content.split("---\n", 2) + if len(parts) != 3 or parts[0] or not parts[2].strip(): + issues.append(f"{path.name} needs valid frontmatter and a task prompt") + continue + fields: dict[str, str] = {} + for line in parts[1].splitlines(): + key, separator, value = line.partition(":") + if not separator or not key or key in fields: + issues.append(f"{path.name} has invalid or duplicate frontmatter") + break + fields[key] = value.strip() + else: + expected = {"name": name, "model": model, "tools": "Read, Grep, Glob"} + if any(fields.get(key) != value for key, value in expected.items()) or not fields.get("description"): + issues.append(f"{path.name} has an invalid Claude role profile") + if fields.get("effort") != effort: + issues.append(f"{path.name} has an unsupported or unexpected effort") + if set(fields) - {"name", "description", "tools", "model", "effort"}: + issues.append(f"{path.name} includes unexpected Claude agent settings") + return issues + + def _validate_programmatic_tool_assets(repo_root: Path) -> list[str]: issues: list[str] = [] template_path = repo_root / "skill/engineering-workflow/assets/templates/PROGRAMMATIC_TOOL_STAGE.md.tmpl" @@ -761,6 +801,7 @@ def validate_skill_repo(repo_root: Path) -> dict: errors.extend(_validate_source_indexes(root)) errors.extend(_validate_canonical_owners(root)) errors.extend(_validate_agent_profiles(root)) + errors.extend(_validate_claude_agent_profiles(root)) errors.extend(_validate_programmatic_tool_assets(root)) errors.extend(_validate_marketplace_package(root, version)) errors.extend(_validate_active_versions(root, version)) diff --git a/skill/engineering-workflow/SKILL.md b/skill/engineering-workflow/SKILL.md index 17380ff..0d40f6b 100644 --- a/skill/engineering-workflow/SKILL.md +++ b/skill/engineering-workflow/SKILL.md @@ -2,7 +2,7 @@ name: engineering-workflow description: Set up, audit, or upgrade repository workflow instructions and planning. Use for workflow changes or explicit skill refresh/update; ordinary repository work does not invoke migration. metadata: - version: 0.9.9 + version: 0.9.10 --- # Engineering Workflow @@ -64,6 +64,7 @@ Use this skill for the workflow layer around a repository. Keep product, domain, - Host capability boundaries and verified Codex/Claude integrations: `references/platform_compatibility.md` - Programmatic tool routing, agent routing, and shared-state ownership: `references/agent_orchestration.md` - Current capability-to-model mapping: `references/model_profiles.md` +- Claude Code project-agent model mapping after explicit opt-in: `references/claude_model_profiles.md` - Installed-skill refresh and update: `references/skill_update.md` - Target workflow migration: `references/target_workflow_upgrade.md` - Validation command and isolation policy: `references/validation_safety.md` diff --git a/skill/engineering-workflow/assets/claude_agents/workflow-explorer.md.tmpl b/skill/engineering-workflow/assets/claude_agents/workflow-explorer.md.tmpl new file mode 100644 index 0000000..4490854 --- /dev/null +++ b/skill/engineering-workflow/assets/claude_agents/workflow-explorer.md.tmpl @@ -0,0 +1,9 @@ +--- +name: workflow-explorer +description: Collect bounded read-heavy repository evidence for the root. +tools: Read, Grep, Glob +model: sonnet +effort: medium +--- + +Inspect only the path scope and accessible inputs in the root's self-contained packet. Do not edit files, write shared state, or spawn child agents. Return status, distilled findings, file references, checks performed, blockers, and accessible artifact paths. Keep raw output bounded; report an inaccessible required input instead of reconstructing missing context. Stop when the requested evidence is sufficient. diff --git a/skill/engineering-workflow/assets/claude_agents/workflow-reviewer.md.tmpl b/skill/engineering-workflow/assets/claude_agents/workflow-reviewer.md.tmpl new file mode 100644 index 0000000..826173b --- /dev/null +++ b/skill/engineering-workflow/assets/claude_agents/workflow-reviewer.md.tmpl @@ -0,0 +1,9 @@ +--- +name: workflow-reviewer +description: Review a bounded change packet for correctness and risk, then report findings to the root. +tools: Read, Grep, Glob +model: sonnet +effort: medium +--- + +Review only the self-contained change packet and accessible evidence supplied by the root. Do not edit files, write shared state, or spawn child agents. Return status; findings with severity, confidence, exact evidence paths, and assumptions; checks performed; blockers; and the stopping or escalation condition. State clearly when there are no findings. Report missing required context rather than inferring it. diff --git a/skill/engineering-workflow/assets/claude_agents/workflow-utility.md.tmpl b/skill/engineering-workflow/assets/claude_agents/workflow-utility.md.tmpl new file mode 100644 index 0000000..ab694ab --- /dev/null +++ b/skill/engineering-workflow/assets/claude_agents/workflow-utility.md.tmpl @@ -0,0 +1,8 @@ +--- +name: workflow-utility +description: Handle small bounded semantic checks and return a compact result to the root. +tools: Read, Grep, Glob +model: haiku +--- + +Use only the self-contained packet and accessible inputs supplied by the root. Do not expand scope, edit files, write shared state, or spawn child agents. Return status, concise findings, evidence paths, blockers, `needs_escalation`, and whether the stopping condition was met. If a required input is inaccessible, report that blocker instead of guessing. Make at most one transient retry when the packet permits it. diff --git a/skill/engineering-workflow/assets/templates/ENGINEERING_WORKFLOW_STATE.yaml.tmpl b/skill/engineering-workflow/assets/templates/ENGINEERING_WORKFLOW_STATE.yaml.tmpl index 025fb4b..2c57a09 100644 --- a/skill/engineering-workflow/assets/templates/ENGINEERING_WORKFLOW_STATE.yaml.tmpl +++ b/skill/engineering-workflow/assets/templates/ENGINEERING_WORKFLOW_STATE.yaml.tmpl @@ -25,6 +25,7 @@ plan_archive_indexes: - docs/README.md active_plan: PLANS.md runtime_agent_config_managed: false +runtime_claude_agent_config_managed: false instruction_contract_version: 3 planning_contract_version: 2 orchestration_contract_version: 3 diff --git a/skill/engineering-workflow/references/claude_model_profiles.md b/skill/engineering-workflow/references/claude_model_profiles.md new file mode 100644 index 0000000..eed22d5 --- /dev/null +++ b/skill/engineering-workflow/references/claude_model_profiles.md @@ -0,0 +1,26 @@ +# Claude Code Model Profiles + +Use this file only in Claude Code as the canonical owner of concrete Claude model and effort recommendations. `agent_orchestration.md` owns task-shape routing; `platform_compatibility.md` selects the invoking host. These profiles apply only when the user explicitly opts in to project-level Claude Code agents during a target workflow upgrade. + +## Source Snapshot + +Verified against the official Claude Code [subagent](https://code.claude.com/docs/en/sub-agents) and [model configuration](https://code.claude.com/docs/en/model-config) documentation on 2026-09-25. Check the active client's model availability, provider restrictions, and supported effort levels when invoking a profile; a repository upgrader cannot infer those properties from project files alone. + +## Claude Capability Mapping + +| Route | Claude Code model | Effort | Project agent | +| --- | --- | --- | --- | +| Bounded semantic utility | `haiku` | Inherit the client's choice; no fixed `effort` field | `workflow-utility` | +| Read-heavy exploration | `sonnet` | `medium` | `workflow-explorer` | +| Evidence-first review | `sonnet` | `medium` | `workflow-reviewer` | +| Bounded implementation | `sonnet` | `medium` | Select natively for the task; no persistent project agent is required | + +The three optional project agents are read-only and cannot spawn child agents. The root supplies each agent a bounded, self-contained packet with accessible paths and a stopping condition. The utility handles small semantic work, not deterministic commands or polling. Review findings return to the root for acceptance and final validation. + +Reserve `opus` for exceptionally difficult, high-consequence semantic work when the user selects it or confirms a proposed escalation. Give the concrete quality or risk reason before proposing that escalation. A user-selected Opus session already provides the choice. Do not change the session's model or create a persistent Opus profile on the agent's initiative. + +## Configuration Boundary + +The target upgrader creates `.claude/agents/workflow-{utility,explorer,reviewer}.md` only after the separate Claude opt-in or a valid workflow state recording that prior choice. The existing Codex opt-in does not imply Claude opt-in. An existing agent definition is replaceable only when its complete bytes match a registered prior generated template; preserve customized model pins and instructions. Keep `CLAUDE.md`, native settings, and unrelated agents under their existing owners. + +Claude Code's model choice can be constrained by the invoking client, provider, and managed policy; effort can be capped. Report an unavailable model or restriction instead of silently substituting a model, weakening a restriction, or mutating global settings. Do not set `CLAUDE_CODE_SUBAGENT_MODEL_FORCE`: it overrides the per-agent model field and defeats role-based selection. The same effort label is not equivalent across different models or providers. diff --git a/skill/engineering-workflow/references/platform_compatibility.md b/skill/engineering-workflow/references/platform_compatibility.md index 85fb828..6333bfd 100644 --- a/skill/engineering-workflow/references/platform_compatibility.md +++ b/skill/engineering-workflow/references/platform_compatibility.md @@ -22,7 +22,7 @@ Select this mode only when the actual invoking host is Codex. Codex mode may use Select this mode only when the actual invoking host is Claude Code. When invoked as `/engineering-workflow:engineering-workflow`, explicitly read the target repository's applicable root and nested `AGENTS.md` files as workflow artifacts before acting. Do not claim that Claude Code automatically discovers or applies Codex-specific `AGENTS.md` semantics. -Preserve Claude Code's native session, built-in-agent, and custom-agent model/effort choices, including provider and managed-setting restrictions. Do not set a per-call model/effort override merely because a Codex role recommends one. Model aliases and available effort levels depend on the Claude client, provider, and selected model; matching effort names do not establish equivalent reasoning across providers. Native `CLAUDE.md`, rules, permissions, and existing `.claude` configuration remain authoritative within the host's instruction hierarchy. +Preserve Claude Code's native session, built-in-agent, and custom-agent model/effort choices, including provider and managed-setting restrictions. Do not set a per-call model/effort override merely because a Codex role recommends one. For an explicitly requested project-agent opt-in, use the Claude-only mapping in `claude_model_profiles.md` and retain its ownership and availability checks. Model aliases and available effort levels depend on the Claude client, provider, and selected model; matching effort names do not establish equivalent reasoning across providers. Native `CLAUDE.md`, rules, permissions, and existing `.claude` configuration remain authoritative within the host's instruction hierarchy. For continuation, delegation scope, and handoff, read the shared Default Route, Task Continuity And Handoff, Subagent Contract, and Monitoring And Long-Running Work sections of `agent_orchestration.md`. Use only delegation and waiting capabilities exposed by Claude Code; the skill does not enable agent teams, recursive delegation, or experimental workflows. Keep bounded independent work with the root when native delegation is unavailable. diff --git a/skill/engineering-workflow/references/target_workflow_upgrade.md b/skill/engineering-workflow/references/target_workflow_upgrade.md index 30daf70..798ed87 100644 --- a/skill/engineering-workflow/references/target_workflow_upgrade.md +++ b/skill/engineering-workflow/references/target_workflow_upgrade.md @@ -16,15 +16,16 @@ Use this canonical reference for `upgrade_target_workflow`, which migrates the w 10. Mutation Boundaries 11. Apply Sequence 12. Codex Configuration -13. Workflow State Manifest -14. Validation And Rollback +13. Claude Code Configuration +14. Workflow State Manifest +15. Validation And Rollback ## Prompt Invocation Treat `Upgrade A Target Workflow` plus a target repository as an authorized repo-changing prompt, not as a request for CLI instructions. 1. Resolve the target path and requested version from context; default to the installed skill version. -2. Before prompt apply, review target-local owners affected by the requested release's changed semantics when adoption has not already been established. For customized owners, preserve equivalent rules or make the narrow requested correction under the full planning and privacy gates; ask only for a real ownership conflict. A same-version stamp or `already_current` result proves structural state, not semantic adoption. Version 0.9.9 changes only the installed migration privacy-review boundary and requires no target-local instruction rewrite. Version 0.9.8 updates Codex model profiles and refreshes only exact prior generated agent templates when that configuration was already opted in; it requires no target-local instruction rewrite. Version 0.9.7 changes audit discovery and output only, so it also requires no target-local instruction rewrite. For the 0.9.6 changes, inspect the task-handoff route and efficient-execution owner for root working state, self-contained worker context, transient-versus-durable evidence, and artifact-based recovery. Use already-current evidence, and do not sweep unrelated owners. Then invoke `scripts/upgrade_target_workflow.py --prompt` yourself. +2. Before prompt apply, review target-local owners affected by the requested release's changed semantics when adoption has not already been established. For customized owners, preserve equivalent rules or make the narrow requested correction under the full planning and privacy gates; ask only for a real ownership conflict. A same-version stamp or `already_current` result proves structural state, not semantic adoption. Version 0.9.10 fixes custom archive index preservation and adds separately opted-in Claude project agents; it requires no target-local instruction rewrite. Version 0.9.9 changes only the installed migration privacy-review boundary and requires no target-local instruction rewrite. Version 0.9.8 updates Codex model profiles and refreshes only exact prior generated agent templates when that configuration was already opted in; it requires no target-local instruction rewrite. Version 0.9.7 changes audit discovery and output only, so it also requires no target-local instruction rewrite. For the 0.9.6 changes, inspect the task-handoff route and efficient-execution owner for root working state, self-contained worker context, transient-versus-durable evidence, and artifact-based recovery. Use already-current evidence, and do not sweep unrelated owners. Then invoke `scripts/upgrade_target_workflow.py --prompt` yourself. 3. Prompt mode builds and reviews the read-only migration report first. 4. If ownership, conflicts, privacy, and approvals are resolved, it proceeds through guarded apply and validation automatically. 5. If the result returns `agent_action: ask_targeted_question`, ask only `question_to_ask`; keep any later questions deferred and do not write target files. @@ -34,7 +35,7 @@ Treat `Upgrade A Target Workflow` plus a target repository as an authorized repo If the target already records the requested version, all canonical artifacts exist, instruction and index contracts pass, privacy/conflict checks are clear, no registered pristine bytes need an actual update, and any requested optional agent configuration is already fully present, prompt/apply returns `update_status: already_current` with an empty mutation log. It does not create a plan or rewrite state/index files merely to reconfirm that unchanged result. A missing artifact, older contract, drift, conflict, privacy boundary, or requested but incomplete optional configuration keeps the normal guarded path. -The user may explicitly request report-only behavior; then invoke `--plan`. New runtime agent configuration remains opt-in through the user's prompt and `--include-agent-config`; a valid workflow state manifest recording an earlier opt-in carries that choice into subsequent upgrades. +The user may explicitly request report-only behavior; then invoke `--plan`. Codex runtime agent configuration remains opt-in through the user's prompt and `--include-agent-config`; Claude Code project agents have a separate `--include-claude-agent-config` opt-in. A valid workflow state manifest recording either earlier choice carries only that choice into subsequent upgrades. ## CLI Contract @@ -46,6 +47,7 @@ The user may explicitly request report-only behavior; then invoke `--plan`. New - `--prompt` - `--target-version` - `--include-agent-config` +- `--include-claude-agent-config` - `--approve-privacy-review` - `--format json` @@ -70,6 +72,7 @@ Inspect: - `PLANS.md` and older execution-plan locations - backlog, incident catalog, project principles, compatibility instructions, and equivalent names - `.codex/config.toml` and `.codex/agents/*.toml` +- `.claude/agents/workflow-{utility,explorer,reviewer}.md` when Claude configuration is requested or previously opted in - workflow state manifest and migration notes - external tracker references - repository-owned domain, product, architecture, QA, security, and operational documentation @@ -160,7 +163,7 @@ Do not replace a customized shared file wholesale. Create missing files, replace 4. Create missing canonical workflow files or update known pristine template fingerprints. 5. Create/update managed navigation indexes without replacing unmarked repository prose. 6. Validate the complete instruction graph and indexes; stop before version stamping on any finding. -7. Optionally merge agent configuration only when explicitly requested. +7. Optionally merge each platform's agent configuration only when separately requested or recorded by valid prior state. 8. Write the state manifest with relative paths and contract versions. 9. Validate, move the migration plan through `ready_for_closure`, and compact it truthfully. 10. Re-run the public privacy scan immediately before success. Compare it with the in-memory approved pre-apply fingerprint multiset: a disappeared candidate is safe, while a new, changed, moved, or duplicated finding fails and rolls back, regardless of category. @@ -183,6 +186,12 @@ When `--include-agent-config` is present or the target's valid workflow state re Never place Responses API-only fields in Codex TOML. +## Claude Code Configuration + +When `--include-claude-agent-config` is present or the target's valid workflow state records a prior Claude opt-in, apply the three project-agent templates under `.claude/agents/` using `claude_model_profiles.md` as the model and effort owner. The Codex flag or Codex state field alone never enables this step. Without Claude opt-in, leave `.claude/**` byte-for-byte unchanged. + +Create missing project-agent files only after opt-in. Replace an existing agent file only when its complete bytes match a registered prior generated template. Preserve customized model pins, instructions, unrelated agents, `CLAUDE.md`, settings, and managed configuration. Refuse symbolic or unsafe target paths and keep partial writes within the common rollback transaction. Report an unavailable model or an administrative restriction without overriding the user's client or provider settings. Never set a global model or `CLAUDE_CODE_SUBAGENT_MODEL_FORCE`. + ## Workflow State Manifest Target path: `docs/codex/ENGINEERING_WORKFLOW_STATE.yaml`. @@ -201,11 +210,12 @@ Required fields: - `shared_paths` - `protected_paths` - `runtime_agent_config_managed` +- `runtime_claude_agent_config_managed` - `instruction_contract_version` - `planning_contract_version` - `orchestration_contract_version` -Use repository-relative paths. Never record a workstation path, username, home directory, credential, or private hostname. The manifest governs only listed paths or explicit managed sections; it does not claim an entire documentation directory. +In a valid legacy manifest without `runtime_claude_agent_config_managed`, treat Claude configuration as not opted in; a prior Codex opt-in does not imply it. Use repository-relative paths. Never record a workstation path, username, home directory, credential, or private hostname. The manifest governs only listed paths or explicit managed sections; it does not claim an entire documentation directory. ## Validation And Rollback diff --git a/skill/engineering-workflow/scripts/plan_lifecycle.py b/skill/engineering-workflow/scripts/plan_lifecycle.py index ee918f1..c64952e 100644 --- a/skill/engineering-workflow/scripts/plan_lifecycle.py +++ b/skill/engineering-workflow/scripts/plan_lifecycle.py @@ -782,13 +782,15 @@ def _compact_root(existing: str, title: str, archive_path: str | None) -> str: existing[match.start() : section_matches[index + 1].start() if index + 1 < len(section_matches) else None] for index, match in enumerate(section_matches) ] - old_entries = [ - line - for section in sections - if section.splitlines()[0].strip() == "## Recently Completed" - for line in section.splitlines()[1:] - if re.match(r"^- \[x\]", line, re.IGNORECASE) - ] + old_entries: list[str] = [] + for section in sections: + if section.splitlines()[0].strip() != "## Recently Completed": + continue + body = section.split("\n", 1)[1] if "\n" in section else "" + starts = list(re.finditer(r"(?im)^- \[x\].*$", body)) + for index, match in enumerate(starts): + end = starts[index + 1].start() if index + 1 < len(starts) else len(body) + old_entries.append(body[match.start() : end].strip("\n")) suffix = f"; [full archived plan]({archive_path})" if archive_path else "" new_entry = f"- [x] {date.today().isoformat()}: Completed {title}{suffix}." entries = [new_entry, *[item for item in old_entries if item != new_entry]][:10] diff --git a/skill/engineering-workflow/scripts/upgrade_target_workflow.py b/skill/engineering-workflow/scripts/upgrade_target_workflow.py index 7958aa5..151c39f 100644 --- a/skill/engineering-workflow/scripts/upgrade_target_workflow.py +++ b/skill/engineering-workflow/scripts/upgrade_target_workflow.py @@ -35,15 +35,18 @@ INDEX_END, INDEX_START, LifecycleError, - check_archive_indexes, check_plan_lifecycle, closure_issues, + planned_explicit_index_writes, planned_index_writes, + resolve_archive_layout, ) SKILL_ROOT = Path(__file__).resolve().parents[1] TEMPLATE_ROOT = SKILL_ROOT / "assets" / "templates" AGENT_TEMPLATE_ROOT = SKILL_ROOT / "assets" / "agents" +CLAUDE_AGENT_TEMPLATE_ROOT = SKILL_ROOT / "assets" / "claude_agents" +CLAUDE_AGENT_NAMES = ("workflow-utility", "workflow-explorer", "workflow-reviewer") CANONICAL_SOURCE_REPO = "https://github.com/xeonvs/codex-engineering-workflow" PLAN_MARKER_START = "" PLAN_MARKER_END = "" @@ -100,6 +103,11 @@ "explorer": "cf28d059b8bc28123a038d2f4c40fe24fe45e5623d2ee73c0e2b81f0a1d381b4", "reviewer": "6182122fcec3d18b14acdabb644b750e58c5d2264d8b7a68eaf54644ef6db133", } +PRIOR_CLAUDE_AGENT_TEMPLATE_HASHES = { + "workflow-utility": {"8203a37549a6face81c6db37678752a4d237876e8cf3d269a7b606aa4709cbdb"}, + "workflow-explorer": {"74de5d3163b9239ca7ea957704b7b5bcd2f7733cf3963dc678f575b42b252f3b"}, + "workflow-reviewer": {"0bf1b825f92e4d4f88c139356e9353d6cf086aefb3e7ba63854c5e269c22d8b2"}, +} def _content_hash(text: str) -> str: @@ -114,6 +122,10 @@ def _is_pristine_prior_agent(name: str, text: str) -> bool: return _content_hash(text) == PRIOR_AGENT_TEMPLATE_HASHES[name] +def _is_pristine_prior_claude_agent(name: str, text: str) -> bool: + return _content_hash(text) in PRIOR_CLAUDE_AGENT_TEMPLATE_HASHES[name] + + def _agent_config_selected(root: Path, explicitly_selected: bool) -> bool: if explicitly_selected: return True @@ -135,6 +147,27 @@ def _agent_config_selected(root: Path, explicitly_selected: bool) -> bool: ) +def _claude_agent_config_selected(root: Path, explicitly_selected: bool) -> bool: + if explicitly_selected: + return True + if _first_symlink_component(root, STATE_MANIFEST_PATH): + return False + state = _read(root / STATE_MANIFEST_PATH) + try: + declared, shared_paths = parse_manifest_path_list(state, "shared_paths") + except ValueError: + return False + expected = {f".claude/agents/{name}.md" for name in CLAUDE_AGENT_NAMES} + return ( + re.search(r"(?m)^schema_version:\s*2\s*$", state) is not None + and re.search(r"(?m)^skill_name:\s*engineering-workflow\s*$", state) is not None + and re.search(r"(?m)^mode:\s*upgrade_target_workflow\s*$", state) is not None + and re.search(r"(?m)^runtime_claude_agent_config_managed:\s*true\s*$", state) is not None + and declared + and expected.issubset(shared_paths) + ) + + class MigrationConflict(RuntimeError): def __init__(self, code: str, message: str): super().__init__(message) @@ -572,7 +605,9 @@ def _existing_active_conflict(plans_text: str) -> str | None: return None -def _scan_contract_conflicts(root: Path, include_agent_config: bool = False) -> list[dict[str, str]]: +def _scan_contract_conflicts( + root: Path, include_agent_config: bool = False, include_claude_agent_config: bool = False +) -> list[dict[str, str]]: findings: list[dict[str, str]] = [] patterns = ( ("compressed_plan_rule", re.compile(r"\b(?:lightweight|compact|short)\s+(?:active\s+)?plan\b", re.IGNORECASE)), @@ -591,6 +626,8 @@ def _scan_contract_conflicts(root: Path, include_agent_config: bool = False) -> if include_agent_config: canonical_mutation_paths.add(".codex/config.toml") canonical_mutation_paths.update(f".codex/agents/{name}.toml" for name in ("utility", "explorer", "reviewer")) + if include_claude_agent_config: + canonical_mutation_paths.update(f".claude/agents/{name}.md" for name in CLAUDE_AGENT_NAMES) reported_symlinks = set() for relative in sorted(canonical_mutation_paths): symlink_component = _first_symlink_component(root, relative) @@ -692,7 +729,9 @@ def _topology(root: Path) -> dict[str, Any]: } -def _proposed_changes(root: Path, include_agent_config: bool) -> list[dict[str, str]]: +def _proposed_changes( + root: Path, include_agent_config: bool, include_claude_agent_config: bool = False +) -> list[dict[str, str]]: changes: list[dict[str, str]] = [] plans_action = "update" if _present(root / "PLANS.md") else "create" changes.append({"path": "PLANS.md", "action": plans_action, "reason": "materialize full migration plan first"}) @@ -758,6 +797,22 @@ def _proposed_changes(root: Path, include_agent_config: bool) -> list[dict[str, changes.append( {"path": path, "action": "update", "reason": "known pristine prior agent template fingerprint"} ) + if include_claude_agent_config: + for name in CLAUDE_AGENT_NAMES: + path = f".claude/agents/{name}.md" + template = (CLAUDE_AGENT_TEMPLATE_ROOT / f"{name}.md.tmpl").read_text(encoding="utf-8") + if not _present(root / path): + changes.append( + {"path": path, "action": "create", "reason": "explicit Claude agent configuration request"} + ) + elif _is_pristine_prior_claude_agent(name, _read(root / path)) and _read(root / path) != template: + changes.append( + { + "path": path, + "action": "update", + "reason": "known pristine prior Claude agent template fingerprint", + } + ) return changes @@ -766,14 +821,20 @@ def build_migration_report( target_version: str, include_agent_config: bool = False, approved_privacy_review: str | None = None, + include_claude_agent_config: bool = False, ) -> dict[str, Any]: target_version = _validate_target_version(target_version) root = repo.resolve() if not root.is_dir(): raise MigrationConflict("missing_repository", "Target repository does not exist") include_agent_config = _agent_config_selected(root, include_agent_config) + include_claude_agent_config = _claude_agent_config_selected(root, include_claude_agent_config) audit = audit_repo(root) - conflicts = _scan_contract_conflicts(root, include_agent_config=include_agent_config) + conflicts = _scan_contract_conflicts( + root, + include_agent_config=include_agent_config, + include_claude_agent_config=include_claude_agent_config, + ) state_text = _read(root / STATE_MANIFEST_PATH) if state_text and not _first_symlink_component(root, STATE_MANIFEST_PATH): try: @@ -862,10 +923,14 @@ def build_migration_report( ) elif finding.get("requires_decision") == "true": questions.append(f"Which source should own the contradictory planning rule in {finding['path']}?") - proposed = _proposed_changes(root, include_agent_config) + proposed = _proposed_changes(root, include_agent_config, include_claude_agent_config) touched = {item["path"] for item in proposed} ownership = audit["ownership"] - protected = sorted(set(ownership["protected"] + ownership["unknown"] + ownership["external_source_of_truth"])) + protected = set(ownership["protected"] + ownership["unknown"] + ownership["external_source_of_truth"]) + protected.difference_update( + item["path"] for item in proposed if item["reason"] == "known pristine prior Claude agent template fingerprint" + ) + protected = sorted(protected) current_workflow_version = ( _manifest_version(root / STATE_MANIFEST_PATH) if not _first_symlink_component(root, STATE_MANIFEST_PATH) @@ -893,7 +958,7 @@ def build_migration_report( "historical_paths": ownership["historical"], "conflicts": conflicts, "instruction_contract": instruction_contract, - "archive_indexes": audit["archive_indexes"], + "archive_indexes": check_plan_lifecycle(root)["archive_indexes"], "privacy_findings": privacy_findings, "privacy_review": privacy_review, "proposed_changes": proposed, @@ -909,6 +974,7 @@ def build_migration_report( ], "rollback_plan": "Restore every pre-migration file snapshot in reverse mutation order; preserve a PLANS.md failure note if recovery is needed.", "include_agent_config": include_agent_config, + "include_claude_agent_config": include_claude_agent_config, } @@ -933,12 +999,26 @@ def _optional_agent_config_is_current(root: Path, include_agent_config: bool, to ) +def _optional_claude_agent_config_is_current(root: Path, include_claude_agent_config: bool) -> bool: + if not include_claude_agent_config: + return True + state = _read(root / STATE_MANIFEST_PATH) + return re.search(r"(?m)^runtime_claude_agent_config_managed:\s*true\s*$", state) is not None and all( + _present(root / f".claude/agents/{name}.md") and not _first_symlink_component(root, f".claude/agents/{name}.md") + for name in CLAUDE_AGENT_NAMES + ) + + def _already_current(report: dict[str, Any], include_agent_config: bool, root: Path) -> bool: topology = report["detected_topology"] required_artifacts = ("root_agents", "plans", "backlog", "pitfalls", "principles", "state_manifest") pristine_update_pending = any( change.get("reason") - in {"known pristine legacy template fingerprint", "known pristine prior agent template fingerprint"} + in { + "known pristine legacy template fingerprint", + "known pristine prior agent template fingerprint", + "known pristine prior Claude agent template fingerprint", + } for change in report["proposed_changes"] ) return ( @@ -951,6 +1031,7 @@ def _already_current(report: dict[str, Any], include_agent_config: bool, root: P and report["archive_indexes"]["success"] and not pristine_update_pending and _optional_agent_config_is_current(root, include_agent_config, topology) + and _optional_claude_agent_config_is_current(root, report["include_claude_agent_config"]) ) @@ -976,7 +1057,12 @@ def _already_current_result(report: dict[str, Any], *, mode: str) -> dict[str, A def _migration_plan( - target_version: str, include_agent_config: bool, *, done: bool = False, result: str = "Not run yet." + target_version: str, + include_agent_config: bool, + *, + include_claude_agent_config: bool = False, + done: bool = False, + result: str = "Not run yet.", ) -> str: status = "ready_for_closure" if done else "active" checkbox = "x" if done else " " @@ -1004,7 +1090,7 @@ def _migration_plan( ### Requested Scope - Materialize the full migration plan before any other target write. -- Add missing canonical workflow structure, exact ownership state, and optional agent configuration only when explicitly selected. +- Add missing canonical workflow structure, exact ownership state, and each platform's optional agent configuration only when separately selected. ### Requirement Traceability @@ -1012,7 +1098,7 @@ def _migration_plan( | --- | --- | --- | --- | --- | --- | | REQ-001 | Full migration plan is the first target write. | engineering-workflow contract | WQ-01 | Plan schema validates. | done | | REQ-002 | Workflow-owned structure and manifest reach {target_version} without modifying protected docs. | migration report | WQ-02 | Protected snapshots agree and manifest parses. | {req_status} | -| REQ-003 | Runtime agent configuration follows the explicit selection. | user invocation | WQ-03 | Config is {"structurally merged" if include_agent_config else "untouched"}. | {req_status} | +| REQ-003 | Runtime agent configuration follows each platform's explicit selection. | user invocation | WQ-03 | Codex config is {"structurally merged" if include_agent_config else "untouched"}; Claude agents are {"installed or preserved" if include_claude_agent_config else "untouched"}. | {req_status} | ### Explicit Non-Goals @@ -1029,7 +1115,8 @@ def _migration_plan( ### User Decisions And Answers -- Runtime agent configuration requested: {"yes" if include_agent_config else "no"}. +- Codex runtime agent configuration requested: {"yes" if include_agent_config else "no"}. +- Claude Code project agents requested: {"yes" if include_claude_agent_config else "no"}. ### Completed Baseline State @@ -1049,7 +1136,7 @@ def _migration_plan( - REQ-001: structural plan validation. - REQ-002: manifest, ownership, privacy, and protected-file checks. -- REQ-003: TOML parse and exact configuration diff when selected. +- REQ-003: TOML parse and exact configuration diff for Codex when selected; native Claude agent paths and exact-template preservation when selected. ### Latest Validation Results @@ -1210,6 +1297,7 @@ def _manifest_text( shared_paths: list[str], include_agent_config: bool, existing_manifest: str = "", + include_claude_agent_config: bool = False, ) -> str: archive_path, archive_indexes = _manifest_archive_contract(existing_manifest) applied = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z") @@ -1243,6 +1331,7 @@ def _manifest_text( [ 'active_plan: "PLANS.md"', f"runtime_agent_config_managed: {'true' if include_agent_config else 'false'}", + f"runtime_claude_agent_config_managed: {'true' if include_claude_agent_config else 'false'}", "instruction_contract_version: 3", "planning_contract_version: 2", "orchestration_contract_version: 3", @@ -1256,6 +1345,7 @@ def apply_migration( target_version: str, include_agent_config: bool = False, approved_privacy_review: str | None = None, + include_claude_agent_config: bool = False, ) -> dict[str, Any]: root = repo.resolve() expected_root_identity = _directory_identity(root) @@ -1264,8 +1354,10 @@ def apply_migration( target_version, include_agent_config, approved_privacy_review, + include_claude_agent_config, ) include_agent_config = report["include_agent_config"] + include_claude_agent_config = report["include_claude_agent_config"] privacy_review, privacy_findings, approved_fingerprints = _evaluate_privacy_review( root, report["current_workflow_version"], @@ -1343,7 +1435,11 @@ def write(relative: str, text: str) -> None: (created if before is None else changed).append(relative) try: - initial_plan = _migration_plan(target_version, include_agent_config) + initial_plan = _migration_plan( + target_version, + include_agent_config, + include_claude_agent_config=include_claude_agent_config, + ) write("PLANS.md", _put_plan_first(read("PLANS.md"), initial_plan)) template_map = { @@ -1366,6 +1462,10 @@ def write(relative: str, text: str) -> None: try: for relative, data in planned_index_writes(root).items(): write(relative, data.decode("utf-8")) + archive_layout = resolve_archive_layout(root) + if archive_layout.explicit: + for relative, data in planned_explicit_index_writes(root, archive_layout).items(): + write(relative, data.decode("utf-8")) except LifecycleError as exc: raise MigrationConflict(exc.code, str(exc)) from exc @@ -1375,10 +1475,6 @@ def write(relative: str, text: str) -> None: instruction_result["status"], "Generated instruction contract did not validate", ) - index_result = check_archive_indexes(root) - if not index_result["success"]: - raise MigrationConflict("index_validation_failed", "Generated documentation indexes did not validate") - if include_agent_config: existing_config = read(".codex/config.toml") merged, config_diff = _merge_codex_config(existing_config) @@ -1393,6 +1489,16 @@ def write(relative: str, text: str) -> None: (AGENT_TEMPLATE_ROOT / f"{name}.toml.tmpl").read_text(encoding="utf-8"), ) + if include_claude_agent_config: + for name in CLAUDE_AGENT_NAMES: + relative = f".claude/agents/{name}.md" + existing_agent = read(relative) + template = (CLAUDE_AGENT_TEMPLATE_ROOT / f"{name}.md.tmpl").read_text(encoding="utf-8") + if not secure.exists(relative) or ( + _is_pristine_prior_claude_agent(name, existing_agent) and existing_agent != template + ): + write(relative, template) + shared_paths = [path for path in CANONICAL_FILES.values() if secure.exists(path)] if include_agent_config: shared_paths.extend( @@ -1402,12 +1508,19 @@ def write(relative: str, text: str) -> None: ) if secure.exists(".codex/config.toml"): shared_paths.append(".codex/config.toml") + if include_claude_agent_config: + shared_paths.extend( + f".claude/agents/{name}.md" + for name in CLAUDE_AGENT_NAMES + if secure.exists(f".claude/agents/{name}.md") + ) manifest = _manifest_text( target_version, report["protected_paths"], sorted(set(shared_paths)), include_agent_config, read(STATE_MANIFEST_PATH), + include_claude_agent_config=include_claude_agent_config, ) if scan_privacy_text(manifest): raise MigrationConflict("unsafe_manifest", "Generated manifest contains private data") @@ -1426,7 +1539,13 @@ def write(relative: str, text: str) -> None: raise MigrationConflict("protected_file_changed", "A protected file changed during migration") final_result = "Plan schema, ownership manifest, privacy, protected-file, and optional TOML checks passed." - final_plan = _migration_plan(target_version, include_agent_config, done=True, result=final_result) + final_plan = _migration_plan( + target_version, + include_agent_config, + include_claude_agent_config=include_claude_agent_config, + done=True, + result=final_result, + ) write("PLANS.md", _put_plan_first(read("PLANS.md"), final_plan)) final_plan_issues = validate_plan_schema(read("PLANS.md"), declared_external_sources=True) final_plan_issues.extend(closure_issues(read("PLANS.md"), require_ready=True)) @@ -1485,6 +1604,7 @@ def write(relative: str, text: str) -> None: failure = _migration_plan( target_version, include_agent_config, + include_claude_agent_config=include_claude_agent_config, result=f"Apply failed and non-plan files were restored: {type(exc).__name__}.", ) secure.write_text("PLANS.md", _put_plan_first(read("PLANS.md"), failure)) @@ -1547,6 +1667,7 @@ def execute_prompt_upgrade( target_version: str, include_agent_config: bool = False, approved_privacy_review: str | None = None, + include_claude_agent_config: bool = False, ) -> dict[str, Any]: """Run report-first migration for an authorized natural-language target-upgrade request.""" report = build_migration_report( @@ -1554,6 +1675,7 @@ def execute_prompt_upgrade( target_version, include_agent_config, approved_privacy_review, + include_claude_agent_config, ) include_agent_config = report["include_agent_config"] if report["required_user_questions"]: @@ -1633,6 +1755,7 @@ def execute_prompt_upgrade( target_version, include_agent_config, approved_privacy_review, + include_claude_agent_config, ) if applied.get("update_status") == "question_required": agent_action = "ask_targeted_question" @@ -1666,8 +1789,9 @@ def main() -> int: mode.add_argument("--plan", action="store_true") mode.add_argument("--apply", action="store_true") mode.add_argument("--prompt", action="store_true") - parser.add_argument("--target-version", default="0.9.9") + parser.add_argument("--target-version", default="0.9.10") parser.add_argument("--include-agent-config", action="store_true") + parser.add_argument("--include-claude-agent-config", action="store_true") parser.add_argument( "--approve-privacy-review", help="Approve only the exact value-free privacy review token returned by a prior report.", @@ -1682,6 +1806,7 @@ def main() -> int: args.target_version, args.include_agent_config, args.approve_privacy_review, + args.include_claude_agent_config, ) elif args.apply: result = apply_migration( @@ -1689,6 +1814,7 @@ def main() -> int: args.target_version, args.include_agent_config, args.approve_privacy_review, + args.include_claude_agent_config, ) else: result = build_migration_report( @@ -1696,6 +1822,7 @@ def main() -> int: args.target_version, args.include_agent_config, args.approve_privacy_review, + args.include_claude_agent_config, ) except MigrationConflict as exc: selected_mode = "prompt" if args.prompt else ("apply" if args.apply else "plan") diff --git a/skill/engineering-workflow/scripts/validate_skill_repo.py b/skill/engineering-workflow/scripts/validate_skill_repo.py index 6d4e179..853d867 100644 --- a/skill/engineering-workflow/scripts/validate_skill_repo.py +++ b/skill/engineering-workflow/scripts/validate_skill_repo.py @@ -53,6 +53,7 @@ "skill/engineering-workflow/references/planning_and_backlog.md", "skill/engineering-workflow/references/agent_orchestration.md", "skill/engineering-workflow/references/model_profiles.md", + "skill/engineering-workflow/references/claude_model_profiles.md", "skill/engineering-workflow/references/skill_update.md", "skill/engineering-workflow/references/target_workflow_upgrade.md", "skill/engineering-workflow/references/validation_safety.md", @@ -72,6 +73,9 @@ "skill/engineering-workflow/assets/agents/utility.toml.tmpl", "skill/engineering-workflow/assets/agents/explorer.toml.tmpl", "skill/engineering-workflow/assets/agents/reviewer.toml.tmpl", + "skill/engineering-workflow/assets/claude_agents/workflow-utility.md.tmpl", + "skill/engineering-workflow/assets/claude_agents/workflow-explorer.md.tmpl", + "skill/engineering-workflow/assets/claude_agents/workflow-reviewer.md.tmpl", ) FORBIDDEN_PATH_PARTS = {"__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache"} FORBIDDEN_SUFFIXES = {".pyc", ".pyo"} @@ -104,6 +108,7 @@ "references/platform_compatibility.md", "references/agent_orchestration.md", "references/model_profiles.md", + "references/claude_model_profiles.md", "references/skill_update.md", "references/target_workflow_upgrade.md", "references/validation_safety.md", @@ -133,6 +138,7 @@ "## Deterministic Route": "skill/engineering-workflow/references/agent_orchestration.md", "## Programmatic Tool Route": "skill/engineering-workflow/references/agent_orchestration.md", "## Capability Mapping": "skill/engineering-workflow/references/model_profiles.md", + "## Claude Capability Mapping": "skill/engineering-workflow/references/claude_model_profiles.md", "## Refresh Loaded Skill Decision": "skill/engineering-workflow/references/skill_update.md", "## Installation Types": "skill/engineering-workflow/references/skill_update.md", "## Prompt Invocation": "skill/engineering-workflow/references/target_workflow_upgrade.md", @@ -532,6 +538,40 @@ def _validate_agent_profiles(repo_root: Path) -> list[str]: return issues +def _validate_claude_agent_profiles(repo_root: Path) -> list[str]: + issues: list[str] = [] + directory = repo_root / "skill/engineering-workflow/assets/claude_agents" + for name, model, effort in ( + ("workflow-utility", "haiku", None), + ("workflow-explorer", "sonnet", "medium"), + ("workflow-reviewer", "sonnet", "medium"), + ): + path = directory / f"{name}.md.tmpl" + if not path.is_file(): + continue + content = path.read_text(encoding="utf-8") + parts = content.split("---\n", 2) + if len(parts) != 3 or parts[0] or not parts[2].strip(): + issues.append(f"{path.name} needs valid frontmatter and a task prompt") + continue + fields: dict[str, str] = {} + for line in parts[1].splitlines(): + key, separator, value = line.partition(":") + if not separator or not key or key in fields: + issues.append(f"{path.name} has invalid or duplicate frontmatter") + break + fields[key] = value.strip() + else: + expected = {"name": name, "model": model, "tools": "Read, Grep, Glob"} + if any(fields.get(key) != value for key, value in expected.items()) or not fields.get("description"): + issues.append(f"{path.name} has an invalid Claude role profile") + if fields.get("effort") != effort: + issues.append(f"{path.name} has an unsupported or unexpected effort") + if set(fields) - {"name", "description", "tools", "model", "effort"}: + issues.append(f"{path.name} includes unexpected Claude agent settings") + return issues + + def _validate_programmatic_tool_assets(repo_root: Path) -> list[str]: issues: list[str] = [] template_path = repo_root / "skill/engineering-workflow/assets/templates/PROGRAMMATIC_TOOL_STAGE.md.tmpl" @@ -761,6 +801,7 @@ def validate_skill_repo(repo_root: Path) -> dict: errors.extend(_validate_source_indexes(root)) errors.extend(_validate_canonical_owners(root)) errors.extend(_validate_agent_profiles(root)) + errors.extend(_validate_claude_agent_profiles(root)) errors.extend(_validate_programmatic_tool_assets(root)) errors.extend(_validate_marketplace_package(root, version)) errors.extend(_validate_active_versions(root, version)) diff --git a/tests/test_claude_agent_upgrade.py b/tests/test_claude_agent_upgrade.py new file mode 100644 index 0000000..406eb4f --- /dev/null +++ b/tests/test_claude_agent_upgrade.py @@ -0,0 +1,167 @@ +from __future__ import annotations + +import hashlib +import tempfile +import unittest +from pathlib import Path +from unittest import mock + +from test_support import load_script_module +from test_upgrade_target_workflow import make_target, snapshot + +common = load_script_module("common") +migrator = load_script_module("upgrade_target_workflow") + +CLAUDE_NAMES = ("workflow-utility", "workflow-explorer", "workflow-reviewer") + + +class ClaudeAgentUpgradeTests(unittest.TestCase): + def test_codex_opt_in_does_not_install_claude_agents(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + make_target(root) + settings = root / ".claude/settings.json" + settings.parent.mkdir(parents=True) + settings.write_text('{"model":"team-owned"}\n', encoding="utf-8") + + result = migrator.apply_migration(root, "0.9.10", include_agent_config=True) + + self.assertTrue(result["success"], result) + self.assertFalse(result["include_claude_agent_config"]) + self.assertFalse((root / ".claude/agents").exists()) + self.assertEqual(settings.read_text(encoding="utf-8"), '{"model":"team-owned"}\n') + + def test_separate_claude_opt_in_creates_only_native_agents_and_is_idempotent(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + make_target(root) + report = migrator.build_migration_report(root, "0.9.10", include_claude_agent_config=True) + self.assertTrue(report["success"], report) + self.assertTrue(report["include_claude_agent_config"]) + self.assertFalse(report["include_agent_config"]) + self.assertFalse((root / "PLANS.md").exists()) + + result = migrator.apply_migration(root, "0.9.10", include_claude_agent_config=True) + self.assertTrue(result["success"], result) + self.assertFalse((root / ".codex").exists()) + for name in CLAUDE_NAMES: + path = root / ".claude/agents" / f"{name}.md" + self.assertTrue(path.is_file(), name) + self.assertEqual( + path.read_bytes(), + (migrator.CLAUDE_AGENT_TEMPLATE_ROOT / f"{name}.md.tmpl").read_bytes(), + ) + state = (root / common.STATE_MANIFEST_PATH).read_text(encoding="utf-8") + self.assertIn("runtime_claude_agent_config_managed: true", state) + self.assertIn('".claude/agents/workflow-reviewer.md"', state) + self.assertIn("runtime_agent_config_managed: false", state) + before = snapshot(root) + + current = migrator.execute_prompt_upgrade(root, "0.9.10") + self.assertTrue(current["success"], current) + self.assertTrue(current["include_claude_agent_config"]) + self.assertEqual(current["update_status"], "already_current") + self.assertEqual(current["mutation_log"], []) + self.assertEqual(snapshot(root), before) + + def test_claude_opt_in_preserves_user_pins_and_settings(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + make_target(root) + reviewer = root / ".claude/agents/workflow-reviewer.md" + reviewer.parent.mkdir(parents=True) + pinned = ( + "---\nname: workflow-reviewer\ndescription: Team reviewer\nmodel: inherit\n---\nTeam-owned guidance.\n" + ) + reviewer.write_text(pinned, encoding="utf-8") + settings = root / ".claude/settings.json" + settings.write_text('{"model":"team-owned","effortLevel":"high"}\n', encoding="utf-8") + claude_md = root / "CLAUDE.md" + claude_md.write_text("# Team instructions\n", encoding="utf-8") + + result = migrator.apply_migration(root, "0.9.10", include_claude_agent_config=True) + + self.assertTrue(result["success"], result) + self.assertEqual(reviewer.read_text(encoding="utf-8"), pinned) + self.assertEqual(settings.read_text(encoding="utf-8"), '{"model":"team-owned","effortLevel":"high"}\n') + self.assertEqual(claude_md.read_text(encoding="utf-8"), "# Team instructions\n") + self.assertFalse((root / ".codex").exists()) + + def test_claude_opt_in_preserves_existing_empty_agent(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + make_target(root) + utility = root / ".claude/agents/workflow-utility.md" + utility.parent.mkdir(parents=True) + utility.write_bytes(b"") + + report = migrator.build_migration_report(root, "0.9.10", include_claude_agent_config=True) + self.assertTrue(report["success"], report) + proposed = {change["path"] for change in report["proposed_changes"]} + self.assertNotIn(".claude/agents/workflow-utility.md", proposed) + + result = migrator.apply_migration(root, "0.9.10", include_claude_agent_config=True) + self.assertTrue(result["success"], result) + self.assertEqual(utility.read_bytes(), b"") + self.assertNotIn(".claude/agents/workflow-utility.md", result["mutation_log"]) + + def test_pristine_prior_template_refreshes_but_customized_agent_does_not(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + make_target(root) + initial = migrator.apply_migration(root, "0.9.10", include_claude_agent_config=True) + self.assertTrue(initial["success"], initial) + reviewer = root / ".claude/agents/workflow-reviewer.md" + utility = root / ".claude/agents/workflow-utility.md" + prior = reviewer.read_text(encoding="utf-8").replace("model: sonnet", "model: inherit") + self.assertNotEqual(prior, reviewer.read_text(encoding="utf-8")) + reviewer.write_text(prior, encoding="utf-8") + custom = utility.read_text(encoding="utf-8").replace("model: haiku", "model: inherit") + utility.write_text(custom, encoding="utf-8") + old_hash = hashlib.sha256(prior.encode("utf-8")).hexdigest() + with mock.patch.dict(migrator.PRIOR_CLAUDE_AGENT_TEMPLATE_HASHES, {"workflow-reviewer": {old_hash}}): + report = migrator.build_migration_report(root, "0.9.11") + self.assertTrue(report["include_claude_agent_config"]) + updates = {change["path"] for change in report["proposed_changes"] if change["action"] == "update"} + self.assertIn(".claude/agents/workflow-reviewer.md", updates) + self.assertNotIn(".claude/agents/workflow-utility.md", updates) + result = migrator.apply_migration(root, "0.9.11") + self.assertTrue(result["success"], result) + self.assertEqual( + reviewer.read_bytes(), (migrator.CLAUDE_AGENT_TEMPLATE_ROOT / "workflow-reviewer.md.tmpl").read_bytes() + ) + self.assertEqual(utility.read_text(encoding="utf-8"), custom) + + def test_unsafe_claude_agent_path_blocks_before_writes(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + make_target(root) + native = root / "native.md" + native.write_text("# Native\n", encoding="utf-8") + agent = root / ".claude/agents/workflow-utility.md" + agent.parent.mkdir(parents=True) + agent.symlink_to("../../native.md") + + report = migrator.build_migration_report(root, "0.9.10", include_claude_agent_config=True) + self.assertFalse(report["success"], report) + self.assertIn("canonical_symlink", {item["type"] for item in report["conflicts"]}) + result = migrator.apply_migration(root, "0.9.10", include_claude_agent_config=True) + self.assertFalse(result["success"]) + self.assertEqual(result["mutation_log"], []) + self.assertFalse((root / "PLANS.md").exists()) + self.assertTrue(agent.is_symlink()) + + def test_claude_agent_creation_rolls_back_on_later_failure(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + make_target(root) + with mock.patch.object(migrator, "_manifest_text", side_effect=RuntimeError("synthetic failure")): + result = migrator.apply_migration(root, "0.9.10", include_claude_agent_config=True) + self.assertFalse(result["success"]) + self.assertEqual(result["update_status"], "rolled_back") + self.assertFalse((root / ".claude").exists()) + self.assertFalse((root / "AGENTS.md").exists()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_marketplace_package.py b/tests/test_marketplace_package.py index 55fbf4f..a407541 100644 --- a/tests/test_marketplace_package.py +++ b/tests/test_marketplace_package.py @@ -30,7 +30,7 @@ def test_repository_package_matches_deterministic_builder(self): result = json.loads(completed.stdout) self.assertEqual(completed.returncode, 0, completed.stderr) self.assertTrue(result["success"], result) - self.assertEqual(result["version"], "0.9.9") + self.assertEqual(result["version"], "0.9.10") self.assertEqual(result["drift"], []) def test_check_detects_packaged_skill_byte_drift(self): @@ -111,7 +111,7 @@ def test_manifests_declare_only_self_contained_skill_capability(self): (REPO_ROOT / "plugins/engineering-workflow/.claude-plugin/plugin.json").read_text(encoding="utf-8") ) for manifest in (codex, claude): - self.assertEqual(manifest["version"], "0.9.9") + self.assertEqual(manifest["version"], "0.9.10") self.assertEqual(manifest["repository"], builder.REPOSITORY_URL) self.assertNotIn("mcpServers", manifest) self.assertNotIn("apps", manifest) diff --git a/tests/test_plan_lifecycle.py b/tests/test_plan_lifecycle.py index 12e8bed..eec5369 100644 --- a/tests/test_plan_lifecycle.py +++ b/tests/test_plan_lifecycle.py @@ -162,6 +162,26 @@ def test_closure_preserves_custom_sibling_sections_around_active_plan(self): self.assertTrue((root / result["archive_path"]).is_file()) self.assertTrue(lifecycle.check_plan_lifecycle(root)["success"]) + def test_closure_preserves_wrapped_recent_archive_pointer(self): + previous = ( + "- [x] 2026-01-01: Completed earlier synthetic\n" + " task; [full archived plan](docs/archive/plans/earlier-synthetic.md)." + ) + for disposition in ("compact", "archive"): + with self.subTest(disposition=disposition), tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + text = ready_plan().replace( + "## Recently Completed\n\n", "## Recently Completed\n\n" + previous + "\n", 1 + ) + (root / "PLANS.md").write_text(text, encoding="utf-8") + + result = lifecycle.close_plan(root, disposition) + + self.assertTrue(result["success"], result) + closed = (root / "PLANS.md").read_text(encoding="utf-8") + self.assertIn(previous, closed) + self.assertTrue(lifecycle.check_plan_lifecycle(root)["success"]) + def test_atomic_failure_restores_original_bytes(self): with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) diff --git a/tests/test_skill_repo_validation.py b/tests/test_skill_repo_validation.py index ffcca1c..f1d0453 100644 --- a/tests/test_skill_repo_validation.py +++ b/tests/test_skill_repo_validation.py @@ -13,7 +13,7 @@ validate_skill_repo = load_script_module("validate_skill_repo") REPO_ROOT = Path(__file__).resolve().parents[1] -CURRENT_VERSION = "0.9.9" +CURRENT_VERSION = "0.9.10" class SkillRepoValidationTests(unittest.TestCase): @@ -256,6 +256,22 @@ def test_profile_drift_and_unsupported_reviewer_effort_are_rejected(self): reviewer.write_text(original, encoding="utf-8") self.assertEqual(validate_skill_repo._validate_agent_profiles(root), []) + def test_claude_profiles_keep_native_role_model_and_read_only_tools(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + self._copy_repo_subset(root) + agents = root / "skill/engineering-workflow/assets/claude_agents" + utility = agents / "workflow-utility.md.tmpl" + original = utility.read_text(encoding="utf-8") + self.assertEqual(validate_skill_repo._validate_claude_agent_profiles(root), []) + for changed in ( + original.replace("model: haiku", "model: opus"), + original.replace("tools: Read, Grep, Glob", "tools: Read, Edit"), + original.replace("model: haiku", "model: haiku\neffort: high"), + ): + utility.write_text(changed, encoding="utf-8") + self.assertTrue(validate_skill_repo._validate_claude_agent_profiles(root)) + def test_invented_pro_slug_is_rejected(self): with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) diff --git a/tests/test_upgrade_target_workflow.py b/tests/test_upgrade_target_workflow.py index f28b8a0..8e60fd1 100644 --- a/tests/test_upgrade_target_workflow.py +++ b/tests/test_upgrade_target_workflow.py @@ -15,6 +15,7 @@ common = load_script_module("common") migrator = load_script_module("upgrade_target_workflow") +lifecycle = load_script_module("plan_lifecycle") LEGACY_INSTRUCTIONS = Path(__file__).resolve().parent / "fixtures/legacy_instructions" @@ -35,6 +36,51 @@ def snapshot(root: Path) -> dict[str, bytes]: return {path.relative_to(root).as_posix(): path.read_bytes() for path in root.rglob("*") if path.is_file()} +def make_custom_archive_target(root: Path, *, unmanaged_leaf: bool = False) -> None: + make_target(root) + archive = root / "docs/product/plans/archive" + archive.mkdir(parents=True) + (archive / "previous.md").write_text("# Previously retained plan\n", encoding="utf-8") + indexes = ( + "docs/product/plans/archive/README.md", + "docs/product/PLANS_ARCHIVE.md", + "docs/README.md", + ) + (root / indexes[0]).write_text( + "# Product Plan Archive\n\nOwner note before index.\n\n" + + ( + "Repository-owned index without managed markers.\n" + if unmanaged_leaf + else f"{lifecycle.INDEX_START}\n- [previous.md](previous.md)\n{lifecycle.INDEX_END}\n" + ) + + "\nOwner note after index.\n", + encoding="utf-8", + ) + (root / indexes[1]).write_text( + f"# Product Plans\n\n{lifecycle.INDEX_START}\n- [Archive](plans/archive/README.md)\n{lifecycle.INDEX_END}\n", + encoding="utf-8", + ) + (root / indexes[2]).write_text( + "# Documentation\n\nOwner note before index.\n\n" + f"{lifecycle.INDEX_START}\n" + "- [Product plans](product/PLANS_ARCHIVE.md)\n" + f"{lifecycle.INDEX_END}\n" + "\nOwner note after index.\n", + encoding="utf-8", + ) + state = root / common.STATE_MANIFEST_PATH + state.write_text( + "schema_version: 2\n" + 'skill_version: "0.9.9"\n' + "managed_paths:\n" + f" - {common.STATE_MANIFEST_PATH}\n" + + "".join(f" - {path}\n" for path in indexes) + + "plan_archive_path: docs/product/plans/archive\n" + "plan_archive_indexes:\n" + "".join(f" - {path}\n" for path in indexes) + "active_plan: null\n", + encoding="utf-8", + ) + + def synthetic_review_lines() -> list[str]: return [ "pass" + "word" + "=" + "synthetic-placeholder", @@ -1370,6 +1416,48 @@ def test_manifest_preserves_complete_custom_archive_contract(self): self.assertIn('active_plan: "PLANS.md"', rendered) self.assertNotIn('plan_archive_path: "docs/archive/plans"', rendered) + def test_upgrade_preserves_three_level_custom_archive_graph(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + make_custom_archive_target(root) + + result = migrator.apply_migration(root, "0.9.10") + + self.assertTrue(result["success"], result) + self.assertFalse((root / "docs/archive").exists()) + leaf = (root / "docs/product/plans/archive/README.md").read_text(encoding="utf-8") + middle = (root / "docs/product/PLANS_ARCHIVE.md").read_text(encoding="utf-8") + docs = (root / "docs/README.md").read_text(encoding="utf-8") + self.assertIn("Owner note before index.", leaf) + self.assertIn("Owner note after index.", leaf) + self.assertIn("Owner note before index.", docs) + self.assertIn("Owner note after index.", docs) + self.assertEqual(leaf.count("(previous.md)"), 1) + self.assertEqual(middle.count("(plans/archive/README.md)"), 1) + self.assertEqual(docs.count("(product/PLANS_ARCHIVE.md)"), 1) + self.assertTrue(lifecycle.check_plan_lifecycle(root)["success"]) + + before = snapshot(root) + repeat = migrator.apply_migration(root, "0.9.10") + self.assertEqual(repeat["update_status"], "already_current", repeat) + self.assertEqual(snapshot(root), before) + + def test_unmanaged_custom_archive_index_rolls_back_partial_upgrade(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + make_custom_archive_target(root, unmanaged_leaf=True) + before = snapshot(root) + + result = migrator.apply_migration(root, "0.9.10") + + self.assertFalse(result["success"], result) + self.assertEqual(result["update_status"], "rolled_back") + self.assertEqual(result["errors"][0]["code"], "unmanaged_index_conflict") + after = snapshot(root) + self.assertEqual({path: data for path, data in after.items() if path != "PLANS.md"}, before) + self.assertIn("Apply failed", (root / "PLANS.md").read_text(encoding="utf-8")) + self.assertFalse((root / "docs/archive").exists()) + def test_manifest_rejects_partial_custom_archive_contract(self): existing = "plan_archive_path: docs/product/plans/archive\nactive_plan: null\n"