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