@codex-agent/cli 0.1.0-main.12.sha31ff9e5 → 0.1.0-main.13.sha3ad2ccf

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.
package/README.md CHANGED
@@ -9,6 +9,7 @@ npx --yes @codex-agent/cli@latest context init --json
9
9
  npx --yes @codex-agent/cli@latest context refresh --json
10
10
  npx --yes @codex-agent/cli@latest doctor --json
11
11
  npx --yes @codex-agent/cli@latest context save --proposal context-proposal.json --json
12
+ npx --yes @codex-agent/cli@latest context lint --json
12
13
  npx --yes @codex-agent/cli@latest migrate navigation --from /path/to/project --json
13
14
  npx --yes @codex-agent/cli@latest eval --json
14
15
  ```
@@ -17,4 +18,4 @@ Run these commands from the target repository. Use `npx @codex-agent/cli@latest
17
18
 
18
19
  `migrate navigation` discovers navigation-based Markdown context trees, skips incompatible runtime material by default, and writes native indexed context only with `--apply`.
19
20
 
20
- `eval` validates focused positive, negative, and overlap skill-routing fixtures plus required and forbidden behavior contracts for every bundled skill and canonical agent. It does not run end-to-end or A/B prompt comparisons.
21
+ `context lint` is read-only and checks catalog lifecycle, provenance hashes, review dates, duplicates, and orphan documents; `--strict` makes warnings fail. `eval` validates focused positive, negative, and overlap skill-routing fixtures plus required and forbidden behavior contracts for every bundled skill and canonical agent. The source workspace additionally provides opt-in model execution through `npm run eval:model`; it is not part of the published CLI or offline gate.
@@ -1,12 +1,12 @@
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 fs10 from "node:fs";
5
+ import path10 from "node:path";
6
6
 
7
7
  // src/core.mjs
8
- import fs8 from "node:fs";
9
- import path8 from "node:path";
8
+ import fs9 from "node:fs";
9
+ import path9 from "node:path";
10
10
 
11
11
  // ../../plugins/codex-agent/scripts/context-project.mjs
12
12
  import crypto4 from "node:crypto";
@@ -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",
@@ -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 {
@@ -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);
@@ -1385,11 +1567,11 @@ var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
1385
1567
  "vendor"
1386
1568
  ]);
1387
1569
  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"]
1570
+ ["architecture", "architecture/system.md", "System architecture, modules, entrypoints, and detected boundaries.", ["architecture", "modules", "entrypoints"], "high", ["modules", "entrypoints", "conventions.boundaries"]],
1571
+ ["code-quality", "standards/code-quality.md", "Detected source layout, naming, and engineering conventions.", ["code", "quality", "conventions"], "critical", ["conventions", "languages"]],
1572
+ ["testing", "standards/testing.md", "Detected test tooling, locations, and repository commands.", ["test", "verification", "commands"], "high", ["testing", "commands"]],
1573
+ ["security", "standards/security.md", "Detected security-sensitive boundaries and baseline safeguards.", ["security", "auth", "secrets"], "critical", ["security"]],
1574
+ ["project-intelligence", "project-intelligence/project.md", "Detected stack, package tooling, CI, and project intelligence.", ["project", "stack", "ci"], "medium", ["project", "packageManager", "languages", "frameworks", "ciCd"]]
1393
1575
  ];
1394
1576
  var CODEX_AGENT_IGNORE_RULES = [
1395
1577
  ".codex-agent/analysis.json",
@@ -1823,6 +2005,30 @@ var managedIndexOwnershipConflicts = (existingIndex) => {
1823
2005
  return [];
1824
2006
  });
1825
2007
  };
2008
+ var nestedValue = (value, dottedPath) => dottedPath.split(".").reduce((current, key) => current?.[key], value);
2009
+ var collectSignalEvidence = (value) => {
2010
+ if (!value || typeof value !== "object") return [];
2011
+ return [
2012
+ ...Array.isArray(value.evidence) ? value.evidence : [],
2013
+ ...Object.values(value).flatMap((item) => collectSignalEvidence(item))
2014
+ ];
2015
+ };
2016
+ var managedEvidence = (analysis, id, signalPaths) => {
2017
+ const projectRoot = path5.resolve(analysis.root);
2018
+ const locators = unique(signalPaths.flatMap((signalPath) => collectSignalEvidence(nestedValue(analysis, signalPath)))).map((locator) => String(locator).split("#")[0]).filter((locator) => locator && !path5.isAbsolute(locator));
2019
+ return unique(locators).flatMap((locator) => {
2020
+ const target = path5.resolve(projectRoot, locator);
2021
+ const relativePath = relative(projectRoot, target);
2022
+ if (relativePath.startsWith("../") || path5.isAbsolute(relativePath) || relativePath.startsWith(".codex-agent/context/") || relativePath.startsWith(".agents/context/")) return [];
2023
+ try {
2024
+ assertNoSymlink(projectRoot, target, `Managed context evidence ${locator}`);
2025
+ if (!fs5.existsSync(target) || !fs5.statSync(target).isFile()) return [];
2026
+ return [{ type: "repository", locator: relativePath, note: `Repository evidence used to generate ${id}.`, sha256: sha256(fs5.readFileSync(target)) }];
2027
+ } catch {
2028
+ return [];
2029
+ }
2030
+ }).slice(0, 20);
2031
+ };
1826
2032
  var renderProjectFiles = (analysis, existingIndex = null) => {
1827
2033
  if (containsSensitiveContent(JSON.stringify({ analysis, existingIndex }))) {
1828
2034
  throw new Error("Project context rendering input appears to contain a secret or credential");
@@ -1842,13 +2048,28 @@ var renderProjectFiles = (analysis, existingIndex = null) => {
1842
2048
  [".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
2049
  ]);
1844
2050
  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 : [];
2051
+ const upgradedIndex = upgradeContextIndex(existingIndex ?? { version: 1, entries: [] });
2052
+ const priorEntries = upgradedIndex.entries;
2053
+ const priorByPair = new Map(priorEntries.map((entry) => [`${entry.id}\0${entry.path}`, entry]));
1846
2054
  const managedPairs = new Set(MANAGED_CONTEXT.map(([id, file]) => `${id}\0${file}`));
1847
2055
  const customEntries = priorEntries.filter((entry) => !managedPairs.has(`${entry.id}\0${entry.path}`));
1848
2056
  const index = {
1849
2057
  ...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]
2058
+ version: 2,
2059
+ entries: [
2060
+ ...MANAGED_CONTEXT.map(([id, file, summary2, tags, priority, signalPaths]) => upgradeContextIndexEntry({
2061
+ ...priorByPair.get(`${id}\0${file}`),
2062
+ id,
2063
+ path: file,
2064
+ summary: summary2,
2065
+ tags,
2066
+ priority,
2067
+ status: "active",
2068
+ lastVerifiedAt: contextDate(),
2069
+ evidence: managedEvidence(analysis, id, signalPaths)
2070
+ })),
2071
+ ...customEntries
2072
+ ]
1852
2073
  };
1853
2074
  files.set(".codex-agent/context/index.json", { kind: "json", content: `${JSON.stringify(index, null, 2)}
1854
2075
  ` });
@@ -2555,7 +2776,7 @@ var refreshContext = (options) => runContextLifecycle({ ...options, operation: "
2555
2776
  import fs6 from "node:fs";
2556
2777
  import path6 from "node:path";
2557
2778
  import { pathToFileURL } from "node:url";
2558
- var KINDS = {
2779
+ var KINDS2 = {
2559
2780
  decision: "decisions",
2560
2781
  constraint: "constraints",
2561
2782
  operation: "operations",
@@ -2563,7 +2784,7 @@ var KINDS = {
2563
2784
  pitfall: "pitfalls"
2564
2785
  };
2565
2786
  var PRIORITIES2 = /* @__PURE__ */ new Set(["critical", "high", "medium", "low"]);
2566
- var CONFIDENCE = /* @__PURE__ */ new Set(["high", "medium"]);
2787
+ var CONFIDENCE2 = /* @__PURE__ */ new Set(["high", "medium"]);
2567
2788
  var PROPOSAL_FIELDS = /* @__PURE__ */ new Set([
2568
2789
  "version",
2569
2790
  "title",
@@ -2575,7 +2796,10 @@ var PROPOSAL_FIELDS = /* @__PURE__ */ new Set([
2575
2796
  "tags",
2576
2797
  "priority",
2577
2798
  "confidence",
2578
- "reviewWhen"
2799
+ "reviewWhen",
2800
+ "reviewAfter",
2801
+ "aliases",
2802
+ "supersedes"
2579
2803
  ]);
2580
2804
  var unique2 = (items) => [...new Set(items)];
2581
2805
  var safeText2 = (value, limit = 300) => String(value).replace(/[\r\n]+/g, " ").replace(/`/g, "'").trim().slice(0, limit);
@@ -2608,7 +2832,7 @@ var prepareContextIndex = ({ root, pendingDocuments = [] }) => {
2608
2832
  if (!fs6.existsSync(contextRoot) && pendingDocuments.length === 0) throw new Error(`Context directory not found: ${contextRoot}`);
2609
2833
  assertNoSymlink(projectRoot, contextRoot, "Canonical context root");
2610
2834
  const indexPath = path6.join(contextRoot, "index.json");
2611
- const prior = writable.index;
2835
+ const prior = upgradeContextIndex(writable.index);
2612
2836
  const priorByPath = new Map(prior.entries.map((entry) => [entry.path, entry]));
2613
2837
  const pending = /* @__PURE__ */ new Map();
2614
2838
  for (const [index2, document] of pendingDocuments.entries()) {
@@ -2634,18 +2858,19 @@ var prepareContextIndex = ({ root, pendingDocuments = [] }) => {
2634
2858
  ...relative2.replace(/\.md$/, "").split("/"),
2635
2859
  ...title.toLowerCase().split(/[^a-z0-9_-]+/).filter((term) => term.length > 2)
2636
2860
  ].map(slug).filter(Boolean)).slice(0, 10);
2637
- return {
2861
+ return upgradeContextIndexEntry({
2862
+ ...existing,
2638
2863
  id: existing?.id || slug(relative2.replace(/\.md$/, "").replaceAll("/", "-")),
2639
2864
  path: relative2,
2640
2865
  summary: existing?.summary || firstParagraph(content2, `${title} project context.`),
2641
2866
  tags: existing?.tags?.length ? existing.tags : tags,
2642
2867
  priority: existing?.priority || "medium"
2643
- };
2868
+ });
2644
2869
  }).sort((left, right) => left.path.localeCompare(right.path));
2645
2870
  const schemaPath = path6.join(projectRoot, "schemas", "context-index.schema.json");
2646
2871
  const index = {
2647
2872
  ...fs6.existsSync(schemaPath) ? { $schema: "../../schemas/context-index.schema.json" } : prior.$schema ? { $schema: prior.$schema } : {},
2648
- version: 1,
2873
+ version: 2,
2649
2874
  entries
2650
2875
  };
2651
2876
  assertValidContextIndex(index, { root: projectRoot, contextRoot, pendingPaths: [...pending.keys()] });
@@ -2683,9 +2908,9 @@ var validateContextProposal = (proposal, { root } = {}) => {
2683
2908
  checkString(errors, proposal, "summary", 10, 240);
2684
2909
  checkString(errors, proposal, "scope", 2, 120);
2685
2910
  checkString(errors, proposal, "contentMarkdown", 20, 1e4);
2686
- if (!Object.hasOwn(KINDS, proposal.kind)) errors.push(`kind must be one of: ${Object.keys(KINDS).join(", ")}`);
2911
+ if (!Object.hasOwn(KINDS2, proposal.kind)) errors.push(`kind must be one of: ${Object.keys(KINDS2).join(", ")}`);
2687
2912
  if (!PRIORITIES2.has(proposal.priority)) errors.push("priority is invalid");
2688
- if (!CONFIDENCE.has(proposal.confidence)) errors.push("confidence must be high or medium");
2913
+ if (!CONFIDENCE2.has(proposal.confidence)) errors.push("confidence must be high or medium");
2689
2914
  if (!Array.isArray(proposal.tags) || proposal.tags.length < 1 || proposal.tags.length > 10) errors.push("tags must contain between 1 and 10 values");
2690
2915
  else {
2691
2916
  if (new Set(proposal.tags).size !== proposal.tags.length) errors.push("tags must be unique");
@@ -2693,23 +2918,41 @@ var validateContextProposal = (proposal, { root } = {}) => {
2693
2918
  }
2694
2919
  if (!Array.isArray(proposal.evidence) || proposal.evidence.length < 1 || proposal.evidence.length > 20) errors.push("evidence must contain between 1 and 20 entries");
2695
2920
  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))) {
2921
+ const allowed = ["type", "path", "url", "note", "title", "version", "retrievedAt", "publishedAt", "decisionId", "decidedAt"];
2922
+ if (!item || typeof item !== "object" || Array.isArray(item) || Object.keys(item).some((key) => !allowed.includes(key))) {
2697
2923
  errors.push(`evidence[${index}] is invalid`);
2698
2924
  continue;
2699
2925
  }
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`);
2926
+ const type = item.type ?? "repository";
2927
+ if (!["repository", "decision", "external"].includes(type)) errors.push(`evidence[${index}].type is invalid`);
2928
+ if (type === "external") {
2929
+ try {
2930
+ const url = new URL(item.url);
2931
+ if (url.protocol !== "https:" || url.username || url.password) errors.push(`evidence[${index}].url must be an https URL without credentials`);
2932
+ } catch {
2933
+ errors.push(`evidence[${index}].url must be an absolute https URL`);
2934
+ }
2935
+ } else if (typeof item.path !== "string" || !item.path || item.path.length > 300 || path6.isAbsolute(item.path)) {
2936
+ errors.push(`evidence[${index}].path must be repository-relative`);
2937
+ }
2701
2938
  if (typeof item.note !== "string" || item.note.trim().length < 5 || item.note.length > 300) errors.push(`evidence[${index}].note is invalid`);
2702
2939
  }
2940
+ if (Array.isArray(proposal.evidence) && !proposal.evidence.some((item) => ["repository", "decision"].includes(item?.type ?? "repository"))) {
2941
+ errors.push("evidence must include at least one repository or decision source");
2942
+ }
2703
2943
  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
2944
  errors.push("reviewWhen must contain up to 5 non-empty strings");
2705
2945
  }
2946
+ if (proposal.reviewAfter !== void 0 && !/^\d{4}-\d{2}-\d{2}$/.test(proposal.reviewAfter)) errors.push("reviewAfter must use YYYY-MM-DD");
2947
+ 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");
2948
+ 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
2949
  const combined = JSON.stringify(proposal);
2707
2950
  if (combined.includes("codex-agent:context:start") || combined.includes("codex-agent:context:end")) errors.push("proposal must not contain managed marker text");
2708
2951
  if (containsSensitiveContent(combined)) errors.push("proposal appears to contain a secret or credential");
2709
2952
  if (root && Array.isArray(proposal.evidence)) {
2710
2953
  const projectRoot = fs6.realpathSync(path6.resolve(root));
2711
2954
  for (const [index, item] of proposal.evidence.entries()) {
2712
- if (!item || typeof item.path !== "string" || path6.isAbsolute(item.path)) continue;
2955
+ if (!item || item.type === "external" || typeof item.path !== "string" || path6.isAbsolute(item.path)) continue;
2713
2956
  const target = path6.resolve(projectRoot, item.path);
2714
2957
  if (target !== projectRoot && !target.startsWith(`${projectRoot}${path6.sep}`)) {
2715
2958
  errors.push(`evidence[${index}].path escapes the repository`);
@@ -2721,7 +2964,11 @@ var validateContextProposal = (proposal, { root } = {}) => {
2721
2964
  errors.push(`evidence[${index}].path must not traverse a symbolic link`);
2722
2965
  continue;
2723
2966
  }
2724
- if (!fs6.existsSync(target)) errors.push(`evidence[${index}].path does not exist: ${item.path}`);
2967
+ const relative2 = slash(path6.relative(projectRoot, target));
2968
+ if (relative2 === ".codex-agent/context" || relative2.startsWith(".codex-agent/context/") || relative2 === ".agents/context" || relative2.startsWith(".agents/context/") || relative2.startsWith(".codex-agent/sessions/") || relative2.startsWith(".codex-agent/backups/")) {
2969
+ errors.push(`evidence[${index}].path must reference primary repository evidence, not derived context`);
2970
+ } else if (!fs6.existsSync(target)) errors.push(`evidence[${index}].path does not exist: ${item.path}`);
2971
+ else if (!fs6.statSync(target).isFile()) errors.push(`evidence[${index}].path must be a file`);
2725
2972
  }
2726
2973
  }
2727
2974
  return { ok: errors.length === 0, errors };
@@ -2733,15 +2980,32 @@ var normalizeContextProposal = (proposal) => ({
2733
2980
  summary: proposal.summary.trim(),
2734
2981
  scope: proposal.scope.trim(),
2735
2982
  contentMarkdown: proposal.contentMarkdown.trim(),
2736
- evidence: proposal.evidence.map((item) => ({ path: slash(item.path), note: item.note.trim() })),
2983
+ evidence: proposal.evidence.map((item) => item.type === "external" ? {
2984
+ type: "external",
2985
+ url: item.url,
2986
+ note: item.note.trim(),
2987
+ ...item.title ? { title: item.title.trim() } : {},
2988
+ ...item.version ? { version: item.version.trim() } : {},
2989
+ ...item.retrievedAt ? { retrievedAt: item.retrievedAt } : {},
2990
+ ...item.publishedAt ? { publishedAt: item.publishedAt } : {}
2991
+ } : {
2992
+ type: item.type ?? "repository",
2993
+ path: slash(item.path),
2994
+ note: item.note.trim(),
2995
+ ...item.decisionId ? { decisionId: item.decisionId.trim() } : {},
2996
+ ...item.decidedAt ? { decidedAt: item.decidedAt } : {}
2997
+ }),
2737
2998
  tags: proposal.tags,
2738
2999
  priority: proposal.priority,
2739
3000
  confidence: proposal.confidence,
2740
- ...proposal.reviewWhen?.length ? { reviewWhen: proposal.reviewWhen.map((item) => item.trim()) } : {}
3001
+ ...proposal.reviewWhen?.length ? { reviewWhen: proposal.reviewWhen.map((item) => item.trim()) } : {},
3002
+ ...proposal.reviewAfter ? { reviewAfter: proposal.reviewAfter } : {},
3003
+ ...proposal.aliases?.length ? { aliases: unique2(proposal.aliases.map((item) => item.trim())) } : {},
3004
+ ...proposal.supersedes?.length ? { supersedes: unique2(proposal.supersedes) } : {}
2741
3005
  });
2742
3006
  var renderContextProposal = (proposal, { recordedAt = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) } = {}) => {
2743
3007
  const id = `${proposal.kind}-${slug(proposal.title)}`;
2744
- const evidence = proposal.evidence.map((item) => `- ${mdCode2(item.path)} \u2014 ${safeText2(item.note)}`).join("\n");
3008
+ const evidence = proposal.evidence.map((item) => `- ${mdCode2(item.type === "external" ? item.url : item.path)} \u2014 ${safeText2(item.note)}`).join("\n");
2745
3009
  const review = proposal.reviewWhen?.length ? `
2746
3010
 
2747
3011
  ## Review when
@@ -2804,31 +3068,62 @@ var prepareContextProposal = ({ root, proposal, apply, update }) => {
2804
3068
  const rendered = renderContextProposal(normalized);
2805
3069
  const writable = assertWritableContextCatalog({ root: projectRoot });
2806
3070
  const contextRoot = writable.contextRoot;
2807
- const relativePath = `${KINDS[normalized.kind]}/${slug(normalized.title)}.md`;
3071
+ const relativePath = `${KINDS2[normalized.kind]}/${slug(normalized.title)}.md`;
2808
3072
  const destination = path6.join(contextRoot, ...relativePath.split("/"));
2809
3073
  assertInside(contextRoot, destination, "context destination");
2810
3074
  assertNoSymlink(projectRoot, contextRoot, "Canonical context root");
2811
3075
  assertNoSymlink(projectRoot, destination, "Context destination");
2812
3076
  const indexPath = writable.indexPath;
2813
- const index = writable.index;
3077
+ const index = upgradeContextIndex(writable.index);
2814
3078
  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)));
3079
+ 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
3080
  if (duplicate) throw new Error(`Duplicate context candidate: ${duplicate.path}`);
2817
3081
  const current = fs6.existsSync(destination) ? fs6.readFileSync(destination, "utf8") : null;
2818
3082
  const merge = mergeManaged(current, rendered, update);
2819
3083
  const priorEntry = index.entries.find((entry) => entry.path === relativePath || entry.id === rendered.id);
2820
3084
  if (priorEntry && priorEntry.path !== relativePath) throw new Error(`Context id already belongs to another path: ${priorEntry.path}`);
2821
- const nextEntry = {
3085
+ const evidence = normalized.evidence.map((item) => item.type === "external" ? {
3086
+ type: "external",
3087
+ locator: item.url,
3088
+ note: item.note,
3089
+ retrievedAt: item.retrievedAt ?? contextDate(),
3090
+ ...item.title ? { title: item.title } : {},
3091
+ ...item.version ? { version: item.version } : {},
3092
+ ...item.publishedAt ? { publishedAt: item.publishedAt } : {}
3093
+ } : {
3094
+ type: item.type,
3095
+ locator: item.path,
3096
+ note: item.note,
3097
+ sha256: sha256(fs6.readFileSync(path6.resolve(projectRoot, item.path))),
3098
+ ...item.decisionId ? { decisionId: item.decisionId } : {},
3099
+ ...item.decidedAt ? { decidedAt: item.decidedAt } : {}
3100
+ });
3101
+ const nextEntry = upgradeContextIndexEntry({
3102
+ ...priorEntry,
2822
3103
  id: rendered.id,
2823
3104
  path: relativePath,
2824
3105
  summary: normalized.summary,
2825
3106
  tags: unique2([normalized.kind, ...normalized.tags]).slice(0, 10),
2826
- priority: normalized.priority
2827
- };
3107
+ priority: normalized.priority,
3108
+ kind: normalized.kind,
3109
+ scope: normalized.scope,
3110
+ confidence: normalized.confidence,
3111
+ status: "active",
3112
+ lastVerifiedAt: contextDate(),
3113
+ evidence,
3114
+ ...normalized.reviewAfter ? { reviewAfter: normalized.reviewAfter } : {},
3115
+ ...normalized.aliases?.length ? { aliases: normalized.aliases } : {},
3116
+ ...normalized.supersedes?.length ? { supersedes: normalized.supersedes } : {}
3117
+ });
3118
+ for (const supersededId of normalized.supersedes ?? []) {
3119
+ if (supersededId === rendered.id) throw new Error("Context proposal must not supersede itself");
3120
+ if (!index.entries.some((entry) => entry.id === supersededId)) throw new Error(`Context proposal supersedes unknown id: ${supersededId}`);
3121
+ }
3122
+ const supersededEntries = index.entries.map((entry) => (normalized.supersedes ?? []).includes(entry.id) ? upgradeContextIndexEntry({ ...entry, status: "superseded", supersededBy: unique2([...entry.supersededBy ?? [], rendered.id]) }) : entry);
2828
3123
  const nextIndex = {
2829
3124
  ...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))
3125
+ version: 2,
3126
+ entries: [...supersededEntries.filter((entry) => entry.path !== relativePath && entry.id !== rendered.id), nextEntry].sort((left, right) => left.path.localeCompare(right.path))
2832
3127
  };
2833
3128
  assertValidContextIndex(nextIndex, { root: projectRoot, contextRoot, pendingPaths: [relativePath] });
2834
3129
  const indexContent = `${JSON.stringify(nextIndex, null, 2)}
@@ -3141,7 +3436,7 @@ ${MANAGED_END(id)}`;
3141
3436
  });
3142
3437
  }
3143
3438
  const indexPath = path7.join(contextRoot, "index.json");
3144
- const targetIndex = writableCatalog.index;
3439
+ const targetIndex = upgradeContextIndex(writableCatalog.index);
3145
3440
  const priorIndexContent = fs7.existsSync(indexPath) ? fs7.readFileSync(indexPath, "utf8") : null;
3146
3441
  const changes = [];
3147
3442
  const conflicts = [];
@@ -3195,19 +3490,21 @@ ${MANAGED_END(id)}`;
3195
3490
  before: current,
3196
3491
  content: merge.content
3197
3492
  });
3198
- migrationEntries.push({
3493
+ migrationEntries.push(upgradeContextIndexEntry({
3199
3494
  id: candidate.id,
3200
3495
  path: candidate.destinationRelative,
3201
3496
  summary: candidate.summary,
3202
3497
  tags: candidate.tags,
3203
- priority: candidate.priority
3204
- });
3498
+ priority: candidate.priority,
3499
+ kind: "imported",
3500
+ confidence: "low"
3501
+ }));
3205
3502
  }
3206
3503
  const migratingIds = new Set(migrationEntries.map((entry) => entry.id));
3207
3504
  const migratingPaths = new Set(migrationEntries.map((entry) => entry.path));
3208
3505
  const nextIndex = {
3209
3506
  ...targetIndex.$schema ? { $schema: targetIndex.$schema } : {},
3210
- version: 1,
3507
+ version: 2,
3211
3508
  entries: [
3212
3509
  ...targetIndex.entries.filter((entry) => !migratingIds.has(entry.id) && !migratingPaths.has(entry.path)),
3213
3510
  ...migrationEntries
@@ -3297,13 +3594,155 @@ if (process.argv[1] && path7.basename(process.argv[1]) === "navigation-migrate.m
3297
3594
  }
3298
3595
  }
3299
3596
 
3597
+ // ../../plugins/codex-agent/skills/context-lint/scripts/context-lint.mjs
3598
+ import fs8 from "node:fs";
3599
+ import path8 from "node:path";
3600
+ import { pathToFileURL as pathToFileURL3 } from "node:url";
3601
+ var healthOrder = ["conflict", "orphan", "duplicate", "insufficient-evidence", "review-due", "healthy"];
3602
+ var normalize = (value) => String(value ?? "").normalize("NFKC").toLocaleLowerCase("en-US").replace(/\s+/g, " ").trim();
3603
+ var findingSort = (left, right) => (left.path ?? "").localeCompare(right.path ?? "") || (left.id ?? "").localeCompare(right.id ?? "") || left.code.localeCompare(right.code);
3604
+ var healthFor = (reasons) => healthOrder.find((health) => reasons.includes(health)) ?? "healthy";
3605
+ var lintContext = ({ root, strict = false, now = /* @__PURE__ */ new Date() }) => {
3606
+ const checkedAt = now.toISOString();
3607
+ const resolution = resolveContextCatalog({ root });
3608
+ const findings = [];
3609
+ if (["invalid", "both-divergent"].includes(resolution.state)) {
3610
+ for (const message of resolution.errors.length ? resolution.errors : resolution.warnings) {
3611
+ findings.push({ severity: "error", code: "catalog-invalid", message });
3612
+ }
3613
+ return {
3614
+ ok: false,
3615
+ healthy: false,
3616
+ checkedAt,
3617
+ state: resolution.state,
3618
+ summary: { entries: 0, healthy: 0, findings: findings.length, errors: findings.length, warnings: 0 },
3619
+ entries: [],
3620
+ findings: findings.sort(findingSort)
3621
+ };
3622
+ }
3623
+ const catalog = getReadableContextCatalog({ root });
3624
+ if (!catalog.index) {
3625
+ findings.push({ severity: "warning", code: "catalog-missing", message: "context index not found" });
3626
+ return {
3627
+ ok: !strict,
3628
+ healthy: false,
3629
+ checkedAt,
3630
+ state: catalog.state,
3631
+ summary: { entries: 0, healthy: 0, findings: 1, errors: 0, warnings: 1 },
3632
+ entries: [],
3633
+ findings
3634
+ };
3635
+ }
3636
+ const reasonsById = new Map(catalog.index.entries.map((entry) => [entry.id, /* @__PURE__ */ new Set()]));
3637
+ const add = (severity, code, entry, message, health = null) => {
3638
+ findings.push({ severity, code, ...entry?.id ? { id: entry.id } : {}, ...entry?.path ? { path: entry.path } : {}, message });
3639
+ if (health && entry?.id) reasonsById.get(entry.id)?.add(health);
3640
+ };
3641
+ const today = checkedAt.slice(0, 10);
3642
+ for (const entry of catalog.index.entries) {
3643
+ if (catalog.index.version === 1 || !entry.evidence?.length) {
3644
+ add("warning", "evidence-missing", entry, "entry has no machine-verifiable provenance", "insufficient-evidence");
3645
+ }
3646
+ if (entry.status === "conflicted") add("error", "lifecycle-conflicted", entry, "entry is marked conflicted", "conflict");
3647
+ if (entry.reviewAfter && entry.reviewAfter <= today) add("warning", "review-due", entry, `entry review was due on ${entry.reviewAfter}`, "review-due");
3648
+ const localEvidence = (entry.evidence ?? []).filter((evidence) => ["repository", "decision"].includes(evidence.type));
3649
+ if (entry.evidence?.length && !localEvidence.length) {
3650
+ add("warning", "evidence-derived-only", entry, "external evidence alone cannot establish repository-specific context", "insufficient-evidence");
3651
+ }
3652
+ for (const evidence of localEvidence) {
3653
+ const target = path8.resolve(catalog.root, evidence.locator);
3654
+ const relative2 = slash(path8.relative(catalog.root, target));
3655
+ if (relative2.startsWith("../") || path8.isAbsolute(relative2)) {
3656
+ add("error", "evidence-escape", entry, `evidence escapes repository: ${evidence.locator}`, "insufficient-evidence");
3657
+ continue;
3658
+ }
3659
+ if (relative2 === ".codex-agent/context" || relative2.startsWith(".codex-agent/context/") || relative2 === ".agents/context" || relative2.startsWith(".agents/context/")) {
3660
+ add("warning", "evidence-derived", entry, `evidence points to derived context: ${evidence.locator}`, "insufficient-evidence");
3661
+ continue;
3662
+ }
3663
+ let stat;
3664
+ try {
3665
+ assertNoSymlink(catalog.root, target, `context evidence ${evidence.locator}`);
3666
+ } catch {
3667
+ add("error", "evidence-invalid-file", entry, `evidence traverses a symbolic link: ${evidence.locator}`, "insufficient-evidence");
3668
+ continue;
3669
+ }
3670
+ try {
3671
+ stat = fs8.lstatSync(target);
3672
+ } catch {
3673
+ stat = null;
3674
+ }
3675
+ if (!stat) add("error", "evidence-missing-file", entry, `evidence file is missing: ${evidence.locator}`, "insufficient-evidence");
3676
+ else if (stat.isSymbolicLink() || !stat.isFile()) add("error", "evidence-invalid-file", entry, `evidence is not a regular file: ${evidence.locator}`, "insufficient-evidence");
3677
+ else if (sha256(fs8.readFileSync(target)) !== evidence.sha256) add("error", "evidence-digest-mismatch", entry, `evidence changed: ${evidence.locator}`, "conflict");
3678
+ }
3679
+ for (const relatedId of entry.related ?? []) {
3680
+ const related = catalog.index.entries.find((item) => item.id === relatedId);
3681
+ if (related && !(related.related ?? []).includes(entry.id)) add("warning", "relation-asymmetric", entry, `related link is not reciprocal: ${relatedId}`, "conflict");
3682
+ }
3683
+ }
3684
+ const summaryOwners = /* @__PURE__ */ new Map();
3685
+ const contentOwners = /* @__PURE__ */ new Map();
3686
+ for (const entry of catalog.index.entries.filter((item) => item.status !== "superseded")) {
3687
+ const summaryKey = normalize(entry.summary);
3688
+ const contentKey = sha256(fs8.readFileSync(path8.join(catalog.contextRoot, ...entry.path.split("/"))));
3689
+ for (const [key, owners, label] of [[summaryKey, summaryOwners, "summary"], [contentKey, contentOwners, "content"]]) {
3690
+ const owner = owners.get(key);
3691
+ if (owner && !(entry.supersedes ?? []).includes(owner.id) && !(owner.supersedes ?? []).includes(entry.id)) {
3692
+ add("warning", `duplicate-${label}`, entry, `${label} duplicates ${owner.id}`, "duplicate");
3693
+ reasonsById.get(owner.id)?.add("duplicate");
3694
+ } else if (!owner) owners.set(key, entry);
3695
+ }
3696
+ }
3697
+ const indexed = new Set(catalog.index.entries.map((entry) => entry.path));
3698
+ for (const file of listTreeFiles(catalog.contextRoot).filter((item) => item.relative.toLowerCase().endsWith(".md"))) {
3699
+ if (!indexed.has(file.relative)) add("warning", "orphan-document", { path: file.relative }, "Markdown document is not indexed");
3700
+ }
3701
+ const entries = catalog.index.entries.map((entry) => {
3702
+ const reasons = [...reasonsById.get(entry.id)].sort((left, right) => healthOrder.indexOf(left) - healthOrder.indexOf(right));
3703
+ return { id: entry.id, path: entry.path, lifecycle: entry.status ?? "active", health: healthFor(reasons), reasons };
3704
+ }).sort((left, right) => left.path.localeCompare(right.path));
3705
+ const sortedFindings = findings.sort(findingSort);
3706
+ const errors = sortedFindings.filter((finding) => finding.severity === "error").length;
3707
+ const warnings = sortedFindings.filter((finding) => finding.severity === "warning").length;
3708
+ const healthy = entries.length > 0 && entries.every((entry) => entry.health === "healthy") && warnings === 0 && errors === 0;
3709
+ return {
3710
+ ok: errors === 0 && (!strict || warnings === 0),
3711
+ healthy,
3712
+ checkedAt,
3713
+ state: catalog.state,
3714
+ summary: { entries: entries.length, healthy: entries.filter((entry) => entry.health === "healthy").length, findings: sortedFindings.length, errors, warnings },
3715
+ entries,
3716
+ findings: sortedFindings
3717
+ };
3718
+ };
3719
+ var option3 = (args, name, fallback) => {
3720
+ const index = args.indexOf(name);
3721
+ return index >= 0 && args[index + 1] ? args[index + 1] : fallback;
3722
+ };
3723
+ var main3 = (args = process.argv.slice(2)) => {
3724
+ const result = lintContext({ root: path8.resolve(option3(args, "--root", process.cwd())), strict: args.includes("--strict") });
3725
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
3726
+ `);
3727
+ if (!result.ok) process.exitCode = 1;
3728
+ };
3729
+ if (process.argv[1] && path8.basename(process.argv[1]) === "context-lint.mjs" && import.meta.url === pathToFileURL3(process.argv[1]).href) {
3730
+ try {
3731
+ main3();
3732
+ } catch (error) {
3733
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}
3734
+ `);
3735
+ process.exitCode = 1;
3736
+ }
3737
+ }
3738
+
3300
3739
  // src/core.mjs
3301
3740
  var listFiles = (root) => {
3302
- if (!fs8.existsSync(root)) return [];
3741
+ if (!fs9.existsSync(root)) return [];
3303
3742
  const files = [];
3304
3743
  const visit = (directory) => {
3305
- for (const entry of fs8.readdirSync(directory, { withFileTypes: true })) {
3306
- const absolute = path8.join(directory, entry.name);
3744
+ for (const entry of fs9.readdirSync(directory, { withFileTypes: true })) {
3745
+ const absolute = path9.join(directory, entry.name);
3307
3746
  if (entry.isDirectory()) visit(absolute);
3308
3747
  else if (entry.isFile()) files.push(absolute);
3309
3748
  }
@@ -3313,41 +3752,41 @@ var listFiles = (root) => {
3313
3752
  };
3314
3753
  var prepareContextMigration = ({ root, source, dryRun = false, force = false }) => {
3315
3754
  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"));
3755
+ const projectRoot = path9.resolve(root);
3756
+ const sourceRoot = path9.resolve(source);
3757
+ if (!fs9.existsSync(sourceRoot)) throw new Error(`Migration source not found: ${sourceRoot}`);
3758
+ const sourceFiles = (fs9.statSync(sourceRoot).isDirectory() ? listFiles(sourceRoot) : [sourceRoot]).filter((file) => file.endsWith(".md"));
3320
3759
  if (!sourceFiles.length) throw new Error("Migration source contains no Markdown context files.");
3321
3760
  const writableCatalog = assertWritableContextCatalog({ root: projectRoot });
3322
3761
  const catalogProjectRoot = writableCatalog.root;
3323
3762
  const contextRoot = writableCatalog.contextRoot;
3324
3763
  if (!contextRoot) throw new Error("Writable context catalog did not resolve a destination root.");
3325
- const destinationRoot = path8.join(contextRoot, "imported");
3764
+ const destinationRoot = path9.join(contextRoot, "imported");
3326
3765
  const result = { imported: [], unchanged: [], conflicts: [], backedUp: [], dryRun, applied: false };
3327
3766
  const documents = [];
3328
3767
  const backupPaths = [];
3329
3768
  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);
3769
+ const relative2 = fs9.statSync(sourceRoot).isDirectory() ? path9.relative(sourceRoot, sourceFile) : path9.basename(sourceFile);
3770
+ const destination = path9.join(destinationRoot, relative2);
3771
+ const content = fs9.readFileSync(sourceFile);
3333
3772
  if (containsSensitiveContent(content.toString("utf8"))) {
3334
3773
  throw new Error(`Migration source appears to contain a secret or credential: ${sourceFile}`);
3335
3774
  }
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") });
3775
+ if (!fs9.existsSync(destination)) {
3776
+ result.imported.push(path9.relative(catalogProjectRoot, destination));
3777
+ documents.push({ path: path9.relative(contextRoot, destination).split(path9.sep).join("/"), content: content.toString("utf8") });
3339
3778
  continue;
3340
3779
  }
3341
- if (content.equals(fs8.readFileSync(destination))) {
3342
- result.unchanged.push(path8.relative(catalogProjectRoot, destination));
3780
+ if (content.equals(fs9.readFileSync(destination))) {
3781
+ result.unchanged.push(path9.relative(catalogProjectRoot, destination));
3343
3782
  continue;
3344
3783
  }
3345
3784
  if (!force) {
3346
- result.conflicts.push(path8.relative(catalogProjectRoot, destination));
3785
+ result.conflicts.push(path9.relative(catalogProjectRoot, destination));
3347
3786
  continue;
3348
3787
  }
3349
- result.imported.push(path8.relative(catalogProjectRoot, destination));
3350
- const documentPath = path8.relative(contextRoot, destination).split(path8.sep).join("/");
3788
+ result.imported.push(path9.relative(catalogProjectRoot, destination));
3789
+ const documentPath = path9.relative(contextRoot, destination).split(path9.sep).join("/");
3351
3790
  documents.push({ path: documentPath, content: content.toString("utf8") });
3352
3791
  backupPaths.push(documentPath);
3353
3792
  }
@@ -3359,7 +3798,7 @@ var prepareContextMigration = ({ root, source, dryRun = false, force = false })
3359
3798
  index,
3360
3799
  backupPaths: [
3361
3800
  ...backupPaths,
3362
- ...index && fs8.existsSync(index.path) ? ["index.json"] : []
3801
+ ...index && fs9.existsSync(index.path) ? ["index.json"] : []
3363
3802
  ]
3364
3803
  };
3365
3804
  };
@@ -3384,23 +3823,23 @@ var migrateContext = (options) => {
3384
3823
  var check = (checks, name, ok, detail) => checks.push({ name, ok: Boolean(ok), detail });
3385
3824
  var parseJson = (file) => {
3386
3825
  try {
3387
- return { value: JSON.parse(fs8.readFileSync(file, "utf8")) };
3826
+ return { value: JSON.parse(fs9.readFileSync(file, "utf8")) };
3388
3827
  } catch (error) {
3389
3828
  return { error: error instanceof Error ? error.message : String(error) };
3390
3829
  }
3391
3830
  };
3392
3831
  var diagnoseProject = ({ root }) => {
3393
- const projectRoot = path8.resolve(root);
3832
+ const projectRoot = path9.resolve(root);
3394
3833
  const checks = [];
3395
3834
  const nodeMajor = Number.parseInt(process.versions.node.split(".")[0], 10);
3396
3835
  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);
3836
+ const manifest = path9.join(projectRoot, "plugins", "codex-agent", ".codex-plugin", "plugin.json");
3837
+ const isSourceWorkspace = fs9.existsSync(manifest);
3399
3838
  check(checks, "mode", true, isSourceWorkspace ? "plugin source workspace" : "initialized consumer project");
3400
3839
  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)) {
3840
+ const marketplace = path9.join(projectRoot, ".agents", "plugins", "marketplace.json");
3841
+ check(checks, "marketplace", fs9.existsSync(marketplace), marketplace);
3842
+ if (fs9.existsSync(marketplace)) {
3404
3843
  const parsed2 = parseJson(marketplace);
3405
3844
  check(checks, "marketplace-json", !parsed2.error, parsed2.error || parsed2.value.name);
3406
3845
  check(
@@ -3415,10 +3854,10 @@ var diagnoseProject = ({ root }) => {
3415
3854
  check(checks, "plugin-json", !parsed.error, parsed.error || parsed.value.name);
3416
3855
  check(checks, "plugin-name", parsed.value?.name === "codex-agent", parsed.value?.name || "missing");
3417
3856
  } else {
3418
- const config = path8.join(projectRoot, ".codex", "config.toml");
3419
- const agents = path8.join(projectRoot, ".codex", "agents");
3857
+ const config = path9.join(projectRoot, ".codex", "config.toml");
3858
+ const agents = path9.join(projectRoot, ".codex", "agents");
3420
3859
  const profiles = listFiles(agents).filter((file) => file.endsWith(".toml"));
3421
- check(checks, "project-config", fs8.existsSync(config), config);
3860
+ check(checks, "project-config", fs9.existsSync(config), config);
3422
3861
  check(checks, "project-agents", profiles.length === agentProfiles.length, `${profiles.length}/${agentProfiles.length} profiles in ${agents}`);
3423
3862
  }
3424
3863
  const resolvedCatalog = resolveContextCatalog({ root: projectRoot });
@@ -3429,38 +3868,38 @@ var diagnoseProject = ({ root }) => {
3429
3868
  } catch (error) {
3430
3869
  check(checks, "context-readable", false, error instanceof Error ? error.message : String(error));
3431
3870
  }
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)) {
3871
+ const contextIndex = readableCatalog2?.indexPath ?? (readableCatalog2?.root ? path9.join(readableCatalog2.root, "index.json") : null);
3872
+ check(checks, "context-index", Boolean(contextIndex && fs9.existsSync(contextIndex)), contextIndex ?? "context index not found");
3873
+ if (contextIndex && fs9.existsSync(contextIndex)) {
3435
3874
  const parsed = parseJson(contextIndex);
3436
3875
  check(checks, "context-json", !parsed.error, parsed.error || `${parsed.value.entries?.length ?? 0} entries`);
3437
- const contextRoot = path8.dirname(contextIndex);
3876
+ const contextRoot = path9.dirname(contextIndex);
3438
3877
  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);
3878
+ const target = path9.resolve(contextRoot, entry.path || "");
3879
+ return !target.startsWith(`${contextRoot}${path9.sep}`) || !fs9.existsSync(target);
3441
3880
  });
3442
3881
  check(checks, "context-paths", invalid.length === 0, invalid.map((entry) => entry.path).join(", ") || "all paths valid");
3443
3882
  }
3444
3883
  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;
3884
+ const skillsRoot = path9.join(projectRoot, "plugins", "codex-agent", "skills");
3885
+ const skillFiles = listFiles(skillsRoot).filter((file) => file.endsWith(`${path9.sep}SKILL.md`));
3886
+ const skillDirectories = fs9.readdirSync(skillsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).length;
3448
3887
  check(checks, "skills", skillFiles.length > 0 && skillFiles.length === skillDirectories, `${skillFiles.length}/${skillDirectories} skill entrypoints`);
3449
- const agentRoot = path8.join(projectRoot, "plugins", "codex-agent", "agents");
3888
+ const agentRoot = path9.join(projectRoot, "plugins", "codex-agent", "agents");
3450
3889
  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);
3890
+ const hooks = path9.join(projectRoot, "plugins", "codex-agent", "hooks", "hooks.json");
3891
+ check(checks, "hooks", fs9.existsSync(hooks) && !parseJson(hooks).error, hooks);
3453
3892
  }
3454
3893
  return { root: projectRoot, ok: checks.every((item) => item.ok), checks };
3455
3894
  };
3456
3895
  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");
3896
+ const projectRoot = path9.resolve(root);
3897
+ const suitePath = path9.join(projectRoot, "evals", "skill-routing.json");
3898
+ if (!fs9.existsSync(suitePath)) throw new Error(`Routing suite not found: ${suitePath}`);
3899
+ const suite = JSON.parse(fs9.readFileSync(suitePath, "utf8"));
3900
+ const skillsRoot = path9.join(projectRoot, "plugins", "codex-agent", "skills");
3462
3901
  const available = new Set(
3463
- fs8.readdirSync(skillsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name)
3902
+ fs9.readdirSync(skillsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name)
3464
3903
  );
3465
3904
  const ids = /* @__PURE__ */ new Set();
3466
3905
  const failures = [];
@@ -3498,13 +3937,13 @@ var evaluateRouting = ({ root }) => {
3498
3937
  return { ok: failures.length === 0, scenarios: suite.cases?.length ?? 0, skills: available.size, byKind, failures };
3499
3938
  };
3500
3939
  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");
3940
+ const projectRoot = path9.resolve(root);
3941
+ const suitePath = path9.join(projectRoot, "evals", "behavior-contracts.json");
3942
+ if (!fs9.existsSync(suitePath)) throw new Error(`Behavior suite not found: ${suitePath}`);
3943
+ const suite = JSON.parse(fs9.readFileSync(suitePath, "utf8"));
3944
+ const skillsRoot = path9.join(projectRoot, "plugins", "codex-agent", "skills");
3506
3945
  const availableSkills = new Set(
3507
- fs8.readdirSync(skillsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name)
3946
+ fs9.readdirSync(skillsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name)
3508
3947
  );
3509
3948
  const availableAgents = new Set(agentProfiles.map((profile) => profile.name));
3510
3949
  const coveredSkills = /* @__PURE__ */ new Set();
@@ -3556,6 +3995,7 @@ Usage:
3556
3995
  codex-agent context init [--root PATH] [--analysis FILE] [--apply --plan-hash HASH] [--force] [--json]
3557
3996
  codex-agent context refresh [--root PATH] [--analysis FILE] [--apply --plan-hash HASH] [--force] [--json]
3558
3997
  codex-agent context index [--root PATH] [--dry-run] [--json]
3998
+ codex-agent context lint [--root PATH] [--strict] [--json]
3559
3999
  codex-agent context save --proposal FILE [--root PATH] [--apply] [--update] [--json]
3560
4000
 
3561
4001
  Both init and refresh preview by default. Apply the exact reviewed preview by passing --apply with its planHash.`;
@@ -3574,6 +4014,7 @@ var FLAG_KEYS = /* @__PURE__ */ new Map([
3574
4014
  ["--include-templates", "includeTemplates"],
3575
4015
  ["--include-workflows", "includeWorkflows"],
3576
4016
  ["--json", "json"],
4017
+ ["--strict", "strict"],
3577
4018
  ["--update", "update"]
3578
4019
  ]);
3579
4020
  var parseOptions = (args, { command, options = [], flags = [] }) => {
@@ -3599,13 +4040,13 @@ var parseOptions = (args, { command, options = [], flags = [] }) => {
3599
4040
  }
3600
4041
  throw new Error(`Unknown option for ${command}: ${argument}`);
3601
4042
  }
3602
- parsed.root = path9.resolve(parsed.root ?? process.cwd());
4043
+ parsed.root = path10.resolve(parsed.root ?? process.cwd());
3603
4044
  return parsed;
3604
4045
  };
3605
4046
  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"));
4047
+ const absolute = path10.resolve(file);
4048
+ if (!fs10.existsSync(absolute)) throw new Error(`${label} file not found: ${absolute}`);
4049
+ return JSON.parse(fs10.readFileSync(absolute, "utf8"));
3609
4050
  };
3610
4051
  var write = (value, json) => {
3611
4052
  if (json) {
@@ -3622,7 +4063,7 @@ var finishWithConflicts = (result, json) => {
3622
4063
  write(result, json);
3623
4064
  if (result.conflicts.length) process.exitCode = 2;
3624
4065
  };
3625
- var main3 = async (args) => {
4066
+ var main4 = async (args) => {
3626
4067
  const [command, ...rest] = args;
3627
4068
  if (!command || command === "help") {
3628
4069
  if (rest.length === 1 && rest[0] === "context") write(contextUsage, false);
@@ -3637,7 +4078,7 @@ var main3 = async (args) => {
3637
4078
  return;
3638
4079
  }
3639
4080
  if (contextArgs.includes("--help") || contextArgs.includes("-h")) {
3640
- if (!["init", "refresh", "index", "save"].includes(subcommand)) {
4081
+ if (!["init", "refresh", "index", "lint", "save"].includes(subcommand)) {
3641
4082
  throw new Error(`Unknown context command: ${subcommand}
3642
4083
 
3643
4084
  ${contextUsage}`);
@@ -3667,6 +4108,17 @@ ${contextUsage}`);
3667
4108
  write({ path: result.path, entries: result.index.entries.length, dryRun: result.dryRun }, options.json);
3668
4109
  return;
3669
4110
  }
4111
+ if (subcommand === "lint") {
4112
+ const options = parseOptions(contextArgs, {
4113
+ command: "context lint",
4114
+ options: ["--root"],
4115
+ flags: ["--strict", "--json"]
4116
+ });
4117
+ const result = lintContext(options);
4118
+ write(result, options.json);
4119
+ if (!result.ok) process.exitCode = 1;
4120
+ return;
4121
+ }
3670
4122
  if (subcommand === "save") {
3671
4123
  const options = parseOptions(contextArgs, {
3672
4124
  command: "context save",
@@ -3741,7 +4193,7 @@ ${usage}`);
3741
4193
  };
3742
4194
 
3743
4195
  // bin/codex-agent.mjs
3744
- main3(process.argv.slice(2)).catch((error) => {
4196
+ main4(process.argv.slice(2)).catch((error) => {
3745
4197
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}
3746
4198
  `);
3747
4199
  process.exitCode = 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codex-agent/cli",
3
- "version": "0.1.0-main.12.sha31ff9e5",
3
+ "version": "0.1.0-main.13.sha3ad2ccf",
4
4
  "description": "Diagnostics and project bootstrap helpers for the codex-agent plugin.",
5
5
  "type": "module",
6
6
  "bin": {