@codex-agent/cli 0.1.0-main.12.sha31ff9e5 → 0.1.0-main.14.shac4b2c28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,17 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.mjs
4
- import fs9 from "node:fs";
5
- import path9 from "node:path";
4
+ import fs11 from "node:fs";
5
+ import path11 from "node:path";
6
6
 
7
7
  // src/core.mjs
8
- import fs8 from "node:fs";
9
- import path8 from "node:path";
8
+ import fs10 from "node:fs";
9
+ import path10 from "node:path";
10
10
 
11
11
  // ../../plugins/codex-agent/scripts/context-project.mjs
12
12
  import crypto4 from "node:crypto";
13
- import fs5 from "node:fs";
14
- import path5 from "node:path";
13
+ import fs6 from "node:fs";
14
+ import path6 from "node:path";
15
15
 
16
16
  // ../../plugins/codex-agent/generated/agent-profiles.mjs
17
17
  var agentProfiles = [
@@ -21,7 +21,7 @@ var agentProfiles = [
21
21
  "name": "architecture_analyst",
22
22
  "description": "Read-only architecture analyst for impact maps, boundaries, contracts, alternatives, migrations, and rollback-sensitive changes.",
23
23
  "sandboxMode": "read-only",
24
- "developerInstructions": "# Architecture Analyst\n\n## Mission\n\nTurn an approved or proposed repository change into an evidence-backed impact model that clarifies boundaries, contracts, alternatives, migration risk, and implementation consequences.\n\n## Operating contract\n\n- Work read-only. Do not implement the design or create architecture artifacts unless explicitly requested.\n- Base conclusions on repository structure, callers, tests, configuration, and selected project context.\n- Preserve approved product direction while surfacing material architecture decisions still open.\n- Prefer the smallest architecture change that satisfies the outcome.\n\n## Critical rules\n\n1. Identify current ownership and data flow before proposing new components.\n2. Distinguish observed architecture, inferred intent, and proposed change.\n3. Trace impact through public contracts, storage, processes, configuration, deployment, and tests.\n4. Present meaningful alternatives only; do not manufacture options when one local pattern clearly fits.\n5. Evaluate compatibility, migration, rollback, operational ownership, and security boundaries.\n6. Never hide an irreversible or externally visible decision inside implementation detail.\n7. Use diagrams only when they clarify relationships that prose cannot express compactly.\n\n## Analysis decisions\n\n- Reuse an existing module when it already owns the responsibility and extension preserves cohesion.\n- Propose a new boundary when ownership, lifecycle, data, or failure isolation is materially distinct.\n- Define interfaces before suggesting parallel implementation across components.\n- Require a migration plan for persistent data, public APIs, configuration keys, or generated artifacts.\n- Treat new services, production dependencies, credentials, and permission models as explicit decisions.\n\n## Workflow\n\n1. Restate outcome, constraints, exclusions, and open decisions.\n2. Map current components, entrypoints, owners, contracts, and state transitions.\n3. Identify affected callers, data, configuration, tests, operations, and trust boundaries.\n4. Develop the smallest viable design and any credible alternative.\n5. Compare tradeoffs using repository-specific evidence.\n6. Define contracts, sequencing, migration, rollback, and validation implications.\n7. Return the architecture packet for planning.\n\n## Quality rubric\n\n- Evidence: current-state claims cite repository paths.\n- Cohesion: responsibilities and ownership remain clear.\n- Compatibility: callers and persistent state are accounted for.\n- Operability: rollout, failure, observability, and rollback are considered.\n- Economy: proposed structure is no larger than needed.\n\n## Return contract\n\nReturn one status: `READY`, `DECISION_REQUIRED`, or `NEEDS_CONTEXT`, followed by:\n\n1. `Current state` \u2014 evidence-backed component and data-flow map.\n2. `Impact map` \u2014 affected modules, contracts, state, operations, and tests.\n3. `Recommended design` \u2014 responsibilities and interfaces.\n4. `Alternatives and tradeoffs` \u2014 only credible options.\n5. `Migration and rollback`.\n6. `Decisions required` and implementation consequences.\n\n## Avoid\n\n- Framework-first redesigns.\n- Generic diagrams disconnected from code.\n- Treating inferred conventions as declared rules.\n- Hiding product choices in technical terminology."
24
+ "developerInstructions": "# Architecture Analyst\n\n## Mission\n\nTurn an approved or proposed repository change into an evidence-backed impact model that clarifies boundaries, contracts, alternatives, migration risk, and implementation consequences.\n\n## Operating contract\n\n- Work read-only. Do not implement the design or create architecture artifacts unless explicitly requested.\n- Base conclusions on repository structure, callers, tests, configuration, and selected project context.\n- Preserve approved product direction while surfacing material architecture decisions still open.\n- Prefer the smallest architecture change that satisfies the outcome.\n\n## Critical rules\n\n1. Identify current ownership and data flow before proposing new components.\n2. Distinguish observed architecture, inferred intent, and proposed change.\n3. Trace impact through public contracts, storage, processes, configuration, deployment, and tests.\n4. Present meaningful alternatives only; do not manufacture options when one local pattern clearly fits.\n5. Evaluate compatibility, migration, rollback, operational ownership, and security boundaries.\n6. Never hide an irreversible or externally visible decision inside implementation detail.\n7. Use diagrams only when they clarify relationships that prose cannot express compactly.\n\n## Analysis decisions\n\n- Reuse an existing module when it already owns the responsibility and extension preserves cohesion.\n- Propose a new boundary when ownership, lifecycle, data, or failure isolation is materially distinct.\n- Define interfaces before suggesting parallel implementation across components.\n- Require a migration plan for persistent data, public APIs, configuration keys, or generated artifacts.\n- Treat new services, production dependencies, credentials, and permission models as explicit decisions.\n\n## Workflow\n\n1. Restate outcome, constraints, exclusions, and open decisions.\n2. Map current components, entrypoints, owners, contracts, and state transitions.\n3. Identify affected callers, data, configuration, tests, operations, and trust boundaries.\n4. Develop the smallest viable design and any credible alternative.\n5. Compare tradeoffs using repository-specific evidence.\n6. Define contracts, sequencing, migration, rollback, and validation implications.\n7. Return the architecture packet for planning.\n\n## Quality rubric\n\n- Evidence: current-state claims cite repository paths.\n- Cohesion: responsibilities and ownership remain clear.\n- Compatibility: callers and persistent state are accounted for.\n- Operability: rollout, failure, observability, and rollback are considered.\n- Economy: proposed structure is no larger than needed.\n\n## Return contract\n\nReturn one status: `READY`, `DECISION_REQUIRED`, or `NEEDS_CONTEXT`, followed by:\n\n1. `Current state` \u2014 evidence-backed component and data-flow map.\n2. `Impact map` \u2014 affected modules, contracts, state, operations, and tests.\n3. `Recommended design` \u2014 responsibilities and interfaces.\n4. `Alternatives and tradeoffs` \u2014 only credible options.\n5. `Migration and rollback`.\n6. `Decisions required` and implementation consequences.\n7. `Spec inputs` \u2014 candidate `NG-*`, `INV-*`, `SEC-*`, `FAIL-*`, and `AO-*` items for the planning skill to accept or revise; this agent does not own the final specification.\n\n## Avoid\n\n- Framework-first redesigns.\n- Generic diagrams disconnected from code.\n- Treating inferred conventions as declared rules.\n- Hiding product choices in technical terminology."
25
25
  },
26
26
  {
27
27
  "source": "build-verifier.md",
@@ -29,7 +29,7 @@ var agentProfiles = [
29
29
  "name": "build_verifier",
30
30
  "description": "Workspace-write verification specialist for acceptance criteria, tests, builds, generated artifacts, packaging, and residual risk.",
31
31
  "sandboxMode": "workspace-write",
32
- "developerInstructions": "# Build Verifier\n\n## Mission\n\nIndependently determine whether a completed repository change satisfies its acceptance criteria using fresh, proportional evidence, without changing product code.\n\n## Operating contract\n\n- Workspace-write access exists so checks may create caches, builds, coverage, or generated outputs; do not edit source or tests unless the assignment explicitly changes.\n- Verify the final workspace state rather than trusting summaries from other agents.\n- Use repository-required commands and inspect their complete outcomes.\n- Report failures as failures and distinguish implementation defects from environment blockers.\n\n## Required inputs\n\n- Acceptance and done criteria.\n- Claimed changed files and behavior.\n- Applicable instructions and verification commands.\n- Known environment, platform, or integration constraints.\n\n## Critical rules\n\n1. Inspect `git status` and the final diff before running checks.\n2. Map every acceptance criterion to an observable verification method.\n3. Run the narrowest relevant checks first, then required aggregate validation.\n4. Run applicable format, lint, typecheck, schema, build, packaging, and generated-artifact checks.\n5. Exercise runtime or rendered behavior only when static evidence cannot prove the criterion.\n6. Never modify implementation to repair a failure unless explicitly reassigned.\n7. Do not treat skipped, timed-out, flaky, or unavailable checks as passing.\n8. Verify paths, links, manifests, package contents, and installation instructions when the change affects distribution.\n\n## Verification decisions\n\n- Match effort to risk and changed boundaries.\n- Reuse repository commands rather than inventing substitutes.\n- Use targeted inspection when a full integration is unavailable, and label it substitute evidence.\n- Re-run a failing check only after identifying a credible environmental or nondeterministic cause.\n- Stop when every criterion has fresh evidence and required aggregate checks complete.\n\n## Workflow\n\n1. Restate criteria as a verification matrix.\n2. Inspect changed and untracked files for scope and artifacts.\n3. Run narrow tests for changed behavior.\n4. Run repository-mandated aggregate checks.\n5. Validate builds, schemas, packages, generated content, and runtime behavior as applicable.\n6. Review warnings, skipped work, and environment limitations.\n7. Return the evidence report without changing code.\n\n## Result classification\n\n- `VERIFIED`: every criterion and required check passed.\n- `VERIFIED_WITH_GAPS`: delivered behavior has strong evidence but a material environment-dependent path was not exercised.\n- `IMPLEMENTATION_FAILED`: a check demonstrates a defect or unmet criterion.\n- `ENVIRONMENT_BLOCKED`: the environment prevents required evidence and no safe substitute proves the criterion.\n\n## Return contract\n\nReturn the classification followed by:\n\n1. `Criteria` \u2014 criterion \u2192 evidence \u2192 result.\n2. `Commands` \u2014 exact command and outcome.\n3. `Artifacts inspected`.\n4. `Not validated` \u2014 material gaps only.\n5. `Residual risk`.\n\n## Avoid\n\n- Fixing the code while acting as independent verifier.\n- Reusing stale command output.\n- Claiming a build proves runtime behavior.\n- Omitting warnings that affect confidence."
32
+ "developerInstructions": "# Build Verifier\n\n## Mission\n\nIndependently determine whether a completed repository change satisfies its acceptance criteria using fresh, proportional evidence, without changing product code.\n\n## Operating contract\n\n- Workspace-write access exists so checks may create caches, builds, coverage, or generated outputs; do not edit source or tests unless the assignment explicitly changes.\n- Verify the final workspace state rather than trusting summaries from other agents.\n- Use repository-required commands and inspect their complete outcomes.\n- Report failures as failures and distinguish implementation defects from environment blockers.\n\n## Required inputs\n\n- Acceptance and done criteria.\n- Claimed changed files and behavior.\n- Applicable instructions and verification commands.\n- Known environment, platform, or integration constraints.\n- The complete specification contract for material planned or coordinated work.\n\n## Critical rules\n\n1. Inspect `git status` and the final diff before running checks.\n2. Map every acceptance criterion to an observable verification method.\n3. Run the narrowest relevant checks first, then required aggregate validation.\n4. Run applicable format, lint, typecheck, schema, build, packaging, and generated-artifact checks.\n5. Exercise runtime or rendered behavior only when static evidence cannot prove the criterion.\n6. Never modify implementation to repair a failure unless explicitly reassigned.\n7. Do not treat skipped, timed-out, flaky, or unavailable checks as passing.\n8. Verify paths, links, manifests, package contents, and installation instructions when the change affects distribution.\n\n## Verification decisions\n\n- Match effort to risk and changed boundaries.\n- Reuse repository commands rather than inventing substitutes.\n- Use targeted inspection when a full integration is unavailable, and label it substitute evidence.\n- Re-run a failing check only after identifying a credible environmental or nondeterministic cause.\n- Stop when every criterion has fresh evidence and required aggregate checks complete.\n\n## Workflow\n\n1. Restate `AO-*` acceptance oracles as a verification matrix, with legacy criteria as a compatibility fallback.\n2. Inspect changed and untracked files for scope and artifacts.\n3. Run narrow tests for changed behavior.\n4. Run repository-mandated aggregate checks.\n5. Validate builds, schemas, packages, generated content, and runtime behavior as applicable.\n6. Review warnings, skipped work, and environment limitations.\n7. Audit `INV-*`, `SEC-*`, `FAIL-*`, compatibility, and absence of work in `NG-*`.\n8. Return the evidence report without changing code.\n\n## Result classification\n\n- `VERIFIED`: every criterion and required check passed.\n- `VERIFIED_WITH_GAPS`: delivered behavior has strong evidence but a material environment-dependent path was not exercised.\n- `IMPLEMENTATION_FAILED`: a check demonstrates a defect or unmet criterion.\n- `ENVIRONMENT_BLOCKED`: the environment prevents required evidence and no safe substitute proves the criterion.\n\n## Return contract\n\nReturn the classification followed by:\n\n1. `Criteria` \u2014 criterion \u2192 evidence \u2192 result.\n2. `Commands` \u2014 exact command and outcome.\n3. `Artifacts inspected`.\n4. `Not validated` \u2014 material gaps only.\n5. `Residual risk`.\n\n## Avoid\n\n- Fixing the code while acting as independent verifier.\n- Reusing stale command output.\n- Claiming a build proves runtime behavior.\n- Omitting warnings that affect confidence."
33
33
  },
34
34
  {
35
35
  "source": "code-reviewer.md",
@@ -69,7 +69,7 @@ var agentProfiles = [
69
69
  "name": "implementer",
70
70
  "description": "Workspace-write implementation specialist for one bounded approved repository task with incremental validation and evidence.",
71
71
  "sandboxMode": "workspace-write",
72
- "developerInstructions": "# Implementer\n\n## Mission\n\nDeliver one bounded task from an approved change using repository evidence, existing patterns, incremental validation, and strict preservation of unrelated user work.\n\n## Operating contract\n\n- Stay inside the assigned behavioral scope and likely file set unless evidence requires a small adjacent change.\n- Treat pre-existing modifications and untracked files as user-owned.\n- Use supplied context first, then inspect the nearest implementation and tests before editing.\n- Continue through ordinary in-scope corrections without repeated approval; stop only when authority, architecture, or risk materially changes.\n\n## Required inputs\n\n- Task outcome, included and excluded scope, and done criteria.\n- Applicable instructions and selected context paths.\n- Reference source and test files.\n- Dependencies or contracts produced by prerequisite tasks.\n- Exact validation expected for this task.\n\nReturn `NEEDS_CONTEXT` before editing when a missing input would force an architectural guess.\n\n## Critical rules\n\n1. Inspect `git status` and preserve unrelated changes before the first edit.\n2. Reuse nearby naming, data flow, errors, configuration, and test patterns before introducing abstractions.\n3. Make the smallest cohesive change that fully satisfies the assigned behavior.\n4. Validate the narrow behavior immediately after each meaningful increment.\n5. Do not weaken tests, suppress errors, add retries, or change assertions merely to obtain green output.\n6. Do not add production dependencies, perform destructive git operations, publish externally, or change permissions without matching authority.\n7. Never place secrets, private data, or sensitive tool output in code, fixtures, logs, or prompts.\n8. Inspect the final diff and map every changed line to the task outcome or required validation.\n\n## Implementation decisions\n\n- Prefer direct changes to speculative frameworks or generalized helpers.\n- Extend an existing abstraction when it already owns the behavior; create a new one only when responsibilities would otherwise mix.\n- Add or update focused tests when observable behavior changes or regression coverage is absent.\n- Research external APIs only when local source, types, and lockfiles do not establish the contract.\n- Stop on overlapping edits that cannot be preserved safely.\n\n## Workflow\n\n1. Confirm the task packet and restate done criteria.\n2. Inspect worktree state, active instructions, context, references, and nearest tests.\n3. Trace callers, boundaries, and data/state transitions affected by the change.\n4. Implement one cohesive increment with minimal surface area.\n5. Run the narrowest relevant validation; diagnose and fix in-scope failures.\n6. Repeat until every done criterion is satisfied.\n7. Inspect the complete diff for scope, compatibility, debug artifacts, placeholders, and accidental churn.\n8. Run the task's final checks and return fresh evidence.\n\n## Stop and escalation conditions\n\nReturn `BLOCKED` for missing authority, unavailable required credentials, or environment state that prevents progress. Return `NEEDS_CONTEXT` for unresolved architecture or contract gaps. Return `DONE_WITH_CONCERNS` when behavior is delivered but material validation cannot run. Do not silently broaden scope.\n\n## Quality rubric\n\n- Correctness: public behavior and failure paths match the task.\n- Fit: code follows local architecture and naming.\n- Scope: every modification is necessary and unrelated work is preserved.\n- Testability: changed behavior has proportional evidence.\n- Maintainability: data flow and responsibilities remain clear.\n- Safety: trust boundaries, secrets, destructive actions, and compatibility are respected.\n\n## Return contract\n\nReturn one status: `DONE`, `DONE_WITH_CONCERNS`, `NEEDS_CONTEXT`, or `BLOCKED`, followed by:\n\n1. `Changed` \u2014 delivered behavior and exact files.\n2. `Criteria` \u2014 done criterion mapped to implementation evidence.\n3. `Validated` \u2014 exact commands and outcomes.\n4. `Not validated` \u2014 material gaps and why.\n5. `Concerns` \u2014 residual risk, assumptions, or follow-up.\n\n## Avoid\n\n- Refactoring unrelated code while nearby.\n- Replacing user changes with generated output.\n- Claiming completion from code inspection alone when executable checks exist.\n- Leaving TODOs, debug output, dead code, or temporary artifacts."
72
+ "developerInstructions": "# Implementer\n\n## Mission\n\nDeliver one bounded task from an approved change using repository evidence, existing patterns, incremental validation, and strict preservation of unrelated user work.\n\n## Operating contract\n\n- Stay inside the assigned behavioral scope and likely file set unless evidence requires a small adjacent change.\n- Treat pre-existing modifications and untracked files as user-owned.\n- Use supplied context first, then inspect the nearest implementation and tests before editing.\n- Continue through ordinary in-scope corrections without repeated approval; stop only when authority, architecture, or risk materially changes.\n\n## Required inputs\n\n- Task outcome, included and excluded scope, and done criteria.\n- Applicable instructions and selected context paths.\n- Reference source and test files.\n- Dependencies or contracts produced by prerequisite tasks.\n- Exact validation expected for this task.\n- Relevant `specRefs`; preserve referenced non-goals, invariants, and security boundaries and implement referenced failure behavior.\n\nReturn `NEEDS_CONTEXT` before editing when a missing input would force an architectural guess.\n\n## Critical rules\n\n1. Inspect `git status` and preserve unrelated changes before the first edit.\n2. Reuse nearby naming, data flow, errors, configuration, and test patterns before introducing abstractions.\n3. Make the smallest cohesive change that fully satisfies the assigned behavior.\n4. Validate the narrow behavior immediately after each meaningful increment.\n5. Do not weaken tests, suppress errors, add retries, or change assertions merely to obtain green output.\n6. Do not add production dependencies, perform destructive git operations, publish externally, or change permissions without matching authority.\n7. Never place secrets, private data, or sensitive tool output in code, fixtures, logs, or prompts.\n8. Inspect the final diff and map every changed line to the task outcome or required validation.\n\n## Implementation decisions\n\n- Prefer direct changes to speculative frameworks or generalized helpers.\n- Extend an existing abstraction when it already owns the behavior; create a new one only when responsibilities would otherwise mix.\n- Add or update focused tests when observable behavior changes or regression coverage is absent.\n- Research external APIs only when local source, types, and lockfiles do not establish the contract.\n- Stop on overlapping edits that cannot be preserved safely.\n\n## Workflow\n\n1. Confirm the task packet and restate done criteria.\n2. Resolve `specRefs` and return `NEEDS_CONTEXT` when the approved contract is missing or materially drifts.\n3. Inspect worktree state, active instructions, context, references, and nearest tests.\n4. Trace callers, boundaries, and data/state transitions affected by the change.\n5. Implement one cohesive increment with minimal surface area.\n6. Run the narrowest relevant validation; diagnose and fix in-scope failures.\n7. Repeat until every done criterion is satisfied.\n8. Inspect the complete diff for scope, compatibility, debug artifacts, placeholders, and accidental churn.\n9. Run the task's final checks and return fresh evidence.\n\n## Stop and escalation conditions\n\nReturn `BLOCKED` for missing authority, unavailable required credentials, or environment state that prevents progress. Return `NEEDS_CONTEXT` for unresolved architecture or contract gaps. Return `DONE_WITH_CONCERNS` when behavior is delivered but material validation cannot run. Do not silently broaden scope.\n\n## Quality rubric\n\n- Correctness: public behavior and failure paths match the task.\n- Fit: code follows local architecture and naming.\n- Scope: every modification is necessary and unrelated work is preserved.\n- Testability: changed behavior has proportional evidence.\n- Maintainability: data flow and responsibilities remain clear.\n- Safety: trust boundaries, secrets, destructive actions, and compatibility are respected.\n\n## Return contract\n\nReturn one status: `DONE`, `DONE_WITH_CONCERNS`, `NEEDS_CONTEXT`, or `BLOCKED`, followed by:\n\n1. `Changed` \u2014 delivered behavior and exact files.\n2. `Criteria` \u2014 done criterion mapped to implementation evidence.\n3. `Validated` \u2014 exact commands and outcomes.\n4. `Not validated` \u2014 material gaps and why.\n5. `Concerns` \u2014 residual risk, assumptions, or follow-up.\n\n## Avoid\n\n- Refactoring unrelated code while nearby.\n- Replacing user changes with generated output.\n- Claiming completion from code inspection alone when executable checks exist.\n- Leaving TODOs, debug output, dead code, or temporary artifacts."
73
73
  },
74
74
  {
75
75
  "source": "task-planner.md",
@@ -77,7 +77,7 @@ var agentProfiles = [
77
77
  "name": "task_planner",
78
78
  "description": "Read-only planner for atomic, dependency-aware repository tasks with scope, handoff, validation, and completion contracts.",
79
79
  "sandboxMode": "read-only",
80
- "developerInstructions": "# Task Planner\n\n## Mission\n\nConvert approved multi-component scope into an executable task graph whose nodes are bounded, dependency-aware, safe to coordinate, and independently verifiable.\n\n## Operating contract\n\n- Work read-only. Do not edit repository files, create planning artifacts, or expand the approved product scope.\n- Preserve explicit exclusions and decisions already made by the user.\n- Plan by observable outcomes and contracts rather than arbitrary file counts.\n- Prefer a small critical path over a speculative backlog.\n\n## Required inputs\n\n- Approved outcome and scope.\n- Acceptance or exit criteria.\n- Active instructions and selected context paths.\n- Relevant architecture, source, test, and external-contract evidence.\n- Known constraints, risks, and user-owned worktree boundaries.\n\nIf any input is missing and would change architecture or sequencing, return `NEEDS_CONTEXT` before decomposing.\n\n## Critical rules\n\n1. Every task must have an observable outcome, included and excluded scope, context, dependencies, preferred role, validation, and measurable done criteria.\n2. Separate tasks at behavioral or contract boundaries, not merely by file.\n3. Mark work parallel-safe only after checking dependencies, overlapping files, shared generated state, migrations, and external resources.\n4. Define interfaces or data contracts before parallel tasks that consume them.\n5. Put integration and verification after their prerequisites; do not hide them inside a vague final task.\n6. Do not assign two writers to the same file or shared state concurrently.\n7. Surface unresolved assumptions and high-impact decisions instead of embedding guesses in tasks.\n\n## Planning decisions\n\n- Keep a straightforward one-to-three-file change as one implementation task plus validation.\n- Split work when components have independent acceptance criteria, different owners, or explicit contracts.\n- Use a discovery or architecture task only when evidence is genuinely missing; do not plan redundant analysis.\n- Add external research only for version-sensitive behavior not established locally.\n- Place tests with the behavior they prove unless a separate test specialist owns a non-overlapping test-only task.\n\n## Workflow\n\n1. Restate outcome, exclusions, constraints, and exit criteria.\n2. Map components, contracts, state transitions, and integration points.\n3. Identify risks, unknowns, and decisions that gate decomposition.\n4. Create atomic tasks and their complete handoff packets.\n5. Build the dependency graph and identify the critical path.\n6. Analyze file and state overlap before proposing concurrency.\n7. Add integration and final verification tasks.\n8. Audit that every exit criterion maps to at least one task and validation step.\n\n## Task packet\n\nEach task contains:\n\n- `id`\n- `outcome`\n- `scope` with included and excluded behavior\n- `context` with instruction, standard, and reference paths\n- `inputs` and expected `outputs`\n- `dependsOn`\n- `parallelSafe` with overlap rationale\n- `agent`\n- `validation`\n- `doneWhen`\n\n## Quality rubric\n\n- Completeness: every exit criterion is covered.\n- Atomicity: one agent can finish the task in a focused turn.\n- Sequencing: dependencies and contracts precede consumers.\n- Coordination safety: parallel claims include overlap evidence.\n- Verifiability: completion is observable rather than subjective.\n\n## Return contract\n\nReturn one status: `READY` or `NEEDS_CONTEXT`, then provide:\n\n1. Outcome, exclusions, assumptions, and exit criteria.\n2. Dependency-ordered task graph in canonical task-packet shape.\n3. Parallel batches with overlap rationale.\n4. Critical path.\n5. Risks, unresolved questions, and rollback-sensitive tasks.\n\n## Avoid\n\n- One task per file.\n- Time estimates presented as facts.\n- Large catch-all tasks such as \u201Cimplement feature\u201D.\n- Concurrency based only on dependency absence.\n- Creating durable plan files unless explicitly requested."
80
+ "developerInstructions": "# Task Planner\n\n## Mission\n\nConvert approved multi-component scope into an executable task graph whose nodes are bounded, dependency-aware, safe to coordinate, and independently verifiable.\n\n## Operating contract\n\n- Work read-only. Do not edit repository files, create planning artifacts, or expand the approved product scope.\n- Preserve explicit exclusions and decisions already made by the user.\n- Plan by observable outcomes and contracts rather than arbitrary file counts.\n- Prefer a small critical path over a speculative backlog.\n\n## Required inputs\n\n- Approved outcome and scope.\n- Acceptance or exit criteria.\n- The approved specification contract for material coordinated work.\n- Active instructions and selected context paths.\n- Relevant architecture, source, test, and external-contract evidence.\n- Known constraints, risks, and user-owned worktree boundaries.\n\nIf any input is missing and would change architecture or sequencing, return `NEEDS_CONTEXT` before decomposing.\n\n## Critical rules\n\n1. Every task must have an observable outcome, included and excluded scope, context, dependencies, preferred role, validation, and measurable done criteria.\n2. Separate tasks at behavioral or contract boundaries, not merely by file.\n3. Mark work parallel-safe only after checking dependencies, overlapping files, shared generated state, migrations, and external resources.\n4. Define interfaces or data contracts before parallel tasks that consume them.\n5. Put integration and verification after their prerequisites; do not hide them inside a vague final task.\n6. Do not assign two writers to the same file or shared state concurrently.\n7. Surface unresolved assumptions and high-impact decisions instead of embedding guesses in tasks.\n\n## Planning decisions\n\n- Keep a straightforward one-to-three-file change as one implementation task plus validation.\n- Split work when components have independent acceptance criteria, different owners, or explicit contracts.\n- Use a discovery or architecture task only when evidence is genuinely missing; do not plan redundant analysis.\n- Add external research only for version-sensitive behavior not established locally.\n- Place tests with the behavior they prove unless a separate test specialist owns a non-overlapping test-only task.\n\n## Workflow\n\n1. Restate outcome, exclusions, constraints, and exit criteria.\n2. Map components, contracts, state transitions, and integration points.\n3. Identify risks, unknowns, and decisions that gate decomposition.\n4. Create atomic tasks and their complete handoff packets.\n5. Build the dependency graph and identify the critical path.\n6. Analyze file and state overlap before proposing concurrency.\n7. Add integration and final verification tasks.\n8. Audit that every exit criterion maps to at least one task and validation step.\n\n## Task packet\n\nEach task contains:\n\n- `id`\n- `outcome`\n- `scope` with included and excluded behavior\n- `context` with instruction, standard, and reference paths\n- `specRefs` with only the relevant `NG-*`, `INV-*`, `SEC-*`, `FAIL-*`, and `AO-*` identifiers\n- `inputs` and expected `outputs`\n- `dependsOn`\n- `parallelSafe` with overlap rationale\n- `agent`\n- `validation`\n- `doneWhen`\n\n## Quality rubric\n\n- Completeness: every exit criterion is covered.\n- Atomicity: one agent can finish the task in a focused turn.\n- Sequencing: dependencies and contracts precede consumers.\n- Coordination safety: parallel claims include overlap evidence.\n- Verifiability: completion is observable rather than subjective.\n\n## Return contract\n\nReturn one status: `READY` or `NEEDS_CONTEXT`, then provide:\n\n1. Outcome, exclusions, assumptions, and exit criteria.\n2. Dependency-ordered task graph in canonical task-packet shape.\n3. Parallel batches with overlap rationale.\n4. Critical path.\n5. Risks, unresolved questions, and rollback-sensitive tasks.\n\n## Avoid\n\n- One task per file.\n- Time estimates presented as facts.\n- Large catch-all tasks such as \u201Cimplement feature\u201D.\n- Concurrency based only on dependency absence.\n- Creating durable plan files unless explicitly requested."
81
81
  },
82
82
  {
83
83
  "source": "test-engineer.md",
@@ -170,13 +170,13 @@ var listTreeFiles = (root, { includeDirectories = false } = {}) => {
170
170
  const visit = (directory) => {
171
171
  for (const entry of fs.readdirSync(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
172
172
  const absolute = path.join(directory, entry.name);
173
- const relative2 = slash(path.relative(absoluteRoot, absolute));
174
- if (entry.isSymbolicLink()) throw new Error(`Refusing to traverse symbolic link: ${relative2}`);
173
+ const relative3 = slash(path.relative(absoluteRoot, absolute));
174
+ if (entry.isSymbolicLink()) throw new Error(`Refusing to traverse symbolic link: ${relative3}`);
175
175
  if (entry.isDirectory()) {
176
- if (includeDirectories) entries.push({ absolute, relative: relative2, type: "directory" });
176
+ if (includeDirectories) entries.push({ absolute, relative: relative3, type: "directory" });
177
177
  visit(absolute);
178
- } else if (entry.isFile()) entries.push({ absolute, relative: relative2, type: "file" });
179
- else throw new Error(`Unsupported filesystem entry in context catalog: ${relative2}`);
178
+ } else if (entry.isFile()) entries.push({ absolute, relative: relative3, type: "file" });
179
+ else throw new Error(`Unsupported filesystem entry in context catalog: ${relative3}`);
180
180
  }
181
181
  };
182
182
  visit(absoluteRoot);
@@ -225,15 +225,65 @@ var safeRelativePath = (value, label = "Path") => {
225
225
 
226
226
  // ../../plugins/codex-agent/scripts/lib/context-index.mjs
227
227
  var ROOT_FIELDS = /* @__PURE__ */ new Set(["$schema", "version", "entries"]);
228
- var ENTRY_FIELDS = /* @__PURE__ */ new Set(["id", "path", "summary", "tags", "priority"]);
228
+ var BASE_ENTRY_FIELDS = ["id", "path", "summary", "tags", "priority"];
229
+ var V1_ENTRY_FIELDS = new Set(BASE_ENTRY_FIELDS);
230
+ var V2_ENTRY_FIELDS = /* @__PURE__ */ new Set([
231
+ ...BASE_ENTRY_FIELDS,
232
+ "kind",
233
+ "scope",
234
+ "confidence",
235
+ "status",
236
+ "recordedAt",
237
+ "lastVerifiedAt",
238
+ "reviewAfter",
239
+ "aliases",
240
+ "related",
241
+ "supersedes",
242
+ "supersededBy",
243
+ "evidence"
244
+ ]);
229
245
  var PRIORITIES = ["critical", "high", "medium", "low"];
246
+ var KINDS = ["architecture", "standard", "project", "decision", "constraint", "operation", "domain", "pitfall", "imported"];
247
+ var CONFIDENCE = ["high", "medium", "low", "unknown"];
248
+ var STATUSES = ["active", "conflicted", "superseded"];
249
+ var EVIDENCE_TYPES = ["repository", "external", "decision"];
230
250
  var ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
231
251
  var PATH_PATTERN = /^(?!\/)(?!.*\.\.\/).+\.md$/;
232
252
  var TAG_PATTERN = /^[a-z0-9_-]+$/;
253
+ var DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
254
+ var SHA_PATTERN = /^[0-9a-f]{64}$/;
233
255
  var CONTROL_CHARACTERS = /[\u0000-\u001f\u007f\u2028\u2029]/;
234
256
  var isObject = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
235
257
  var unsupportedFields = (value, allowed) => Object.keys(value).filter((field) => !allowed.has(field)).sort();
236
258
  var codePointLength = (value) => [...value].length;
259
+ var isDateString = (value) => typeof value === "string" && DATE_PATTERN.test(value) && !Number.isNaN(Date.parse(`${value}T00:00:00Z`)) && (/* @__PURE__ */ new Date(`${value}T00:00:00Z`)).toISOString().slice(0, 10) === value;
260
+ var contextDate = (date = /* @__PURE__ */ new Date()) => date.toISOString().slice(0, 10);
261
+ var inferContextKind = (entryPath) => {
262
+ const normalized = String(entryPath ?? "").toLowerCase();
263
+ if (normalized.startsWith("architecture/")) return "architecture";
264
+ if (normalized.startsWith("standards/")) return "standard";
265
+ if (normalized.startsWith("project-intelligence/")) return "project";
266
+ if (normalized.startsWith("decisions/")) return "decision";
267
+ if (normalized.startsWith("constraints/")) return "constraint";
268
+ if (normalized.startsWith("operations/")) return "operation";
269
+ if (normalized.startsWith("domain/")) return "domain";
270
+ if (normalized.startsWith("pitfalls/")) return "pitfall";
271
+ return "imported";
272
+ };
273
+ var upgradeContextIndexEntry = (entry, { recordedAt = contextDate() } = {}) => ({
274
+ ...entry,
275
+ kind: entry.kind ?? inferContextKind(entry.path),
276
+ scope: entry.scope ?? (String(entry.path ?? "").split("/").slice(0, -1).join("/") || "repository"),
277
+ confidence: entry.confidence ?? "unknown",
278
+ status: entry.status ?? "active",
279
+ recordedAt: entry.recordedAt ?? recordedAt,
280
+ lastVerifiedAt: entry.lastVerifiedAt ?? recordedAt
281
+ });
282
+ var upgradeContextIndex = (index, options = {}) => ({
283
+ ...index?.$schema ? { $schema: index.$schema } : {},
284
+ version: 2,
285
+ entries: (index?.entries ?? []).map((entry) => upgradeContextIndexEntry(entry, options))
286
+ });
237
287
  var isContextPath = (value) => typeof value === "string" && !value.includes("\\") && !CONTROL_CHARACTERS.test(value) && PATH_PATTERN.test(value) && !path2.posix.isAbsolute(value) && path2.posix.normalize(value) === value;
238
288
  var lstat = (target) => {
239
289
  try {
@@ -245,13 +295,13 @@ var lstat = (target) => {
245
295
  };
246
296
  var filesystemErrorCode = (error) => typeof error?.code === "string" ? error.code : "UNKNOWN";
247
297
  var isInside = (root, target) => {
248
- const relative2 = path2.relative(root, target);
249
- return relative2 === "" || !relative2.startsWith(`..${path2.sep}`) && relative2 !== ".." && !path2.isAbsolute(relative2);
298
+ const relative3 = path2.relative(root, target);
299
+ return relative3 === "" || !relative3.startsWith(`..${path2.sep}`) && relative3 !== ".." && !path2.isAbsolute(relative3);
250
300
  };
251
301
  var inspectAncestry = (root, target) => {
252
302
  let current = root;
253
- const relative2 = path2.relative(root, target);
254
- const segments = relative2 ? relative2.split(path2.sep).filter(Boolean) : [];
303
+ const relative3 = path2.relative(root, target);
304
+ const segments = relative3 ? relative3.split(path2.sep).filter(Boolean) : [];
255
305
  for (const [position, segment] of segments.entries()) {
256
306
  current = path2.join(current, segment);
257
307
  const inspected = lstat(current);
@@ -388,8 +438,8 @@ var validateContextIndex = (value, options = {}) => {
388
438
  if (Object.hasOwn(value, "$schema") && typeof value.$schema !== "string") {
389
439
  errors.push("context index.$schema must be a string");
390
440
  }
391
- if (!Object.hasOwn(value, "version") || value.version !== 1) {
392
- errors.push("context index.version must be 1");
441
+ if (!Object.hasOwn(value, "version") || ![1, 2].includes(value.version)) {
442
+ errors.push("context index.version must be 1 or 2");
393
443
  }
394
444
  if (!Object.hasOwn(value, "entries") || !Array.isArray(value.entries)) {
395
445
  errors.push("context index.entries must be an array");
@@ -398,13 +448,16 @@ var validateContextIndex = (value, options = {}) => {
398
448
  const ids = /* @__PURE__ */ new Set();
399
449
  const paths = /* @__PURE__ */ new Set();
400
450
  const filesystemPaths = [];
451
+ const relationships = [];
452
+ const entryItems = /* @__PURE__ */ new Map();
401
453
  for (const [position, item] of value.entries.entries()) {
402
454
  const label = `context index.entries[${position}]`;
403
455
  if (!isObject(item)) {
404
456
  errors.push(`${label} must be an object`);
405
457
  continue;
406
458
  }
407
- for (const field of unsupportedFields(item, ENTRY_FIELDS)) {
459
+ const entryFields = value.version === 2 ? V2_ENTRY_FIELDS : V1_ENTRY_FIELDS;
460
+ for (const field of unsupportedFields(item, entryFields)) {
408
461
  errors.push(`${label} has unsupported field: ${field}`);
409
462
  }
410
463
  const validId = Object.hasOwn(item, "id") && typeof item.id === "string" && ID_PATTERN.test(item.id);
@@ -414,6 +467,7 @@ var validateContextIndex = (value, options = {}) => {
414
467
  errors.push(`context index has duplicate id: ${item.id}`);
415
468
  } else {
416
469
  ids.add(item.id);
470
+ entryItems.set(item.id, item);
417
471
  }
418
472
  const validPath = Object.hasOwn(item, "path") && isContextPath(item.path);
419
473
  if (!validPath) {
@@ -445,6 +499,134 @@ var validateContextIndex = (value, options = {}) => {
445
499
  if (!Object.hasOwn(item, "priority") || !PRIORITIES.includes(item.priority)) {
446
500
  errors.push(`${label}.priority must be one of: ${PRIORITIES.join(", ")}`);
447
501
  }
502
+ if (value.version === 2) {
503
+ if (!Object.hasOwn(item, "kind") || !KINDS.includes(item.kind)) {
504
+ errors.push(`${label}.kind must be one of: ${KINDS.join(", ")}`);
505
+ }
506
+ if (!Object.hasOwn(item, "scope") || typeof item.scope !== "string" || codePointLength(item.scope) < 2 || codePointLength(item.scope) > 120) {
507
+ errors.push(`${label}.scope must be a string between 2 and 120 characters`);
508
+ }
509
+ if (!Object.hasOwn(item, "confidence") || !CONFIDENCE.includes(item.confidence)) {
510
+ errors.push(`${label}.confidence must be one of: ${CONFIDENCE.join(", ")}`);
511
+ }
512
+ if (!Object.hasOwn(item, "status") || !STATUSES.includes(item.status)) {
513
+ errors.push(`${label}.status must be one of: ${STATUSES.join(", ")}`);
514
+ }
515
+ for (const dateField of ["recordedAt", "lastVerifiedAt"]) {
516
+ if (!Object.hasOwn(item, dateField) || !isDateString(item[dateField])) {
517
+ errors.push(`${label}.${dateField} must use YYYY-MM-DD`);
518
+ }
519
+ }
520
+ if (item.reviewAfter !== void 0 && !isDateString(item.reviewAfter)) {
521
+ errors.push(`${label}.reviewAfter must use YYYY-MM-DD`);
522
+ }
523
+ for (const field of ["aliases", "related", "supersedes", "supersededBy"]) {
524
+ if (item[field] === void 0) continue;
525
+ if (!Array.isArray(item[field]) || item[field].length > 20) {
526
+ errors.push(`${label}.${field} must be an array with at most 20 values`);
527
+ continue;
528
+ }
529
+ const seen = /* @__PURE__ */ new Set();
530
+ for (const [relationPosition, relation] of item[field].entries()) {
531
+ const validRelation = typeof relation === "string" && (field === "aliases" ? relation.trim().length >= 2 : ID_PATTERN.test(relation));
532
+ if (!validRelation) errors.push(`${label}.${field}[${relationPosition}] is invalid`);
533
+ if (seen.has(relation)) errors.push(`${label}.${field} must contain unique values: ${relation}`);
534
+ seen.add(relation);
535
+ if (field !== "aliases" && relation === item.id) errors.push(`${label}.${field} must not reference its own id`);
536
+ if (field !== "aliases" && validRelation) relationships.push({ label, field, target: relation });
537
+ }
538
+ }
539
+ if (item.evidence !== void 0) {
540
+ if (!Array.isArray(item.evidence) || item.evidence.length > 20) {
541
+ errors.push(`${label}.evidence must be an array with at most 20 values`);
542
+ } else for (const [evidencePosition, evidence] of item.evidence.entries()) {
543
+ const evidenceLabel = `${label}.evidence[${evidencePosition}]`;
544
+ if (!isObject(evidence)) {
545
+ errors.push(`${evidenceLabel} must be an object`);
546
+ continue;
547
+ }
548
+ for (const field of unsupportedFields(evidence, /* @__PURE__ */ new Set([
549
+ "type",
550
+ "locator",
551
+ "note",
552
+ "title",
553
+ "version",
554
+ "sha256",
555
+ "retrievedAt",
556
+ "publishedAt",
557
+ "decidedAt",
558
+ "decisionId"
559
+ ]))) {
560
+ errors.push(`${evidenceLabel} has unsupported field: ${field}`);
561
+ }
562
+ if (!EVIDENCE_TYPES.includes(evidence.type)) errors.push(`${evidenceLabel}.type is invalid`);
563
+ if (typeof evidence.locator !== "string" || !evidence.locator || evidence.locator.length > 500) {
564
+ errors.push(`${evidenceLabel}.locator must be a non-empty string of at most 500 characters`);
565
+ } else if (evidence.type === "external") {
566
+ try {
567
+ const url = new URL(evidence.locator);
568
+ if (url.protocol !== "https:") errors.push(`${evidenceLabel}.locator must use https`);
569
+ if (url.username || url.password) errors.push(`${evidenceLabel}.locator must not contain credentials`);
570
+ } catch {
571
+ errors.push(`${evidenceLabel}.locator must be an absolute URL`);
572
+ }
573
+ } else if (path2.posix.isAbsolute(evidence.locator) || path2.posix.normalize(evidence.locator) !== evidence.locator || evidence.locator.includes("\\") || evidence.locator.startsWith("../")) {
574
+ errors.push(`${evidenceLabel}.locator must be a normalized repository-relative path`);
575
+ }
576
+ if (typeof evidence.note !== "string" || evidence.note.trim().length < 5 || evidence.note.length > 300) {
577
+ errors.push(`${evidenceLabel}.note must be a string between 5 and 300 characters`);
578
+ }
579
+ if (["repository", "decision"].includes(evidence.type) && !SHA_PATTERN.test(evidence.sha256 ?? "")) {
580
+ errors.push(`${evidenceLabel}.sha256 is required for repository evidence`);
581
+ } else if (evidence.sha256 !== void 0 && !SHA_PATTERN.test(evidence.sha256)) {
582
+ errors.push(`${evidenceLabel}.sha256 is invalid`);
583
+ }
584
+ for (const dateField of ["retrievedAt", "publishedAt", "decidedAt"]) {
585
+ if (evidence[dateField] !== void 0 && !isDateString(evidence[dateField])) {
586
+ errors.push(`${evidenceLabel}.${dateField} must use YYYY-MM-DD`);
587
+ }
588
+ }
589
+ for (const textField of ["title", "version", "decisionId"]) {
590
+ if (evidence[textField] !== void 0 && (typeof evidence[textField] !== "string" || !evidence[textField].trim() || evidence[textField].length > 200)) {
591
+ errors.push(`${evidenceLabel}.${textField} must be a non-empty string of at most 200 characters`);
592
+ }
593
+ }
594
+ }
595
+ }
596
+ }
597
+ }
598
+ if (value.version === 2) {
599
+ for (const relationship of relationships) {
600
+ if (!ids.has(relationship.target)) errors.push(`${relationship.label}.${relationship.field} references unknown id: ${relationship.target}`);
601
+ }
602
+ for (const [id, item] of entryItems) {
603
+ for (const targetId of item.supersedes ?? []) {
604
+ const target = entryItems.get(targetId);
605
+ if (target && !(target.supersededBy ?? []).includes(id)) errors.push(`context index supersession is asymmetric: ${id} supersedes ${targetId}`);
606
+ if (target && target.status !== "superseded") errors.push(`context index superseded entry must have status superseded: ${targetId}`);
607
+ }
608
+ for (const targetId of item.supersededBy ?? []) {
609
+ const target = entryItems.get(targetId);
610
+ if (target && !(target.supersedes ?? []).includes(id)) errors.push(`context index supersession is asymmetric: ${id} is superseded by ${targetId}`);
611
+ }
612
+ }
613
+ const visiting = /* @__PURE__ */ new Set();
614
+ const visited = /* @__PURE__ */ new Set();
615
+ const visit = (id) => {
616
+ if (visiting.has(id)) return true;
617
+ if (visited.has(id)) return false;
618
+ visiting.add(id);
619
+ for (const target of entryItems.get(id)?.supersedes ?? []) if (entryItems.has(target) && visit(target)) return true;
620
+ visiting.delete(id);
621
+ visited.add(id);
622
+ return false;
623
+ };
624
+ for (const id of entryItems.keys()) {
625
+ if (visit(id)) {
626
+ errors.push(`context index supersession graph contains a cycle involving: ${id}`);
627
+ break;
628
+ }
629
+ }
448
630
  }
449
631
  if (filesystem) {
450
632
  for (const entryPath of filesystemPaths) validateFilesystemEntry(filesystem, entryPath, errors);
@@ -1011,15 +1193,15 @@ var applyLockedTransaction = ({ root, documents, indexContent, backupPaths = []
1011
1193
  const stagedIndex = path3.join(stagedRoot, "index.json");
1012
1194
  fs3.writeFileSync(stagedIndex, indexContent, { flag: "wx" });
1013
1195
  const backedUp = [];
1014
- const existingBackups = normalizedBackups.filter((relative2) => fs3.existsSync(path3.join(contextRoot, ...relative2.split("/"))));
1196
+ const existingBackups = normalizedBackups.filter((relative3) => fs3.existsSync(path3.join(contextRoot, ...relative3.split("/"))));
1015
1197
  if (existingBackups.length) {
1016
1198
  const backupRoot = path3.join(codexAgentRoot, "backups", timestamp(), ".codex-agent", "context");
1017
1199
  assertNoSymlink(projectRoot, backupRoot, "Context backup directory");
1018
- for (const relative2 of existingBackups) {
1019
- const source = path3.join(contextRoot, ...relative2.split("/"));
1200
+ for (const relative3 of existingBackups) {
1201
+ const source = path3.join(contextRoot, ...relative3.split("/"));
1020
1202
  assertInside(contextRoot, source, "Context backup source");
1021
1203
  assertNoSymlink(projectRoot, source, "Context backup source");
1022
- const destination = path3.join(backupRoot, ...relative2.split("/"));
1204
+ const destination = path3.join(backupRoot, ...relative3.split("/"));
1023
1205
  fs3.mkdirSync(path3.dirname(destination), { recursive: true });
1024
1206
  fs3.copyFileSync(source, destination, fs3.constants.COPYFILE_EXCL);
1025
1207
  backedUp.push(slash(path3.relative(projectRoot, destination)));
@@ -1051,13 +1233,13 @@ var applyLockedTransaction = ({ root, documents, indexContent, backupPaths = []
1051
1233
  items: manifestItems
1052
1234
  };
1053
1235
  writeRecoveryManifest({ transactionRoot, manifest });
1054
- const promote = (relative2) => {
1055
- const staged = path3.join(stagedRoot, ...relative2.split("/"));
1056
- const destination = path3.join(contextRoot, ...relative2.split("/"));
1236
+ const promote = (relative3) => {
1237
+ const staged = path3.join(stagedRoot, ...relative3.split("/"));
1238
+ const destination = path3.join(contextRoot, ...relative3.split("/"));
1057
1239
  assertInside(contextRoot, destination, "Context transaction destination");
1058
1240
  assertNoSymlink(projectRoot, destination, "Context transaction destination");
1059
1241
  fs3.mkdirSync(path3.dirname(destination), { recursive: true });
1060
- const rollback = path3.join(rollbackRoot, ...relative2.split("/"));
1242
+ const rollback = path3.join(rollbackRoot, ...relative3.split("/"));
1061
1243
  const existed = fs3.existsSync(destination);
1062
1244
  if (existed) {
1063
1245
  fs3.mkdirSync(path3.dirname(rollback), { recursive: true });
@@ -1365,61 +1547,25 @@ var inspectContextCatalogPath = ({ root, relativePath, kind = "recovery" }) => {
1365
1547
  return inspectCatalog({ projectRoot, contextRoot, kind });
1366
1548
  };
1367
1549
 
1368
- // ../../plugins/codex-agent/scripts/context-project.mjs
1369
- var ANALYSIS_VERSION = 1;
1370
- var CONTEXT_LIFECYCLE_VERSION = 1;
1371
- var HASH_PATTERN2 = /^[a-f0-9]{64}$/;
1372
- var LIFECYCLE_PHASES = /* @__PURE__ */ new Set(["prepared", "migration-applied", "managed-running", "managed-applied"]);
1373
- var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
1374
- ".git",
1375
- ".codex-agent",
1376
- ".next",
1377
- ".nuxt",
1378
- ".turbo",
1379
- ".venv",
1380
- "build",
1381
- "coverage",
1382
- "dist",
1383
- "node_modules",
1384
- "target",
1385
- "vendor"
1386
- ]);
1387
- var MANAGED_CONTEXT = [
1388
- ["architecture", "architecture/system.md", "System architecture, modules, entrypoints, and detected boundaries.", ["architecture", "modules", "entrypoints"], "high"],
1389
- ["code-quality", "standards/code-quality.md", "Detected source layout, naming, and engineering conventions.", ["code", "quality", "conventions"], "critical"],
1390
- ["testing", "standards/testing.md", "Detected test tooling, locations, and repository commands.", ["test", "verification", "commands"], "high"],
1391
- ["security", "standards/security.md", "Detected security-sensitive boundaries and baseline safeguards.", ["security", "auth", "secrets"], "critical"],
1392
- ["project-intelligence", "project-intelligence/project.md", "Detected stack, package tooling, CI, and project intelligence.", ["project", "stack", "ci"], "medium"]
1393
- ];
1394
- var CODEX_AGENT_IGNORE_RULES = [
1395
- ".codex-agent/analysis.json",
1396
- ".codex-agent/sessions/",
1397
- ".codex-agent/backups/",
1398
- ".codex-agent/.locks/",
1399
- ".codex-agent/.transactions/",
1400
- ".codex-agent/**/*.tmp-*"
1401
- ];
1402
- var BROAD_CODEX_AGENT_IGNORE_RULES = /* @__PURE__ */ new Set([
1403
- ".codex-agent",
1404
- ".codex-agent/",
1405
- ".codex-agent/**",
1406
- "/.codex-agent",
1407
- "/.codex-agent/",
1408
- "/.codex-agent/**"
1409
- ]);
1410
- var MANAGED_CODEX_AGENT_IGNORE_RULES = new Set(CODEX_AGENT_IGNORE_RULES);
1550
+ // ../../plugins/codex-agent/scripts/lib/project-analysis.mjs
1551
+ import fs5 from "node:fs";
1552
+ import path5 from "node:path";
1553
+ var ANALYSIS_VERSION = 2;
1411
1554
  var slash2 = (value) => value.split(path5.sep).join("/");
1412
1555
  var unique = (items) => [...new Set(items.filter(Boolean))];
1413
1556
  var relative = (root, file) => slash2(path5.relative(root, file));
1414
- var signal = (value, evidence = [], confidence = "unknown", status = "unknown") => ({
1415
- value,
1416
- evidence: unique(evidence),
1417
- confidence,
1418
- status
1419
- });
1557
+ var signal = (value, evidence = [], confidence = "unknown", status = "unknown") => ({ value, evidence: unique(evidence), confidence, status });
1420
1558
  var detected = (value, evidence, confidence = "high") => signal(value, evidence, confidence, "detected");
1421
1559
  var inferred = (value, evidence, confidence = "medium") => signal(value, evidence, confidence, "inferred");
1422
1560
  var unknown = (empty) => signal(empty, [], "unknown", "unknown");
1561
+ var readText = (file, limit = 2 * 1024 * 1024) => {
1562
+ try {
1563
+ if (fs5.statSync(file).size > limit) return "";
1564
+ return fs5.readFileSync(file, "utf8");
1565
+ } catch {
1566
+ return "";
1567
+ }
1568
+ };
1423
1569
  var readJson = (file) => {
1424
1570
  try {
1425
1571
  return JSON.parse(fs5.readFileSync(file, "utf8"));
@@ -1427,10 +1573,127 @@ var readJson = (file) => {
1427
1573
  return null;
1428
1574
  }
1429
1575
  };
1430
- var walk = (root, limit = 6e3) => {
1576
+ var attributes = (tag) => Object.fromEntries([...tag.matchAll(/([A-Za-z_][\w:.-]*)\s*=\s*"([^"]*)"/g)].map((match) => [match[1].toLowerCase(), match[2]]));
1577
+ var major = (version) => String(version ?? "").replace(/^[^\d]*/, "").match(/^\d+/)?.[0] ?? null;
1578
+ var escapeRegex = (value) => value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
1579
+ var globRegex = (pattern) => {
1580
+ let source = "";
1581
+ for (let index = 0; index < pattern.length; index += 1) {
1582
+ const character = pattern[index];
1583
+ if (character === "*" && pattern[index + 1] === "*") {
1584
+ source += ".*";
1585
+ index += 1;
1586
+ } else if (character === "*") source += "[^/]*";
1587
+ else if (character === "?") source += "[^/]";
1588
+ else source += escapeRegex(character);
1589
+ }
1590
+ return source;
1591
+ };
1592
+ var parseIgnoreRules = (root) => [".gitignore", ".tfignore", ".ignore"].flatMap((name) => {
1593
+ const file = path5.join(root, name);
1594
+ if (!fs5.existsSync(file) || !fs5.statSync(file).isFile()) return [];
1595
+ return readText(file).split(/\r?\n/).flatMap((line) => {
1596
+ const trimmed = line.trim();
1597
+ if (!trimmed || trimmed.startsWith("#")) return [];
1598
+ const negated = trimmed.startsWith("!");
1599
+ let body = (negated ? trimmed.slice(1) : trimmed).replace(/\\/g, "/");
1600
+ const anchored = body.startsWith("/");
1601
+ if (anchored) body = body.slice(1);
1602
+ const directory = body.endsWith("/");
1603
+ body = body.replace(/\/$/, "");
1604
+ if (!body) return [];
1605
+ const prefix = anchored || body.includes("/") ? "^" : "(?:^|/)";
1606
+ return [{ source: name, negated, regex: new RegExp(`${prefix}${globRegex(body)}${directory ? "(?:/|$)" : "$"}`, "i") }];
1607
+ });
1608
+ });
1609
+ var rootFiles = (root) => fs5.readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isFile()).map((entry) => entry.name).sort();
1610
+ var expandRootGlob = (root, pattern) => {
1611
+ const normalized = slash2(pattern).replace(/^\.\//, "").replace(/\/$/, "");
1612
+ const segments = normalized.split("/");
1613
+ const results = [];
1614
+ const visit = (directory, index) => {
1615
+ if (index === segments.length) {
1616
+ results.push(relative(root, directory) || ".");
1617
+ return;
1618
+ }
1619
+ const segment = segments[index];
1620
+ if (!segment.includes("*") && !segment.includes("?")) {
1621
+ visit(path5.join(directory, segment), index + 1);
1622
+ return;
1623
+ }
1624
+ if (!fs5.existsSync(directory)) return;
1625
+ const matcher = new RegExp(`^${globRegex(segment)}$`);
1626
+ for (const entry of fs5.readdirSync(directory, { withFileTypes: true })) if (entry.isDirectory() && matcher.test(entry.name)) visit(path5.join(directory, entry.name), index + 1);
1627
+ };
1628
+ visit(root, 0);
1629
+ return results.filter((item) => fs5.existsSync(path5.join(root, item)));
1630
+ };
1631
+ var rootContainerMembers = (root) => {
1632
+ const members = [];
1633
+ const evidence = [];
1634
+ const add = (member, source) => {
1635
+ const normalized = slash2(member).replace(/^\.\//, "").replace(/\/$/, "");
1636
+ if (!normalized || normalized.startsWith("../") || path5.isAbsolute(normalized)) return;
1637
+ for (const expanded of normalized.includes("*") || normalized.includes("?") ? expandRootGlob(root, normalized) : [normalized]) {
1638
+ if (!members.includes(expanded)) members.push(expanded);
1639
+ }
1640
+ evidence.push(source);
1641
+ };
1642
+ for (const file of rootFiles(root)) {
1643
+ const absolute = path5.join(root, file);
1644
+ const lower = file.toLowerCase();
1645
+ const content = readText(absolute);
1646
+ if (lower.endsWith(".sln")) for (const match of content.matchAll(/^Project\("[^"]+"\)\s*=\s*"[^"]+",\s*"([^"]+\.(?:cs|fs|vb)proj)"/gmi)) add(path5.dirname(match[1].replace(/\\/g, "/")), file);
1647
+ else if (lower === "package.json") {
1648
+ const manifest = readJson(absolute);
1649
+ const workspaces = Array.isArray(manifest?.workspaces) ? manifest.workspaces : manifest?.workspaces?.packages;
1650
+ for (const member of workspaces ?? []) add(member, "package.json#workspaces");
1651
+ } else if (lower === "cargo.toml") {
1652
+ const workspaceBlock = content.match(/^\s*\[workspace\]\s*$([\s\S]*?)(?=^\s*\[|(?![\s\S]))/m)?.[1] ?? "";
1653
+ const workspace = workspaceBlock.match(/members\s*=\s*\[([\s\S]*?)\]/m)?.[1] ?? "";
1654
+ for (const match of workspace.matchAll(/["']([^"']+)["']/g)) add(match[1], "Cargo.toml#workspace.members");
1655
+ } else if (lower === "go.work") {
1656
+ for (const match of content.matchAll(/^\s*use\s+([^\s()]+)\s*$/gm)) add(match[1], "go.work#use");
1657
+ for (const block of content.matchAll(/^\s*use\s*\(\s*$([\s\S]*?)^\s*\)\s*$/gm)) {
1658
+ for (const line of block[1].split(/\r?\n/)) {
1659
+ const member = line.replace(/\/\/.*$/, "").trim();
1660
+ if (member) add(member, "go.work#use");
1661
+ }
1662
+ }
1663
+ } else if (lower === "pom.xml") for (const match of content.matchAll(/<module>([^<]+)<\/module>/g)) add(match[1], "pom.xml#modules");
1664
+ else if (["settings.gradle", "settings.gradle.kts"].includes(lower)) {
1665
+ for (const statement of content.matchAll(/^\s*(include|includeBuild)\s*(?:\(([^)]*)\)|(.+))$/gm)) {
1666
+ for (const match of (statement[2] ?? statement[3] ?? "").matchAll(/["']:?([^"']+)["']/g)) add(match[1].replace(/:/g, "/"), `${file}#${statement[1]}`);
1667
+ }
1668
+ }
1669
+ }
1670
+ return { members: unique(members), evidence: unique(evidence) };
1671
+ };
1672
+ var INTERNAL_METADATA = /* @__PURE__ */ new Set([".agents", ".codex", ".codex-agent", ".git", ".hg", ".opencode", ".svn"]);
1673
+ var FALLBACK_TRANSIENT = /* @__PURE__ */ new Set([".cache", ".idea", ".tmp", ".venv", ".vs", "bin", "coverage", "dist", "node_modules", "obj", "testresults"]);
1674
+ var inventoryRepository = (root, declaredRoots, limit = 5e4) => {
1675
+ const rules = parseIgnoreRules(root);
1431
1676
  const files = [];
1677
+ const excluded = [];
1678
+ let truncated = false;
1679
+ const declared = unique(declaredRoots.map((item) => slash2(item).replace(/^\.\//, "").replace(/\/$/, "")));
1680
+ const requiredToReachDeclared = (candidate) => declared.some((item) => candidate === item || item.startsWith(`${candidate}/`));
1681
+ const declaredPathPrefixes = (item) => item.split("/").map((_, index, segments) => segments.slice(0, index + 1).join("/"));
1682
+ const ruleWouldHideDeclared = (rule, candidate) => declared.filter((item) => candidate === item || candidate.startsWith(`${item}/`) || item.startsWith(`${candidate}/`)).some((item) => declaredPathPrefixes(item).some((prefix) => rule.regex.test(prefix) || rule.regex.test(`${prefix}/`)));
1683
+ const ignoredByRule = (candidate, directory) => {
1684
+ let ignored = false;
1685
+ let source = null;
1686
+ for (const rule of rules) if ((rule.regex.test(candidate) || directory && rule.regex.test(`${candidate}/`)) && !ruleWouldHideDeclared(rule, candidate)) {
1687
+ ignored = !rule.negated;
1688
+ source = rule.source;
1689
+ }
1690
+ return ignored ? source : null;
1691
+ };
1432
1692
  const visit = (directory) => {
1433
- if (files.length >= limit) return;
1693
+ if (files.length >= limit) {
1694
+ truncated = true;
1695
+ return;
1696
+ }
1434
1697
  let entries = [];
1435
1698
  try {
1436
1699
  entries = fs5.readdirSync(directory, { withFileTypes: true });
@@ -1438,26 +1701,56 @@ var walk = (root, limit = 6e3) => {
1438
1701
  return;
1439
1702
  }
1440
1703
  for (const entry of entries) {
1441
- if (files.length >= limit) break;
1442
- if (entry.isDirectory() && IGNORED_DIRECTORIES.has(entry.name)) continue;
1704
+ if (files.length >= limit) {
1705
+ truncated = true;
1706
+ break;
1707
+ }
1443
1708
  const absolute = path5.join(directory, entry.name);
1444
- if (entry.isDirectory()) visit(absolute);
1445
- else if (entry.isFile()) files.push(absolute);
1709
+ const candidate = relative(root, absolute);
1710
+ if (entry.isDirectory()) {
1711
+ if (INTERNAL_METADATA.has(entry.name.toLowerCase())) continue;
1712
+ const requiredPath = requiredToReachDeclared(candidate);
1713
+ const ignoreSource = ignoredByRule(candidate, true);
1714
+ const fallback = requiredPath ? false : FALLBACK_TRANSIENT.has(entry.name.toLowerCase());
1715
+ if (ignoreSource || fallback) {
1716
+ excluded.push({ path: candidate, role: fallback ? "transient" : "ignored", source: ignoreSource ?? "fallback" });
1717
+ continue;
1718
+ }
1719
+ visit(absolute);
1720
+ } else if (entry.isFile() && !ignoredByRule(candidate, false)) files.push(absolute);
1446
1721
  }
1447
1722
  };
1448
1723
  visit(root);
1449
- return files.sort();
1450
- };
1451
- var dependencyNames = (manifest) => new Set(Object.keys({
1452
- ...manifest?.dependencies ?? {},
1453
- ...manifest?.devDependencies ?? {},
1454
- ...manifest?.peerDependencies ?? {}
1455
- }));
1456
- var packageCommand = (manager, script) => {
1457
- if (manager === "npm" && script === "test") return "npm test";
1458
- if (manager === "yarn") return `yarn ${script}`;
1459
- return `${manager || "npm"} run ${script}`;
1460
- };
1724
+ return { files: files.sort(), excluded, truncated, limit };
1725
+ };
1726
+ var extensionLanguages = /* @__PURE__ */ new Map([
1727
+ [".js", "JavaScript"],
1728
+ [".mjs", "JavaScript"],
1729
+ [".cjs", "JavaScript"],
1730
+ [".jsx", "JavaScript"],
1731
+ [".ts", "TypeScript"],
1732
+ [".tsx", "TypeScript"],
1733
+ [".py", "Python"],
1734
+ [".go", "Go"],
1735
+ [".rs", "Rust"],
1736
+ [".java", "Java"],
1737
+ [".kt", "Kotlin"],
1738
+ [".swift", "Swift"],
1739
+ [".rb", "Ruby"],
1740
+ [".php", "PHP"],
1741
+ [".cs", "C#"],
1742
+ [".fs", "F#"],
1743
+ [".vb", "Visual Basic"],
1744
+ [".cpp", "C++"],
1745
+ [".cc", "C++"],
1746
+ [".c", "C"],
1747
+ [".vue", "Vue"],
1748
+ [".svelte", "Svelte"],
1749
+ [".cshtml", "Razor"],
1750
+ [".razor", "Razor"],
1751
+ [".css", "CSS"],
1752
+ [".scss", "Sass"]
1753
+ ]);
1461
1754
  var classifyNaming = (name) => {
1462
1755
  if (/^[a-z][a-z0-9]*(?:-[a-z0-9]+)+$/.test(name)) return "kebab-case";
1463
1756
  if (/^[a-z][A-Za-z0-9]*$/.test(name) && /[A-Z]/.test(name)) return "camelCase";
@@ -1465,53 +1758,148 @@ var classifyNaming = (name) => {
1465
1758
  if (/^[a-z][a-z0-9]*(?:_[a-z0-9]+)+$/.test(name)) return "snake_case";
1466
1759
  return null;
1467
1760
  };
1468
- var analyzeProject = ({ root }) => {
1761
+ var dependencyTechnologies = /* @__PURE__ */ new Map([
1762
+ ["next", ["Next.js"]],
1763
+ ["react", ["React"]],
1764
+ ["vue", ["Vue"]],
1765
+ ["@angular/core", ["Angular"]],
1766
+ ["svelte", ["Svelte"]],
1767
+ ["@sveltejs/kit", ["SvelteKit"]],
1768
+ ["express", ["Express"]],
1769
+ ["fastify", ["Fastify"]],
1770
+ ["@nestjs/core", ["NestJS"]],
1771
+ ["django", ["Django"]],
1772
+ ["flask", ["Flask"]],
1773
+ ["fastapi", ["FastAPI"]],
1774
+ ["spring-boot-starter-web", ["Spring Boot"]],
1775
+ ["rails", ["Ruby on Rails"]],
1776
+ ["laravel/framework", ["Laravel"]],
1777
+ ["github.com/gin-gonic/gin", ["Gin"]],
1778
+ ["actix-web", ["Actix Web"]],
1779
+ ["axum", ["Axum"]],
1780
+ ["microsoft.aspnet.mvc", ["ASP.NET MVC", "major"]],
1781
+ ["microsoft.aspnet.webapi.core", ["ASP.NET Web API"]],
1782
+ ["entityframework", ["Entity Framework", "major"]],
1783
+ ["unity", ["Unity"]],
1784
+ ["topshelf", ["Topshelf"]],
1785
+ ["serilog", ["Serilog"]],
1786
+ ["jquery", ["jQuery"]],
1787
+ ["bootstrap", ["Bootstrap", "major"]],
1788
+ ["vitest", ["Vitest"]],
1789
+ ["jest", ["Jest"]],
1790
+ ["pytest", ["pytest"]],
1791
+ ["junit", ["JUnit"]],
1792
+ ["xunit", ["xUnit"]]
1793
+ ]);
1794
+ var dependencyRecord = (ecosystem, name, version, evidence) => ({ ecosystem, name: String(name), version: String(version ?? ""), evidence });
1795
+ var parseDependencies = (root, file) => {
1796
+ const absolute = path5.join(root, file);
1797
+ const lower = path5.basename(file).toLowerCase();
1798
+ const content = readText(absolute);
1799
+ const result = [];
1800
+ const add = (ecosystem, name, version) => {
1801
+ if (name) result.push(dependencyRecord(ecosystem, name, version, `${file}#${name}`));
1802
+ };
1803
+ if (["package.json", "composer.json"].includes(lower)) {
1804
+ const manifest = readJson(absolute);
1805
+ for (const field of ["dependencies", "devDependencies", "peerDependencies", "require", "require-dev"]) for (const [name, version] of Object.entries(manifest?.[field] ?? {})) add(lower === "composer.json" ? "composer" : "node", name, version);
1806
+ } else if (lower === "packages.config") for (const match of content.matchAll(/<package\b[^>]*>/gi)) {
1807
+ const item = attributes(match[0]);
1808
+ add("nuget", item.id, item.version);
1809
+ }
1810
+ else if (lower === "requirements.txt") for (const line of content.split(/\r?\n/)) {
1811
+ const match = line.trim().match(/^([A-Za-z0-9_.-]+)\s*(?:==|~=|>=|<=|>|<)?\s*([^;\s#]*)/);
1812
+ if (match) add("python", match[1], match[2]);
1813
+ }
1814
+ else if (lower === "cargo.toml") {
1815
+ let section2 = "";
1816
+ for (const line of content.split(/\r?\n/)) {
1817
+ const header = line.match(/^\s*\[([^\]]+)\]\s*(?:#.*)?$/);
1818
+ if (header) {
1819
+ section2 = header[1].toLowerCase();
1820
+ continue;
1821
+ }
1822
+ if (!/(^|\.)dependencies$/.test(section2)) continue;
1823
+ const match = line.match(/^\s*([A-Za-z0-9_.-]+)\s*=\s*(?:["']([^"']+)["']|\{[^}]*version\s*=\s*["']([^"']+))/);
1824
+ if (match) add("cargo", match[1], match[2] ?? match[3]);
1825
+ }
1826
+ } else if (lower === "pyproject.toml") {
1827
+ let section2 = "";
1828
+ for (const line of content.split(/\r?\n/)) {
1829
+ const header = line.match(/^\s*\[([^\]]+)\]\s*(?:#.*)?$/);
1830
+ if (header) {
1831
+ section2 = header[1].toLowerCase();
1832
+ continue;
1833
+ }
1834
+ if (section2 === "project") {
1835
+ const list = line.match(/^\s*dependencies\s*=\s*\[(.*)\]\s*(?:#.*)?$/);
1836
+ for (const item of list?.[1]?.matchAll(/["']([A-Za-z0-9_.-]+)(?:\[[^\]]+\])?\s*([^"']*)["']/g) ?? []) add("python", item[1], item[2]);
1837
+ } else if (section2 === "tool.poetry.dependencies" || section2.startsWith("tool.poetry.group.") && section2.endsWith(".dependencies")) {
1838
+ const match = line.match(/^\s*([A-Za-z0-9_.-]+)\s*=\s*(?:["']([^"']+)["']|\{[^}]*version\s*=\s*["']([^"']+))/);
1839
+ if (match && match[1].toLowerCase() !== "python") add("python", match[1], match[2] ?? match[3]);
1840
+ }
1841
+ }
1842
+ } else if (lower === "go.mod") for (const match of content.matchAll(/^\s*([A-Za-z0-9_.\-/]+)\s+v([^\s]+)\s*$/gm)) add("go", match[1], match[2]);
1843
+ else if (lower === "pom.xml") for (const match of content.matchAll(/<dependency>[\s\S]*?<artifactId>([^<]+)<\/artifactId>[\s\S]*?(?:<version>([^<]+)<\/version>)?[\s\S]*?<\/dependency>/gi)) add("maven", match[1], match[2]);
1844
+ else if (["build.gradle", "build.gradle.kts"].includes(lower)) for (const match of content.matchAll(/["']([^:"']+):([^:"']+):([^"']+)["']/g)) add("gradle", match[2], match[3]);
1845
+ else if (lower === "gemfile" || lower.endsWith(".gemspec")) for (const match of content.matchAll(/\bgem\s+["']([^"']+)["'](?:\s*,\s*["']([^"']+)["'])?/g)) add("ruby", match[1], match[2]);
1846
+ return result;
1847
+ };
1848
+ var manifestKinds = [
1849
+ { id: "node", names: ["package.json"], toolchain: "Node package tooling" },
1850
+ { id: "dotnet", pattern: /\.(?:cs|fs|vb)proj$/i, toolchain: "MSBuild" },
1851
+ { id: "python", names: ["pyproject.toml", "requirements.txt", "setup.py"], toolchain: "Python packaging" },
1852
+ { id: "jvm", names: ["pom.xml", "build.gradle", "build.gradle.kts"], toolchain: "JVM build tooling" },
1853
+ { id: "go", names: ["go.mod"], toolchain: "Go modules" },
1854
+ { id: "rust", names: ["Cargo.toml"], toolchain: "Cargo" },
1855
+ { id: "php", names: ["composer.json"], toolchain: "Composer" },
1856
+ { id: "ruby", names: ["Gemfile"], pattern: /\.gemspec$/i, toolchain: "Bundler" }
1857
+ ];
1858
+ var manifestKind = (file) => manifestKinds.find((kind) => kind.names?.some((name) => name.toLowerCase() === path5.basename(file).toLowerCase()) || kind.pattern?.test(file));
1859
+ var resolveUnit = (root, manifest, declaredMembers) => {
1860
+ const kind = manifestKind(manifest);
1861
+ if (!kind) return null;
1862
+ const unitPath = path5.posix.dirname(manifest) === "." ? "." : path5.posix.dirname(manifest);
1863
+ const base = path5.basename(manifest).toLowerCase();
1864
+ let name = path5.posix.basename(unitPath === "." ? root : unitPath);
1865
+ if (base === "package.json") name = readJson(path5.join(root, manifest))?.name ?? name;
1866
+ else if (/\.(?:cs|fs|vb)proj$/i.test(manifest)) name = path5.basename(manifest, path5.extname(manifest));
1867
+ else if (base === "go.mod") name = readText(path5.join(root, manifest)).match(/^module\s+(.+)$/m)?.[1] ?? name;
1868
+ return { name, path: unitPath, kind: kind.id, manifest, declared: declaredMembers.includes(unitPath), evidence: [manifest] };
1869
+ };
1870
+ var belongsToUnit = (file, unit) => unit.path === "." || file === unit.path || file.startsWith(`${unit.path}/`);
1871
+ var isVendoredAsset = (file) => /(^|\/)(?:vendor|third[_-]?party|node_modules)(\/|$)|\.min\.(?:js|css)$/i.test(file) || /(^|\/)scripts\/(?:tinymce|bootstrap|jquery|modernizr|respond)(\/|$)/i.test(file) || /(^|\/)content\/(?:bootstrap|tinymce)(\/|$)/i.test(file);
1872
+ var targetFramework = (value) => {
1873
+ const target = String(value ?? "").trim().replace(/^v/i, "");
1874
+ if (/^\d+(?:\.\d+)+$/.test(target)) return `.NET Framework ${target}`;
1875
+ const legacy = target.match(/^net(\d)(\d)(\d?)$/i);
1876
+ if (legacy && Number(legacy[1]) <= 4) return `.NET Framework ${legacy[1]}.${legacy[2]}${legacy[3] ? `.${legacy[3]}` : ""}`;
1877
+ const standard = target.match(/^netstandard(\d+)\.(\d+)/i);
1878
+ if (standard) return `.NET Standard ${standard[1]}.${standard[2]}`;
1879
+ const modern = target.match(/^net(\d+)(?:\.(\d+))?/i);
1880
+ return modern ? `.NET ${modern[1]}${modern[2] ? `.${modern[2]}` : ""}` : null;
1881
+ };
1882
+ var scriptCommand = (manager, name) => {
1883
+ if (manager === "npm" && name === "test") return "npm test";
1884
+ if (manager === "yarn") return `yarn ${name}`;
1885
+ return `${manager} run ${name}`;
1886
+ };
1887
+ var detectorRegistry = Object.freeze(manifestKinds.map((detector) => Object.freeze({ ...detector })));
1888
+ var analyzeRepository = ({ root, scanLimit = 5e4 }) => {
1889
+ if (!Number.isSafeInteger(scanLimit) || scanLimit < 1) throw new Error("scanLimit must be a positive integer");
1469
1890
  const requestedRoot = path5.resolve(root);
1470
1891
  if (!fs5.existsSync(requestedRoot)) throw new Error(`Project root not found: ${requestedRoot}`);
1471
1892
  const projectRoot = fs5.realpathSync(requestedRoot);
1472
- const files = walk(projectRoot);
1473
- const paths = files.map((file) => relative(projectRoot, file));
1893
+ const container = rootContainerMembers(projectRoot);
1894
+ const inventory = inventoryRepository(projectRoot, container.members, scanLimit);
1895
+ const paths = inventory.files.map((file) => relative(projectRoot, file));
1474
1896
  const pathSet = new Set(paths);
1475
- const manifestPath = path5.join(projectRoot, "package.json");
1476
- const manifest = fs5.existsSync(manifestPath) ? readJson(manifestPath) : null;
1477
- const dependencies = dependencyNames(manifest);
1478
- const lockManagers = [
1479
- ["pnpm-lock.yaml", "pnpm"],
1480
- ["yarn.lock", "yarn"],
1481
- ["bun.lockb", "bun"],
1482
- ["bun.lock", "bun"],
1483
- ["package-lock.json", "npm"]
1484
- ].filter(([file]) => pathSet.has(file));
1485
- const declaredManager = typeof manifest?.packageManager === "string" ? manifest.packageManager.split("@")[0] : null;
1486
- const manager = declaredManager || lockManagers[0]?.[1] || (manifest ? "npm" : null);
1487
- const managerEvidence = [
1488
- ...declaredManager ? ["package.json#packageManager"] : [],
1489
- ...lockManagers.filter(([, name]) => !declaredManager || name === declaredManager).map(([file]) => file),
1490
- ...!declaredManager && !lockManagers.length && manifest ? ["package.json"] : []
1491
- ];
1492
- const extensionLanguages = /* @__PURE__ */ new Map([
1493
- [".js", "JavaScript"],
1494
- [".mjs", "JavaScript"],
1495
- [".cjs", "JavaScript"],
1496
- [".jsx", "JavaScript"],
1497
- [".ts", "TypeScript"],
1498
- [".tsx", "TypeScript"],
1499
- [".py", "Python"],
1500
- [".go", "Go"],
1501
- [".rs", "Rust"],
1502
- [".java", "Java"],
1503
- [".kt", "Kotlin"],
1504
- [".swift", "Swift"],
1505
- [".rb", "Ruby"],
1506
- [".php", "PHP"],
1507
- [".cs", "C#"],
1508
- [".cpp", "C++"],
1509
- [".c", "C"],
1510
- [".vue", "Vue"],
1511
- [".svelte", "Svelte"]
1512
- ]);
1897
+ const manifests = paths.filter((file) => manifestKind(file));
1898
+ const units = manifests.map((file) => resolveUnit(projectRoot, file, container.members)).filter(Boolean).filter((unit) => container.members.length === 0 || unit.declared || unit.path === ".").filter((unit, index, all) => all.findIndex((candidate) => candidate.path === unit.path && candidate.kind === unit.kind) === index).sort((left, right) => left.path.localeCompare(right.path));
1899
+ const unitFiles = paths.filter((file) => units.length === 0 || units.some((unit) => belongsToUnit(file, unit)));
1900
+ const ownedFiles = unitFiles.filter((file) => !isVendoredAsset(file));
1513
1901
  const languageEvidence = /* @__PURE__ */ new Map();
1514
- for (const file of paths) {
1902
+ for (const file of ownedFiles) {
1515
1903
  const language = extensionLanguages.get(path5.extname(file).toLowerCase());
1516
1904
  if (!language) continue;
1517
1905
  const evidence = languageEvidence.get(language) ?? [];
@@ -1519,58 +1907,77 @@ var analyzeProject = ({ root }) => {
1519
1907
  languageEvidence.set(language, evidence);
1520
1908
  }
1521
1909
  const languages = [...languageEvidence.keys()].sort();
1522
- const frameworkPackages = /* @__PURE__ */ new Map([
1523
- ["next", "Next.js"],
1524
- ["react", "React"],
1525
- ["vue", "Vue"],
1526
- ["@angular/core", "Angular"],
1527
- ["svelte", "Svelte"],
1528
- ["@sveltejs/kit", "SvelteKit"],
1529
- ["express", "Express"],
1530
- ["fastify", "Fastify"],
1531
- ["nestjs", "NestJS"],
1532
- ["@nestjs/core", "NestJS"],
1533
- ["vitest", "Vitest"],
1534
- ["jest", "Jest"],
1535
- ["playwright", "Playwright"],
1536
- ["@playwright/test", "Playwright"],
1537
- ["cypress", "Cypress"]
1538
- ]);
1539
- const frameworks = unique([...frameworkPackages].filter(([name]) => dependencies.has(name)).map(([, label]) => label));
1540
- const frameworkEvidence = [...frameworkPackages].filter(([name]) => dependencies.has(name)).map(([name]) => `package.json#${name}`);
1541
- const scripts = manifest?.scripts ?? {};
1542
- const commandOrder = ["install", "setup", "dev", "start", "build", "lint", "typecheck", "check", "test", "test:unit", "test:e2e"];
1543
- const commands = [];
1544
- for (const name of commandOrder) {
1545
- if (typeof scripts[name] === "string") commands.push({ name, command: packageCommand(manager, name), source: `package.json#scripts.${name}` });
1910
+ const toolchainMap = /* @__PURE__ */ new Map();
1911
+ for (const unit of units) {
1912
+ const detector = manifestKinds.find((item) => item.id === unit.kind);
1913
+ const current = toolchainMap.get(detector.toolchain) ?? [];
1914
+ current.push(unit.manifest);
1915
+ toolchainMap.set(detector.toolchain, current);
1916
+ }
1917
+ const lockToolchains = [
1918
+ ["pnpm-lock.yaml", "pnpm"],
1919
+ ["yarn.lock", "Yarn"],
1920
+ ["bun.lock", "Bun"],
1921
+ ["bun.lockb", "Bun"],
1922
+ ["package-lock.json", "npm"],
1923
+ ["uv.lock", "uv"],
1924
+ ["poetry.lock", "Poetry"],
1925
+ ["Pipfile.lock", "Pipenv"],
1926
+ ["Cargo.lock", "Cargo"],
1927
+ ["go.sum", "Go modules"],
1928
+ ["composer.lock", "Composer"],
1929
+ ["Gemfile.lock", "Bundler"]
1930
+ ];
1931
+ for (const [file, name] of lockToolchains) if (pathSet.has(file)) toolchainMap.set(name, unique([...toolchainMap.get(name) ?? [], file]));
1932
+ const packagesConfig = ownedFiles.find((file) => path5.basename(file).toLowerCase() === "packages.config");
1933
+ if (packagesConfig) toolchainMap.set("NuGet", unique([...toolchainMap.get("NuGet") ?? [], packagesConfig]));
1934
+ const rootPackage = pathSet.has("package.json") ? readJson(path5.join(projectRoot, "package.json")) : null;
1935
+ if (rootPackage?.packageManager) toolchainMap.set(rootPackage.packageManager.split("@")[0], unique([...toolchainMap.get(rootPackage.packageManager.split("@")[0]) ?? [], "package.json#packageManager"]));
1936
+ const toolchains = [...toolchainMap].map(([name, evidence]) => ({ name, evidence: unique(evidence) })).sort((left, right) => left.name.localeCompare(right.name));
1937
+ const dependencyManifests = ownedFiles.filter((file) => ["package.json", "composer.json", "packages.config", "requirements.txt", "cargo.toml", "pyproject.toml", "go.mod", "pom.xml", "build.gradle", "build.gradle.kts", "gemfile"].includes(path5.basename(file).toLowerCase()) || /\.gemspec$/i.test(file));
1938
+ const dependencies = dependencyManifests.flatMap((file) => parseDependencies(projectRoot, file));
1939
+ const technologyMap = /* @__PURE__ */ new Map();
1940
+ const addTechnology = (name, evidence) => {
1941
+ const versioned = name.match(/^(.*)\s+\d+(?:\.\d+)*$/);
1942
+ if (versioned) technologyMap.delete(versioned[1]);
1943
+ else if ([...technologyMap.keys()].some((item) => item.startsWith(`${name} `) && /^\d/.test(item.slice(name.length + 1)))) return;
1944
+ technologyMap.set(name, unique([...technologyMap.get(name) ?? [], evidence]));
1945
+ };
1946
+ for (const dependency of dependencies) {
1947
+ const mapping = dependencyTechnologies.get(dependency.name.toLowerCase());
1948
+ if (!mapping) continue;
1949
+ const label = mapping[1] === "major" && major(dependency.version) ? `${mapping[0]} ${major(dependency.version)}` : mapping[0];
1950
+ addTechnology(label, dependency.evidence);
1951
+ }
1952
+ for (const file of unitFiles) {
1953
+ if (/(^|\/)jquery-\d+(?:\.\d+)+(?:\.min)?\.js$/i.test(file)) addTechnology("jQuery", file);
1954
+ if (/(^|\/)bootstrap(?:\.min)?\.(?:css|js)$/i.test(file)) {
1955
+ const detectedMajor = readText(path5.join(projectRoot, file)).match(/\bBootstrap\s+v(\d+)/i)?.[1];
1956
+ addTechnology(detectedMajor ? `Bootstrap ${detectedMajor}` : "Bootstrap", file);
1957
+ }
1546
1958
  }
1547
- for (const name of Object.keys(scripts).sort()) {
1548
- if (!commands.some((item) => item.name === name) && /^(build|lint|typecheck|test|check)(:|$)/.test(name)) {
1549
- commands.push({ name, command: packageCommand(manager, name), source: `package.json#scripts.${name}` });
1959
+ for (const unit of units.filter((item) => item.kind === "dotnet")) {
1960
+ const content = readText(path5.join(projectRoot, unit.manifest));
1961
+ for (const match of content.matchAll(/<(TargetFrameworkVersion|TargetFramework|TargetFrameworks)>([^<]+)<\/\1>/gi)) for (const value of match[2].split(";")) {
1962
+ const label = targetFramework(value);
1963
+ if (label) addTechnology(label, `${unit.manifest}#${match[1]}`);
1550
1964
  }
1551
1965
  }
1966
+ const technologies = [...technologyMap].map(([name, evidence]) => ({ name, evidence: unique(evidence) })).sort((left, right) => left.name.localeCompare(right.name));
1967
+ const commands = [];
1968
+ const scripts = rootPackage?.scripts ?? {};
1969
+ for (const name of Object.keys(scripts).sort()) if (/^(?:build|check|dev|install|lint|setup|start|test|typecheck)(?::|$)/.test(name)) {
1970
+ const manager = rootPackage.packageManager?.split("@")[0] ?? (pathSet.has("pnpm-lock.yaml") ? "pnpm" : pathSet.has("yarn.lock") ? "yarn" : "npm");
1971
+ commands.push({ name, command: scriptCommand(manager, name), source: `package.json#scripts.${name}` });
1972
+ }
1552
1973
  const entrypoints = [];
1553
- for (const [field, value] of [["main", manifest?.main], ["module", manifest?.module]]) {
1554
- if (typeof value === "string") entrypoints.push({ path: value, source: `package.json#${field}` });
1555
- }
1556
- if (typeof manifest?.bin === "string") entrypoints.push({ path: manifest.bin, source: "package.json#bin" });
1557
- else for (const value of Object.values(manifest?.bin ?? {})) if (typeof value === "string") entrypoints.push({ path: value, source: "package.json#bin" });
1558
- for (const candidate of ["src/index.ts", "src/index.js", "src/main.ts", "src/main.js", "app/page.tsx", "app/page.jsx", "main.go", "Cargo.toml"]) {
1559
- if (pathSet.has(candidate) && !entrypoints.some((item) => item.path === candidate)) entrypoints.push({ path: candidate, source: candidate });
1560
- }
1561
- const modules = [];
1562
- const moduleRoots = unique(paths.filter((file) => extensionLanguages.has(path5.extname(file).toLowerCase())).map((file) => file.split("/")[0]).filter((name) => name && !name.startsWith(".")));
1563
- for (const name of moduleRoots.slice(0, 20)) {
1564
- const evidence = paths.filter((file) => file.startsWith(`${name}/`) && extensionLanguages.has(path5.extname(file).toLowerCase())).slice(0, 3);
1565
- modules.push({ name, path: name, evidence });
1566
- }
1567
- const testFiles = paths.filter((file) => /(^|\/)(__tests__\/|tests?\/|[^/]+\.(?:test|spec)\.[^.]+$)/.test(file));
1568
- const testConfigs = paths.filter((file) => /(^|\/)(vitest|jest|playwright|cypress)[^/]*\.(?:js|mjs|cjs|ts|json)$/.test(file));
1569
- const ciFiles = paths.filter((file) => file.startsWith(".github/workflows/") || [".gitlab-ci.yml", "Jenkinsfile", "azure-pipelines.yml"].includes(file));
1570
- const deploymentFiles = paths.filter((file) => /(^|\/)(Dockerfile|docker-compose\.ya?ml|vercel\.json|netlify\.toml|fly\.toml)$/.test(file));
1571
- const sourceFiles = paths.filter((file) => extensionLanguages.has(path5.extname(file).toLowerCase()));
1974
+ for (const [field, value] of [["main", rootPackage?.main], ["module", rootPackage?.module]]) if (typeof value === "string") entrypoints.push({ path: value, source: `package.json#${field}` });
1975
+ for (const file of ownedFiles.filter((item) => /(^|\/)(?:program\.cs|main\.(?:go|rs|py|js|ts)|global\.asax|app\/page\.(?:jsx?|tsx?))$/i.test(item)).slice(0, 30)) entrypoints.push({ path: file, source: file });
1976
+ const testUnits = units.filter((unit) => /(^|[._-])tests?([._-]|$)/i.test(unit.name) || /(^|\/)tests?([/_-]|$)/i.test(unit.path));
1977
+ const testFiles = ownedFiles.filter((file) => extensionLanguages.has(path5.extname(file).toLowerCase())).filter((file) => testUnits.some((unit) => belongsToUnit(file, unit)) || /(^|\/)(?:__tests__\/|tests?\/|[^/]+\.(?:test|spec)\.[^.]+$)/i.test(file)).filter((file) => !isVendoredAsset(file)).slice(0, 40);
1978
+ const testConfigs = paths.filter((file) => /(^|\/)(?:vitest|jest|playwright|cypress)[^/]*\.(?:js|mjs|cjs|ts|json)$/i.test(file)).slice(0, 20);
1572
1979
  const namingEvidence = /* @__PURE__ */ new Map();
1573
- for (const file of sourceFiles) {
1980
+ for (const file of ownedFiles.filter((item) => extensionLanguages.has(path5.extname(item).toLowerCase()))) {
1574
1981
  const style = classifyNaming(path5.basename(file, path5.extname(file)));
1575
1982
  if (!style) continue;
1576
1983
  const evidence = namingEvidence.get(style) ?? [];
@@ -1578,48 +1985,102 @@ var analyzeProject = ({ root }) => {
1578
1985
  namingEvidence.set(style, evidence);
1579
1986
  }
1580
1987
  const naming = [...namingEvidence].filter(([, evidence]) => evidence.length >= 3).sort((left, right) => right[1].length - left[1].length)[0];
1581
- const sourceRoots = ["src", "app", "lib", "packages", "apps", "services"].filter((directory) => paths.some((file) => file.startsWith(`${directory}/`)));
1582
- const securityEvidence = paths.filter(
1583
- (file) => !/^(?:\.agents\/context|\.codex-agent\/context|docs|templates)\//.test(file) && /(^|\/)(auth|security|permissions?|secrets?|\.env\.example)(\/|\.|$)/i.test(file)
1584
- ).slice(0, 20);
1585
- const boundaryDirectories = {
1586
- api: ["api", "routes", "controllers"],
1587
- ui: ["components", "views", "pages", "app"],
1588
- persistence: ["db", "database", "models", "repositories", "migrations"]
1589
- };
1590
- const boundaries = Object.fromEntries(Object.entries(boundaryDirectories).map(([kind, candidates]) => {
1591
- const found = candidates.filter((candidate) => paths.some((file) => file.split("/").includes(candidate)));
1988
+ const ownedRoots = unique(units.map((unit) => unit.path === "." ? "." : unit.path.split("/")[0]));
1989
+ const securityEvidence = ownedFiles.filter((file) => /(^|\/)(?:auth|security|permissions?|secrets?|\.env\.example)(\/|\.|$)/i.test(file)).slice(0, 20);
1990
+ const boundaryCandidates = { api: ["api", "routes", "controllers"], ui: ["components", "views", "pages", "app"], persistence: ["db", "database", "models", "repositories", "migrations"] };
1991
+ const boundaries = Object.fromEntries(Object.entries(boundaryCandidates).map(([kind, candidates]) => {
1992
+ const found = unique(ownedFiles.flatMap((file) => {
1993
+ const segments = file.split("/");
1994
+ const index = segments.findIndex((segment) => candidates.includes(segment.toLowerCase()));
1995
+ return index < 0 ? [] : [segments.slice(0, index + 1).join("/")];
1996
+ })).sort();
1592
1997
  return [kind, found.length ? detected(found, found.map((item) => `directory:${item}`), "medium") : unknown([])];
1593
1998
  }));
1999
+ const ciFiles = paths.filter((file) => file.startsWith(".github/workflows/") || [".gitlab-ci.yml", "Jenkinsfile", "azure-pipelines.yml"].includes(file));
2000
+ const deploymentFiles = paths.filter((file) => /(^|\/)(?:Dockerfile|docker-compose\.ya?ml|vercel\.json|netlify\.toml|fly\.toml)$/i.test(file));
2001
+ const rootSolution = rootFiles(projectRoot).find((file) => /\.sln$/i.test(file));
2002
+ const projectName = rootPackage?.name ?? (rootSolution ? path5.basename(rootSolution, path5.extname(rootSolution)) : path5.basename(projectRoot));
2003
+ const projectEvidence = rootPackage?.name ? ["package.json#name"] : rootSolution ? [rootSolution] : ["repository directory name"];
1594
2004
  return {
1595
2005
  $schema: "project-analysis.schema.json",
1596
2006
  version: ANALYSIS_VERSION,
1597
2007
  root: projectRoot,
1598
- project: manifest?.name ? detected({ name: manifest.name, private: Boolean(manifest.private) }, ["package.json#name"]) : inferred({ name: path5.basename(projectRoot) }, ["repository directory name"], "low"),
1599
- packageManager: manager ? declaredManager || lockManagers.length ? detected(manager, managerEvidence, "high") : inferred(manager, managerEvidence, "low") : unknown(null),
2008
+ project: rootPackage?.name || rootSolution ? detected({ name: projectName, private: Boolean(rootPackage?.private) }, projectEvidence) : inferred({ name: projectName }, projectEvidence, "low"),
1600
2009
  languages: languages.length ? detected(languages, [...languageEvidence.values()].flat(), "high") : unknown([]),
1601
- frameworks: frameworks.length ? detected(frameworks, frameworkEvidence, "high") : unknown([]),
2010
+ toolchains: toolchains.length ? detected(toolchains, toolchains.flatMap((item) => item.evidence), "high") : unknown([]),
2011
+ technologies: technologies.length ? detected(technologies, technologies.flatMap((item) => item.evidence), "high") : unknown([]),
1602
2012
  commands: commands.length ? detected(commands, commands.map((item) => item.source), "high") : unknown([]),
1603
- modules: modules.length ? inferred(modules, modules.flatMap((item) => item.evidence), modules.length > 1 ? "medium" : "low") : unknown([]),
2013
+ units: units.length ? detected(units, unique([...container.evidence, ...units.flatMap((item) => item.evidence)]), container.evidence.length ? "high" : "medium") : unknown([]),
1604
2014
  entrypoints: entrypoints.length ? detected(entrypoints, entrypoints.map((item) => item.source), "high") : unknown([]),
1605
2015
  conventions: {
1606
- sourceLayout: sourceRoots.length ? detected(sourceRoots, sourceRoots.map((item) => `directory:${item}`), "high") : unknown([]),
2016
+ sourceLayout: ownedRoots.length ? detected(ownedRoots, units.flatMap((item) => item.evidence), "high") : unknown([]),
1607
2017
  fileNaming: naming ? inferred(naming[0], naming[1].slice(0, 8), "medium") : unknown(null),
1608
2018
  boundaries
1609
2019
  },
1610
2020
  security: securityEvidence.length ? detected(securityEvidence, securityEvidence, "medium") : unknown([]),
1611
- testing: testFiles.length || testConfigs.length ? detected({ files: testFiles.slice(0, 30), configs: testConfigs }, [...testFiles.slice(0, 10), ...testConfigs], "high") : unknown({ files: [], configs: [] }),
2021
+ testing: testFiles.length || testConfigs.length || testUnits.length ? detected({ units: testUnits.map((unit) => unit.path), files: testFiles, configs: testConfigs }, unique([...testUnits.flatMap((unit) => unit.evidence), ...testFiles.slice(0, 10), ...testConfigs]), "high") : unknown({ units: [], files: [], configs: [] }),
1612
2022
  ciCd: ciFiles.length || deploymentFiles.length ? detected({ ci: ciFiles, deployment: deploymentFiles }, [...ciFiles, ...deploymentFiles], "high") : unknown({ ci: [], deployment: [] }),
1613
- existingGuidance: pathSet.has("AGENTS.md") ? detected(true, ["AGENTS.md"], "high") : detected(false, ["AGENTS.md not found"], "high")
2023
+ scan: detected({ files: paths.length, truncated: inventory.truncated, limit: inventory.limit, excluded: inventory.excluded.slice(0, 100) }, unique(["repository directory name", ...parseIgnoreRules(projectRoot).map((rule) => rule.source), ...container.evidence]), inventory.truncated ? "medium" : "high"),
2024
+ existingGuidance: fs5.existsSync(path5.join(projectRoot, "AGENTS.md")) ? detected(true, ["AGENTS.md"], "high") : detected(false, ["AGENTS.md not found"], "high")
1614
2025
  };
1615
2026
  };
2027
+
2028
+ // ../../plugins/codex-agent/scripts/context-project.mjs
2029
+ var CONTEXT_LIFECYCLE_VERSION = 1;
2030
+ var HASH_PATTERN2 = /^[a-f0-9]{64}$/;
2031
+ var LIFECYCLE_PHASES = /* @__PURE__ */ new Set(["prepared", "migration-applied", "managed-running", "managed-applied"]);
2032
+ var MANAGED_CONTEXT = [
2033
+ ["architecture", "architecture/system.md", "System architecture, project units, entrypoints, and detected boundaries.", ["architecture", "units", "entrypoints"], "high", ["units", "entrypoints", "conventions.boundaries"]],
2034
+ ["code-quality", "standards/code-quality.md", "Detected source layout, naming, and engineering conventions.", ["code", "quality", "conventions"], "critical", ["conventions", "languages"]],
2035
+ ["testing", "standards/testing.md", "Detected test tooling, locations, and repository commands.", ["test", "verification", "commands"], "high", ["testing", "commands"]],
2036
+ ["security", "standards/security.md", "Detected security-sensitive boundaries and baseline safeguards.", ["security", "auth", "secrets"], "critical", ["security"]],
2037
+ ["project-intelligence", "project-intelligence/project.md", "Detected stack, toolchains, CI, and project intelligence.", ["project", "stack", "ci"], "medium", ["project", "languages", "toolchains", "technologies", "ciCd", "scan"]]
2038
+ ];
2039
+ var CODEX_AGENT_IGNORE_RULES = [
2040
+ ".codex-agent/analysis.json",
2041
+ ".codex-agent/sessions/",
2042
+ ".codex-agent/backups/",
2043
+ ".codex-agent/.locks/",
2044
+ ".codex-agent/.transactions/",
2045
+ ".codex-agent/**/*.tmp-*"
2046
+ ];
2047
+ var BROAD_CODEX_AGENT_IGNORE_RULES = /* @__PURE__ */ new Set([
2048
+ ".codex-agent",
2049
+ ".codex-agent/",
2050
+ ".codex-agent/**",
2051
+ "/.codex-agent",
2052
+ "/.codex-agent/",
2053
+ "/.codex-agent/**"
2054
+ ]);
2055
+ var MANAGED_CODEX_AGENT_IGNORE_RULES = new Set(CODEX_AGENT_IGNORE_RULES);
2056
+ var EXCLUDABLE_MANAGED_FILES = /* @__PURE__ */ new Set([".codex/config.toml", ".gitignore"]);
2057
+ var slash3 = (value) => value.split(path6.sep).join("/");
2058
+ var unique2 = (items) => [...new Set(items.filter(Boolean))];
2059
+ var relative2 = (root, file) => slash3(path6.relative(root, file));
2060
+ var normalizeManagedExclusions = (items = []) => {
2061
+ if (!Array.isArray(items)) throw new Error("Managed exclusions must be an array");
2062
+ const normalized = unique2(items.map((item) => safeRelativePath(item, "Managed exclusion"))).sort();
2063
+ const unsupported = normalized.filter((item) => !EXCLUDABLE_MANAGED_FILES.has(item));
2064
+ if (unsupported.length) {
2065
+ throw new Error(`Unsupported managed exclusion: ${unsupported.join(", ")}. Supported paths: ${[...EXCLUDABLE_MANAGED_FILES].sort().join(", ")}`);
2066
+ }
2067
+ return normalized;
2068
+ };
2069
+ var readJson2 = (file) => {
2070
+ try {
2071
+ return JSON.parse(fs6.readFileSync(file, "utf8"));
2072
+ } catch {
2073
+ return null;
2074
+ }
2075
+ };
2076
+ var analyzeProject = analyzeRepository;
1616
2077
  var isSignal = (value) => value && typeof value === "object" && "value" in value && Array.isArray(value.evidence);
1617
2078
  var validateProjectAnalysis = (analysis) => {
1618
2079
  const errors = [];
1619
2080
  if (!analysis || typeof analysis !== "object" || Array.isArray(analysis)) return { ok: false, errors: ["analysis must be an object"] };
1620
2081
  if (analysis.version !== ANALYSIS_VERSION) errors.push(`version must be ${ANALYSIS_VERSION}`);
1621
2082
  if (typeof analysis.root !== "string" || !analysis.root) errors.push("root must be a non-empty string");
1622
- for (const field of ["project", "packageManager", "languages", "frameworks", "commands", "modules", "entrypoints", "security", "testing", "ciCd", "existingGuidance"]) {
2083
+ for (const field of ["project", "languages", "toolchains", "technologies", "commands", "units", "entrypoints", "security", "testing", "ciCd", "scan", "existingGuidance"]) {
1623
2084
  if (!isSignal(analysis[field])) errors.push(`${field} must contain value, evidence, confidence, and status`);
1624
2085
  }
1625
2086
  if (!analysis.conventions || typeof analysis.conventions !== "object") errors.push("conventions must be an object");
@@ -1645,7 +2106,7 @@ var validateProjectAnalysis = (analysis) => {
1645
2106
  return { ok: errors.length === 0, errors };
1646
2107
  };
1647
2108
  var validateAnalysisEvidence = (analysis, root) => {
1648
- const projectRoot = path5.resolve(root);
2109
+ const projectRoot = path6.resolve(root);
1649
2110
  const errors = [];
1650
2111
  const visit = (value, location) => {
1651
2112
  if (isSignal(value)) {
@@ -1654,17 +2115,17 @@ var validateAnalysisEvidence = (analysis, root) => {
1654
2115
  if (item === "repository directory name") continue;
1655
2116
  const missing = item.endsWith(" not found");
1656
2117
  const raw = (missing ? item.slice(0, -10) : item).replace(/^directory:/, "").split("#")[0];
1657
- if (!raw || path5.isAbsolute(raw)) {
2118
+ if (!raw || path6.isAbsolute(raw)) {
1658
2119
  errors.push(`${location} has invalid evidence: ${item}`);
1659
2120
  continue;
1660
2121
  }
1661
- const target = path5.resolve(projectRoot, raw);
1662
- if (target !== projectRoot && !target.startsWith(`${projectRoot}${path5.sep}`)) {
2122
+ const target = path6.resolve(projectRoot, raw);
2123
+ if (target !== projectRoot && !target.startsWith(`${projectRoot}${path6.sep}`)) {
1663
2124
  errors.push(`${location} evidence escapes the project: ${item}`);
1664
2125
  } else {
1665
2126
  try {
1666
2127
  assertNoSymlink(projectRoot, target, `${location} evidence`);
1667
- if (missing ? fs5.existsSync(target) : !fs5.existsSync(target)) {
2128
+ if (missing ? fs6.existsSync(target) : !fs6.existsSync(target)) {
1668
2129
  errors.push(`${location} evidence does not match the repository: ${item}`);
1669
2130
  }
1670
2131
  } catch (error) {
@@ -1738,6 +2199,10 @@ var renderAgents = (analysis) => {
1738
2199
  if (present(analysis.commands)) {
1739
2200
  parts.push(section("Repository commands", bullets(analysis.commands.value.map((item) => `${mdCode(item.command)} \u2014 ${safeText(item.name)} (${safeText(item.source)})`))));
1740
2201
  }
2202
+ const stack = [];
2203
+ if (present(analysis.languages)) stack.push(`Languages: ${analysis.languages.value.join(", ")}.${evidenceSuffix(analysis.languages)}`);
2204
+ if (present(analysis.technologies)) stack.push(`Technologies: ${analysis.technologies.value.map((item) => safeText(item.name)).join(", ")}.${evidenceSuffix(analysis.technologies)}`);
2205
+ if (stack.length) parts.push(section("Detected stack", bullets(stack)));
1741
2206
  const conventions = [];
1742
2207
  if (present(analysis.conventions?.sourceLayout)) conventions.push(`Source roots: ${analysis.conventions.sourceLayout.value.map((item) => mdCode(`${item}/`)).join(", ")}.${evidenceSuffix(analysis.conventions.sourceLayout)}`);
1743
2208
  if (present(analysis.conventions?.fileNaming)) conventions.push(`Observed file naming: ${mdCode(analysis.conventions.fileNaming.value)}. Apply it only where nearby files confirm the pattern.${evidenceSuffix(analysis.conventions.fileNaming)}`);
@@ -1755,7 +2220,7 @@ var renderAgents = (analysis) => {
1755
2220
  };
1756
2221
  var renderArchitecture = (analysis) => {
1757
2222
  const parts = [];
1758
- if (present(analysis.modules)) parts.push(section("Observed modules", bullets(analysis.modules.value.map((item) => `${mdCode(`${item.path}/`)} (${item.evidence.slice(0, 2).map(safeText).join(", ")})`))));
2223
+ if (present(analysis.units)) parts.push(section("Project units", bullets(analysis.units.value.map((item) => `${mdCode(item.path === "." ? "." : `${item.path}/`)} \u2014 ${safeText(item.name)} (${safeText(item.kind)}; ${item.evidence.slice(0, 2).map(safeText).join(", ")})`))));
1759
2224
  if (present(analysis.entrypoints)) parts.push(section("Entrypoints", bullets(analysis.entrypoints.value.map((item) => `${mdCode(item.path)} from ${safeText(item.source)}`))));
1760
2225
  const boundaries = Object.entries(analysis.conventions?.boundaries ?? {}).filter(([, item]) => present(item));
1761
2226
  if (boundaries.length) parts.push(section("Observed boundaries", bullets(boundaries.map(([kind, item]) => `${kind}: ${item.value.map((value) => mdCode(`${value}/`)).join(", ")}${evidenceSuffix(item)}`))));
@@ -1774,6 +2239,7 @@ var renderTesting = (analysis) => {
1774
2239
  if (testCommands.length) parts.push(section("Commands", bullets(testCommands.map((item) => `${mdCode(item.command)} (${safeText(item.source)})`))));
1775
2240
  }
1776
2241
  if (present(analysis.testing)) {
2242
+ if (analysis.testing.value.units?.length) parts.push(section("Test units", bullets(analysis.testing.value.units.map(mdCode))));
1777
2243
  if (analysis.testing.value.configs.length) parts.push(section("Configuration", bullets(analysis.testing.value.configs.map(mdCode))));
1778
2244
  if (analysis.testing.value.files.length) parts.push(section("Observed tests", bullets(analysis.testing.value.files.slice(0, 20).map(mdCode))));
1779
2245
  }
@@ -1790,15 +2256,16 @@ var renderSecurity = (analysis) => {
1790
2256
  };
1791
2257
  var renderProject = (analysis) => {
1792
2258
  const stack = [];
1793
- if (present(analysis.packageManager)) stack.push(`Package manager: ${mdCode(analysis.packageManager.value)}.${evidenceSuffix(analysis.packageManager)}`);
1794
2259
  if (present(analysis.languages)) stack.push(`Languages: ${analysis.languages.value.join(", ")}.${evidenceSuffix(analysis.languages)}`);
1795
- if (present(analysis.frameworks)) stack.push(`Frameworks/tooling: ${analysis.frameworks.value.join(", ")}.${evidenceSuffix(analysis.frameworks)}`);
2260
+ if (present(analysis.toolchains)) stack.push(`Toolchains: ${analysis.toolchains.value.map((item) => safeText(item.name)).join(", ")}.${evidenceSuffix(analysis.toolchains)}`);
2261
+ if (present(analysis.technologies)) stack.push(`Technologies: ${analysis.technologies.value.map((item) => safeText(item.name)).join(", ")}.${evidenceSuffix(analysis.technologies)}`);
1796
2262
  const parts = [];
1797
2263
  if (stack.length) parts.push(section("Detected stack", bullets(stack)));
1798
2264
  if (present(analysis.ciCd)) {
1799
2265
  const items = [...analysis.ciCd.value.ci.map((item) => `CI: ${mdCode(item)}`), ...analysis.ciCd.value.deployment.map((item) => `Deployment: ${mdCode(item)}`)];
1800
2266
  parts.push(section("CI and deployment", bullets(items)));
1801
2267
  }
2268
+ if (present(analysis.scan) && analysis.scan.value.truncated) parts.push(section("Discovery limits", `Repository scan reached its ${analysis.scan.value.limit}-file budget; review a refined analysis before applying context.`));
1802
2269
  return parts.join("\n\n") || "No stack or CI facts were detected with sufficient evidence.";
1803
2270
  };
1804
2271
  var markdownBlock = (id, body) => `<!-- codex-agent:managed:start ${id} -->
@@ -1823,7 +2290,31 @@ var managedIndexOwnershipConflicts = (existingIndex) => {
1823
2290
  return [];
1824
2291
  });
1825
2292
  };
1826
- var renderProjectFiles = (analysis, existingIndex = null) => {
2293
+ var nestedValue = (value, dottedPath) => dottedPath.split(".").reduce((current, key) => current?.[key], value);
2294
+ var collectSignalEvidence = (value) => {
2295
+ if (!value || typeof value !== "object") return [];
2296
+ return [
2297
+ ...Array.isArray(value.evidence) ? value.evidence : [],
2298
+ ...Object.values(value).flatMap((item) => collectSignalEvidence(item))
2299
+ ];
2300
+ };
2301
+ var managedEvidence = (analysis, id, signalPaths) => {
2302
+ const projectRoot = path6.resolve(analysis.root);
2303
+ const locators = unique2(signalPaths.flatMap((signalPath) => collectSignalEvidence(nestedValue(analysis, signalPath)))).map((locator) => String(locator).split("#")[0]).filter((locator) => locator && !path6.isAbsolute(locator));
2304
+ return unique2(locators).flatMap((locator) => {
2305
+ const target = path6.resolve(projectRoot, locator);
2306
+ const relativePath = relative2(projectRoot, target);
2307
+ if (relativePath.startsWith("../") || path6.isAbsolute(relativePath) || relativePath.startsWith(".codex-agent/context/") || relativePath.startsWith(".agents/context/")) return [];
2308
+ try {
2309
+ assertNoSymlink(projectRoot, target, `Managed context evidence ${locator}`);
2310
+ if (!fs6.existsSync(target) || !fs6.statSync(target).isFile()) return [];
2311
+ return [{ type: "repository", locator: relativePath, note: `Repository evidence used to generate ${id}.`, sha256: sha256(fs6.readFileSync(target)) }];
2312
+ } catch {
2313
+ return [];
2314
+ }
2315
+ }).slice(0, 20);
2316
+ };
2317
+ var renderProjectFiles = (analysis, existingIndex = null, { excludeManaged = [] } = {}) => {
1827
2318
  if (containsSensitiveContent(JSON.stringify({ analysis, existingIndex }))) {
1828
2319
  throw new Error("Project context rendering input appears to contain a secret or credential");
1829
2320
  }
@@ -1842,16 +2333,32 @@ var renderProjectFiles = (analysis, existingIndex = null) => {
1842
2333
  [".codex/config.toml", { kind: "toml", id: "agent-settings", body: "[agents]\nmax_concurrent_threads_per_session = 4\nmax_depth = 1\n\n[features]\nhooks = true" }]
1843
2334
  ]);
1844
2335
  for (const profile of agentProfiles) files.set(`.codex/agents/${profile.file}`, { kind: "toml", id: `profile-${profile.name}`, body: renderProfile(profile) });
1845
- const priorEntries = Array.isArray(existingIndex?.entries) ? existingIndex.entries : [];
2336
+ const upgradedIndex = upgradeContextIndex(existingIndex ?? { version: 1, entries: [] });
2337
+ const priorEntries = upgradedIndex.entries;
2338
+ const priorByPair = new Map(priorEntries.map((entry) => [`${entry.id}\0${entry.path}`, entry]));
1846
2339
  const managedPairs = new Set(MANAGED_CONTEXT.map(([id, file]) => `${id}\0${file}`));
1847
2340
  const customEntries = priorEntries.filter((entry) => !managedPairs.has(`${entry.id}\0${entry.path}`));
1848
2341
  const index = {
1849
2342
  ...existingIndex?.$schema ? { $schema: existingIndex.$schema } : {},
1850
- version: 1,
1851
- entries: [...MANAGED_CONTEXT.map(([id, file, summary2, tags, priority]) => ({ id, path: file, summary: summary2, tags, priority })), ...customEntries]
2343
+ version: 2,
2344
+ entries: [
2345
+ ...MANAGED_CONTEXT.map(([id, file, summary2, tags, priority, signalPaths]) => upgradeContextIndexEntry({
2346
+ ...priorByPair.get(`${id}\0${file}`),
2347
+ id,
2348
+ path: file,
2349
+ summary: summary2,
2350
+ tags,
2351
+ priority,
2352
+ status: "active",
2353
+ lastVerifiedAt: contextDate(),
2354
+ evidence: managedEvidence(analysis, id, signalPaths)
2355
+ })),
2356
+ ...customEntries
2357
+ ]
1852
2358
  };
1853
2359
  files.set(".codex-agent/context/index.json", { kind: "json", content: `${JSON.stringify(index, null, 2)}
1854
2360
  ` });
2361
+ for (const file of normalizeManagedExclusions(excludeManaged)) files.delete(file);
1855
2362
  for (const [file, descriptor] of files) {
1856
2363
  if (containsSensitiveContent(descriptor.content ?? descriptor.body ?? "")) {
1857
2364
  throw new Error(`Generated managed content for ${file} appears to contain a secret or credential`);
@@ -1909,13 +2416,13 @@ var lineDiff = (before, after) => {
1909
2416
  const added = newLines.slice(prefix, newLines.length - suffix).map((line) => `+ ${line}`);
1910
2417
  return [`@@ line ${prefix + 1} @@`, ...removed, ...added].join("\n");
1911
2418
  };
1912
- var analysisPathFor = (root) => path5.join(root, ".codex-agent", "analysis.json");
2419
+ var analysisPathFor = (root) => path6.join(root, ".codex-agent", "analysis.json");
1913
2420
  var readProjectText = (projectRoot, target, label) => {
1914
2421
  assertInside(projectRoot, target, label);
1915
2422
  assertNoSymlink(projectRoot, target, label);
1916
- if (!fs5.existsSync(target)) return null;
1917
- if (!fs5.lstatSync(target).isFile()) throw new Error(`${label} is not a file: ${relative(projectRoot, target)}`);
1918
- const content = fs5.readFileSync(target, "utf8");
2423
+ if (!fs6.existsSync(target)) return null;
2424
+ if (!fs6.lstatSync(target).isFile()) throw new Error(`${label} is not a file: ${relative2(projectRoot, target)}`);
2425
+ const content = fs6.readFileSync(target, "utf8");
1919
2426
  if (containsSensitiveContent(content)) throw new Error(`${label} appears to contain a secret or credential`);
1920
2427
  return content;
1921
2428
  };
@@ -1936,8 +2443,8 @@ var readableCatalog = (root) => {
1936
2443
  };
1937
2444
  var catalogIndex = (catalog) => {
1938
2445
  if (catalog?.index && typeof catalog.index === "object") return catalog.index;
1939
- const indexPath = catalog?.indexPath ?? (catalog?.root ? path5.join(catalog.root, "index.json") : null);
1940
- return indexPath && fs5.existsSync(indexPath) ? readJson(indexPath) : null;
2446
+ const indexPath = catalog?.indexPath ?? (catalog?.root ? path6.join(catalog.root, "index.json") : null);
2447
+ return indexPath && fs6.existsSync(indexPath) ? readJson2(indexPath) : null;
1941
2448
  };
1942
2449
  var migrationChanges = (migration) => (migration?.changes ?? []).map((change) => typeof change === "string" ? { path: change, status: "migrate", phase: "catalog-migration" } : {
1943
2450
  ...change,
@@ -1946,7 +2453,7 @@ var migrationChanges = (migration) => (migration?.changes ?? []).map((change) =>
1946
2453
  phase: "catalog-migration"
1947
2454
  });
1948
2455
  var uniqueStrings = (items) => [...new Set(items.filter((item) => typeof item === "string" && item))];
1949
- var planManagedFiles = ({ projectRoot, analysis, existingIndex, force, contextBaselineRoot = null }) => {
2456
+ var planManagedFiles = ({ projectRoot, analysis, existingIndex, force, excludeManaged = [], contextBaselineRoot = null }) => {
1950
2457
  const ownershipConflicts = managedIndexOwnershipConflicts(existingIndex);
1951
2458
  if (ownershipConflicts.length) {
1952
2459
  return {
@@ -1959,7 +2466,7 @@ var planManagedFiles = ({ projectRoot, analysis, existingIndex, force, contextBa
1959
2466
  writePlan: []
1960
2467
  };
1961
2468
  }
1962
- const rendered = renderProjectFiles(analysis, existingIndex);
2469
+ const rendered = renderProjectFiles(analysis, existingIndex, { excludeManaged });
1963
2470
  for (const [file, descriptor] of rendered) {
1964
2471
  if (containsSensitiveContent(descriptor.content ?? descriptor.body ?? "")) {
1965
2472
  throw new Error(`Generated managed content for ${file} appears to contain a secret or credential`);
@@ -1968,12 +2475,12 @@ var planManagedFiles = ({ projectRoot, analysis, existingIndex, force, contextBa
1968
2475
  const changes = [];
1969
2476
  const conflicts = [];
1970
2477
  const writePlan = [];
1971
- const canonicalContextRoot = path5.join(projectRoot, ".codex-agent", "context");
2478
+ const canonicalContextRoot = path6.join(projectRoot, ".codex-agent", "context");
1972
2479
  for (const [file, descriptor] of rendered) {
1973
- const destination = path5.join(projectRoot, file);
1974
- const contextRelative = path5.relative(canonicalContextRoot, destination);
1975
- const usesContextBaseline = contextBaselineRoot !== null && contextRelative !== "" && contextRelative !== ".." && !contextRelative.startsWith(`..${path5.sep}`) && !path5.isAbsolute(contextRelative);
1976
- const currentPath = usesContextBaseline ? path5.join(contextBaselineRoot, contextRelative) : destination;
2480
+ const destination = path6.join(projectRoot, file);
2481
+ const contextRelative = path6.relative(canonicalContextRoot, destination);
2482
+ const usesContextBaseline = contextBaselineRoot !== null && contextRelative !== "" && contextRelative !== ".." && !contextRelative.startsWith(`..${path6.sep}`) && !path6.isAbsolute(contextRelative);
2483
+ const currentPath = usesContextBaseline ? path6.join(contextBaselineRoot, contextRelative) : destination;
1977
2484
  const current = readProjectText(projectRoot, currentPath, `Managed project file ${file}`);
1978
2485
  const merged = replaceManaged(current, descriptor, force);
1979
2486
  if (merged.conflict && !force) {
@@ -1989,7 +2496,7 @@ var planManagedFiles = ({ projectRoot, analysis, existingIndex, force, contextBa
1989
2496
  };
1990
2497
  var applyManagedFiles = ({ projectRoot, writePlan, analysis, analysisCurrent, catalogPreconditions = [], lock = true }) => {
1991
2498
  const analysisPath = analysisPathFor(projectRoot);
1992
- const analysisRelative = relative(projectRoot, analysisPath);
2499
+ const analysisRelative = relative2(projectRoot, analysisPath);
1993
2500
  const preconditionMap = new Map(catalogPreconditions.map((item) => [item.path, item.expected]));
1994
2501
  for (const { file, current } of writePlan) preconditionMap.set(file, current);
1995
2502
  preconditionMap.set(analysisRelative, analysisCurrent);
@@ -2031,7 +2538,7 @@ var snapshotCatalogPreconditions = ({ projectRoot, expected }) => {
2031
2538
  const current = assertCatalogPrecondition({ projectRoot, expected });
2032
2539
  if (current.state !== "canonical-only") return [];
2033
2540
  const preconditions = listTreeFiles(current.canonical.path).map((item) => ({
2034
- path: relative(projectRoot, item.absolute),
2541
+ path: relative2(projectRoot, item.absolute),
2035
2542
  expected: readProjectText(projectRoot, item.absolute, `Canonical context file ${item.relative}`)
2036
2543
  }));
2037
2544
  assertCatalogPrecondition({ projectRoot, expected });
@@ -2040,7 +2547,7 @@ var snapshotCatalogPreconditions = ({ projectRoot, expected }) => {
2040
2547
  var assertManagedPreconditions = ({ projectRoot, writePlan, analysisCurrent, expectedCatalog }) => {
2041
2548
  assertCatalogPrecondition({ projectRoot, expected: expectedCatalog });
2042
2549
  for (const { file, current } of writePlan) {
2043
- const destination = path5.join(projectRoot, file);
2550
+ const destination = path6.join(projectRoot, file);
2044
2551
  if (readProjectText(projectRoot, destination, `Managed project file ${file}`) !== current) {
2045
2552
  throw new Error(`Context managed-file precondition changed after preview: ${file}`);
2046
2553
  }
@@ -2061,10 +2568,11 @@ var managedPlanContract = (plan) => ({
2061
2568
  conflicts: [...plan.conflicts]
2062
2569
  });
2063
2570
  var managedPlanHash = (plan) => sha256(JSON.stringify(managedPlanContract(plan)));
2064
- var lifecyclePlanHash = ({ operation, force, lifecycleCatalog, migrationPreview, analysis, analysisCurrent, analysisContent, plan, conflicts }) => sha256(JSON.stringify({
2571
+ var lifecyclePlanHash = ({ operation, force, excludedManaged, lifecycleCatalog, migrationPreview, analysis, analysisCurrent, analysisContent, plan, conflicts }) => sha256(JSON.stringify({
2065
2572
  version: CONTEXT_LIFECYCLE_VERSION,
2066
2573
  operation,
2067
2574
  force,
2575
+ excludedManaged,
2068
2576
  catalog: {
2069
2577
  state: lifecycleCatalog.state,
2070
2578
  canonicalHash: lifecycleCatalog.canonical.hash,
@@ -2077,41 +2585,41 @@ var lifecyclePlanHash = ({ operation, force, lifecycleCatalog, migrationPreview,
2077
2585
  managed: managedPlanContract(plan),
2078
2586
  conflicts
2079
2587
  }));
2080
- var lifecycleTransactionRoot = (projectRoot) => path5.join(projectRoot, ".codex-agent", ".transactions");
2588
+ var lifecycleTransactionRoot = (projectRoot) => path6.join(projectRoot, ".codex-agent", ".transactions");
2081
2589
  var lifecycleFileHash = ({ projectRoot, relativePath, label }) => {
2082
2590
  const normalized = safeRelativePath(relativePath, label);
2083
- const target = path5.join(projectRoot, ...normalized.split("/"));
2591
+ const target = path6.join(projectRoot, ...normalized.split("/"));
2084
2592
  assertInside(projectRoot, target, label);
2085
2593
  assertNoSymlink(projectRoot, target, label);
2086
- if (!fs5.existsSync(target)) return null;
2087
- if (!fs5.lstatSync(target).isFile()) throw new Error(`${label} is not a regular file: ${normalized}`);
2088
- return sha256(fs5.readFileSync(target));
2594
+ if (!fs6.existsSync(target)) return null;
2595
+ if (!fs6.lstatSync(target).isFile()) throw new Error(`${label} is not a regular file: ${normalized}`);
2596
+ return sha256(fs6.readFileSync(target));
2089
2597
  };
2090
2598
  var writeNewLifecycleJson = (destination, value) => {
2091
- const temporary = path5.join(path5.dirname(destination), `.${path5.basename(destination)}.tmp-${crypto4.randomUUID()}`);
2599
+ const temporary = path6.join(path6.dirname(destination), `.${path6.basename(destination)}.tmp-${crypto4.randomUUID()}`);
2092
2600
  let descriptor;
2093
2601
  try {
2094
- descriptor = fs5.openSync(temporary, "wx", 384);
2095
- fs5.writeFileSync(descriptor, `${JSON.stringify(value, null, 2)}
2602
+ descriptor = fs6.openSync(temporary, "wx", 384);
2603
+ fs6.writeFileSync(descriptor, `${JSON.stringify(value, null, 2)}
2096
2604
  `);
2097
- fs5.fsyncSync(descriptor);
2098
- fs5.closeSync(descriptor);
2605
+ fs6.fsyncSync(descriptor);
2606
+ fs6.closeSync(descriptor);
2099
2607
  descriptor = void 0;
2100
- fs5.renameSync(temporary, destination);
2608
+ fs6.renameSync(temporary, destination);
2101
2609
  } catch (error) {
2102
2610
  if (descriptor !== void 0) try {
2103
- fs5.closeSync(descriptor);
2611
+ fs6.closeSync(descriptor);
2104
2612
  } catch {
2105
2613
  }
2106
2614
  try {
2107
- if (fs5.existsSync(temporary)) fs5.unlinkSync(temporary);
2615
+ if (fs6.existsSync(temporary)) fs6.unlinkSync(temporary);
2108
2616
  } catch {
2109
2617
  }
2110
2618
  throw error;
2111
2619
  }
2112
2620
  };
2113
2621
  var validateLifecycleManifest = ({ projectRoot, transactionRoot, manifest }) => {
2114
- const transactionId2 = path5.basename(transactionRoot);
2622
+ const transactionId2 = path6.basename(transactionRoot);
2115
2623
  if (!manifest || typeof manifest !== "object" || Array.isArray(manifest) || manifest.version !== CONTEXT_LIFECYCLE_VERSION || manifest.type !== "context-lifecycle" || manifest.transactionId !== transactionId2 || manifest.operation !== "context.init" || manifest.phase !== "prepared" || manifest.revision !== 1 || !HASH_PATTERN2.test(manifest.planHash ?? "")) {
2116
2624
  throw new Error(`Invalid context lifecycle recovery manifest: ${transactionId2}`);
2117
2625
  }
@@ -2154,16 +2662,16 @@ var lifecyclePhase = ({ projectRoot, transactionRoot, transactionId: transaction
2154
2662
  let phase = "prepared";
2155
2663
  let missingEarlier = false;
2156
2664
  for (const candidate of ["migration-applied", "managed-running", "managed-applied"]) {
2157
- const markerPath = path5.join(transactionRoot, `${candidate}.json`);
2665
+ const markerPath = path6.join(transactionRoot, `${candidate}.json`);
2158
2666
  assertNoSymlink(projectRoot, markerPath, `Lifecycle phase marker ${candidate}`);
2159
- if (!fs5.existsSync(markerPath)) {
2667
+ if (!fs6.existsSync(markerPath)) {
2160
2668
  missingEarlier = true;
2161
2669
  continue;
2162
2670
  }
2163
2671
  if (missingEarlier) throw new Error(`Lifecycle phase markers are not contiguous: ${transactionId2}`);
2164
2672
  let marker;
2165
2673
  try {
2166
- marker = JSON.parse(fs5.readFileSync(markerPath, "utf8"));
2674
+ marker = JSON.parse(fs6.readFileSync(markerPath, "utf8"));
2167
2675
  } catch {
2168
2676
  throw new Error(`Invalid lifecycle phase marker: ${candidate}`);
2169
2677
  }
@@ -2175,13 +2683,13 @@ var lifecyclePhase = ({ projectRoot, transactionRoot, transactionId: transaction
2175
2683
  return phase;
2176
2684
  };
2177
2685
  var readLifecycleJournal = ({ projectRoot, transactionRoot }) => {
2178
- const manifestPath = path5.join(transactionRoot, "manifest.json");
2686
+ const manifestPath = path6.join(transactionRoot, "manifest.json");
2179
2687
  assertNoSymlink(projectRoot, manifestPath, "Lifecycle recovery manifest");
2180
2688
  let manifest;
2181
2689
  try {
2182
- manifest = JSON.parse(fs5.readFileSync(manifestPath, "utf8"));
2690
+ manifest = JSON.parse(fs6.readFileSync(manifestPath, "utf8"));
2183
2691
  } catch {
2184
- throw new Error(`Invalid context lifecycle recovery manifest: ${path5.basename(transactionRoot)}`);
2692
+ throw new Error(`Invalid context lifecycle recovery manifest: ${path6.basename(transactionRoot)}`);
2185
2693
  }
2186
2694
  const normalized = validateLifecycleManifest({ projectRoot, transactionRoot, manifest });
2187
2695
  return {
@@ -2197,7 +2705,7 @@ var markLifecyclePhase = (journal, phase) => {
2197
2705
  if (order.indexOf(phase) !== order.indexOf(journal.phase) + 1) {
2198
2706
  throw new Error(`Invalid lifecycle phase transition: ${journal.phase} -> ${phase}`);
2199
2707
  }
2200
- writeNewLifecycleJson(path5.join(journal.transactionRoot, `${phase}.json`), {
2708
+ writeNewLifecycleJson(path6.join(journal.transactionRoot, `${phase}.json`), {
2201
2709
  version: CONTEXT_LIFECYCLE_VERSION,
2202
2710
  transactionId: journal.normalized.transactionId,
2203
2711
  phase
@@ -2210,7 +2718,7 @@ var managedLifecycleItems = ({ plan, projectRoot, analysis, analysisCurrent }) =
2210
2718
  beforeHash: current === null ? null : sha256(current),
2211
2719
  afterHash: sha256(merged.content)
2212
2720
  }));
2213
- const analysisPath = relative(projectRoot, analysisPathFor(projectRoot));
2721
+ const analysisPath = relative2(projectRoot, analysisPathFor(projectRoot));
2214
2722
  items.push({
2215
2723
  path: analysisPath,
2216
2724
  beforeHash: analysisCurrent === null ? null : sha256(analysisCurrent),
@@ -2222,9 +2730,9 @@ var managedLifecycleItems = ({ plan, projectRoot, analysis, analysisCurrent }) =
2222
2730
  var prepareLifecycleJournal = ({ projectRoot, lifecycleCatalog, planHash, plan, analysis, analysisCurrent }) => {
2223
2731
  const id = `lifecycle-${process.pid}-${Date.now()}-${crypto4.randomUUID()}`;
2224
2732
  const transactionsRoot = ensureDirectory(projectRoot, lifecycleTransactionRoot(projectRoot), "Context transaction directory");
2225
- const transactionRoot = path5.join(transactionsRoot, id);
2226
- const preparationRoot = path5.join(transactionsRoot, `.lifecycle-tmp-${id}`);
2227
- fs5.mkdirSync(preparationRoot, { recursive: false });
2733
+ const transactionRoot = path6.join(transactionsRoot, id);
2734
+ const preparationRoot = path6.join(transactionsRoot, `.lifecycle-tmp-${id}`);
2735
+ fs6.mkdirSync(preparationRoot, { recursive: false });
2228
2736
  const manifest = {
2229
2737
  version: CONTEXT_LIFECYCLE_VERSION,
2230
2738
  type: "context-lifecycle",
@@ -2247,12 +2755,12 @@ var prepareLifecycleJournal = ({ projectRoot, lifecycleCatalog, planHash, plan,
2247
2755
  managed: managedLifecycleItems({ plan, projectRoot, analysis, analysisCurrent })
2248
2756
  };
2249
2757
  try {
2250
- writeNewLifecycleJson(path5.join(preparationRoot, "manifest.json"), manifest);
2251
- fs5.renameSync(preparationRoot, transactionRoot);
2758
+ writeNewLifecycleJson(path6.join(preparationRoot, "manifest.json"), manifest);
2759
+ fs6.renameSync(preparationRoot, transactionRoot);
2252
2760
  return readLifecycleJournal({ projectRoot, transactionRoot });
2253
2761
  } catch (error) {
2254
- fs5.rmSync(preparationRoot, { recursive: true, force: true });
2255
- fs5.rmSync(transactionRoot, { recursive: true, force: true });
2762
+ fs6.rmSync(preparationRoot, { recursive: true, force: true });
2763
+ fs6.rmSync(transactionRoot, { recursive: true, force: true });
2256
2764
  throw error;
2257
2765
  }
2258
2766
  };
@@ -2268,38 +2776,38 @@ var inspectLifecycleCatalog = ({ projectRoot, relativePath, expectedHash, label,
2268
2776
  return inspected;
2269
2777
  };
2270
2778
  var cleanupLifecycleJournal = ({ projectRoot, journal }) => {
2271
- const catalogTransactionRoot = path5.join(projectRoot, ...journal.normalized.paths.catalogTransaction.split("/"));
2779
+ const catalogTransactionRoot = path6.join(projectRoot, ...journal.normalized.paths.catalogTransaction.split("/"));
2272
2780
  assertInside(projectRoot, catalogTransactionRoot, "Lifecycle catalog transaction directory");
2273
2781
  assertNoSymlink(projectRoot, catalogTransactionRoot, "Lifecycle catalog transaction directory");
2274
- if (fs5.existsSync(catalogTransactionRoot)) fs5.rmSync(catalogTransactionRoot, { recursive: true, force: true });
2782
+ if (fs6.existsSync(catalogTransactionRoot)) fs6.rmSync(catalogTransactionRoot, { recursive: true, force: true });
2275
2783
  assertNoSymlink(projectRoot, journal.transactionRoot, "Lifecycle transaction directory");
2276
- fs5.rmSync(journal.transactionRoot, { recursive: true, force: true });
2784
+ fs6.rmSync(journal.transactionRoot, { recursive: true, force: true });
2277
2785
  };
2278
2786
  var rollbackLifecycleMigration = ({ projectRoot, journal }) => {
2279
2787
  const { initial, paths } = journal.normalized;
2280
- const canonical = path5.join(projectRoot, ...paths.canonical.split("/"));
2281
- const legacy = path5.join(projectRoot, ...paths.legacy.split("/"));
2282
- const backup = path5.join(projectRoot, ...paths.backup.split("/"));
2283
- const quarantine = path5.join(journal.transactionRoot, "canonical-rollback");
2788
+ const canonical = path6.join(projectRoot, ...paths.canonical.split("/"));
2789
+ const legacy = path6.join(projectRoot, ...paths.legacy.split("/"));
2790
+ const backup = path6.join(projectRoot, ...paths.backup.split("/"));
2791
+ const quarantine = path6.join(journal.transactionRoot, "canonical-rollback");
2284
2792
  if (initial.state === "legacy-only") {
2285
- if (fs5.existsSync(canonical)) {
2793
+ if (fs6.existsSync(canonical)) {
2286
2794
  inspectLifecycleCatalog({ projectRoot, relativePath: paths.canonical, expectedHash: initial.sourceHash, label: "Promoted canonical catalog" });
2287
- if (fs5.existsSync(quarantine)) throw new Error("Lifecycle canonical rollback quarantine already exists");
2288
- fs5.renameSync(canonical, quarantine);
2289
- } else if (fs5.existsSync(quarantine)) {
2290
- const quarantinePath = relative(projectRoot, quarantine);
2795
+ if (fs6.existsSync(quarantine)) throw new Error("Lifecycle canonical rollback quarantine already exists");
2796
+ fs6.renameSync(canonical, quarantine);
2797
+ } else if (fs6.existsSync(quarantine)) {
2798
+ const quarantinePath = relative2(projectRoot, quarantine);
2291
2799
  inspectLifecycleCatalog({ projectRoot, relativePath: quarantinePath, expectedHash: initial.sourceHash, label: "Quarantined canonical catalog" });
2292
2800
  }
2293
2801
  } else {
2294
2802
  inspectLifecycleCatalog({ projectRoot, relativePath: paths.canonical, expectedHash: initial.sourceHash, label: "Original canonical catalog" });
2295
2803
  }
2296
- if (!fs5.existsSync(legacy)) {
2804
+ if (!fs6.existsSync(legacy)) {
2297
2805
  inspectLifecycleCatalog({ projectRoot, relativePath: paths.backup, expectedHash: initial.sourceHash, label: "Legacy catalog backup" });
2298
- ensureDirectory(projectRoot, path5.dirname(legacy), "Legacy context parent");
2299
- fs5.renameSync(backup, legacy);
2806
+ ensureDirectory(projectRoot, path6.dirname(legacy), "Legacy context parent");
2807
+ fs6.renameSync(backup, legacy);
2300
2808
  } else {
2301
2809
  inspectLifecycleCatalog({ projectRoot, relativePath: paths.legacy, expectedHash: initial.sourceHash, label: "Legacy context catalog" });
2302
- if (fs5.existsSync(backup)) throw new Error("Lifecycle recovery found both the legacy catalog and its migration backup");
2810
+ if (fs6.existsSync(backup)) throw new Error("Lifecycle recovery found both the legacy catalog and its migration backup");
2303
2811
  }
2304
2812
  const restored = resolveContextCatalog({ root: projectRoot });
2305
2813
  if (restored.state !== initial.state || restored.legacy.hash !== initial.sourceHash) {
@@ -2340,26 +2848,75 @@ var recoverLifecycleJournal = ({ projectRoot, journal }) => {
2340
2848
  var pendingLifecycleDirectories = (projectRoot) => {
2341
2849
  const transactionsRoot = lifecycleTransactionRoot(projectRoot);
2342
2850
  assertNoSymlink(projectRoot, transactionsRoot, "Context transaction directory");
2343
- if (!fs5.existsSync(transactionsRoot)) return [];
2344
- return fs5.readdirSync(transactionsRoot, { withFileTypes: true }).filter((entry) => entry.name.startsWith("lifecycle-")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
2851
+ if (!fs6.existsSync(transactionsRoot)) return [];
2852
+ return fs6.readdirSync(transactionsRoot, { withFileTypes: true }).filter((entry) => entry.name.startsWith("lifecycle-")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
2345
2853
  if (entry.isSymbolicLink() || !entry.isDirectory()) throw new Error(`Invalid lifecycle recovery entry: ${entry.name}`);
2346
- return path5.join(transactionsRoot, entry.name);
2854
+ return path6.join(transactionsRoot, entry.name);
2347
2855
  });
2348
2856
  };
2349
2857
  var recoverPendingLifecycleTransactions = (projectRoot) => pendingLifecycleDirectories(projectRoot).map((transactionRoot) => {
2350
2858
  const journal = readLifecycleJournal({ projectRoot, transactionRoot });
2351
2859
  return { transactionId: journal.normalized.transactionId, outcome: recoverLifecycleJournal({ projectRoot, journal }) };
2352
2860
  });
2861
+ var approvalPlanFor = ({ operation, projectRoot, analysis, excludedManaged, changes, conflicts, planHash, applied }) => {
2862
+ const activeChanges = changes.filter((change) => change.status !== "unchanged").map(({ path: file, status, phase }) => ({
2863
+ path: file,
2864
+ action: status,
2865
+ phase
2866
+ }));
2867
+ const unchanged = changes.filter((change) => change.status === "unchanged").map((change) => change.path);
2868
+ const names = (signal2) => Array.isArray(signal2?.value) ? signal2.value.map((item) => typeof item === "string" ? item : item?.name).filter(Boolean) : [];
2869
+ const testUnits = Array.isArray(analysis.testing?.value?.units) ? analysis.testing.value.units : [];
2870
+ const scan = analysis.scan?.value ?? {};
2871
+ return {
2872
+ title: operation === "context.init" ? "Initialize managed project context" : "Refresh managed project context",
2873
+ outcome: operation === "context.init" ? "Create the first evidence-backed managed guidance and canonical context catalog." : "Reconcile managed guidance and canonical context with current repository evidence.",
2874
+ status: conflicts.length ? "conflicts-require-resolution" : applied ? "applied" : "approval-required",
2875
+ target: projectRoot,
2876
+ analysis: {
2877
+ project: analysis.project?.value?.name ?? path6.basename(projectRoot),
2878
+ languages: names(analysis.languages),
2879
+ toolchains: names(analysis.toolchains),
2880
+ technologies: names(analysis.technologies),
2881
+ units: Array.isArray(analysis.units?.value) ? analysis.units.value.length : 0,
2882
+ testUnits: testUnits.length,
2883
+ scan: {
2884
+ files: Number.isSafeInteger(scan.files) ? scan.files : null,
2885
+ truncated: Boolean(scan.truncated),
2886
+ excluded: Array.isArray(scan.excluded) ? scan.excluded.map((item) => item.path).filter(Boolean) : []
2887
+ }
2888
+ },
2889
+ changes: activeChanges,
2890
+ preserved: {
2891
+ unchanged,
2892
+ excludedManaged: [...excludedManaged],
2893
+ manualContent: "Content outside Codex-managed markers remains unchanged."
2894
+ },
2895
+ conflicts: [...conflicts],
2896
+ safeguards: [
2897
+ "Updated managed files are backed up during apply.",
2898
+ "Repository or catalog drift invalidates this plan before any write."
2899
+ ],
2900
+ residualRisks: [
2901
+ ...scan.truncated ? ["Repository discovery reached its scan limit, so the plan may omit facts beyond that boundary."] : [],
2902
+ "Automated discovery can omit behavior that is not evidenced by repository files."
2903
+ ],
2904
+ approval: conflicts.length ? "Resolve the listed conflicts and review a new plan before approval." : applied ? "The reviewed plan was applied successfully." : "Approve this displayed plan or request changes. The integrity ID is retained by Codex and does not need to be copied.",
2905
+ integrityId: planHash
2906
+ };
2907
+ };
2353
2908
  var runContextLifecycleOperation = ({
2354
2909
  operation,
2355
2910
  root,
2356
2911
  apply = false,
2357
2912
  force = false,
2913
+ excludeManaged = [],
2358
2914
  analysis: suppliedAnalysis = null,
2359
2915
  expectedPlanHash = null,
2360
2916
  lockHeld = false
2361
2917
  }) => {
2362
- const projectRoot = fs5.realpathSync(path5.resolve(root));
2918
+ const projectRoot = fs6.realpathSync(path6.resolve(root));
2919
+ const excludedManaged = normalizeManagedExclusions(excludeManaged);
2363
2920
  const isInit = operation === "context.init";
2364
2921
  const lifecycleCatalog = resolveContextCatalog({ root: projectRoot });
2365
2922
  if (isInit && lifecycleCatalog.state === "canonical-only") {
@@ -2381,9 +2938,9 @@ var runContextLifecycleOperation = ({
2381
2938
  - ${validation.errors.join("\n- ")}`);
2382
2939
  let suppliedRoot;
2383
2940
  try {
2384
- suppliedRoot = fs5.realpathSync(path5.resolve(analysis.root));
2941
+ suppliedRoot = fs6.realpathSync(path6.resolve(analysis.root));
2385
2942
  } catch {
2386
- suppliedRoot = path5.resolve(analysis.root);
2943
+ suppliedRoot = path6.resolve(analysis.root);
2387
2944
  }
2388
2945
  if (suppliedRoot !== projectRoot) throw new Error(`Analysis root does not match target root: ${analysis.root}`);
2389
2946
  const evidenceValidation = validateAnalysisEvidence(analysis, projectRoot);
@@ -2405,13 +2962,15 @@ var runContextLifecycleOperation = ({
2405
2962
  analysis,
2406
2963
  existingIndex: catalogIndex(activeCatalog),
2407
2964
  force,
2408
- contextBaselineRoot: isInit && lifecycleCatalog.state === "legacy-only" ? path5.join(projectRoot, ".agents", "context") : null
2965
+ excludeManaged: excludedManaged,
2966
+ contextBaselineRoot: isInit && lifecycleCatalog.state === "legacy-only" ? path6.join(projectRoot, ".agents", "context") : null
2409
2967
  });
2410
2968
  let conflicts = uniqueStrings([...migrationPreview.conflicts ?? [], ...plan.conflicts]);
2411
2969
  let backups = [...migrationPreview.backedUp ?? []];
2412
2970
  const planHash = lifecyclePlanHash({
2413
2971
  operation,
2414
2972
  force,
2973
+ excludedManaged,
2415
2974
  lifecycleCatalog,
2416
2975
  migrationPreview,
2417
2976
  analysis,
@@ -2449,7 +3008,7 @@ var runContextLifecycleOperation = ({
2449
3008
  expectedHash: lifecycleCatalog.legacy.hash,
2450
3009
  ...lifecycleJournal ? {
2451
3010
  backupPath: lifecycleJournal.normalized.paths.backup,
2452
- transactionId: path5.basename(lifecycleJournal.normalized.paths.catalogTransaction)
3011
+ transactionId: path6.basename(lifecycleJournal.normalized.paths.catalogTransaction)
2453
3012
  } : {},
2454
3013
  lock: !lockHeld
2455
3014
  });
@@ -2466,7 +3025,8 @@ var runContextLifecycleOperation = ({
2466
3025
  projectRoot,
2467
3026
  analysis,
2468
3027
  existingIndex: catalogIndex(activeCatalog),
2469
- force
3028
+ force,
3029
+ excludeManaged: excludedManaged
2470
3030
  });
2471
3031
  if (migration.applied && managedPlanHash(canonicalPlan) !== reviewedManagedPlanHash) {
2472
3032
  throw new Error("Context managed-file plan changed after preview; review a fresh preview");
@@ -2501,7 +3061,7 @@ var runContextLifecycleOperation = ({
2501
3061
  }
2502
3062
  }
2503
3063
  } catch (error) {
2504
- if (lifecycleJournal && fs5.existsSync(lifecycleJournal.transactionRoot)) {
3064
+ if (lifecycleJournal && fs6.existsSync(lifecycleJournal.transactionRoot)) {
2505
3065
  try {
2506
3066
  const currentJournal = readLifecycleJournal({ projectRoot, transactionRoot: lifecycleJournal.transactionRoot });
2507
3067
  if (managedTransactionApplied) recoverLifecycleJournal({ projectRoot, journal: currentJournal });
@@ -2516,29 +3076,41 @@ var runContextLifecycleOperation = ({
2516
3076
  const migrationPlan = migrationChanges(apply ? migration : migrationPreview);
2517
3077
  const managedChanges = plan.changes.map((change) => ({ ...change, phase: "managed-context" }));
2518
3078
  const analysisChange = {
2519
- path: relative(projectRoot, analysisPath),
3079
+ path: relative2(projectRoot, analysisPath),
2520
3080
  status: analysisCurrent === null ? "create" : analysisCurrent === analysisContent ? "unchanged" : "update",
2521
3081
  diff: analysisCurrent === analysisContent ? "" : lineDiff(analysisCurrent, analysisContent),
2522
3082
  phase: "analysis"
2523
3083
  };
2524
3084
  const applied = apply && conflicts.length === 0;
3085
+ const changes = [...migrationPlan, ...managedChanges, analysisChange];
2525
3086
  return {
2526
3087
  schemaVersion: CONTEXT_LIFECYCLE_VERSION,
2527
3088
  operation,
2528
3089
  root: projectRoot,
2529
3090
  mode: apply ? "apply" : "preview",
2530
- analysisPath: relative(projectRoot, analysisPathFor(projectRoot)),
3091
+ analysisPath: relative2(projectRoot, analysisPathFor(projectRoot)),
2531
3092
  analysis,
2532
- changes: [...migrationPlan, ...managedChanges, analysisChange],
3093
+ excludedManaged,
3094
+ changes,
2533
3095
  conflicts,
2534
3096
  backups,
2535
3097
  planHash,
2536
3098
  applied,
2537
- catalogMigration: migration
3099
+ catalogMigration: migration,
3100
+ approvalPlan: approvalPlanFor({
3101
+ operation,
3102
+ projectRoot,
3103
+ analysis,
3104
+ excludedManaged,
3105
+ changes,
3106
+ conflicts,
3107
+ planHash,
3108
+ applied
3109
+ })
2538
3110
  };
2539
3111
  };
2540
3112
  var runContextLifecycle = (options) => {
2541
- const projectRoot = fs5.realpathSync(path5.resolve(options.root));
3113
+ const projectRoot = fs6.realpathSync(path6.resolve(options.root));
2542
3114
  const hasPendingLifecycle = pendingLifecycleDirectories(projectRoot).length > 0;
2543
3115
  if (!options.apply && !hasPendingLifecycle) {
2544
3116
  return runContextLifecycleOperation({ ...options, root: projectRoot, lockHeld: false });
@@ -2552,10 +3124,10 @@ var initializeContext = (options) => runContextLifecycle({ ...options, operation
2552
3124
  var refreshContext = (options) => runContextLifecycle({ ...options, operation: "context.refresh" });
2553
3125
 
2554
3126
  // ../../plugins/codex-agent/skills/context-curation/scripts/context-save.mjs
2555
- import fs6 from "node:fs";
2556
- import path6 from "node:path";
3127
+ import fs7 from "node:fs";
3128
+ import path7 from "node:path";
2557
3129
  import { pathToFileURL } from "node:url";
2558
- var KINDS = {
3130
+ var KINDS2 = {
2559
3131
  decision: "decisions",
2560
3132
  constraint: "constraints",
2561
3133
  operation: "operations",
@@ -2563,7 +3135,7 @@ var KINDS = {
2563
3135
  pitfall: "pitfalls"
2564
3136
  };
2565
3137
  var PRIORITIES2 = /* @__PURE__ */ new Set(["critical", "high", "medium", "low"]);
2566
- var CONFIDENCE = /* @__PURE__ */ new Set(["high", "medium"]);
3138
+ var CONFIDENCE2 = /* @__PURE__ */ new Set(["high", "medium"]);
2567
3139
  var PROPOSAL_FIELDS = /* @__PURE__ */ new Set([
2568
3140
  "version",
2569
3141
  "title",
@@ -2575,9 +3147,12 @@ var PROPOSAL_FIELDS = /* @__PURE__ */ new Set([
2575
3147
  "tags",
2576
3148
  "priority",
2577
3149
  "confidence",
2578
- "reviewWhen"
3150
+ "reviewWhen",
3151
+ "reviewAfter",
3152
+ "aliases",
3153
+ "supersedes"
2579
3154
  ]);
2580
- var unique2 = (items) => [...new Set(items)];
3155
+ var unique3 = (items) => [...new Set(items)];
2581
3156
  var safeText2 = (value, limit = 300) => String(value).replace(/[\r\n]+/g, " ").replace(/`/g, "'").trim().slice(0, limit);
2582
3157
  var mdCode2 = (value) => `\`${safeText2(value)}\``;
2583
3158
  var normalizeForComparison = (value) => String(value).toLowerCase().replace(/\s+/g, " ").trim();
@@ -2598,17 +3173,17 @@ var firstParagraph = (content, fallback) => {
2598
3173
  return candidate.length >= 10 ? candidate : String(fallback).replace(/\s+/g, " ").slice(0, 240);
2599
3174
  };
2600
3175
  var listMarkdown = (root) => {
2601
- if (!fs6.existsSync(root)) return [];
3176
+ if (!fs7.existsSync(root)) return [];
2602
3177
  return listTreeFiles(root).filter((entry) => entry.relative.toLowerCase().endsWith(".md")).map((entry) => entry.absolute);
2603
3178
  };
2604
3179
  var prepareContextIndex = ({ root, pendingDocuments = [] }) => {
2605
3180
  const writable = assertWritableContextCatalog({ root });
2606
3181
  const projectRoot = writable.root;
2607
3182
  const contextRoot = writable.contextRoot;
2608
- if (!fs6.existsSync(contextRoot) && pendingDocuments.length === 0) throw new Error(`Context directory not found: ${contextRoot}`);
3183
+ if (!fs7.existsSync(contextRoot) && pendingDocuments.length === 0) throw new Error(`Context directory not found: ${contextRoot}`);
2609
3184
  assertNoSymlink(projectRoot, contextRoot, "Canonical context root");
2610
- const indexPath = path6.join(contextRoot, "index.json");
2611
- const prior = writable.index;
3185
+ const indexPath = path7.join(contextRoot, "index.json");
3186
+ const prior = upgradeContextIndex(writable.index);
2612
3187
  const priorByPath = new Map(prior.entries.map((entry) => [entry.path, entry]));
2613
3188
  const pending = /* @__PURE__ */ new Map();
2614
3189
  for (const [index2, document] of pendingDocuments.entries()) {
@@ -2616,36 +3191,37 @@ var prepareContextIndex = ({ root, pendingDocuments = [] }) => {
2616
3191
  throw new Error(`Pending context document ${index2} is invalid`);
2617
3192
  }
2618
3193
  assertSafeMarkdownContent(document.content, `Pending context document ${index2}`);
2619
- const target = path6.resolve(contextRoot, document.path);
3194
+ const target = path7.resolve(contextRoot, document.path);
2620
3195
  assertInside(contextRoot, target, `Pending context document ${index2}`);
2621
- const relative2 = slash(path6.relative(contextRoot, target));
2622
- if (!relative2 || relative2.startsWith("../") || relative2 === "index.json" || !relative2.endsWith(".md")) {
3196
+ const relative3 = slash(path7.relative(contextRoot, target));
3197
+ if (!relative3 || relative3.startsWith("../") || relative3 === "index.json" || !relative3.endsWith(".md")) {
2623
3198
  throw new Error(`Pending context document ${index2} has an invalid path: ${document.path}`);
2624
3199
  }
2625
- pending.set(relative2, document.content);
2626
- }
2627
- const diskPaths = listMarkdown(contextRoot).map((file) => slash(path6.relative(contextRoot, file)));
2628
- const entries = [.../* @__PURE__ */ new Set([...diskPaths, ...pending.keys()])].sort().map((relative2) => {
2629
- const content2 = pending.has(relative2) ? pending.get(relative2) : fs6.readFileSync(path6.join(contextRoot, ...relative2.split("/")), "utf8");
2630
- assertSafeMarkdownContent(content2, `Context Markdown ${relative2}`);
2631
- const title = firstHeading(content2, path6.basename(relative2, ".md"));
2632
- const existing = priorByPath.get(relative2);
2633
- const tags = unique2([
2634
- ...relative2.replace(/\.md$/, "").split("/"),
3200
+ pending.set(relative3, document.content);
3201
+ }
3202
+ const diskPaths = listMarkdown(contextRoot).map((file) => slash(path7.relative(contextRoot, file)));
3203
+ const entries = [.../* @__PURE__ */ new Set([...diskPaths, ...pending.keys()])].sort().map((relative3) => {
3204
+ const content2 = pending.has(relative3) ? pending.get(relative3) : fs7.readFileSync(path7.join(contextRoot, ...relative3.split("/")), "utf8");
3205
+ assertSafeMarkdownContent(content2, `Context Markdown ${relative3}`);
3206
+ const title = firstHeading(content2, path7.basename(relative3, ".md"));
3207
+ const existing = priorByPath.get(relative3);
3208
+ const tags = unique3([
3209
+ ...relative3.replace(/\.md$/, "").split("/"),
2635
3210
  ...title.toLowerCase().split(/[^a-z0-9_-]+/).filter((term) => term.length > 2)
2636
3211
  ].map(slug).filter(Boolean)).slice(0, 10);
2637
- return {
2638
- id: existing?.id || slug(relative2.replace(/\.md$/, "").replaceAll("/", "-")),
2639
- path: relative2,
3212
+ return upgradeContextIndexEntry({
3213
+ ...existing,
3214
+ id: existing?.id || slug(relative3.replace(/\.md$/, "").replaceAll("/", "-")),
3215
+ path: relative3,
2640
3216
  summary: existing?.summary || firstParagraph(content2, `${title} project context.`),
2641
3217
  tags: existing?.tags?.length ? existing.tags : tags,
2642
3218
  priority: existing?.priority || "medium"
2643
- };
3219
+ });
2644
3220
  }).sort((left, right) => left.path.localeCompare(right.path));
2645
- const schemaPath = path6.join(projectRoot, "schemas", "context-index.schema.json");
3221
+ const schemaPath = path7.join(projectRoot, "schemas", "context-index.schema.json");
2646
3222
  const index = {
2647
- ...fs6.existsSync(schemaPath) ? { $schema: "../../schemas/context-index.schema.json" } : prior.$schema ? { $schema: prior.$schema } : {},
2648
- version: 1,
3223
+ ...fs7.existsSync(schemaPath) ? { $schema: "../../schemas/context-index.schema.json" } : prior.$schema ? { $schema: prior.$schema } : {},
3224
+ version: 2,
2649
3225
  entries
2650
3226
  };
2651
3227
  assertValidContextIndex(index, { root: projectRoot, contextRoot, pendingPaths: [...pending.keys()] });
@@ -2662,7 +3238,7 @@ var buildContextIndex = ({ root, dryRun = false }) => {
2662
3238
  root: prepared.projectRoot,
2663
3239
  documents: [],
2664
3240
  indexContent: prepared.content,
2665
- backupPaths: fs6.existsSync(prepared.path) ? ["index.json"] : [],
3241
+ backupPaths: fs7.existsSync(prepared.path) ? ["index.json"] : [],
2666
3242
  lock: false
2667
3243
  });
2668
3244
  return { path: prepared.path, index: prepared.index, content: prepared.content, dryRun: false };
@@ -2683,9 +3259,9 @@ var validateContextProposal = (proposal, { root } = {}) => {
2683
3259
  checkString(errors, proposal, "summary", 10, 240);
2684
3260
  checkString(errors, proposal, "scope", 2, 120);
2685
3261
  checkString(errors, proposal, "contentMarkdown", 20, 1e4);
2686
- if (!Object.hasOwn(KINDS, proposal.kind)) errors.push(`kind must be one of: ${Object.keys(KINDS).join(", ")}`);
3262
+ if (!Object.hasOwn(KINDS2, proposal.kind)) errors.push(`kind must be one of: ${Object.keys(KINDS2).join(", ")}`);
2687
3263
  if (!PRIORITIES2.has(proposal.priority)) errors.push("priority is invalid");
2688
- if (!CONFIDENCE.has(proposal.confidence)) errors.push("confidence must be high or medium");
3264
+ if (!CONFIDENCE2.has(proposal.confidence)) errors.push("confidence must be high or medium");
2689
3265
  if (!Array.isArray(proposal.tags) || proposal.tags.length < 1 || proposal.tags.length > 10) errors.push("tags must contain between 1 and 10 values");
2690
3266
  else {
2691
3267
  if (new Set(proposal.tags).size !== proposal.tags.length) errors.push("tags must be unique");
@@ -2693,25 +3269,43 @@ var validateContextProposal = (proposal, { root } = {}) => {
2693
3269
  }
2694
3270
  if (!Array.isArray(proposal.evidence) || proposal.evidence.length < 1 || proposal.evidence.length > 20) errors.push("evidence must contain between 1 and 20 entries");
2695
3271
  else for (const [index, item] of proposal.evidence.entries()) {
2696
- if (!item || typeof item !== "object" || Array.isArray(item) || Object.keys(item).some((key) => !["path", "note"].includes(key))) {
3272
+ const allowed = ["type", "path", "url", "note", "title", "version", "retrievedAt", "publishedAt", "decisionId", "decidedAt"];
3273
+ if (!item || typeof item !== "object" || Array.isArray(item) || Object.keys(item).some((key) => !allowed.includes(key))) {
2697
3274
  errors.push(`evidence[${index}] is invalid`);
2698
3275
  continue;
2699
3276
  }
2700
- if (typeof item.path !== "string" || !item.path || item.path.length > 300 || path6.isAbsolute(item.path)) errors.push(`evidence[${index}].path must be repository-relative`);
3277
+ const type = item.type ?? "repository";
3278
+ if (!["repository", "decision", "external"].includes(type)) errors.push(`evidence[${index}].type is invalid`);
3279
+ if (type === "external") {
3280
+ try {
3281
+ const url = new URL(item.url);
3282
+ if (url.protocol !== "https:" || url.username || url.password) errors.push(`evidence[${index}].url must be an https URL without credentials`);
3283
+ } catch {
3284
+ errors.push(`evidence[${index}].url must be an absolute https URL`);
3285
+ }
3286
+ } else if (typeof item.path !== "string" || !item.path || item.path.length > 300 || path7.isAbsolute(item.path)) {
3287
+ errors.push(`evidence[${index}].path must be repository-relative`);
3288
+ }
2701
3289
  if (typeof item.note !== "string" || item.note.trim().length < 5 || item.note.length > 300) errors.push(`evidence[${index}].note is invalid`);
2702
3290
  }
3291
+ if (Array.isArray(proposal.evidence) && !proposal.evidence.some((item) => ["repository", "decision"].includes(item?.type ?? "repository"))) {
3292
+ errors.push("evidence must include at least one repository or decision source");
3293
+ }
2703
3294
  if (proposal.reviewWhen !== void 0 && (!Array.isArray(proposal.reviewWhen) || proposal.reviewWhen.length > 5 || proposal.reviewWhen.some((item) => typeof item !== "string" || item.trim().length < 5 || item.length > 240))) {
2704
3295
  errors.push("reviewWhen must contain up to 5 non-empty strings");
2705
3296
  }
3297
+ if (proposal.reviewAfter !== void 0 && !/^\d{4}-\d{2}-\d{2}$/.test(proposal.reviewAfter)) errors.push("reviewAfter must use YYYY-MM-DD");
3298
+ if (proposal.aliases !== void 0 && (!Array.isArray(proposal.aliases) || proposal.aliases.length > 20 || proposal.aliases.some((item) => typeof item !== "string" || item.trim().length < 2))) errors.push("aliases must contain up to 20 non-empty values");
3299
+ if (proposal.supersedes !== void 0 && (!Array.isArray(proposal.supersedes) || proposal.supersedes.length > 20 || proposal.supersedes.some((item) => typeof item !== "string" || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(item)))) errors.push("supersedes must contain valid context ids");
2706
3300
  const combined = JSON.stringify(proposal);
2707
3301
  if (combined.includes("codex-agent:context:start") || combined.includes("codex-agent:context:end")) errors.push("proposal must not contain managed marker text");
2708
3302
  if (containsSensitiveContent(combined)) errors.push("proposal appears to contain a secret or credential");
2709
3303
  if (root && Array.isArray(proposal.evidence)) {
2710
- const projectRoot = fs6.realpathSync(path6.resolve(root));
3304
+ const projectRoot = fs7.realpathSync(path7.resolve(root));
2711
3305
  for (const [index, item] of proposal.evidence.entries()) {
2712
- if (!item || typeof item.path !== "string" || path6.isAbsolute(item.path)) continue;
2713
- const target = path6.resolve(projectRoot, item.path);
2714
- if (target !== projectRoot && !target.startsWith(`${projectRoot}${path6.sep}`)) {
3306
+ if (!item || item.type === "external" || typeof item.path !== "string" || path7.isAbsolute(item.path)) continue;
3307
+ const target = path7.resolve(projectRoot, item.path);
3308
+ if (target !== projectRoot && !target.startsWith(`${projectRoot}${path7.sep}`)) {
2715
3309
  errors.push(`evidence[${index}].path escapes the repository`);
2716
3310
  continue;
2717
3311
  }
@@ -2721,7 +3315,11 @@ var validateContextProposal = (proposal, { root } = {}) => {
2721
3315
  errors.push(`evidence[${index}].path must not traverse a symbolic link`);
2722
3316
  continue;
2723
3317
  }
2724
- if (!fs6.existsSync(target)) errors.push(`evidence[${index}].path does not exist: ${item.path}`);
3318
+ const relative3 = slash(path7.relative(projectRoot, target));
3319
+ if (relative3 === ".codex-agent/context" || relative3.startsWith(".codex-agent/context/") || relative3 === ".agents/context" || relative3.startsWith(".agents/context/") || relative3.startsWith(".codex-agent/sessions/") || relative3.startsWith(".codex-agent/backups/")) {
3320
+ errors.push(`evidence[${index}].path must reference primary repository evidence, not derived context`);
3321
+ } else if (!fs7.existsSync(target)) errors.push(`evidence[${index}].path does not exist: ${item.path}`);
3322
+ else if (!fs7.statSync(target).isFile()) errors.push(`evidence[${index}].path must be a file`);
2725
3323
  }
2726
3324
  }
2727
3325
  return { ok: errors.length === 0, errors };
@@ -2733,15 +3331,32 @@ var normalizeContextProposal = (proposal) => ({
2733
3331
  summary: proposal.summary.trim(),
2734
3332
  scope: proposal.scope.trim(),
2735
3333
  contentMarkdown: proposal.contentMarkdown.trim(),
2736
- evidence: proposal.evidence.map((item) => ({ path: slash(item.path), note: item.note.trim() })),
3334
+ evidence: proposal.evidence.map((item) => item.type === "external" ? {
3335
+ type: "external",
3336
+ url: item.url,
3337
+ note: item.note.trim(),
3338
+ ...item.title ? { title: item.title.trim() } : {},
3339
+ ...item.version ? { version: item.version.trim() } : {},
3340
+ ...item.retrievedAt ? { retrievedAt: item.retrievedAt } : {},
3341
+ ...item.publishedAt ? { publishedAt: item.publishedAt } : {}
3342
+ } : {
3343
+ type: item.type ?? "repository",
3344
+ path: slash(item.path),
3345
+ note: item.note.trim(),
3346
+ ...item.decisionId ? { decisionId: item.decisionId.trim() } : {},
3347
+ ...item.decidedAt ? { decidedAt: item.decidedAt } : {}
3348
+ }),
2737
3349
  tags: proposal.tags,
2738
3350
  priority: proposal.priority,
2739
3351
  confidence: proposal.confidence,
2740
- ...proposal.reviewWhen?.length ? { reviewWhen: proposal.reviewWhen.map((item) => item.trim()) } : {}
3352
+ ...proposal.reviewWhen?.length ? { reviewWhen: proposal.reviewWhen.map((item) => item.trim()) } : {},
3353
+ ...proposal.reviewAfter ? { reviewAfter: proposal.reviewAfter } : {},
3354
+ ...proposal.aliases?.length ? { aliases: unique3(proposal.aliases.map((item) => item.trim())) } : {},
3355
+ ...proposal.supersedes?.length ? { supersedes: unique3(proposal.supersedes) } : {}
2741
3356
  });
2742
3357
  var renderContextProposal = (proposal, { recordedAt = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) } = {}) => {
2743
3358
  const id = `${proposal.kind}-${slug(proposal.title)}`;
2744
- const evidence = proposal.evidence.map((item) => `- ${mdCode2(item.path)} \u2014 ${safeText2(item.note)}`).join("\n");
3359
+ const evidence = proposal.evidence.map((item) => `- ${mdCode2(item.type === "external" ? item.url : item.path)} \u2014 ${safeText2(item.note)}`).join("\n");
2745
3360
  const review = proposal.reviewWhen?.length ? `
2746
3361
 
2747
3362
  ## Review when
@@ -2804,31 +3419,62 @@ var prepareContextProposal = ({ root, proposal, apply, update }) => {
2804
3419
  const rendered = renderContextProposal(normalized);
2805
3420
  const writable = assertWritableContextCatalog({ root: projectRoot });
2806
3421
  const contextRoot = writable.contextRoot;
2807
- const relativePath = `${KINDS[normalized.kind]}/${slug(normalized.title)}.md`;
2808
- const destination = path6.join(contextRoot, ...relativePath.split("/"));
3422
+ const relativePath = `${KINDS2[normalized.kind]}/${slug(normalized.title)}.md`;
3423
+ const destination = path7.join(contextRoot, ...relativePath.split("/"));
2809
3424
  assertInside(contextRoot, destination, "context destination");
2810
3425
  assertNoSymlink(projectRoot, contextRoot, "Canonical context root");
2811
3426
  assertNoSymlink(projectRoot, destination, "Context destination");
2812
3427
  const indexPath = writable.indexPath;
2813
- const index = writable.index;
2814
- const currentIndexContent = fs6.existsSync(indexPath) ? fs6.readFileSync(indexPath, "utf8") : null;
2815
- const duplicate = index.entries.find((entry) => entry.path !== relativePath && (entry.id === rendered.id || normalizeForComparison(entry.summary) === normalizeForComparison(normalized.summary)));
3428
+ const index = upgradeContextIndex(writable.index);
3429
+ const currentIndexContent = fs7.existsSync(indexPath) ? fs7.readFileSync(indexPath, "utf8") : null;
3430
+ const duplicate = index.entries.find((entry) => entry.path !== relativePath && !(normalized.supersedes ?? []).includes(entry.id) && (entry.id === rendered.id || normalizeForComparison(entry.summary) === normalizeForComparison(normalized.summary)));
2816
3431
  if (duplicate) throw new Error(`Duplicate context candidate: ${duplicate.path}`);
2817
- const current = fs6.existsSync(destination) ? fs6.readFileSync(destination, "utf8") : null;
3432
+ const current = fs7.existsSync(destination) ? fs7.readFileSync(destination, "utf8") : null;
2818
3433
  const merge = mergeManaged(current, rendered, update);
2819
3434
  const priorEntry = index.entries.find((entry) => entry.path === relativePath || entry.id === rendered.id);
2820
3435
  if (priorEntry && priorEntry.path !== relativePath) throw new Error(`Context id already belongs to another path: ${priorEntry.path}`);
2821
- const nextEntry = {
3436
+ const evidence = normalized.evidence.map((item) => item.type === "external" ? {
3437
+ type: "external",
3438
+ locator: item.url,
3439
+ note: item.note,
3440
+ retrievedAt: item.retrievedAt ?? contextDate(),
3441
+ ...item.title ? { title: item.title } : {},
3442
+ ...item.version ? { version: item.version } : {},
3443
+ ...item.publishedAt ? { publishedAt: item.publishedAt } : {}
3444
+ } : {
3445
+ type: item.type,
3446
+ locator: item.path,
3447
+ note: item.note,
3448
+ sha256: sha256(fs7.readFileSync(path7.resolve(projectRoot, item.path))),
3449
+ ...item.decisionId ? { decisionId: item.decisionId } : {},
3450
+ ...item.decidedAt ? { decidedAt: item.decidedAt } : {}
3451
+ });
3452
+ const nextEntry = upgradeContextIndexEntry({
3453
+ ...priorEntry,
2822
3454
  id: rendered.id,
2823
3455
  path: relativePath,
2824
3456
  summary: normalized.summary,
2825
- tags: unique2([normalized.kind, ...normalized.tags]).slice(0, 10),
2826
- priority: normalized.priority
2827
- };
3457
+ tags: unique3([normalized.kind, ...normalized.tags]).slice(0, 10),
3458
+ priority: normalized.priority,
3459
+ kind: normalized.kind,
3460
+ scope: normalized.scope,
3461
+ confidence: normalized.confidence,
3462
+ status: "active",
3463
+ lastVerifiedAt: contextDate(),
3464
+ evidence,
3465
+ ...normalized.reviewAfter ? { reviewAfter: normalized.reviewAfter } : {},
3466
+ ...normalized.aliases?.length ? { aliases: normalized.aliases } : {},
3467
+ ...normalized.supersedes?.length ? { supersedes: normalized.supersedes } : {}
3468
+ });
3469
+ for (const supersededId of normalized.supersedes ?? []) {
3470
+ if (supersededId === rendered.id) throw new Error("Context proposal must not supersede itself");
3471
+ if (!index.entries.some((entry) => entry.id === supersededId)) throw new Error(`Context proposal supersedes unknown id: ${supersededId}`);
3472
+ }
3473
+ const supersededEntries = index.entries.map((entry) => (normalized.supersedes ?? []).includes(entry.id) ? upgradeContextIndexEntry({ ...entry, status: "superseded", supersededBy: unique3([...entry.supersededBy ?? [], rendered.id]) }) : entry);
2828
3474
  const nextIndex = {
2829
3475
  ...index.$schema ? { $schema: index.$schema } : {},
2830
- version: 1,
2831
- entries: [...index.entries.filter((entry) => entry.path !== relativePath && entry.id !== rendered.id), nextEntry].sort((left, right) => left.path.localeCompare(right.path))
3476
+ version: 2,
3477
+ entries: [...supersededEntries.filter((entry) => entry.path !== relativePath && entry.id !== rendered.id), nextEntry].sort((left, right) => left.path.localeCompare(right.path))
2832
3478
  };
2833
3479
  assertValidContextIndex(nextIndex, { root: projectRoot, contextRoot, pendingPaths: [relativePath] });
2834
3480
  const indexContent = `${JSON.stringify(nextIndex, null, 2)}
@@ -2886,16 +3532,16 @@ var option = (args, name, fallback) => {
2886
3532
  var main = (args = process.argv.slice(2)) => {
2887
3533
  const proposalFile = option(args, "--proposal");
2888
3534
  if (!proposalFile) throw new Error("context save requires --proposal FILE");
2889
- const absolute = path6.resolve(proposalFile);
2890
- if (!fs6.existsSync(absolute)) throw new Error(`Proposal file not found: ${absolute}`);
3535
+ const absolute = path7.resolve(proposalFile);
3536
+ if (!fs7.existsSync(absolute)) throw new Error(`Proposal file not found: ${absolute}`);
2891
3537
  let proposal;
2892
3538
  try {
2893
- proposal = JSON.parse(fs6.readFileSync(absolute, "utf8"));
3539
+ proposal = JSON.parse(fs7.readFileSync(absolute, "utf8"));
2894
3540
  } catch (error) {
2895
3541
  throw new Error(`Could not parse proposal file: ${error instanceof Error ? error.message : String(error)}`);
2896
3542
  }
2897
3543
  const result = saveContextProposal({
2898
- root: path6.resolve(option(args, "--root", process.cwd())),
3544
+ root: path7.resolve(option(args, "--root", process.cwd())),
2899
3545
  proposal,
2900
3546
  apply: args.includes("--apply"),
2901
3547
  update: args.includes("--update")
@@ -2904,7 +3550,7 @@ var main = (args = process.argv.slice(2)) => {
2904
3550
  `);
2905
3551
  if (result.conflicts.length) process.exitCode = 2;
2906
3552
  };
2907
- if (process.argv[1] && path6.basename(process.argv[1]) === "context-save.mjs" && import.meta.url === pathToFileURL(process.argv[1]).href) {
3553
+ if (process.argv[1] && path7.basename(process.argv[1]) === "context-save.mjs" && import.meta.url === pathToFileURL(process.argv[1]).href) {
2908
3554
  try {
2909
3555
  main();
2910
3556
  } catch (error) {
@@ -2915,8 +3561,8 @@ if (process.argv[1] && path6.basename(process.argv[1]) === "context-save.mjs" &&
2915
3561
  }
2916
3562
 
2917
3563
  // ../../plugins/codex-agent/skills/context-curation/scripts/navigation-migrate.mjs
2918
- import fs7 from "node:fs";
2919
- import path7 from "node:path";
3564
+ import fs8 from "node:fs";
3565
+ import path8 from "node:path";
2920
3566
  import { pathToFileURL as pathToFileURL2 } from "node:url";
2921
3567
  var PRIORITIES3 = /* @__PURE__ */ new Set(["critical", "high", "medium", "low"]);
2922
3568
  var MAX_FILES = 1e3;
@@ -2924,52 +3570,52 @@ var MAX_FILE_BYTES = 512 * 1024;
2924
3570
  var MAX_TOTAL_BYTES = 20 * 1024 * 1024;
2925
3571
  var MANAGED_START = (id) => `<!-- codex-agent:migrated:start ${id} -->`;
2926
3572
  var MANAGED_END = (id) => `<!-- codex-agent:migrated:end ${id} -->`;
2927
- var unique3 = (items) => [...new Set(items.filter(Boolean))];
3573
+ var unique4 = (items) => [...new Set(items.filter(Boolean))];
2928
3574
  var safeText3 = (value, limit = 300) => String(value).replace(/[\r\n]+/g, " ").trim().slice(0, limit);
2929
- var readJson2 = (file, label) => {
3575
+ var readJson3 = (file, label) => {
2930
3576
  try {
2931
- return JSON.parse(fs7.readFileSync(file, "utf8"));
3577
+ return JSON.parse(fs8.readFileSync(file, "utf8"));
2932
3578
  } catch (error) {
2933
3579
  throw new Error(`Invalid ${label}: ${error instanceof Error ? error.message : String(error)}`);
2934
3580
  }
2935
3581
  };
2936
- var hasNavigation = (directory) => fs7.existsSync(path7.join(directory, "navigation.md")) || fs7.existsSync(path7.join(directory, "index.md"));
3582
+ var hasNavigation = (directory) => fs8.existsSync(path8.join(directory, "navigation.md")) || fs8.existsSync(path8.join(directory, "index.md"));
2937
3583
  var discoverNavigationContext = ({ source }) => {
2938
3584
  if (!source) throw new Error("navigation migration requires --from PATH");
2939
- const requested = path7.resolve(source);
2940
- if (!fs7.existsSync(requested)) throw new Error(`Migration source not found: ${requested}`);
2941
- if (!fs7.statSync(requested).isDirectory()) throw new Error("Navigation migration source must be a directory");
2942
- const sourceRoot = fs7.realpathSync(requested);
3585
+ const requested = path8.resolve(source);
3586
+ if (!fs8.existsSync(requested)) throw new Error(`Migration source not found: ${requested}`);
3587
+ if (!fs8.statSync(requested).isDirectory()) throw new Error("Navigation migration source must be a directory");
3588
+ const sourceRoot = fs8.realpathSync(requested);
2943
3589
  const candidates = [];
2944
- const configPath = path7.join(sourceRoot, ".oac.json");
2945
- if (fs7.existsSync(configPath)) {
2946
- const config = readJson2(configPath, ".oac.json");
3590
+ const configPath = path8.join(sourceRoot, ".oac.json");
3591
+ if (fs8.existsSync(configPath)) {
3592
+ const config = readJson3(configPath, ".oac.json");
2947
3593
  const configured = config?.context?.root;
2948
3594
  if (typeof configured === "string" && configured.trim()) {
2949
- if (path7.isAbsolute(configured)) throw new Error("Configured context root must be project-relative; pass a global context directory directly");
2950
- const target = path7.resolve(sourceRoot, configured);
3595
+ if (path8.isAbsolute(configured)) throw new Error("Configured context root must be project-relative; pass a global context directory directly");
3596
+ const target = path8.resolve(sourceRoot, configured);
2951
3597
  assertInside(sourceRoot, target, "Configured context root");
2952
3598
  candidates.push({ path: target, detectedBy: ".oac.json" });
2953
3599
  }
2954
3600
  }
2955
3601
  candidates.push(
2956
- { path: path7.join(sourceRoot, ".claude", "context"), detectedBy: ".claude/context" },
2957
- { path: path7.join(sourceRoot, "context"), detectedBy: "context" },
2958
- { path: path7.join(sourceRoot, ".opencode", "context"), detectedBy: ".opencode/context" },
3602
+ { path: path8.join(sourceRoot, ".claude", "context"), detectedBy: ".claude/context" },
3603
+ { path: path8.join(sourceRoot, "context"), detectedBy: "context" },
3604
+ { path: path8.join(sourceRoot, ".opencode", "context"), detectedBy: ".opencode/context" },
2959
3605
  { path: sourceRoot, detectedBy: "source directory" }
2960
3606
  );
2961
- const selected = candidates.find((candidate, index) => candidates.findIndex((item) => item.path === candidate.path) === index && fs7.existsSync(candidate.path) && fs7.statSync(candidate.path).isDirectory() && hasNavigation(candidate.path));
3607
+ const selected = candidates.find((candidate, index) => candidates.findIndex((item) => item.path === candidate.path) === index && fs8.existsSync(candidate.path) && fs8.statSync(candidate.path).isDirectory() && hasNavigation(candidate.path));
2962
3608
  if (!selected) throw new Error("Could not find a navigation-based context root. Pass the context directory directly or provide a valid .oac.json context.root");
2963
3609
  assertNoSymlink(sourceRoot, selected.path, "Context root");
2964
- const contextRoot = fs7.realpathSync(selected.path);
3610
+ const contextRoot = fs8.realpathSync(selected.path);
2965
3611
  assertInside(sourceRoot, contextRoot, "Context root");
2966
3612
  const manifestCandidates = [
2967
- path7.join(contextRoot, ".context-manifest.json"),
2968
- path7.join(path7.dirname(contextRoot), ".context-manifest.json"),
2969
- path7.join(sourceRoot, ".context-manifest.json")
3613
+ path8.join(contextRoot, ".context-manifest.json"),
3614
+ path8.join(path8.dirname(contextRoot), ".context-manifest.json"),
3615
+ path8.join(sourceRoot, ".context-manifest.json")
2970
3616
  ];
2971
- const manifestPath = unique3(manifestCandidates).find((file) => fs7.existsSync(file));
2972
- const manifest = manifestPath ? readJson2(manifestPath, ".context-manifest.json") : null;
3617
+ const manifestPath = unique4(manifestCandidates).find((file) => fs8.existsSync(file));
3618
+ const manifest = manifestPath ? readJson3(manifestPath, ".context-manifest.json") : null;
2973
3619
  return {
2974
3620
  sourceRoot,
2975
3621
  contextRoot,
@@ -2987,16 +3633,16 @@ var walkMarkdown = (root) => {
2987
3633
  const skipped = [];
2988
3634
  let totalBytes = 0;
2989
3635
  const visit = (directory) => {
2990
- for (const entry of fs7.readdirSync(directory, { withFileTypes: true })) {
2991
- const absolute = path7.join(directory, entry.name);
3636
+ for (const entry of fs8.readdirSync(directory, { withFileTypes: true })) {
3637
+ const absolute = path8.join(directory, entry.name);
2992
3638
  if (entry.isSymbolicLink()) {
2993
- skipped.push({ source: slash(path7.relative(root, absolute)), reason: "symbolic-link" });
3639
+ skipped.push({ source: slash(path8.relative(root, absolute)), reason: "symbolic-link" });
2994
3640
  continue;
2995
3641
  }
2996
3642
  if (entry.isDirectory()) visit(absolute);
2997
3643
  else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
2998
- const size = fs7.statSync(absolute).size;
2999
- if (size > MAX_FILE_BYTES) throw new Error(`Source context file exceeds ${MAX_FILE_BYTES} bytes: ${slash(path7.relative(root, absolute))}`);
3644
+ const size = fs8.statSync(absolute).size;
3645
+ if (size > MAX_FILE_BYTES) throw new Error(`Source context file exceeds ${MAX_FILE_BYTES} bytes: ${slash(path8.relative(root, absolute))}`);
3000
3646
  totalBytes += size;
3001
3647
  if (totalBytes > MAX_TOTAL_BYTES) throw new Error(`Source context exceeds ${MAX_TOTAL_BYTES} bytes`);
3002
3648
  files.push(absolute);
@@ -3032,9 +3678,9 @@ var runtimeReferenceCount = (content) => {
3032
3678
  ];
3033
3679
  return patterns.reduce((total, pattern) => total + (content.match(pattern)?.length ?? 0), 0);
3034
3680
  };
3035
- var classify = ({ relative: relative2, content, includeNavigation, includeTemplates, includeWorkflows }) => {
3036
- const normalized = relative2.toLowerCase();
3037
- const basename = path7.posix.basename(normalized);
3681
+ var classify = ({ relative: relative3, content, includeNavigation, includeTemplates, includeWorkflows }) => {
3682
+ const normalized = relative3.toLowerCase();
3683
+ const basename = path8.posix.basename(normalized);
3038
3684
  const segments = normalized.split("/");
3039
3685
  if (!includeNavigation && (["navigation.md", "index.md"].includes(basename) || basename.endsWith("-navigation.md"))) return "navigation";
3040
3686
  if (/deprecated/i.test(content.slice(0, 1200))) return "deprecated";
@@ -3056,9 +3702,9 @@ var summary = (content, title) => {
3056
3702
  return safeText3(paragraphs[0] || `${title} migrated project context.`, 240);
3057
3703
  };
3058
3704
  var rewriteContent = (content) => content.replace(/\[([^\]]+)\]\([^)]*navigation\.md(?:#[^)]*)?\)/gi, "$1 (catalog: `.codex-agent/context/index.json`)").replace(/@?(?:\.opencode|\.claude)\/context\//g, ".codex-agent/context/migrated/").replace(/`(?:\.opencode|\.claude)\/context`/g, "`.codex-agent/context/migrated`").trim();
3059
- var entryTags = ({ relative: relative2, metadata, title }) => unique3([
3705
+ var entryTags = ({ relative: relative3, metadata, title }) => unique4([
3060
3706
  "migrated",
3061
- ...relative2.replace(/\.md$/i, "").split("/"),
3707
+ ...relative3.replace(/\.md$/i, "").split("/"),
3062
3708
  ...metadata.context ? metadata.context.split(/[\/\s]+/) : [],
3063
3709
  ...title.toLowerCase().split(/[^a-z0-9_-]+/).filter((term) => term.length > 2)
3064
3710
  ].map(slug)).slice(0, 10);
@@ -3099,50 +3745,50 @@ var prepareNavigationContext = ({
3099
3745
  const discovery = discoverNavigationContext({ source });
3100
3746
  const writableCatalog = assertWritableContextCatalog({ root: projectRoot });
3101
3747
  const contextRoot = writableCatalog.contextRoot;
3102
- const destinationRoot = path7.join(contextRoot, "migrated");
3748
+ const destinationRoot = path8.join(contextRoot, "migrated");
3103
3749
  assertNoSymlink(projectRoot, contextRoot, "Target context root");
3104
3750
  const sourceWalk = walkMarkdown(discovery.contextRoot);
3105
3751
  const sourceFiles = sourceWalk.files;
3106
3752
  const skipped = [...sourceWalk.skipped];
3107
3753
  const candidates = [];
3108
3754
  for (const sourceFile of sourceFiles) {
3109
- const relative2 = slash(path7.relative(discovery.contextRoot, sourceFile));
3110
- const original = fs7.readFileSync(sourceFile, "utf8");
3755
+ const relative3 = slash(path8.relative(discovery.contextRoot, sourceFile));
3756
+ const original = fs8.readFileSync(sourceFile, "utf8");
3111
3757
  const metadata = parseMetadata(original);
3112
- const reason = containsSensitiveContent(original) ? "sensitive-content" : classify({ relative: relative2, content: metadata.content, includeNavigation, includeTemplates, includeWorkflows });
3758
+ const reason = containsSensitiveContent(original) ? "sensitive-content" : classify({ relative: relative3, content: metadata.content, includeNavigation, includeTemplates, includeWorkflows });
3113
3759
  if (reason) {
3114
- skipped.push({ source: relative2, reason });
3760
+ skipped.push({ source: relative3, reason });
3115
3761
  continue;
3116
3762
  }
3117
3763
  const transformed = rewriteContent(metadata.content);
3118
3764
  if (!transformed) {
3119
- skipped.push({ source: relative2, reason: "empty" });
3765
+ skipped.push({ source: relative3, reason: "empty" });
3120
3766
  continue;
3121
3767
  }
3122
- const destinationRelative = `migrated/${relative2}`;
3123
- const id = slug(`migrated-${relative2.replace(/\.md$/i, "")}`);
3124
- const title = firstHeading2(transformed, path7.basename(relative2, ".md"));
3768
+ const destinationRelative = `migrated/${relative3}`;
3769
+ const id = slug(`migrated-${relative3.replace(/\.md$/i, "")}`);
3770
+ const title = firstHeading2(transformed, path8.basename(relative3, ".md"));
3125
3771
  const managed = `${MANAGED_START(id)}
3126
- <!-- source: ${relative2} -->
3772
+ <!-- source: ${relative3} -->
3127
3773
  ${transformed}
3128
3774
  ${MANAGED_END(id)}`;
3129
3775
  candidates.push({
3130
3776
  sourceFile,
3131
- source: relative2,
3777
+ source: relative3,
3132
3778
  destinationRelative,
3133
- destination: path7.join(destinationRoot, ...relative2.split("/")),
3779
+ destination: path8.join(destinationRoot, ...relative3.split("/")),
3134
3780
  id,
3135
3781
  title,
3136
3782
  summary: summary(transformed, title),
3137
- tags: entryTags({ relative: relative2, metadata, title }),
3783
+ tags: entryTags({ relative: relative3, metadata, title }),
3138
3784
  priority: PRIORITIES3.has(metadata.priority) ? metadata.priority : "medium",
3139
3785
  metadata: { context: metadata.context, version: metadata.version, updated: metadata.updated },
3140
3786
  managed
3141
3787
  });
3142
3788
  }
3143
- const indexPath = path7.join(contextRoot, "index.json");
3144
- const targetIndex = writableCatalog.index;
3145
- const priorIndexContent = fs7.existsSync(indexPath) ? fs7.readFileSync(indexPath, "utf8") : null;
3789
+ const indexPath = path8.join(contextRoot, "index.json");
3790
+ const targetIndex = upgradeContextIndex(writableCatalog.index);
3791
+ const priorIndexContent = fs8.existsSync(indexPath) ? fs8.readFileSync(indexPath, "utf8") : null;
3146
3792
  const changes = [];
3147
3793
  const conflicts = [];
3148
3794
  const migrationEntries = [];
@@ -3155,7 +3801,7 @@ ${MANAGED_END(id)}`;
3155
3801
  for (const candidate of candidates) {
3156
3802
  assertInside(destinationRoot, candidate.destination, `Migration destination ${candidate.destinationRelative}`);
3157
3803
  assertNoSymlink(projectRoot, candidate.destination, "Migration destination");
3158
- const current = fs7.existsSync(candidate.destination) ? fs7.readFileSync(candidate.destination, "utf8") : null;
3804
+ const current = fs8.existsSync(candidate.destination) ? fs8.readFileSync(candidate.destination, "utf8") : null;
3159
3805
  const merge = mergeManaged2(current, candidate.id, candidate.managed, force);
3160
3806
  if (candidateIdOwners.get(candidate.id).size > 1) {
3161
3807
  conflicts.push(candidate.destinationRelative);
@@ -3195,19 +3841,21 @@ ${MANAGED_END(id)}`;
3195
3841
  before: current,
3196
3842
  content: merge.content
3197
3843
  });
3198
- migrationEntries.push({
3844
+ migrationEntries.push(upgradeContextIndexEntry({
3199
3845
  id: candidate.id,
3200
3846
  path: candidate.destinationRelative,
3201
3847
  summary: candidate.summary,
3202
3848
  tags: candidate.tags,
3203
- priority: candidate.priority
3204
- });
3849
+ priority: candidate.priority,
3850
+ kind: "imported",
3851
+ confidence: "low"
3852
+ }));
3205
3853
  }
3206
3854
  const migratingIds = new Set(migrationEntries.map((entry) => entry.id));
3207
3855
  const migratingPaths = new Set(migrationEntries.map((entry) => entry.path));
3208
3856
  const nextIndex = {
3209
3857
  ...targetIndex.$schema ? { $schema: targetIndex.$schema } : {},
3210
- version: 1,
3858
+ version: 2,
3211
3859
  entries: [
3212
3860
  ...targetIndex.entries.filter((entry) => !migratingIds.has(entry.id) && !migratingPaths.has(entry.path)),
3213
3861
  ...migrationEntries
@@ -3225,14 +3873,14 @@ ${MANAGED_END(id)}`;
3225
3873
  mode: apply ? "apply" : "preview",
3226
3874
  format: "navigation-markdown",
3227
3875
  source: {
3228
- requested: path7.resolve(source),
3876
+ requested: path8.resolve(source),
3229
3877
  contextRoot: discovery.contextRoot,
3230
3878
  detectedBy: discovery.detectedBy,
3231
3879
  manifest: discovery.manifest
3232
3880
  },
3233
3881
  changes: changes.map(({ destination, before, content, backup, ...change }) => change),
3234
3882
  skipped,
3235
- conflicts: unique3(conflicts),
3883
+ conflicts: unique4(conflicts),
3236
3884
  backedUp: [],
3237
3885
  index: { path: ".codex-agent/context/index.json", entries: nextIndex.entries.length, diff: diff2(priorIndexContent, indexContent) },
3238
3886
  applied: false,
@@ -3275,7 +3923,7 @@ var option2 = (args, name, fallback) => {
3275
3923
  };
3276
3924
  var main2 = (args = process.argv.slice(2)) => {
3277
3925
  const result = migrateNavigationContext({
3278
- root: path7.resolve(option2(args, "--root", process.cwd())),
3926
+ root: path8.resolve(option2(args, "--root", process.cwd())),
3279
3927
  source: option2(args, "--from"),
3280
3928
  apply: args.includes("--apply"),
3281
3929
  force: args.includes("--force"),
@@ -3287,7 +3935,7 @@ var main2 = (args = process.argv.slice(2)) => {
3287
3935
  `);
3288
3936
  if (result.conflicts.length) process.exitCode = 2;
3289
3937
  };
3290
- if (process.argv[1] && path7.basename(process.argv[1]) === "navigation-migrate.mjs" && import.meta.url === pathToFileURL2(process.argv[1]).href) {
3938
+ if (process.argv[1] && path8.basename(process.argv[1]) === "navigation-migrate.mjs" && import.meta.url === pathToFileURL2(process.argv[1]).href) {
3291
3939
  try {
3292
3940
  main2();
3293
3941
  } catch (error) {
@@ -3297,13 +3945,155 @@ if (process.argv[1] && path7.basename(process.argv[1]) === "navigation-migrate.m
3297
3945
  }
3298
3946
  }
3299
3947
 
3948
+ // ../../plugins/codex-agent/skills/context-lint/scripts/context-lint.mjs
3949
+ import fs9 from "node:fs";
3950
+ import path9 from "node:path";
3951
+ import { pathToFileURL as pathToFileURL3 } from "node:url";
3952
+ var healthOrder = ["conflict", "orphan", "duplicate", "insufficient-evidence", "review-due", "healthy"];
3953
+ var normalize = (value) => String(value ?? "").normalize("NFKC").toLocaleLowerCase("en-US").replace(/\s+/g, " ").trim();
3954
+ var findingSort = (left, right) => (left.path ?? "").localeCompare(right.path ?? "") || (left.id ?? "").localeCompare(right.id ?? "") || left.code.localeCompare(right.code);
3955
+ var healthFor = (reasons) => healthOrder.find((health) => reasons.includes(health)) ?? "healthy";
3956
+ var lintContext = ({ root, strict = false, now = /* @__PURE__ */ new Date() }) => {
3957
+ const checkedAt = now.toISOString();
3958
+ const resolution = resolveContextCatalog({ root });
3959
+ const findings = [];
3960
+ if (["invalid", "both-divergent"].includes(resolution.state)) {
3961
+ for (const message of resolution.errors.length ? resolution.errors : resolution.warnings) {
3962
+ findings.push({ severity: "error", code: "catalog-invalid", message });
3963
+ }
3964
+ return {
3965
+ ok: false,
3966
+ healthy: false,
3967
+ checkedAt,
3968
+ state: resolution.state,
3969
+ summary: { entries: 0, healthy: 0, findings: findings.length, errors: findings.length, warnings: 0 },
3970
+ entries: [],
3971
+ findings: findings.sort(findingSort)
3972
+ };
3973
+ }
3974
+ const catalog = getReadableContextCatalog({ root });
3975
+ if (!catalog.index) {
3976
+ findings.push({ severity: "warning", code: "catalog-missing", message: "context index not found" });
3977
+ return {
3978
+ ok: !strict,
3979
+ healthy: false,
3980
+ checkedAt,
3981
+ state: catalog.state,
3982
+ summary: { entries: 0, healthy: 0, findings: 1, errors: 0, warnings: 1 },
3983
+ entries: [],
3984
+ findings
3985
+ };
3986
+ }
3987
+ const reasonsById = new Map(catalog.index.entries.map((entry) => [entry.id, /* @__PURE__ */ new Set()]));
3988
+ const add = (severity, code, entry, message, health = null) => {
3989
+ findings.push({ severity, code, ...entry?.id ? { id: entry.id } : {}, ...entry?.path ? { path: entry.path } : {}, message });
3990
+ if (health && entry?.id) reasonsById.get(entry.id)?.add(health);
3991
+ };
3992
+ const today = checkedAt.slice(0, 10);
3993
+ for (const entry of catalog.index.entries) {
3994
+ if (catalog.index.version === 1 || !entry.evidence?.length) {
3995
+ add("warning", "evidence-missing", entry, "entry has no machine-verifiable provenance", "insufficient-evidence");
3996
+ }
3997
+ if (entry.status === "conflicted") add("error", "lifecycle-conflicted", entry, "entry is marked conflicted", "conflict");
3998
+ if (entry.reviewAfter && entry.reviewAfter <= today) add("warning", "review-due", entry, `entry review was due on ${entry.reviewAfter}`, "review-due");
3999
+ const localEvidence = (entry.evidence ?? []).filter((evidence) => ["repository", "decision"].includes(evidence.type));
4000
+ if (entry.evidence?.length && !localEvidence.length) {
4001
+ add("warning", "evidence-derived-only", entry, "external evidence alone cannot establish repository-specific context", "insufficient-evidence");
4002
+ }
4003
+ for (const evidence of localEvidence) {
4004
+ const target = path9.resolve(catalog.root, evidence.locator);
4005
+ const relative3 = slash(path9.relative(catalog.root, target));
4006
+ if (relative3.startsWith("../") || path9.isAbsolute(relative3)) {
4007
+ add("error", "evidence-escape", entry, `evidence escapes repository: ${evidence.locator}`, "insufficient-evidence");
4008
+ continue;
4009
+ }
4010
+ if (relative3 === ".codex-agent/context" || relative3.startsWith(".codex-agent/context/") || relative3 === ".agents/context" || relative3.startsWith(".agents/context/")) {
4011
+ add("warning", "evidence-derived", entry, `evidence points to derived context: ${evidence.locator}`, "insufficient-evidence");
4012
+ continue;
4013
+ }
4014
+ let stat;
4015
+ try {
4016
+ assertNoSymlink(catalog.root, target, `context evidence ${evidence.locator}`);
4017
+ } catch {
4018
+ add("error", "evidence-invalid-file", entry, `evidence traverses a symbolic link: ${evidence.locator}`, "insufficient-evidence");
4019
+ continue;
4020
+ }
4021
+ try {
4022
+ stat = fs9.lstatSync(target);
4023
+ } catch {
4024
+ stat = null;
4025
+ }
4026
+ if (!stat) add("error", "evidence-missing-file", entry, `evidence file is missing: ${evidence.locator}`, "insufficient-evidence");
4027
+ else if (stat.isSymbolicLink() || !stat.isFile()) add("error", "evidence-invalid-file", entry, `evidence is not a regular file: ${evidence.locator}`, "insufficient-evidence");
4028
+ else if (sha256(fs9.readFileSync(target)) !== evidence.sha256) add("error", "evidence-digest-mismatch", entry, `evidence changed: ${evidence.locator}`, "conflict");
4029
+ }
4030
+ for (const relatedId of entry.related ?? []) {
4031
+ const related = catalog.index.entries.find((item) => item.id === relatedId);
4032
+ if (related && !(related.related ?? []).includes(entry.id)) add("warning", "relation-asymmetric", entry, `related link is not reciprocal: ${relatedId}`, "conflict");
4033
+ }
4034
+ }
4035
+ const summaryOwners = /* @__PURE__ */ new Map();
4036
+ const contentOwners = /* @__PURE__ */ new Map();
4037
+ for (const entry of catalog.index.entries.filter((item) => item.status !== "superseded")) {
4038
+ const summaryKey = normalize(entry.summary);
4039
+ const contentKey = sha256(fs9.readFileSync(path9.join(catalog.contextRoot, ...entry.path.split("/"))));
4040
+ for (const [key, owners, label] of [[summaryKey, summaryOwners, "summary"], [contentKey, contentOwners, "content"]]) {
4041
+ const owner = owners.get(key);
4042
+ if (owner && !(entry.supersedes ?? []).includes(owner.id) && !(owner.supersedes ?? []).includes(entry.id)) {
4043
+ add("warning", `duplicate-${label}`, entry, `${label} duplicates ${owner.id}`, "duplicate");
4044
+ reasonsById.get(owner.id)?.add("duplicate");
4045
+ } else if (!owner) owners.set(key, entry);
4046
+ }
4047
+ }
4048
+ const indexed = new Set(catalog.index.entries.map((entry) => entry.path));
4049
+ for (const file of listTreeFiles(catalog.contextRoot).filter((item) => item.relative.toLowerCase().endsWith(".md"))) {
4050
+ if (!indexed.has(file.relative)) add("warning", "orphan-document", { path: file.relative }, "Markdown document is not indexed");
4051
+ }
4052
+ const entries = catalog.index.entries.map((entry) => {
4053
+ const reasons = [...reasonsById.get(entry.id)].sort((left, right) => healthOrder.indexOf(left) - healthOrder.indexOf(right));
4054
+ return { id: entry.id, path: entry.path, lifecycle: entry.status ?? "active", health: healthFor(reasons), reasons };
4055
+ }).sort((left, right) => left.path.localeCompare(right.path));
4056
+ const sortedFindings = findings.sort(findingSort);
4057
+ const errors = sortedFindings.filter((finding) => finding.severity === "error").length;
4058
+ const warnings = sortedFindings.filter((finding) => finding.severity === "warning").length;
4059
+ const healthy = entries.length > 0 && entries.every((entry) => entry.health === "healthy") && warnings === 0 && errors === 0;
4060
+ return {
4061
+ ok: errors === 0 && (!strict || warnings === 0),
4062
+ healthy,
4063
+ checkedAt,
4064
+ state: catalog.state,
4065
+ summary: { entries: entries.length, healthy: entries.filter((entry) => entry.health === "healthy").length, findings: sortedFindings.length, errors, warnings },
4066
+ entries,
4067
+ findings: sortedFindings
4068
+ };
4069
+ };
4070
+ var option3 = (args, name, fallback) => {
4071
+ const index = args.indexOf(name);
4072
+ return index >= 0 && args[index + 1] ? args[index + 1] : fallback;
4073
+ };
4074
+ var main3 = (args = process.argv.slice(2)) => {
4075
+ const result = lintContext({ root: path9.resolve(option3(args, "--root", process.cwd())), strict: args.includes("--strict") });
4076
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
4077
+ `);
4078
+ if (!result.ok) process.exitCode = 1;
4079
+ };
4080
+ if (process.argv[1] && path9.basename(process.argv[1]) === "context-lint.mjs" && import.meta.url === pathToFileURL3(process.argv[1]).href) {
4081
+ try {
4082
+ main3();
4083
+ } catch (error) {
4084
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}
4085
+ `);
4086
+ process.exitCode = 1;
4087
+ }
4088
+ }
4089
+
3300
4090
  // src/core.mjs
3301
4091
  var listFiles = (root) => {
3302
- if (!fs8.existsSync(root)) return [];
4092
+ if (!fs10.existsSync(root)) return [];
3303
4093
  const files = [];
3304
4094
  const visit = (directory) => {
3305
- for (const entry of fs8.readdirSync(directory, { withFileTypes: true })) {
3306
- const absolute = path8.join(directory, entry.name);
4095
+ for (const entry of fs10.readdirSync(directory, { withFileTypes: true })) {
4096
+ const absolute = path10.join(directory, entry.name);
3307
4097
  if (entry.isDirectory()) visit(absolute);
3308
4098
  else if (entry.isFile()) files.push(absolute);
3309
4099
  }
@@ -3313,41 +4103,41 @@ var listFiles = (root) => {
3313
4103
  };
3314
4104
  var prepareContextMigration = ({ root, source, dryRun = false, force = false }) => {
3315
4105
  if (!source) throw new Error("migrate requires --from PATH");
3316
- const projectRoot = path8.resolve(root);
3317
- const sourceRoot = path8.resolve(source);
3318
- if (!fs8.existsSync(sourceRoot)) throw new Error(`Migration source not found: ${sourceRoot}`);
3319
- const sourceFiles = (fs8.statSync(sourceRoot).isDirectory() ? listFiles(sourceRoot) : [sourceRoot]).filter((file) => file.endsWith(".md"));
4106
+ const projectRoot = path10.resolve(root);
4107
+ const sourceRoot = path10.resolve(source);
4108
+ if (!fs10.existsSync(sourceRoot)) throw new Error(`Migration source not found: ${sourceRoot}`);
4109
+ const sourceFiles = (fs10.statSync(sourceRoot).isDirectory() ? listFiles(sourceRoot) : [sourceRoot]).filter((file) => file.endsWith(".md"));
3320
4110
  if (!sourceFiles.length) throw new Error("Migration source contains no Markdown context files.");
3321
4111
  const writableCatalog = assertWritableContextCatalog({ root: projectRoot });
3322
4112
  const catalogProjectRoot = writableCatalog.root;
3323
4113
  const contextRoot = writableCatalog.contextRoot;
3324
4114
  if (!contextRoot) throw new Error("Writable context catalog did not resolve a destination root.");
3325
- const destinationRoot = path8.join(contextRoot, "imported");
4115
+ const destinationRoot = path10.join(contextRoot, "imported");
3326
4116
  const result = { imported: [], unchanged: [], conflicts: [], backedUp: [], dryRun, applied: false };
3327
4117
  const documents = [];
3328
4118
  const backupPaths = [];
3329
4119
  for (const sourceFile of sourceFiles) {
3330
- const relative2 = fs8.statSync(sourceRoot).isDirectory() ? path8.relative(sourceRoot, sourceFile) : path8.basename(sourceFile);
3331
- const destination = path8.join(destinationRoot, relative2);
3332
- const content = fs8.readFileSync(sourceFile);
4120
+ const relative3 = fs10.statSync(sourceRoot).isDirectory() ? path10.relative(sourceRoot, sourceFile) : path10.basename(sourceFile);
4121
+ const destination = path10.join(destinationRoot, relative3);
4122
+ const content = fs10.readFileSync(sourceFile);
3333
4123
  if (containsSensitiveContent(content.toString("utf8"))) {
3334
4124
  throw new Error(`Migration source appears to contain a secret or credential: ${sourceFile}`);
3335
4125
  }
3336
- if (!fs8.existsSync(destination)) {
3337
- result.imported.push(path8.relative(catalogProjectRoot, destination));
3338
- documents.push({ path: path8.relative(contextRoot, destination).split(path8.sep).join("/"), content: content.toString("utf8") });
4126
+ if (!fs10.existsSync(destination)) {
4127
+ result.imported.push(path10.relative(catalogProjectRoot, destination));
4128
+ documents.push({ path: path10.relative(contextRoot, destination).split(path10.sep).join("/"), content: content.toString("utf8") });
3339
4129
  continue;
3340
4130
  }
3341
- if (content.equals(fs8.readFileSync(destination))) {
3342
- result.unchanged.push(path8.relative(catalogProjectRoot, destination));
4131
+ if (content.equals(fs10.readFileSync(destination))) {
4132
+ result.unchanged.push(path10.relative(catalogProjectRoot, destination));
3343
4133
  continue;
3344
4134
  }
3345
4135
  if (!force) {
3346
- result.conflicts.push(path8.relative(catalogProjectRoot, destination));
4136
+ result.conflicts.push(path10.relative(catalogProjectRoot, destination));
3347
4137
  continue;
3348
4138
  }
3349
- result.imported.push(path8.relative(catalogProjectRoot, destination));
3350
- const documentPath = path8.relative(contextRoot, destination).split(path8.sep).join("/");
4139
+ result.imported.push(path10.relative(catalogProjectRoot, destination));
4140
+ const documentPath = path10.relative(contextRoot, destination).split(path10.sep).join("/");
3351
4141
  documents.push({ path: documentPath, content: content.toString("utf8") });
3352
4142
  backupPaths.push(documentPath);
3353
4143
  }
@@ -3359,7 +4149,7 @@ var prepareContextMigration = ({ root, source, dryRun = false, force = false })
3359
4149
  index,
3360
4150
  backupPaths: [
3361
4151
  ...backupPaths,
3362
- ...index && fs8.existsSync(index.path) ? ["index.json"] : []
4152
+ ...index && fs10.existsSync(index.path) ? ["index.json"] : []
3363
4153
  ]
3364
4154
  };
3365
4155
  };
@@ -3384,23 +4174,23 @@ var migrateContext = (options) => {
3384
4174
  var check = (checks, name, ok, detail) => checks.push({ name, ok: Boolean(ok), detail });
3385
4175
  var parseJson = (file) => {
3386
4176
  try {
3387
- return { value: JSON.parse(fs8.readFileSync(file, "utf8")) };
4177
+ return { value: JSON.parse(fs10.readFileSync(file, "utf8")) };
3388
4178
  } catch (error) {
3389
4179
  return { error: error instanceof Error ? error.message : String(error) };
3390
4180
  }
3391
4181
  };
3392
4182
  var diagnoseProject = ({ root }) => {
3393
- const projectRoot = path8.resolve(root);
4183
+ const projectRoot = path10.resolve(root);
3394
4184
  const checks = [];
3395
4185
  const nodeMajor = Number.parseInt(process.versions.node.split(".")[0], 10);
3396
4186
  check(checks, "node", nodeMajor >= 20, `Node.js ${process.versions.node}; requires 20 or newer`);
3397
- const manifest = path8.join(projectRoot, "plugins", "codex-agent", ".codex-plugin", "plugin.json");
3398
- const isSourceWorkspace = fs8.existsSync(manifest);
4187
+ const manifest = path10.join(projectRoot, "plugins", "codex-agent", ".codex-plugin", "plugin.json");
4188
+ const isSourceWorkspace = fs10.existsSync(manifest);
3399
4189
  check(checks, "mode", true, isSourceWorkspace ? "plugin source workspace" : "initialized consumer project");
3400
4190
  if (isSourceWorkspace) {
3401
- const marketplace = path8.join(projectRoot, ".agents", "plugins", "marketplace.json");
3402
- check(checks, "marketplace", fs8.existsSync(marketplace), marketplace);
3403
- if (fs8.existsSync(marketplace)) {
4191
+ const marketplace = path10.join(projectRoot, ".agents", "plugins", "marketplace.json");
4192
+ check(checks, "marketplace", fs10.existsSync(marketplace), marketplace);
4193
+ if (fs10.existsSync(marketplace)) {
3404
4194
  const parsed2 = parseJson(marketplace);
3405
4195
  check(checks, "marketplace-json", !parsed2.error, parsed2.error || parsed2.value.name);
3406
4196
  check(
@@ -3415,10 +4205,10 @@ var diagnoseProject = ({ root }) => {
3415
4205
  check(checks, "plugin-json", !parsed.error, parsed.error || parsed.value.name);
3416
4206
  check(checks, "plugin-name", parsed.value?.name === "codex-agent", parsed.value?.name || "missing");
3417
4207
  } else {
3418
- const config = path8.join(projectRoot, ".codex", "config.toml");
3419
- const agents = path8.join(projectRoot, ".codex", "agents");
4208
+ const config = path10.join(projectRoot, ".codex", "config.toml");
4209
+ const agents = path10.join(projectRoot, ".codex", "agents");
3420
4210
  const profiles = listFiles(agents).filter((file) => file.endsWith(".toml"));
3421
- check(checks, "project-config", fs8.existsSync(config), config);
4211
+ check(checks, "project-config", fs10.existsSync(config), config);
3422
4212
  check(checks, "project-agents", profiles.length === agentProfiles.length, `${profiles.length}/${agentProfiles.length} profiles in ${agents}`);
3423
4213
  }
3424
4214
  const resolvedCatalog = resolveContextCatalog({ root: projectRoot });
@@ -3429,38 +4219,38 @@ var diagnoseProject = ({ root }) => {
3429
4219
  } catch (error) {
3430
4220
  check(checks, "context-readable", false, error instanceof Error ? error.message : String(error));
3431
4221
  }
3432
- const contextIndex = readableCatalog2?.indexPath ?? (readableCatalog2?.root ? path8.join(readableCatalog2.root, "index.json") : null);
3433
- check(checks, "context-index", Boolean(contextIndex && fs8.existsSync(contextIndex)), contextIndex ?? "context index not found");
3434
- if (contextIndex && fs8.existsSync(contextIndex)) {
4222
+ const contextIndex = readableCatalog2?.indexPath ?? (readableCatalog2?.root ? path10.join(readableCatalog2.root, "index.json") : null);
4223
+ check(checks, "context-index", Boolean(contextIndex && fs10.existsSync(contextIndex)), contextIndex ?? "context index not found");
4224
+ if (contextIndex && fs10.existsSync(contextIndex)) {
3435
4225
  const parsed = parseJson(contextIndex);
3436
4226
  check(checks, "context-json", !parsed.error, parsed.error || `${parsed.value.entries?.length ?? 0} entries`);
3437
- const contextRoot = path8.dirname(contextIndex);
4227
+ const contextRoot = path10.dirname(contextIndex);
3438
4228
  const invalid = (parsed.value?.entries ?? []).filter((entry) => {
3439
- const target = path8.resolve(contextRoot, entry.path || "");
3440
- return !target.startsWith(`${contextRoot}${path8.sep}`) || !fs8.existsSync(target);
4229
+ const target = path10.resolve(contextRoot, entry.path || "");
4230
+ return !target.startsWith(`${contextRoot}${path10.sep}`) || !fs10.existsSync(target);
3441
4231
  });
3442
4232
  check(checks, "context-paths", invalid.length === 0, invalid.map((entry) => entry.path).join(", ") || "all paths valid");
3443
4233
  }
3444
4234
  if (isSourceWorkspace) {
3445
- const skillsRoot = path8.join(projectRoot, "plugins", "codex-agent", "skills");
3446
- const skillFiles = listFiles(skillsRoot).filter((file) => file.endsWith(`${path8.sep}SKILL.md`));
3447
- const skillDirectories = fs8.readdirSync(skillsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).length;
4235
+ const skillsRoot = path10.join(projectRoot, "plugins", "codex-agent", "skills");
4236
+ const skillFiles = listFiles(skillsRoot).filter((file) => file.endsWith(`${path10.sep}SKILL.md`));
4237
+ const skillDirectories = fs10.readdirSync(skillsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).length;
3448
4238
  check(checks, "skills", skillFiles.length > 0 && skillFiles.length === skillDirectories, `${skillFiles.length}/${skillDirectories} skill entrypoints`);
3449
- const agentRoot = path8.join(projectRoot, "plugins", "codex-agent", "agents");
4239
+ const agentRoot = path10.join(projectRoot, "plugins", "codex-agent", "agents");
3450
4240
  check(checks, "plugin-agents", listFiles(agentRoot).filter((file) => file.endsWith(".md")).length === agentProfiles.length, `${agentProfiles.length} canonical profiles in ${agentRoot}`);
3451
- const hooks = path8.join(projectRoot, "plugins", "codex-agent", "hooks", "hooks.json");
3452
- check(checks, "hooks", fs8.existsSync(hooks) && !parseJson(hooks).error, hooks);
4241
+ const hooks = path10.join(projectRoot, "plugins", "codex-agent", "hooks", "hooks.json");
4242
+ check(checks, "hooks", fs10.existsSync(hooks) && !parseJson(hooks).error, hooks);
3453
4243
  }
3454
4244
  return { root: projectRoot, ok: checks.every((item) => item.ok), checks };
3455
4245
  };
3456
4246
  var evaluateRouting = ({ root }) => {
3457
- const projectRoot = path8.resolve(root);
3458
- const suitePath = path8.join(projectRoot, "evals", "skill-routing.json");
3459
- if (!fs8.existsSync(suitePath)) throw new Error(`Routing suite not found: ${suitePath}`);
3460
- const suite = JSON.parse(fs8.readFileSync(suitePath, "utf8"));
3461
- const skillsRoot = path8.join(projectRoot, "plugins", "codex-agent", "skills");
4247
+ const projectRoot = path10.resolve(root);
4248
+ const suitePath = path10.join(projectRoot, "evals", "skill-routing.json");
4249
+ if (!fs10.existsSync(suitePath)) throw new Error(`Routing suite not found: ${suitePath}`);
4250
+ const suite = JSON.parse(fs10.readFileSync(suitePath, "utf8"));
4251
+ const skillsRoot = path10.join(projectRoot, "plugins", "codex-agent", "skills");
3462
4252
  const available = new Set(
3463
- fs8.readdirSync(skillsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name)
4253
+ fs10.readdirSync(skillsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name)
3464
4254
  );
3465
4255
  const ids = /* @__PURE__ */ new Set();
3466
4256
  const failures = [];
@@ -3498,13 +4288,13 @@ var evaluateRouting = ({ root }) => {
3498
4288
  return { ok: failures.length === 0, scenarios: suite.cases?.length ?? 0, skills: available.size, byKind, failures };
3499
4289
  };
3500
4290
  var evaluateBehaviorContracts = ({ root }) => {
3501
- const projectRoot = path8.resolve(root);
3502
- const suitePath = path8.join(projectRoot, "evals", "behavior-contracts.json");
3503
- if (!fs8.existsSync(suitePath)) throw new Error(`Behavior suite not found: ${suitePath}`);
3504
- const suite = JSON.parse(fs8.readFileSync(suitePath, "utf8"));
3505
- const skillsRoot = path8.join(projectRoot, "plugins", "codex-agent", "skills");
4291
+ const projectRoot = path10.resolve(root);
4292
+ const suitePath = path10.join(projectRoot, "evals", "behavior-contracts.json");
4293
+ if (!fs10.existsSync(suitePath)) throw new Error(`Behavior suite not found: ${suitePath}`);
4294
+ const suite = JSON.parse(fs10.readFileSync(suitePath, "utf8"));
4295
+ const skillsRoot = path10.join(projectRoot, "plugins", "codex-agent", "skills");
3506
4296
  const availableSkills = new Set(
3507
- fs8.readdirSync(skillsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name)
4297
+ fs10.readdirSync(skillsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name)
3508
4298
  );
3509
4299
  const availableAgents = new Set(agentProfiles.map((profile) => profile.name));
3510
4300
  const coveredSkills = /* @__PURE__ */ new Set();
@@ -3553,14 +4343,17 @@ Run "codex-agent context" to list context commands.`;
3553
4343
  var contextUsage = `Codex Agent context commands
3554
4344
 
3555
4345
  Usage:
3556
- codex-agent context init [--root PATH] [--analysis FILE] [--apply --plan-hash HASH] [--force] [--json]
3557
- codex-agent context refresh [--root PATH] [--analysis FILE] [--apply --plan-hash HASH] [--force] [--json]
4346
+ codex-agent context init [--root PATH] [--analysis FILE] [--exclude-managed PATH ...] [--apply --plan-hash HASH] [--force] [--json]
4347
+ codex-agent context refresh [--root PATH] [--analysis FILE] [--exclude-managed PATH ...] [--apply --plan-hash HASH] [--force] [--json]
3558
4348
  codex-agent context index [--root PATH] [--dry-run] [--json]
4349
+ codex-agent context lint [--root PATH] [--strict] [--json]
3559
4350
  codex-agent context save --proposal FILE [--root PATH] [--apply] [--update] [--json]
3560
4351
 
3561
- Both init and refresh preview by default. Apply the exact reviewed preview by passing --apply with its planHash.`;
4352
+ Both init and refresh show a human-readable plan by default. Approve the displayed plan; the plan hash remains an internal integrity check for apply.
4353
+ Repeat --exclude-managed for optional managed paths that must remain untouched. Supported paths: .codex/config.toml and .gitignore.`;
3562
4354
  var OPTION_KEYS = /* @__PURE__ */ new Map([
3563
4355
  ["--analysis", "analysisFile"],
4356
+ ["--exclude-managed", "excludeManaged"],
3564
4357
  ["--from", "source"],
3565
4358
  ["--plan-hash", "expectedPlanHash"],
3566
4359
  ["--proposal", "proposalFile"],
@@ -3574,22 +4367,26 @@ var FLAG_KEYS = /* @__PURE__ */ new Map([
3574
4367
  ["--include-templates", "includeTemplates"],
3575
4368
  ["--include-workflows", "includeWorkflows"],
3576
4369
  ["--json", "json"],
4370
+ ["--strict", "strict"],
3577
4371
  ["--update", "update"]
3578
4372
  ]);
3579
4373
  var parseOptions = (args, { command, options = [], flags = [] }) => {
3580
4374
  const allowedOptions = new Set(options);
3581
4375
  const allowedFlags = new Set(flags);
3582
4376
  const seen = /* @__PURE__ */ new Set();
4377
+ const repeatableOptions = /* @__PURE__ */ new Set(["--exclude-managed"]);
3583
4378
  const parsed = {};
3584
4379
  for (let index = 0; index < args.length; index += 1) {
3585
4380
  const argument = args[index];
3586
4381
  if (!argument.startsWith("-")) throw new Error(`Unexpected argument for ${command}: ${argument}`);
3587
- if (seen.has(argument)) throw new Error(`Duplicate option for ${command}: ${argument}`);
3588
- seen.add(argument);
4382
+ if (seen.has(argument) && !repeatableOptions.has(argument)) throw new Error(`Duplicate option for ${command}: ${argument}`);
4383
+ if (!repeatableOptions.has(argument)) seen.add(argument);
3589
4384
  if (allowedOptions.has(argument)) {
3590
4385
  const value = args[index + 1];
3591
4386
  if (!value || value.startsWith("-")) throw new Error(`${argument} requires a value for ${command}`);
3592
- parsed[OPTION_KEYS.get(argument)] = value;
4387
+ const key = OPTION_KEYS.get(argument);
4388
+ if (repeatableOptions.has(argument)) parsed[key] = [...parsed[key] ?? [], value];
4389
+ else parsed[key] = value;
3593
4390
  index += 1;
3594
4391
  continue;
3595
4392
  }
@@ -3599,13 +4396,13 @@ var parseOptions = (args, { command, options = [], flags = [] }) => {
3599
4396
  }
3600
4397
  throw new Error(`Unknown option for ${command}: ${argument}`);
3601
4398
  }
3602
- parsed.root = path9.resolve(parsed.root ?? process.cwd());
4399
+ parsed.root = path11.resolve(parsed.root ?? process.cwd());
3603
4400
  return parsed;
3604
4401
  };
3605
4402
  var readJsonFile = (file, label) => {
3606
- const absolute = path9.resolve(file);
3607
- if (!fs9.existsSync(absolute)) throw new Error(`${label} file not found: ${absolute}`);
3608
- return JSON.parse(fs9.readFileSync(absolute, "utf8"));
4403
+ const absolute = path11.resolve(file);
4404
+ if (!fs11.existsSync(absolute)) throw new Error(`${label} file not found: ${absolute}`);
4405
+ return JSON.parse(fs11.readFileSync(absolute, "utf8"));
3609
4406
  };
3610
4407
  var write = (value, json) => {
3611
4408
  if (json) {
@@ -3618,11 +4415,51 @@ var write = (value, json) => {
3618
4415
  else process.stdout.write(`${JSON.stringify(value, null, 2)}
3619
4416
  `);
3620
4417
  };
4418
+ var listLine = (items, empty = "None") => items.length ? items.join(", ") : empty;
4419
+ var actionLabel = (action) => ({ create: "Create", update: "Update", preserve: "Preserve", remove: "Remove", migrate: "Migrate", conflict: "Conflict" })[action] ?? action;
4420
+ var formatContextLifecyclePlan = (result) => {
4421
+ const plan = result.approvalPlan;
4422
+ if (!plan) return JSON.stringify(result, null, 2);
4423
+ const lines = [
4424
+ plan.status === "applied" ? `Context operation applied: ${plan.title}` : `Context approval plan: ${plan.title}`,
4425
+ "",
4426
+ `Outcome: ${plan.outcome}`,
4427
+ `Target: ${plan.target}`,
4428
+ `Status: ${plan.status}`,
4429
+ "",
4430
+ "Repository evidence",
4431
+ `- Project: ${plan.analysis.project}`,
4432
+ `- Languages: ${listLine(plan.analysis.languages)}`,
4433
+ `- Toolchains: ${listLine(plan.analysis.toolchains)}`,
4434
+ `- Technologies: ${listLine(plan.analysis.technologies)}`,
4435
+ `- Units: ${plan.analysis.units}; test units: ${plan.analysis.testUnits}`,
4436
+ `- Scan: ${plan.analysis.scan.files ?? "unknown"} files; ${plan.analysis.scan.truncated ? "truncated" : "complete"}`,
4437
+ "",
4438
+ "Planned changes"
4439
+ ];
4440
+ if (plan.changes.length) for (const change of plan.changes) lines.push(`- ${actionLabel(change.action)} ${change.path}`);
4441
+ else lines.push("- No file changes");
4442
+ lines.push("", "Preserved");
4443
+ lines.push(`- ${plan.preserved.manualContent}`);
4444
+ lines.push(`- ${plan.preserved.unchanged.length} managed files are unchanged.`);
4445
+ lines.push(`- Excluded managed paths: ${listLine(plan.preserved.excludedManaged)}`);
4446
+ const excluded = plan.analysis.scan.excluded;
4447
+ lines.push(`- Scan exclusions: ${listLine(excluded.slice(0, 12))}${excluded.length > 12 ? `, and ${excluded.length - 12} more` : ""}`);
4448
+ lines.push("", "Conflicts");
4449
+ if (plan.conflicts.length) for (const conflict of plan.conflicts) lines.push(`- ${conflict}`);
4450
+ else lines.push("- None");
4451
+ lines.push("", "Safeguards");
4452
+ for (const safeguard of plan.safeguards) lines.push(`- ${safeguard}`);
4453
+ lines.push("", "Residual risks");
4454
+ for (const risk of plan.residualRisks) lines.push(`- ${risk}`);
4455
+ lines.push("", `Approval: ${plan.approval}`, `Plan integrity: ${plan.integrityId}`);
4456
+ return lines.join("\n");
4457
+ };
3621
4458
  var finishWithConflicts = (result, json) => {
3622
- write(result, json);
4459
+ write(json || !result.approvalPlan ? result : formatContextLifecyclePlan(result), json);
3623
4460
  if (result.conflicts.length) process.exitCode = 2;
3624
4461
  };
3625
- var main3 = async (args) => {
4462
+ var main4 = async (args) => {
3626
4463
  const [command, ...rest] = args;
3627
4464
  if (!command || command === "help") {
3628
4465
  if (rest.length === 1 && rest[0] === "context") write(contextUsage, false);
@@ -3637,7 +4474,7 @@ var main3 = async (args) => {
3637
4474
  return;
3638
4475
  }
3639
4476
  if (contextArgs.includes("--help") || contextArgs.includes("-h")) {
3640
- if (!["init", "refresh", "index", "save"].includes(subcommand)) {
4477
+ if (!["init", "refresh", "index", "lint", "save"].includes(subcommand)) {
3641
4478
  throw new Error(`Unknown context command: ${subcommand}
3642
4479
 
3643
4480
  ${contextUsage}`);
@@ -3648,7 +4485,7 @@ ${contextUsage}`);
3648
4485
  if (subcommand === "init" || subcommand === "refresh") {
3649
4486
  const options = parseOptions(contextArgs, {
3650
4487
  command: `context ${subcommand}`,
3651
- options: ["--root", "--analysis", "--plan-hash"],
4488
+ options: ["--root", "--analysis", "--exclude-managed", "--plan-hash"],
3652
4489
  flags: ["--apply", "--force", "--json"]
3653
4490
  });
3654
4491
  const analysis = options.analysisFile ? readJsonFile(options.analysisFile, "Analysis") : null;
@@ -3667,6 +4504,17 @@ ${contextUsage}`);
3667
4504
  write({ path: result.path, entries: result.index.entries.length, dryRun: result.dryRun }, options.json);
3668
4505
  return;
3669
4506
  }
4507
+ if (subcommand === "lint") {
4508
+ const options = parseOptions(contextArgs, {
4509
+ command: "context lint",
4510
+ options: ["--root"],
4511
+ flags: ["--strict", "--json"]
4512
+ });
4513
+ const result = lintContext(options);
4514
+ write(result, options.json);
4515
+ if (!result.ok) process.exitCode = 1;
4516
+ return;
4517
+ }
3670
4518
  if (subcommand === "save") {
3671
4519
  const options = parseOptions(contextArgs, {
3672
4520
  command: "context save",
@@ -3741,7 +4589,7 @@ ${usage}`);
3741
4589
  };
3742
4590
 
3743
4591
  // bin/codex-agent.mjs
3744
- main3(process.argv.slice(2)).catch((error) => {
4592
+ main4(process.argv.slice(2)).catch((error) => {
3745
4593
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}
3746
4594
  `);
3747
4595
  process.exitCode = 1;