@khoavu882/doflow 0.0.0-stage → 2.0.0-beta.3
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/LICENSE +21 -0
- package/README.md +132 -2
- package/bin/doflow.js +44 -0
- package/core/.antigravity-plugin/hooks.json +39 -0
- package/core/.antigravity-plugin/mcp_config.json +18 -0
- package/core/.antigravity-plugin/plugin.json +31 -0
- package/core/.claude-plugin/marketplace.json +20 -0
- package/core/.claude-plugin/plugin.json +21 -0
- package/core/.codex-plugin/marketplace.json +20 -0
- package/core/.codex-plugin/plugin.json +35 -0
- package/core/.plugin/plugin.json +19 -0
- package/core/harnesses/antigravity/hooks/pre-implementation-gate.sh +9 -0
- package/core/harnesses/antigravity/hooks/stop-check.sh +10 -0
- package/core/harnesses/claude/hooks/lib.sh +304 -0
- package/core/harnesses/claude/hooks/mcp-tool-guard.sh +20 -0
- package/core/harnesses/claude/hooks/permission-audit.sh +47 -0
- package/core/harnesses/claude/hooks/post-compact.sh +74 -0
- package/core/harnesses/claude/hooks/post-edit-lint.sh +36 -0
- package/core/harnesses/claude/hooks/post-tool-failure.sh +47 -0
- package/core/harnesses/claude/hooks/pre-bash-guard.sh +20 -0
- package/core/harnesses/claude/hooks/pre-compact.sh +43 -0
- package/core/harnesses/claude/hooks/pre-implement-gate.sh +21 -0
- package/core/harnesses/claude/hooks/session-end.sh +89 -0
- package/core/harnesses/claude/hooks/session-start.sh +5 -0
- package/core/harnesses/claude/hooks/skill-config-audit.sh +39 -0
- package/core/harnesses/claude/hooks/stop-check.sh +5 -0
- package/core/harnesses/claude/hooks/subagent-audit.sh +7 -0
- package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -0
- package/core/harnesses/claude/settings/keybindings.json +20 -0
- package/core/harnesses/claude/settings/settings.json +201 -0
- package/core/harnesses/claude/statusline/doflow-statusline.sh +49 -0
- package/core/harnesses/codex/agents/core-implementer.toml +7 -0
- package/core/harnesses/codex/agents/quality-guardian.toml +8 -0
- package/core/harnesses/codex/agents/research-writer.toml +8 -0
- package/core/harnesses/codex/agents/spec-analyst.toml +8 -0
- package/core/harnesses/codex/agents/system-architect.toml +9 -0
- package/core/harnesses/codex/config/config.toml +10 -0
- package/core/harnesses/codex/hooks/hooks.json +122 -0
- package/core/harnesses/codex/hooks/lib.sh +335 -0
- package/core/harnesses/codex/hooks/mcp-tool-guard.sh +18 -0
- package/core/harnesses/codex/hooks/post-edit-lint.impl.sh +52 -0
- package/core/harnesses/codex/hooks/post-edit-lint.sh +6 -0
- package/core/harnesses/codex/hooks/pre-bash-guard.sh +18 -0
- package/core/harnesses/codex/hooks/pre-compact.impl.sh +46 -0
- package/core/harnesses/codex/hooks/pre-compact.sh +9 -0
- package/core/harnesses/codex/hooks/pre-implement-gate.sh +19 -0
- package/core/harnesses/codex/hooks/session-end.impl.sh +92 -0
- package/core/harnesses/codex/hooks/session-end.sh +5 -0
- package/core/harnesses/codex/hooks/session-start.sh +5 -0
- package/core/harnesses/codex/hooks/stop-check.sh +8 -0
- package/core/harnesses/codex/hooks/subagent-audit.sh +8 -0
- package/core/harnesses/codex/hooks/user-prompt-submit.sh +6 -0
- package/core/harnesses/gemini/hooks/hooks.json +77 -0
- package/core/harnesses/gemini/hooks/lib.sh +302 -0
- package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +21 -0
- package/core/harnesses/gemini/hooks/post-edit-lint.sh +39 -0
- package/core/harnesses/gemini/hooks/pre-bash-guard.sh +21 -0
- package/core/harnesses/gemini/hooks/pre-compact.sh +38 -0
- package/core/harnesses/gemini/hooks/pre-implement-gate.sh +22 -0
- package/core/harnesses/gemini/hooks/session-end.impl.sh +89 -0
- package/core/harnesses/gemini/hooks/session-end.sh +5 -0
- package/core/harnesses/gemini/hooks/session-start.sh +5 -0
- package/core/harnesses/gemini/settings/adapter-defaults.json +4 -0
- package/core/harnesses/kiro/agents/doflow.json +58 -0
- package/core/harnesses/kiro/hooks/doflow.json +53 -0
- package/core/harnesses/kiro/hooks/lib.sh +324 -0
- package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +9 -0
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +9 -0
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +9 -0
- package/core/harnesses/kiro/hooks/session-start.sh +9 -0
- package/core/harnesses/kiro/hooks/stop-check.sh +43 -0
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +39 -0
- package/core/harnesses/shared/hooks/adapters/antigravity.js +111 -0
- package/core/harnesses/shared/hooks/adapters/arg-fields.js +19 -0
- package/core/harnesses/shared/hooks/adapters/gemini.js +114 -0
- package/core/harnesses/shared/hooks/hooks.json +47 -0
- package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +91 -0
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +97 -0
- package/core/harnesses/shared/hooks/policies/deny-json.sh +29 -0
- package/core/harnesses/shared/hooks/policies/lib.sh +501 -0
- package/core/harnesses/shared/hooks/policies/mcp-policy.conf +21 -0
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +91 -0
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +328 -0
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +496 -0
- package/core/harnesses/shared/hooks/policies/prompt-nudge.jq +131 -0
- package/core/harnesses/shared/hooks/policies/session-context.sh +141 -0
- package/core/harnesses/shared/hooks/policies/stop-check.sh +264 -0
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +46 -0
- package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +235 -0
- package/core/harnesses/shared/hooks/stream-hook-runner.js +590 -0
- package/core/harnesses/shared/locator/doflow-run +87 -0
- package/core/registry/advice.json +101 -0
- package/core/registry/assets.json +792 -0
- package/core/registry/capabilities.json +120 -0
- package/core/registry/contracts.json +189 -0
- package/core/registry/external-tools.json +191 -0
- package/core/registry/harnesses.json +2062 -0
- package/core/registry/lifecycle.json +231 -0
- package/core/registry/mcp.json +18 -0
- package/core/registry/models.json +79 -0
- package/core/registry/readiness-templates.json +118 -0
- package/core/registry/routes.json +47 -0
- package/core/registry/storage.json +1057 -0
- package/core/registry/verification.json +258 -0
- package/core/registry/workflows.json +790 -0
- package/core/shared/agent-specs/core-implementer.md +73 -0
- package/core/shared/agent-specs/quality-guardian.md +28 -0
- package/core/shared/agent-specs/research-writer.md +31 -0
- package/core/shared/agent-specs/spec-analyst.md +33 -0
- package/core/shared/agent-specs/system-architect.md +29 -0
- package/core/shared/guidance/DOFLOW_CORE.md +8 -0
- package/core/shared/guidance/FLAGS.md +25 -0
- package/core/shared/guidance/PRINCIPLES.md +61 -0
- package/core/shared/guidance/VERSION +1 -0
- package/core/shared/guidance/mcp/MCP_Context7.md +18 -0
- package/core/shared/guidance/mcp/MCP_Sequential.md +17 -0
- package/core/shared/guidance/modes/MODE_Brainstorming.md +46 -0
- package/core/shared/guidance/modes/MODE_DeepResearch.md +69 -0
- package/core/shared/guidance/modes/MODE_Introspection.md +33 -0
- package/core/shared/guidance/modes/MODE_Orchestration.md +57 -0
- package/core/shared/guidance/modes/MODE_Task_Management.md +74 -0
- package/core/shared/guidance/pointers/claude-gemini.md +2 -0
- package/core/shared/guidance/pointers/codex.md +6 -0
- package/core/shared/guidance/references/ARTIFACT_FORMAT.md +373 -0
- package/core/shared/guidance/references/ARTIFACT_VOICE.md +104 -0
- package/core/shared/guidance/references/CONSTITUTION_BASE.md +47 -0
- package/core/shared/guidance/references/DIAGRAM_GUIDE.md +161 -0
- package/core/shared/guidance/references/DOFLOW_CHAIN.md +87 -0
- package/core/shared/guidance/references/EVIDENCE_LEDGER.md +93 -0
- package/core/shared/guidance/references/MODEL_SELECTION.md +74 -0
- package/core/shared/guidance/references/ORCHESTRATION.md +36 -0
- package/core/shared/guidance/references/RESEARCH_CONFIG.md +21 -0
- package/core/shared/guidance/references/WORKFLOW_HANDOFF.md +81 -0
- package/core/shared/guidance/rules/RULE_01_SAFETY.md +25 -0
- package/core/shared/guidance/rules/RULE_02_WORKFLOW.md +23 -0
- package/core/shared/guidance/rules/RULE_03_QUALITY.md +14 -0
- package/core/shared/guidance/rules/RULE_04_QUESTIONS.md +46 -0
- package/core/shared/scripts/doflow/assets/mermaid-theme.json +46 -0
- package/core/shared/scripts/doflow/bash/do-exec-paths.sh +167 -0
- package/core/shared/scripts/doflow/bash/do-git-state.sh +546 -0
- package/core/shared/scripts/doflow/bash/do-parallel-check.sh +208 -0
- package/core/shared/scripts/doflow/bash/do-paths.sh +347 -0
- package/core/shared/scripts/doflow/bash/do-prereqs.sh +66 -0
- package/core/shared/scripts/doflow/bash/do-review-package.sh +127 -0
- package/core/shared/scripts/doflow/bash/do-task-brief.sh +698 -0
- package/core/shared/scripts/doflow/bash/render-audit.sh +224 -0
- package/core/shared/scripts/doflow/bash/render-diagrams.sh +177 -0
- package/core/shared/scripts/doflow/bash/render-puml.sh +223 -0
- package/core/shared/scripts/doflow/bash/sync-context.sh +66 -0
- package/core/shared/scripts/doflow/bash/validate-artifacts.sh +526 -0
- package/core/shared/scripts/doflow/bin/doflow-run +592 -0
- package/core/shared/skills/do/SKILL.md +128 -0
- package/core/shared/skills/do/references/estimation.md +13 -0
- package/core/shared/skills/do/references/pm_routing.md +25 -0
- package/core/shared/skills/do/references/tool_matrix.md +52 -0
- package/core/shared/skills/do-brainstorm/SKILL.md +252 -0
- package/core/shared/skills/do-code-review/README.md +49 -0
- package/core/shared/skills/do-code-review/SKILL.md +333 -0
- package/core/shared/skills/do-code-review/assets/sample_c_clean.c +71 -0
- package/core/shared/skills/do-code-review/assets/sample_c_smells.c +66 -0
- package/core/shared/skills/do-code-review/assets/sample_cpp_shift.cpp +19 -0
- package/core/shared/skills/do-code-review/assets/sample_csharp_clean.cs +100 -0
- package/core/shared/skills/do-code-review/assets/sample_csharp_smells.cs +78 -0
- package/core/shared/skills/do-code-review/assets/sample_java_clean.java +55 -0
- package/core/shared/skills/do-code-review/assets/sample_java_smells.java +56 -0
- package/core/shared/skills/do-code-review/assets/sample_json_clean.json +7 -0
- package/core/shared/skills/do-code-review/assets/sample_markdown_clean.md +30 -0
- package/core/shared/skills/do-code-review/assets/sample_markdown_smells.md +29 -0
- package/core/shared/skills/do-code-review/assets/sample_shell_heredoc.sh +38 -0
- package/core/shared/skills/do-code-review/assets/sample_shell_smells.sh +15 -0
- package/core/shared/skills/do-code-review/assets/sample_typescript_strings.ts +36 -0
- package/core/shared/skills/do-code-review/assets/sample_yaml_smells.yaml +6 -0
- package/core/shared/skills/do-code-review/content-types/config.md +83 -0
- package/core/shared/skills/do-code-review/content-types/markdown.md +132 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_c_clean_quality.json +79 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_c_smells_quality.json +169 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_cpp_shift_quality.json +49 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_csharp_clean_quality.json +149 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_csharp_smells_quality.json +157 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_java_clean_quality.json +67 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_java_smells_quality.json +97 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_json_clean_quality.json +33 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_markdown_clean_quality.json +1 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_markdown_smells_quality.json +16 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_shell_heredoc_quality.json +54 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_shell_smells_quality.json +91 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_typescript_strings_quality.json +60 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_yaml_smells_quality.json +46 -0
- package/core/shared/skills/do-code-review/languages/c.md +103 -0
- package/core/shared/skills/do-code-review/languages/cpp.md +103 -0
- package/core/shared/skills/do-code-review/languages/csharp.md +97 -0
- package/core/shared/skills/do-code-review/languages/dart.md +108 -0
- package/core/shared/skills/do-code-review/languages/go.md +92 -0
- package/core/shared/skills/do-code-review/languages/java.md +103 -0
- package/core/shared/skills/do-code-review/languages/kotlin.md +88 -0
- package/core/shared/skills/do-code-review/languages/php.md +109 -0
- package/core/shared/skills/do-code-review/languages/python.md +94 -0
- package/core/shared/skills/do-code-review/languages/ruby.md +105 -0
- package/core/shared/skills/do-code-review/languages/rust.md +99 -0
- package/core/shared/skills/do-code-review/languages/shell.md +90 -0
- package/core/shared/skills/do-code-review/languages/swift.md +88 -0
- package/core/shared/skills/do-code-review/languages/typescript.md +97 -0
- package/core/shared/skills/do-code-review/review-policy.json +47 -0
- package/core/shared/skills/do-code-review/rules/universal.md +49 -0
- package/core/shared/skills/do-code-review/scripts/code_quality_checker.py +1696 -0
- package/core/shared/skills/do-code-review/scripts/doc_quality_checker.py +532 -0
- package/core/shared/skills/do-code-review/scripts/pr_analyzer.py +620 -0
- package/core/shared/skills/do-code-review/scripts/review_report_generator.py +505 -0
- package/core/shared/skills/do-constitution/SKILL.md +72 -0
- package/core/shared/skills/do-design/SKILL.md +212 -0
- package/core/shared/skills/do-diagnose/SKILL.md +156 -0
- package/core/shared/skills/do-diagnose/references/code_audit.md +15 -0
- package/core/shared/skills/do-diagnose/references/refactoring.md +12 -0
- package/core/shared/skills/do-diagnose/references/root_cause.md +17 -0
- package/core/shared/skills/do-document/SKILL.md +81 -0
- package/core/shared/skills/do-document/references/api-reference.md +42 -0
- package/core/shared/skills/do-document/references/deep_research.md +18 -0
- package/core/shared/skills/do-document/references/embedded-research.md +11 -0
- package/core/shared/skills/do-document/references/implementation-flow.md +105 -0
- package/core/shared/skills/do-document/references/index_generator.md +9 -0
- package/core/shared/skills/do-document/references/user-guide.md +22 -0
- package/core/shared/skills/do-execute-plan/SKILL.md +261 -0
- package/core/shared/skills/do-execute-plan/references/handoff_record.md +57 -0
- package/core/shared/skills/do-execute-plan/references/parallel_dispatch.md +92 -0
- package/core/shared/skills/do-execute-plan/references/readiness_gate.md +155 -0
- package/core/shared/skills/do-execute-plan/references/run_position.md +63 -0
- package/core/shared/skills/do-execute-plan/references/scaffold.md +441 -0
- package/core/shared/skills/do-execute-plan/references/task_attempts.md +41 -0
- package/core/shared/skills/do-flow/SKILL.md +186 -0
- package/core/shared/skills/do-flow/references/task_classes.md +134 -0
- package/core/shared/skills/do-git/SKILL.md +115 -0
- package/core/shared/skills/do-git/references/hotfix-ritual.md +44 -0
- package/core/shared/skills/do-git/references/lifecycle-policy.md +54 -0
- package/core/shared/skills/do-git/references/release-ritual.md +64 -0
- package/core/shared/skills/do-implement/SKILL.md +222 -0
- package/core/shared/skills/do-plan/SKILL.md +219 -0
- package/core/shared/skills/do-test/SKILL.md +113 -0
- package/core/shared/templates/doflow/constitution-template.md +28 -0
- package/core/shared/templates/doflow/data-model-template.md +169 -0
- package/core/shared/templates/doflow/design-template.md +193 -0
- package/core/shared/templates/doflow/external-contract-template.md +35 -0
- package/core/shared/templates/doflow/intent-template.md +45 -0
- package/core/shared/templates/doflow/plan-template.md +151 -0
- package/core/shared/templates/doflow/question-log-template.md +51 -0
- package/core/shared/templates/doflow/requirement-template.md +126 -0
- package/core/shared/templates/doflow/specs-template.md +55 -0
- package/core/shared/templates/doflow/state-template.md +59 -0
- package/core/shared/workflows/do-flow-chain.md +17 -0
- package/package.json +46 -4
- package/src/adapters/antigravity/index.ts +631 -0
- package/src/adapters/claude/index.ts +548 -0
- package/src/adapters/claude/mcp.ts +94 -0
- package/src/adapters/codex/agents.ts +208 -0
- package/src/adapters/codex/config.ts +272 -0
- package/src/adapters/codex/hooks.ts +203 -0
- package/src/adapters/codex/index.ts +707 -0
- package/src/adapters/codex/mcp.ts +271 -0
- package/src/adapters/codex/native-projection.ts +34 -0
- package/src/adapters/copilot/index.ts +413 -0
- package/src/adapters/copy-tree.ts +620 -0
- package/src/adapters/gemini/hooks.ts +210 -0
- package/src/adapters/gemini/index.ts +390 -0
- package/src/adapters/hook-commands.ts +60 -0
- package/src/adapters/index.ts +106 -0
- package/src/adapters/kiro/index.ts +310 -0
- package/src/adapters/mcp-entries.ts +331 -0
- package/src/adapters/opencode/index.ts +466 -0
- package/src/adapters/pi/index.ts +279 -0
- package/src/adapters/pi/json-members.ts +185 -0
- package/src/adapters/pi/mcp.ts +373 -0
- package/src/adapters/resolve-source.ts +19 -0
- package/src/cli/commands/install.ts +218 -0
- package/src/cli/commands/list-backups.ts +21 -0
- package/src/cli/commands/reconcile.ts +120 -0
- package/src/cli/commands/remove.ts +82 -0
- package/src/cli/commands/rollback.ts +172 -0
- package/src/cli/commands/self-update.ts +33 -0
- package/src/cli/commands/status.ts +138 -0
- package/src/cli/commands/tools.ts +92 -0
- package/src/cli/commands/update.ts +162 -0
- package/src/cli/index.ts +568 -0
- package/src/cli/runtime-commands.ts +281 -0
- package/src/cli/shared.ts +218 -0
- package/src/helper/bash-helper.ts +34 -0
- package/src/helper/git.ts +24 -0
- package/src/helper/guardrails.ts +49 -0
- package/src/helper/harness-paths.ts +140 -0
- package/src/helper/marker-merge.ts +116 -0
- package/src/helper/prompt.ts +61 -0
- package/src/helper/repo-root.ts +29 -0
- package/src/helper/settings-merge.ts +151 -0
- package/src/helper/settings-scope.ts +33 -0
- package/src/helper/toml.ts +244 -0
- package/src/install/backup-set.ts +149 -0
- package/src/install/backup.ts +724 -0
- package/src/install/context.ts +58 -0
- package/src/install/manifest.ts +145 -0
- package/src/install/mcp.ts +254 -0
- package/src/install/paths.ts +84 -0
- package/src/install/targets.ts +90 -0
- package/src/install/tool-lifecycle.ts +151 -0
- package/src/lifecycle/bash-availability.ts +25 -0
- package/src/lifecycle/index.ts +812 -0
- package/src/lifecycle/mcp-index.ts +28 -0
- package/src/lifecycle/policies.ts +73 -0
- package/src/lifecycle/view.ts +175 -0
- package/src/registry/index.ts +611 -0
- package/src/release/sync-plugin-versions.ts +66 -0
- package/src/runtime/advice/cli.ts +283 -0
- package/src/runtime/advice/client.ts +324 -0
- package/src/runtime/advice/config.ts +189 -0
- package/src/runtime/advice/doctor.ts +169 -0
- package/src/runtime/advice/endpoint-guard.ts +134 -0
- package/src/runtime/advice/kinds.ts +154 -0
- package/src/runtime/advice/providers/ollama-systemone.ts +209 -0
- package/src/runtime/advice/record.ts +166 -0
- package/src/runtime/advice/registry.ts +170 -0
- package/src/runtime/advice/strength.ts +66 -0
- package/src/runtime/brief-tools.ts +342 -0
- package/src/runtime/c4-project.ts +371 -0
- package/src/runtime/capability-router.ts +484 -0
- package/src/runtime/checkouts.ts +170 -0
- package/src/runtime/claims.ts +468 -0
- package/src/runtime/cli-result.ts +55 -0
- package/src/runtime/cli.ts +678 -0
- package/src/runtime/command-detect.ts +516 -0
- package/src/runtime/context-pack.ts +492 -0
- package/src/runtime/decision-register.ts +493 -0
- package/src/runtime/evidence-ledger.ts +338 -0
- package/src/runtime/failure/capture.ts +272 -0
- package/src/runtime/failure/classifier.ts +68 -0
- package/src/runtime/failure/cli.ts +178 -0
- package/src/runtime/failure/home.ts +64 -0
- package/src/runtime/failure/store.ts +226 -0
- package/src/runtime/feature-resolve.ts +79 -0
- package/src/runtime/freshness.ts +177 -0
- package/src/runtime/health.ts +863 -0
- package/src/runtime/history-compactor.ts +237 -0
- package/src/runtime/implementation-gate.ts +321 -0
- package/src/runtime/inventory/coverage.ts +218 -0
- package/src/runtime/inventory/identity.ts +65 -0
- package/src/runtime/inventory/index.ts +705 -0
- package/src/runtime/inventory/precedence.ts +134 -0
- package/src/runtime/inventory/read-scopes.ts +131 -0
- package/src/runtime/inventory/siblings.ts +192 -0
- package/src/runtime/knowledge/chunker.ts +124 -0
- package/src/runtime/knowledge/hybrid.ts +149 -0
- package/src/runtime/knowledge/index-store.ts +145 -0
- package/src/runtime/knowledge/retrieval.ts +194 -0
- package/src/runtime/leak-scan.ts +185 -0
- package/src/runtime/lifecycle/cli.ts +276 -0
- package/src/runtime/lifecycle/event-store.ts +340 -0
- package/src/runtime/lifecycle/fold.ts +353 -0
- package/src/runtime/lifecycle/followup.ts +432 -0
- package/src/runtime/lifecycle/goal.ts +249 -0
- package/src/runtime/lifecycle/intent-writer.ts +86 -0
- package/src/runtime/lifecycle/overview.ts +292 -0
- package/src/runtime/lifecycle/release.ts +241 -0
- package/src/runtime/lifecycle/report-store.ts +221 -0
- package/src/runtime/lifecycle/retention.ts +212 -0
- package/src/runtime/lifecycle/root.ts +71 -0
- package/src/runtime/lifecycle/status.ts +353 -0
- package/src/runtime/lifecycle/store-upkeep.ts +393 -0
- package/src/runtime/locator-resolve.ts +158 -0
- package/src/runtime/mask.ts +210 -0
- package/src/runtime/mcp-consultations.ts +412 -0
- package/src/runtime/mcp-reach.ts +157 -0
- package/src/runtime/model-router.ts +153 -0
- package/src/runtime/outcome.ts +467 -0
- package/src/runtime/reach.ts +148 -0
- package/src/runtime/readiness-record.ts +150 -0
- package/src/runtime/readiness.ts +435 -0
- package/src/runtime/recovery.ts +282 -0
- package/src/runtime/research-request.ts +231 -0
- package/src/runtime/retrieval-plan.ts +578 -0
- package/src/runtime/scaffold/artifacts.ts +217 -0
- package/src/runtime/scaffold/fingerprint.ts +56 -0
- package/src/runtime/scaffold/generate.ts +911 -0
- package/src/runtime/scaffold/languages.ts +245 -0
- package/src/runtime/storage/cli.ts +64 -0
- package/src/runtime/storage/engine.ts +249 -0
- package/src/runtime/storage/registry.ts +130 -0
- package/src/runtime/storage/roots.ts +63 -0
- package/src/runtime/storage/rules.ts +282 -0
- package/src/runtime/storage/upkeep.ts +243 -0
- package/src/runtime/storage/work-status.ts +326 -0
- package/src/runtime/task-attempts.ts +415 -0
- package/src/runtime/task-classifier.ts +550 -0
- package/src/runtime/task-scope.ts +130 -0
- package/src/runtime/task-state.ts +176 -0
- package/src/runtime/trace/indicators.ts +235 -0
- package/src/runtime/trace/ledger.ts +626 -0
- package/src/runtime/trace/render.ts +76 -0
- package/src/runtime/trace/views.ts +459 -0
- package/src/runtime/verification/contract-runner.ts +341 -0
- package/src/runtime/verification/engine.ts +1461 -0
- package/src/runtime/verification/registry.ts +130 -0
- package/src/runtime/verification/scope-bound.ts +418 -0
- package/src/runtime/verification/test-selection.ts +172 -0
- package/src/runtime/workflow-engine.ts +588 -0
- package/src/runtime/workflow-orchestrator.ts +953 -0
- package/src/runtime/worktree.ts +340 -0
- package/src/state/index.ts +193 -0
- package/src/state/lockfile.ts +136 -0
- package/src/state/run-lock.ts +408 -0
- package/types/node-commonjs.d.ts +68 -0
- package/types/runtime.d.ts +91 -0
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
4
|
+
|
|
5
|
+
// Codex adapter boundary. Native parsing and mutation remain in the existing
|
|
6
|
+
// codex-* modules; this module composes their non-mutating plans into the
|
|
7
|
+
// common adapter shape without changing legacy CLI ownership.
|
|
8
|
+
const fs = require('node:fs');
|
|
9
|
+
const os = require('node:os');
|
|
10
|
+
const path = require('node:path');
|
|
11
|
+
const crypto = require('node:crypto');
|
|
12
|
+
const { configPath, fingerprint: configFingerprint, parseToml, planCodexConfig, applyCodexConfig } = require('./config.ts');
|
|
13
|
+
const { renderServer, planCodexMcp, applyCodexMcp, ownedCodexMcpIds, userDefinedCodexMcpIds, serverTableText } = require('./mcp.ts');
|
|
14
|
+
const { agentDirectory, discoverCodexAgents, planCodexAgents, applyCodexAgents } = require('./agents.ts');
|
|
15
|
+
const { planCodexHooks, deployCodexHooks, removeCodexHookScripts } = require('./hooks.ts');
|
|
16
|
+
const { planTree, applyTree, removeTree, verifyTree, copyTreeAssets, copyTreeDestDir, ledgerFileResources, ledgerSiblingFingerprints, siblingReplacedNotices } = require('../copy-tree.ts');
|
|
17
|
+
const { mergeMarkedSection, removeMarkedSection, MARKER_START, MARKER_END } = require('../../helper/marker-merge.ts');
|
|
18
|
+
const { nativeMcpCatalog } = require('../../registry/index.ts');
|
|
19
|
+
const { declaredHarnessPaths, resolveHarnessPaths } = require('../../helper/harness-paths.ts');
|
|
20
|
+
|
|
21
|
+
const HARNESS = 'codex';
|
|
22
|
+
|
|
23
|
+
/** Every server a capability provider names as its MCP server. */
|
|
24
|
+
function providerMcpServers(registry: any) {
|
|
25
|
+
return Object.values<any>(registry?.capabilities || {}).flatMap((capability) => (capability.providers || []).map((provider: any) => provider.mcp?.server)).filter(Boolean);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The servers config.toml defines among `names`, whoever wrote them: the tables DoFlow owns plus the
|
|
29
|
+
* user's own. `null` while the file is absent or cannot be parsed. */
|
|
30
|
+
function configuredCodexMcpIds({ config, scope, fsImpl, mcpRecords, names }: any) {
|
|
31
|
+
try {
|
|
32
|
+
if (!fsImpl.existsSync(config)) return null;
|
|
33
|
+
parseToml(fsImpl.readFileSync(config, 'utf8'));
|
|
34
|
+
} catch { return null; }
|
|
35
|
+
const owned = ownedCodexMcpIds({ file: config, scope, fsImpl, managedResources: mcpRecords });
|
|
36
|
+
const own = userDefinedCodexMcpIds({ file: config, scope, fsImpl, managedResources: mcpRecords, names });
|
|
37
|
+
return [...new Set([...owned, ...own])].sort();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Native path facts live in core/registry/harnesses.json under this harness's "paths" section and
|
|
42
|
+
* resolve through the shared harness-paths resolver. createCodexAdapter({ declaredPaths }) is the
|
|
43
|
+
* injection point buildAdapterRegistry() uses; module-level exports delegate to a default-
|
|
44
|
+
* configured instance so direct callers keep their historical shape.
|
|
45
|
+
*/
|
|
46
|
+
function createCodexAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] } = {}) {
|
|
47
|
+
function normalizeContext({ scope, scopeRoot, projectRoot, codexDir, homeDir }: any = {}) {
|
|
48
|
+
if (!['project', 'global', 'user'].includes(scope)) throw new Error(`Unsupported Codex scope '${scope}'`);
|
|
49
|
+
if (scope === 'project') {
|
|
50
|
+
// Logic residue (input normalization, not a path fact): scopeRoot is accepted as the selected
|
|
51
|
+
// repository root for generic lifecycle callers. A workspace container is never substituted
|
|
52
|
+
// here: the caller must pass that container as projectRoot only when it is itself the
|
|
53
|
+
// repository being configured.
|
|
54
|
+
const root = projectRoot ?? scopeRoot;
|
|
55
|
+
if (!root) throw new Error('Codex project scope requires the selected repository projectRoot');
|
|
56
|
+
const paths = resolveHarnessPaths(declaredPaths, { scope: 'project', scopeRoot: root });
|
|
57
|
+
return { scope: 'project', projectRoot: path.resolve(root), paths };
|
|
58
|
+
}
|
|
59
|
+
// Logic residue (input normalization): the generic lifecycle may pass either global or user;
|
|
60
|
+
// both target the actual user configuration root.
|
|
61
|
+
const home = homeDir ?? os.homedir();
|
|
62
|
+
const paths = resolveHarnessPaths(declaredPaths, { scope: 'user', scopeRoot: home, homeDir: home });
|
|
63
|
+
return { scope: 'global', codexDir: codexDir ? path.resolve(codexDir) : paths.configDir, paths };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const nativeContext = normalizeContext;
|
|
67
|
+
|
|
68
|
+
function discover(options: any, { fsImpl = fs }: any = {}) {
|
|
69
|
+
const context = nativeContext(options);
|
|
70
|
+
const paths = context.paths;
|
|
71
|
+
const config = paths.configFile;
|
|
72
|
+
const agents = paths.agentsDirectory;
|
|
73
|
+
const hooks = paths.hooksFile;
|
|
74
|
+
const mcpRecords = nativeManagedResources(options.managedResources || options.ledger?.resources || [], context, { kind: 'mcp-server', target: config });
|
|
75
|
+
const catalogNames = [...(options.registry?.mcp || []), ...(projectedNativeOptions(options).mcpCatalog || [])].map((server) => server.id);
|
|
76
|
+
return {
|
|
77
|
+
harness: HARNESS,
|
|
78
|
+
scope: options.scope,
|
|
79
|
+
config: { file: config, exists: fsImpl.existsSync(config) },
|
|
80
|
+
agents: { directory: agents, exists: fsImpl.existsSync(agents) },
|
|
81
|
+
hooks: { file: hooks, exists: fsImpl.existsSync(hooks) },
|
|
82
|
+
mcpOwned: ownedCodexMcpIds({ file: config, scope: context.scope, fsImpl, managedResources: mcpRecords }),
|
|
83
|
+
mcpUserDefined: userDefinedCodexMcpIds({ file: config, scope: context.scope, fsImpl, managedResources: mcpRecords, names: catalogNames }),
|
|
84
|
+
mcpConfigured: configuredCodexMcpIds({ config, scope: context.scope, fsImpl, mcpRecords, names: [...catalogNames, ...providerMcpServers(options.registry)] }),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function render({ registry, asset }: any) {
|
|
89
|
+
if (!registry || !asset) throw new Error('render requires registry and asset');
|
|
90
|
+
const harness = registry.harnesses.find((item: any) => item.id === HARNESS);
|
|
91
|
+
if (!harness) throw new Error('Registry does not define the Codex harness');
|
|
92
|
+
const projection = asset.projection?.[HARNESS];
|
|
93
|
+
if (!projection) return { harness: HARNESS, assetId: asset.id, status: 'not-applicable' };
|
|
94
|
+
const capability = harness.capabilities?.[projection.capability];
|
|
95
|
+
if (!capability) throw new Error(`Codex projection '${asset.id}' references unknown capability '${projection.capability}'`);
|
|
96
|
+
return {
|
|
97
|
+
harness: HARNESS,
|
|
98
|
+
assetId: asset.id,
|
|
99
|
+
renderer: projection.renderer,
|
|
100
|
+
capability: projection.capability,
|
|
101
|
+
capabilityStatus: capability.status,
|
|
102
|
+
target: harness.nativeTargets?.[projection.capability] ?? null,
|
|
103
|
+
status: capability.status === 'unavailable' ? 'unavailable' : 'renderable',
|
|
104
|
+
prerequisites: [...(capability.prerequisites || [])],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function assetIdFor(options: any, componentName: any) {
|
|
109
|
+
const explicit = options.projection?.assetIds?.[componentName];
|
|
110
|
+
if (explicit) return explicit;
|
|
111
|
+
const assets = options.assets || [];
|
|
112
|
+
if (componentName === 'agents') return assets.find((asset: any) => asset.id === 'agents.shared')?.id ?? assets[0]?.id;
|
|
113
|
+
return assets.find((asset: any) => asset.id === 'guidance.codex-pointer')?.id ?? assets[0]?.id ?? `codex.${componentName}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function ownershipKind(componentName: any) {
|
|
117
|
+
return ({ config: 'configuration-entry', mcp: 'mcp-server', agents: 'custom-agent', hooks: 'hooks-file' } as Record<string, string>)[componentName] ?? componentName;
|
|
118
|
+
}
|
|
119
|
+
function ownershipIdentity(componentName: any, identity: any) { return `doflow:codex:${ownershipKind(componentName)}:${identity}`; }
|
|
120
|
+
|
|
121
|
+
function addChanges(output: any, assetId: any, component: any, componentName: any) {
|
|
122
|
+
for (const change of component?.changes || []) output.push({
|
|
123
|
+
harness: HARNESS,
|
|
124
|
+
assetId,
|
|
125
|
+
nativeComponent: componentName,
|
|
126
|
+
nativePlan: component,
|
|
127
|
+
operation: change.operation ?? change.type,
|
|
128
|
+
target: change.target ?? change.file ?? component.file ?? component.destination ?? component.directory,
|
|
129
|
+
ownershipIdentity: change.ownershipIdentity ?? ownershipIdentity(componentName, change.identity ?? (componentName === 'hooks' ? 'hooks.json' : assetId)),
|
|
130
|
+
...change,
|
|
131
|
+
// A release drops the ownership row and leaves the file alone, so the lifecycle never hands
|
|
132
|
+
// it to apply() or remove().
|
|
133
|
+
...(change.release ? { retained: true, retainedFor: [] } : {}),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function sha256(text: any) { return `sha256:${crypto.createHash('sha256').update(text).digest('hex')}`; }
|
|
138
|
+
function lifecycleResource({ scope, assetId, kind, identity, target, fingerprint, sourceVersion, selection, recoveryRef, projection }: any) {
|
|
139
|
+
return { harness: HARNESS, scope, assetId, kind, identity, target, ownershipIdentity: `doflow:codex:${kind}:${identity}`,
|
|
140
|
+
fingerprint, sourceVersion: sourceVersion ?? 'unknown', selection: selection ?? true, recoveryRef: recoveryRef ?? null, projection };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Convert neutral ledger entries to the legacy planner's ownership record.
|
|
144
|
+
* An absolute target must match the exact native file this component will
|
|
145
|
+
* reconcile; a record from another Codex project cannot authorize this plan. */
|
|
146
|
+
function nativeManagedResources(resources: any, context: any, { kind, target, directory }: any = {}) {
|
|
147
|
+
return (resources || []).flatMap((resource: any) => {
|
|
148
|
+
if (!resource || resource.kind !== kind) return [];
|
|
149
|
+
// Retain the pre-registry native record shape for direct callers/tests.
|
|
150
|
+
if (!resource.harness && resource.target === HARNESS) {
|
|
151
|
+
return [{ ...resource, scope: context.scope, target: HARNESS }];
|
|
152
|
+
}
|
|
153
|
+
if (resource.harness !== HARNESS) return [];
|
|
154
|
+
const expected = directory && resource.identity?.startsWith('agent:')
|
|
155
|
+
? path.join(directory, `${resource.identity.slice('agent:'.length)}.toml`) : target;
|
|
156
|
+
if (typeof resource.target !== 'string' || path.resolve(resource.target) !== path.resolve(expected)) return [];
|
|
157
|
+
return [{ target: HARNESS, scope: context.scope, kind, identity: resource.identity, fingerprint: resource.fingerprint,
|
|
158
|
+
sourceVersion: resource.sourceVersion, selection: resource.selection, recoveryPoint: resource.recoveryRef }];
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Whether a row of the OTHER scope claims `target` (for a config or MCP entry: the same entry in
|
|
163
|
+
* that file). A project rooted at $HOME shares `~/.codex`, `~/.agents` and the one ledger with the
|
|
164
|
+
* global install, so its removal must leave whatever the global rows still own, and vice versa. */
|
|
165
|
+
function claimedByOtherScope(resources: any, context: any, target: any, identity?: any) {
|
|
166
|
+
if (typeof target !== 'string') return false;
|
|
167
|
+
return (resources || []).some((resource: any) => resource?.scope && resource.scope !== context.scope
|
|
168
|
+
&& typeof resource.target === 'string' && path.resolve(resource.target) === path.resolve(target)
|
|
169
|
+
&& (identity === undefined || resource.identity === identity));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function claimedRowElsewhere(resources: any, context: any, resource: any) {
|
|
173
|
+
return claimedByOtherScope(resources, context, resource.target,
|
|
174
|
+
['configuration-entry', 'config-entry', 'mcp-server'].includes(resource.kind) ? resource.identity : undefined);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The rows a removal may act on: this scope's own, minus any path the other scope still claims. */
|
|
178
|
+
function removableRows(resources: any, context: any) {
|
|
179
|
+
return (resources || []).filter((resource: any) => (!resource?.scope || resource.scope === context.scope)
|
|
180
|
+
&& !claimedRowElsewhere(resources, context, resource));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** This scope's rows on a path the other scope still claims become retained removals: the
|
|
184
|
+
* lifecycle releases the row and never hands the change to remove(), so nothing is deleted. */
|
|
185
|
+
function retainedRemovals(resources: any, context: any) {
|
|
186
|
+
return (resources || []).filter((resource: any) => resource?.harness === HARNESS && resource.scope === context.scope
|
|
187
|
+
&& claimedRowElsewhere(resources, context, resource))
|
|
188
|
+
.map((resource: any) => ({ harness: HARNESS, assetId: resource.assetId, target: resource.target, operation: 'remove',
|
|
189
|
+
ownershipIdentity: resource.ownershipIdentity, kind: resource.kind, identity: resource.identity,
|
|
190
|
+
retained: true, retainedFor: [`${HARNESS} (${context.scope === 'global' ? 'project' : 'global'} scope)`] }));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function ownedRemovalPlan(resources: any, context: any, kind: any, directory?: any) {
|
|
194
|
+
const entries = (resources || []).filter((resource: any) => resource?.harness === HARNESS && resource.kind === kind);
|
|
195
|
+
const changes = []; const conflicts = [];
|
|
196
|
+
for (const resource of entries) {
|
|
197
|
+
const target = kind === 'custom-agent'
|
|
198
|
+
? path.join(directory, `${resource.identity.replace(/^agent:/, '')}.toml`)
|
|
199
|
+
: resource.target;
|
|
200
|
+
if (path.resolve(target) !== path.resolve(resource.target)) continue;
|
|
201
|
+
if (fs.existsSync(target) && sha256(fs.readFileSync(target, 'utf8')) !== resource.fingerprint) {
|
|
202
|
+
conflicts.push(`${kind} '${resource.identity}' was modified outside DoFlow`);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
changes.push({ type: 'remove', identity: resource.identity, file: target, fingerprint: resource.fingerprint });
|
|
206
|
+
}
|
|
207
|
+
return { ok: conflicts.length === 0, status: conflicts.length ? 'conflict' : (changes.length ? 'change' : 'unchanged'), changes, conflicts, directRemove: true };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function configResourcesFrom(config: any, { scope, sourceVersion, recoveryRef }: any) {
|
|
211
|
+
return (config?.managedResources || [])
|
|
212
|
+
.filter((record: any) => record.kind === 'configuration-entry' || record.kind === 'config-entry')
|
|
213
|
+
.map((record: any) => lifecycleResource({ scope, assetId: 'guidance.codex-pointer', kind: 'configuration-entry', identity: record.identity,
|
|
214
|
+
target: config.file, fingerprint: record.fingerprint, sourceVersion: record.sourceVersion ?? sourceVersion, recoveryRef,
|
|
215
|
+
projection: { renderer: 'codex-config' } }));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function mcpResourcesFrom(mcp: any, { scope, sourceVersion, recoveryRef }: any) {
|
|
219
|
+
return (mcp?.managedResources || [])
|
|
220
|
+
.filter((record: any) => record.kind === 'mcp-server')
|
|
221
|
+
.map((record: any) => lifecycleResource({ scope, assetId: 'guidance.codex-pointer', kind: 'mcp-server', identity: record.identity,
|
|
222
|
+
target: mcp.file, fingerprint: record.fingerprint, sourceVersion: record.sourceVersion ?? sourceVersion, selection: record.selection, recoveryRef,
|
|
223
|
+
projection: { renderer: 'codex-mcp' } }));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function agentResourcesFrom(agents: any, { scope, sourceVersion, recoveryRef }: any) {
|
|
227
|
+
return (agents?.managedResources || [])
|
|
228
|
+
.filter((record: any) => record.kind === 'custom-agent')
|
|
229
|
+
.map((record: any) => lifecycleResource({ scope, assetId: 'agents.shared', kind: 'custom-agent', identity: record.identity,
|
|
230
|
+
target: path.join(agents.directory, `${record.identity.replace(/^agent:/, '')}.toml`), fingerprint: record.fingerprint,
|
|
231
|
+
sourceVersion: record.sourceVersion ?? sourceVersion, recoveryRef, projection: { renderer: 'codex-agent' } }));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function hooksResourceFrom(hooks: any, { scope, sourceVersion, recoveryRef }: any) {
|
|
235
|
+
if (!hooks?.applied || !hooks.destination) return [];
|
|
236
|
+
return [lifecycleResource({ scope, assetId: 'guidance.codex-pointer', kind: 'hooks-file', identity: 'hooks.json',
|
|
237
|
+
target: hooks.destination, fingerprint: sha256(fs.readFileSync(hooks.destination, 'utf8')), sourceVersion, recoveryRef,
|
|
238
|
+
projection: { renderer: 'codex-hooks' } })];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function resourcesFromApplied({ components, scope, sourceVersion, recoveryRef }: any) {
|
|
242
|
+
const shared = { scope, sourceVersion, recoveryRef };
|
|
243
|
+
return [
|
|
244
|
+
...configResourcesFrom(components.config, shared),
|
|
245
|
+
...mcpResourcesFrom(components.mcp, shared),
|
|
246
|
+
...agentResourcesFrom(components.agents, shared),
|
|
247
|
+
...hooksResourceFrom(components.hooks, shared),
|
|
248
|
+
];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ---- copy-tree assets (rules, skills, agents, templates, scripts, references) ----
|
|
252
|
+
|
|
253
|
+
function codexConfigDir(context: any) { return context.paths.configDir; }
|
|
254
|
+
function sourceDirFor(asset: any, repoRoot: any) {
|
|
255
|
+
if (!asset || typeof asset.source !== 'string') throw new Error('Codex asset requires a source path');
|
|
256
|
+
const root = repoRoot ? path.resolve(repoRoot) : process.cwd();
|
|
257
|
+
const source = path.resolve(root, asset.source);
|
|
258
|
+
if (!source.startsWith(`${root}${path.sep}`) || !fs.existsSync(source)) {
|
|
259
|
+
throw new Error(`Codex asset source is unavailable: ${asset.source}`);
|
|
260
|
+
}
|
|
261
|
+
return source;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ---- guidance.codex-pointer instructions (AGENTS.md managed-section merge) ----
|
|
265
|
+
// Codex discovers AGENTS.md at the repository root and walks down toward the CWD, plus
|
|
266
|
+
// ~/.codex/AGENTS.md for global scope (learn.chatgpt.com/docs/agent-configuration/agents-md).
|
|
267
|
+
// A project-scoped file inside .codex/ is not in that discovery chain and would never load, so
|
|
268
|
+
// the project-scope managed section must land at the project root itself — both locations are
|
|
269
|
+
// declared in harnesses.json (paths.instructions). The registry declares this asset's codex
|
|
270
|
+
// projection as renderer "codex-agents" (see core/registry/assets.json).
|
|
271
|
+
|
|
272
|
+
function instructionsAsset(assets: any) { return (assets || []).find((asset: any) => asset?.renderer === 'codex-agents'); }
|
|
273
|
+
function instructionsPath(context: any) {
|
|
274
|
+
return context.paths.instructions;
|
|
275
|
+
}
|
|
276
|
+
function sha256Text(text: any) { return crypto.createHash('sha256').update(text).digest('hex'); }
|
|
277
|
+
|
|
278
|
+
function planInstructionsAsset({ assets, context, removing, repoRoot, claimedElsewhere = () => false }: any) {
|
|
279
|
+
const asset = instructionsAsset(assets);
|
|
280
|
+
const changes: any[] = [];
|
|
281
|
+
const conflicts: any[] = [];
|
|
282
|
+
if (!asset) return { changes, conflicts };
|
|
283
|
+
const target = instructionsPath(context);
|
|
284
|
+
// Broken markers are a plan conflict, as for Claude: found only while applying, they would stop
|
|
285
|
+
// the run after hooks, config and skills were already removed.
|
|
286
|
+
const existing = fs.existsSync(target) ? fs.readFileSync(target, 'utf8') : null;
|
|
287
|
+
if (existing !== null) {
|
|
288
|
+
const starts = existing.split(MARKER_START).length - 1;
|
|
289
|
+
if (starts !== existing.split(MARKER_END).length - 1 || starts > 1) {
|
|
290
|
+
conflicts.push(`Codex instructions contain malformed DoFlow markers: ${target}`);
|
|
291
|
+
return { changes, conflicts };
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (removing) {
|
|
295
|
+
if (existing === null || claimedElsewhere(target) || !existing.includes(MARKER_START)) return { changes, conflicts };
|
|
296
|
+
changes.push({ assetId: asset.id, target, operation: 'remove', ownershipIdentity: 'doflow:codex:instructions:managed-section',
|
|
297
|
+
kind: 'instructions-section', identity: 'AGENTS.md', projection: { renderer: 'codex-agents' } });
|
|
298
|
+
return { changes, conflicts };
|
|
299
|
+
}
|
|
300
|
+
const source = sourceDirFor(asset, repoRoot);
|
|
301
|
+
if (!mergeMarkedSection(source, target, { dryRun: true }).changed) return { changes, conflicts };
|
|
302
|
+
const content = fs.readFileSync(source, 'utf8').replace(/\s+$/, '');
|
|
303
|
+
changes.push({ assetId: asset.id, target, source, operation: fs.existsSync(target) ? 'update' : 'create',
|
|
304
|
+
ownershipIdentity: 'doflow:codex:instructions:managed-section', kind: 'instructions-section', identity: 'AGENTS.md',
|
|
305
|
+
afterFingerprint: sha256Text(content), sourceVersion: 'registry-v1', projection: { renderer: 'codex-agents' } });
|
|
306
|
+
return { changes, conflicts };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function applyInstructionsAsset(changes: any) {
|
|
310
|
+
for (const change of changes) {
|
|
311
|
+
if (change.projection?.renderer !== 'codex-agents' || change.operation === 'remove') continue;
|
|
312
|
+
mergeMarkedSection(change.source, change.target);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function removeInstructionsAsset(changes: any) {
|
|
317
|
+
for (const change of changes) {
|
|
318
|
+
if (change.projection?.renderer === 'codex-agents' && change.operation === 'remove') removeMarkedSection(change.target);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function verifyInstructionsAsset({ assets, context, repoRoot }: any) {
|
|
323
|
+
const asset = instructionsAsset(assets);
|
|
324
|
+
const statuses: any[] = []; const resources: any[] = [];
|
|
325
|
+
if (!asset) return { statuses, resources };
|
|
326
|
+
const target = instructionsPath(context);
|
|
327
|
+
const existing = fs.existsSync(target) ? fs.readFileSync(target, 'utf8') : null;
|
|
328
|
+
const managed = existing !== null && existing.includes(MARKER_START) && existing.includes(MARKER_END);
|
|
329
|
+
statuses.push({ assetId: asset.id, capability: asset.capability, status: managed ? 'managed' : 'absent', target });
|
|
330
|
+
if (managed) {
|
|
331
|
+
resources.push({ assetId: asset.id, target, ownershipIdentity: 'doflow:codex:instructions:managed-section',
|
|
332
|
+
kind: 'instructions-section', identity: 'AGENTS.md', fingerprint: sha256Text(existing), sourceVersion: 'registry-v1',
|
|
333
|
+
projection: { renderer: 'codex-agents' } });
|
|
334
|
+
}
|
|
335
|
+
return { statuses, resources };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function planCopyTreeAssets({ assets, context, neutralResources, removing, repoRoot, sourceVersion, force = false }: any) {
|
|
339
|
+
const changes = [];
|
|
340
|
+
const conflicts = [];
|
|
341
|
+
const treeResults = [];
|
|
342
|
+
const notices = [];
|
|
343
|
+
for (const asset of copyTreeAssets(assets)) {
|
|
344
|
+
const destDir = copyTreeDestDir(codexConfigDir(context), asset);
|
|
345
|
+
const sourceDir = sourceDirFor(asset, repoRoot);
|
|
346
|
+
const previousResources = ledgerFileResources(neutralResources, HARNESS, asset.id);
|
|
347
|
+
const result = planTree({ sourceDir, destDir, previousResources, siblingFingerprints: ledgerSiblingFingerprints(neutralResources, HARNESS), operation: removing ? 'remove' : 'apply', layout: asset.layout, keepModified: !removing,
|
|
348
|
+
// `force` is the CLI's --force reaching the one conflict class a plan can actually
|
|
349
|
+
// downgrade: a ledger-owned destination whose bytes were edited underneath us. With force,
|
|
350
|
+
// that is drift to heal rather than a refusal — exactly what `doflow reconcile` (always
|
|
351
|
+
// forced) and `update --force` need. Removal stays strict by design: a hand-edited file is
|
|
352
|
+
// never deleted, forced or not.
|
|
353
|
+
force: !removing && force === true });
|
|
354
|
+
treeResults.push(result);
|
|
355
|
+
conflicts.push(...result.conflicts.map((reason: any) => `${asset.id}: ${reason}`));
|
|
356
|
+
// A hand-edited file at a path DoFlow no longer writes is the user's: it stays, only its
|
|
357
|
+
// ownership row is released.
|
|
358
|
+
notices.push(...result.kept.map((item: any) => `kept hand-edited ${path.relative(context.paths.root, item.target)}; DoFlow no longer manages it`));
|
|
359
|
+
for (const change of result.changes) {
|
|
360
|
+
changes.push({
|
|
361
|
+
assetId: asset.id, target: change.target, source: change.source, operation: change.operation,
|
|
362
|
+
ownershipIdentity: `doflow:codex:copy-tree:${asset.id}:${change.relPath}`,
|
|
363
|
+
kind: 'copy-tree-file', identity: change.relPath,
|
|
364
|
+
afterFingerprint: change.fingerprint, fingerprint: change.fingerprint, sourceVersion: sourceVersion ?? 'unknown',
|
|
365
|
+
...(change.kept ? { retained: true, retainedFor: [] } : {}),
|
|
366
|
+
projection: { renderer: 'copy-tree' },
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return { changes, conflicts, notices: [...siblingReplacedNotices(treeResults), ...notices] };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function applyCopyTreeAssets(changes: any) {
|
|
374
|
+
const treeChanges = changes.filter((change: any) => change.projection?.renderer === 'copy-tree' && change.operation !== 'remove')
|
|
375
|
+
.map((change: any) => ({ relPath: change.identity, target: change.target, source: change.source, operation: change.operation, fingerprint: change.fingerprint }));
|
|
376
|
+
return applyTree({ changes: treeChanges }).applied;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function removeCopyTreeAssets(changes: any) {
|
|
380
|
+
const treeChanges = changes.filter((change: any) => change.projection?.renderer === 'copy-tree' && change.operation === 'remove' && !change.retained)
|
|
381
|
+
.map((change: any) => ({ relPath: change.identity, target: change.target, operation: 'remove', fingerprint: change.fingerprint, pruneGuard: change.pruneGuard }));
|
|
382
|
+
return removeTree({ changes: treeChanges }).removed;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function verifyCopyTreeAssets({ assets, context, repoRoot, sourceVersion, plannedWrites }: any) {
|
|
386
|
+
const statuses = [];
|
|
387
|
+
const resources = [];
|
|
388
|
+
const conflicts = [];
|
|
389
|
+
for (const asset of copyTreeAssets(assets)) {
|
|
390
|
+
const destDir = copyTreeDestDir(codexConfigDir(context), asset);
|
|
391
|
+
const sourceDir = sourceDirFor(asset, repoRoot);
|
|
392
|
+
const result = verifyTree({ sourceDir, destDir, layout: asset.layout, plannedWrites });
|
|
393
|
+
conflicts.push(...result.conflicts.map((reason: any) => `${asset.id}: ${reason}`));
|
|
394
|
+
for (const resource of result.resources) {
|
|
395
|
+
resources.push({
|
|
396
|
+
assetId: asset.id, target: resource.target, ownershipIdentity: `doflow:codex:copy-tree:${asset.id}:${resource.relPath}`,
|
|
397
|
+
kind: 'copy-tree-file', identity: resource.relPath,
|
|
398
|
+
fingerprint: resource.fingerprint, sourceVersion: sourceVersion ?? 'unknown',
|
|
399
|
+
projection: { renderer: 'copy-tree' },
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
statuses.push({ assetId: asset.id, capability: asset.capability, status: result.ok ? 'managed' : 'conflict', target: destDir });
|
|
403
|
+
}
|
|
404
|
+
return { statuses, resources, conflicts };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** Resolve the explicit projected-native shape. During the migration, direct
|
|
408
|
+
* callers may still use legacy option names; lifecycle callers pass `assets`,
|
|
409
|
+
* `mcp`, `policies`, and optionally `projection` for source-bearing surfaces.
|
|
410
|
+
* Native source paths are never guessed from an asset's renderer. */
|
|
411
|
+
function projectedNativeOptions(options: any) {
|
|
412
|
+
const projection = options.projection || {};
|
|
413
|
+
const configResources = projection.configResources ?? options.configResources ?? options.context?.codexConfigResources;
|
|
414
|
+
const mcpCatalog = projection.mcp?.catalog ?? options.mcpCatalog ?? options.mcp ?? [];
|
|
415
|
+
const selectedMcp = projection.mcp?.selected ?? options.selectedMcp ?? (options.mcp ? options.mcp.map((server: any) => server.id) : undefined);
|
|
416
|
+
const agentsSourceDir = projection.agents?.sourceDir ?? options.agentsSourceDir ?? options.context?.codexAgentsSourceDir;
|
|
417
|
+
const hooksSourceFile = projection.hooks?.sourceFile ?? options.hooksSourceFile ?? options.context?.codexHooksSourceFile;
|
|
418
|
+
const hooksSourceDir = projection.hooks?.sourceHooksDir ?? options.hooksSourceDir ?? options.context?.codexHooksSourceDir;
|
|
419
|
+
const hooksConfig = projection.hooks?.config ?? options.hooksConfig;
|
|
420
|
+
const hooksTrusted = projection.hooks?.trusted ?? options.hooksTrusted;
|
|
421
|
+
return { configResources, mcpCatalog, selectedMcp, agentsSourceDir, hooksSourceFile, hooksSourceDir, hooksConfig, hooksTrusted };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function planConfigComponent({ native, neutralResources, context, configFile, removing, adopt = false }: any) {
|
|
425
|
+
if (native.configResources === undefined) return undefined;
|
|
426
|
+
const managedResources = nativeManagedResources(neutralResources, context, { kind: 'configuration-entry', target: configFile });
|
|
427
|
+
const component = planCodexConfig({ file: configFile, scope: context.scope, managedResources, desiredResources: removing ? [] : native.configResources, adopt: !removing && adopt });
|
|
428
|
+
component.scope = context.scope;
|
|
429
|
+
component.desiredResources = removing ? [] : native.configResources;
|
|
430
|
+
// The next ledger intentionally omits a removed record. Keep the pre-plan
|
|
431
|
+
// ownership proof solely for apply's immediate replan over the current file.
|
|
432
|
+
component.reconciliationManagedResources = managedResources;
|
|
433
|
+
return component;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function planMcpComponent({ native, neutralResources, context, configFile, removing, options }: any) {
|
|
437
|
+
if (native.selectedMcp === undefined) return undefined;
|
|
438
|
+
const catalog = nativeMcpCatalog(native.mcpCatalog);
|
|
439
|
+
const managedResources = nativeManagedResources(neutralResources, context, { kind: 'mcp-server', target: configFile });
|
|
440
|
+
return planCodexMcp({ file: configFile, scope: context.scope, managedResources, selected: removing ? [] : native.selectedMcp,
|
|
441
|
+
removing, scopeArg: context.scope === 'global' ? '-g' : context.projectRoot,
|
|
442
|
+
...catalog, sourceVersion: options.sourceVersion ?? options.context?.sourceVersion, recoveryPoint: options.recoveryPoint });
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** Codex hooks.json is only replaced when the file on disk still matches the ledger's last-known
|
|
446
|
+
* fingerprint — a foreign edit (or a record the ledger never owned) downgrades an otherwise-clean
|
|
447
|
+
* plan to a conflict rather than silently overwriting it. */
|
|
448
|
+
function planHooksComponent({ native, neutralResources, context, adopt = false }: any) {
|
|
449
|
+
let hooks = planCodexHooks({ config: native.hooksConfig, sourceFile: native.hooksSourceFile,
|
|
450
|
+
sourceHooksDir: native.hooksSourceDir, trusted: native.hooksTrusted, destinationContext: context });
|
|
451
|
+
const hooksFile = hooks.destination;
|
|
452
|
+
if (hooks.ok && fs.existsSync(hooksFile)) {
|
|
453
|
+
const [record] = nativeManagedResources(neutralResources, context, { kind: 'hooks-file', target: hooksFile });
|
|
454
|
+
const current = sha256(fs.readFileSync(hooksFile, 'utf8'));
|
|
455
|
+
// `adopt` covers the no-record case only: the file is claimed into the ledger and planning
|
|
456
|
+
// proceeds. A record that disagrees with the bytes below is a different situation and stays a
|
|
457
|
+
// conflict — that one is `--force`'s business, not adoption's.
|
|
458
|
+
if (!record && !adopt) {
|
|
459
|
+
hooks = { ...hooks, ok: false, status: 'conflict', changes: [], errors: ['Codex hooks.json exists but is not owned by the neutral ledger'] };
|
|
460
|
+
} else if (record && record.fingerprint !== current) {
|
|
461
|
+
hooks = { ...hooks, ok: false, status: 'conflict', changes: [], errors: ['Codex hooks.json was modified outside DoFlow'] };
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return hooks;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** Agents/hooks share one removing-vs-planning fork: removal always owns both via the neutral
|
|
468
|
+
* ledger's own records, regardless of what native input the caller provided this run. */
|
|
469
|
+
function planAgentsAndHooksComponents({ native, neutralResources, context, agentsDirectory, removing, options, adopt = false }: any) {
|
|
470
|
+
if (removing) {
|
|
471
|
+
return {
|
|
472
|
+
agents: ownedRemovalPlan(neutralResources, context, 'custom-agent', agentsDirectory),
|
|
473
|
+
// The scripts deployed beside hooks.json have no ledger rows; remove() matches them by bytes.
|
|
474
|
+
hooks: { ...ownedRemovalPlan(neutralResources, context, 'hooks-file'),
|
|
475
|
+
scriptsDir: native.hooksSourceDir, destinationHooksDir: context.paths.hooksDirectory },
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
const components: Record<string, any> = {};
|
|
479
|
+
if (native.agentsSourceDir !== undefined) {
|
|
480
|
+
const managedResources = nativeManagedResources(neutralResources, context, { kind: 'custom-agent', directory: agentsDirectory });
|
|
481
|
+
// `...context` is the NARROW native context (scope/codexDir/paths) and carries no adopt, so
|
|
482
|
+
// it is passed explicitly. Reading it off that object is what silently did nothing.
|
|
483
|
+
components.agents = planCodexAgents({ ...context, managedResources, sourceDir: native.agentsSourceDir, sourceVersion: options.sourceVersion ?? options.context?.sourceVersion, adopt });
|
|
484
|
+
}
|
|
485
|
+
if (native.hooksSourceFile !== undefined || native.hooksConfig !== undefined) {
|
|
486
|
+
components.hooks = planHooksComponent({ native, neutralResources, context, adopt });
|
|
487
|
+
}
|
|
488
|
+
return components;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Compose native, non-mutating plans. Callers opt into a component by providing
|
|
493
|
+
* its desired input, which prevents a generic plan from accidentally removing
|
|
494
|
+
* an owned configuration entry or MCP server merely because it was omitted.
|
|
495
|
+
*/
|
|
496
|
+
function plan(options: any) {
|
|
497
|
+
const context = nativeContext(options);
|
|
498
|
+
const removing = options.context?.operation === 'remove';
|
|
499
|
+
const ledgerRows = options.managedResources || options.ledger?.resources || [];
|
|
500
|
+
const neutralResources = removing ? removableRows(ledgerRows, context) : ledgerRows;
|
|
501
|
+
const native = projectedNativeOptions(options);
|
|
502
|
+
const configFile = context.paths.configFile;
|
|
503
|
+
const agentsDirectory = context.paths.agentsDirectory;
|
|
504
|
+
const components: Record<string, any> = {};
|
|
505
|
+
// `options.context` is the lifecycle context; `context` is this adapter's narrow native one.
|
|
506
|
+
// copyTree below already reads force off the former — adopt is read the same way.
|
|
507
|
+
const adopt = options.context?.adopt === true;
|
|
508
|
+
const configComponent = planConfigComponent({ native, neutralResources, context, configFile, removing, adopt });
|
|
509
|
+
if (configComponent) components.config = configComponent;
|
|
510
|
+
const mcpComponent = planMcpComponent({ native, neutralResources, context, configFile, removing, options });
|
|
511
|
+
if (mcpComponent) components.mcp = mcpComponent;
|
|
512
|
+
Object.assign(components, planAgentsAndHooksComponents({ native, neutralResources, context, agentsDirectory, removing, options, adopt }));
|
|
513
|
+
const copyTree = planCopyTreeAssets({ assets: options.assets, context, neutralResources, removing,
|
|
514
|
+
repoRoot: options.context?.repoRoot, sourceVersion: options.sourceVersion ?? options.context?.sourceVersion,
|
|
515
|
+
force: options.context?.force === true });
|
|
516
|
+
const instructions = planInstructionsAsset({ assets: options.assets, context, removing, repoRoot: options.context?.repoRoot,
|
|
517
|
+
claimedElsewhere: (target: any) => claimedByOtherScope(ledgerRows, context, target) });
|
|
518
|
+
// Two pseudo-components: their changes are already in final adapter shape (unlike the other
|
|
519
|
+
// components' native shape, which addChanges() below converts), so they're appended to `changes`
|
|
520
|
+
// directly rather than run through addChanges. Registering them in `components` still gets their
|
|
521
|
+
// conflicts picked up by both the existing `failures` reduction and the lifecycle's own
|
|
522
|
+
// adapterConflicts() component scan, with no special-casing needed in either.
|
|
523
|
+
components.copyTree = { ok: copyTree.conflicts.length === 0, conflicts: copyTree.conflicts };
|
|
524
|
+
components.instructions = { ok: instructions.conflicts.length === 0, conflicts: instructions.conflicts };
|
|
525
|
+
const failures = Object.entries(components).filter(([, component]: [string, any]) => !component.ok);
|
|
526
|
+
const changes: any[] = [];
|
|
527
|
+
addChanges(changes, assetIdFor(options, 'config'), components.config, 'config');
|
|
528
|
+
addChanges(changes, assetIdFor(options, 'mcp'), components.mcp, 'mcp');
|
|
529
|
+
addChanges(changes, assetIdFor(options, 'agents'), components.agents, 'agents');
|
|
530
|
+
addChanges(changes, assetIdFor(options, 'hooks'), components.hooks, 'hooks');
|
|
531
|
+
changes.push(...copyTree.changes, ...instructions.changes);
|
|
532
|
+
if (removing) changes.push(...retainedRemovals(ledgerRows, context));
|
|
533
|
+
const requiredNativeResources = [
|
|
534
|
+
...Object.entries(components).flatMap(([component, result]: [string, any]) => (result.changes || []).map((change: any) => ({
|
|
535
|
+
harness: HARNESS, component, target: change.target ?? change.file ?? result.file ?? result.destination ?? result.directory,
|
|
536
|
+
operation: change.operation ?? change.type, identity: change.identity ?? null,
|
|
537
|
+
}))),
|
|
538
|
+
...copyTree.changes.map((change) => ({ harness: HARNESS, component: 'copyTree', target: change.target, operation: change.operation, identity: change.identity })),
|
|
539
|
+
...instructions.changes.map((change) => ({ harness: HARNESS, component: 'instructions', target: change.target, operation: change.operation, identity: change.identity })),
|
|
540
|
+
];
|
|
541
|
+
return { harness: HARNESS, scope: options.scope, ok: failures.length === 0, safe: failures.length === 0, components, failures, changes, requiredNativeResources,
|
|
542
|
+
notices: [...copyTree.notices, ...(components.mcp?.notices || [])] };
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/** Apply only native plans emitted by this adapter. MCP is first because it and
|
|
546
|
+
* config.toml share a file; config is then re-planned over the MCP result so it
|
|
547
|
+
* cannot overwrite an adjacent MCP table. */
|
|
548
|
+
function apply({ changes = [], dryRun = false, scope, sourceVersion, recoveryRef }: any = {}) {
|
|
549
|
+
const components = new Map();
|
|
550
|
+
for (const change of changes) if (change?.nativeComponent && change.nativePlan) components.set(change.nativeComponent, change.nativePlan);
|
|
551
|
+
let mcp = components.get('mcp');
|
|
552
|
+
if (mcp) mcp = applyCodexMcp(mcp, { dryRun });
|
|
553
|
+
let config = components.get('config');
|
|
554
|
+
if (config) {
|
|
555
|
+
const managedResources = mcp
|
|
556
|
+
? [...(config.reconciliationManagedResources ?? config.managedResources ?? []), ...(mcp.managedResources ?? [])]
|
|
557
|
+
: null;
|
|
558
|
+
const replan = managedResources ? planCodexConfig({ file: config.file, scope: config.scope, managedResources,
|
|
559
|
+
desiredResources: config.desiredResources ?? [] }) : config;
|
|
560
|
+
config = applyCodexConfig(replan, { dryRun });
|
|
561
|
+
}
|
|
562
|
+
const agentsPlan = components.get('agents'); const hooksPlan = components.get('hooks');
|
|
563
|
+
const agents = agentsPlan && !agentsPlan.directRemove ? applyCodexAgents(agentsPlan, { dryRun }) : null;
|
|
564
|
+
const hooks = hooksPlan && !hooksPlan.directRemove ? deployCodexHooks(hooksPlan, { dryRun }) : null;
|
|
565
|
+
// applyCopyTreeAssets/applyInstructionsAsset already filter to non-remove operations, so calling
|
|
566
|
+
// them unconditionally here (including when apply() is invoked from remove(), below) is safe.
|
|
567
|
+
const copyTreeApplied = dryRun ? 0 : applyCopyTreeAssets(changes);
|
|
568
|
+
// Copy-tree removals run here too, for both verbs: an update that no longer ships a file
|
|
569
|
+
// carries its rows as removals inside the apply batch, and remove() delegates to this function.
|
|
570
|
+
if (!dryRun) { removeCopyTreeAssets(changes); applyInstructionsAsset(changes); }
|
|
571
|
+
const nativeComponents = { mcp, config, agents, hooks };
|
|
572
|
+
return { harness: HARNESS, applied: [mcp, config, agents, hooks].filter((result) => result?.applied).length + copyTreeApplied,
|
|
573
|
+
components: nativeComponents, resources: resourcesFromApplied({ components: nativeComponents, scope: scope ?? 'project', sourceVersion, recoveryRef }) };
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/** Removal reuses the same fingerprint-checked native plans. Only config/MCP
|
|
577
|
+
* plans generated with context.operation=remove contain safe removals; hooks
|
|
578
|
+
* and agents are intentionally never blindly deleted. */
|
|
579
|
+
function remove({ changes = [], dryRun = false }: any = {}) {
|
|
580
|
+
const result = apply({ changes, dryRun });
|
|
581
|
+
const direct = new Map();
|
|
582
|
+
for (const change of changes) if (change?.nativePlan?.directRemove) direct.set(change.target, change);
|
|
583
|
+
for (const change of direct.values()) {
|
|
584
|
+
if (!fs.existsSync(change.target)) continue;
|
|
585
|
+
const current = sha256(fs.readFileSync(change.target, 'utf8'));
|
|
586
|
+
if (current !== change.fingerprint) throw new Error(`Refusing to remove modified Codex resource '${change.identity}'`);
|
|
587
|
+
if (!dryRun) fs.unlinkSync(change.target);
|
|
588
|
+
}
|
|
589
|
+
const hooksPlan = changes.find((change: any) => change?.nativeComponent === 'hooks' && change.nativePlan?.directRemove)?.nativePlan;
|
|
590
|
+
if (hooksPlan) removeCodexHookScripts(hooksPlan, { dryRun });
|
|
591
|
+
if (!dryRun) removeInstructionsAsset(changes);
|
|
592
|
+
return { ...result, removed: changes.filter((change: any) => (change.operation ?? change.type) === 'remove').length };
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function verifyConfigResources({ native, parsed, context, options, sourceVersion, recoveryRef }: any) {
|
|
596
|
+
const statuses = []; const resources = [];
|
|
597
|
+
for (const resource of native.configResources || []) {
|
|
598
|
+
const entry = parsed?.entries.get(resource.identity);
|
|
599
|
+
const status = entry && configFingerprint(entry.value) === configFingerprint(resource.value) ? 'managed' : 'missing';
|
|
600
|
+
statuses.push({ assetId: assetIdFor(options, 'config'), capability: 'settings', status, identity: resource.identity,
|
|
601
|
+
ownershipIdentity: ownershipIdentity('config', resource.identity), target: context.paths.configFile });
|
|
602
|
+
if (status === 'managed') resources.push(lifecycleResource({ scope: options.scope, assetId: assetIdFor(options, 'config'), kind: 'configuration-entry', identity: resource.identity,
|
|
603
|
+
target: context.paths.configFile, fingerprint: configFingerprint(entry.value), sourceVersion, recoveryRef, projection: { renderer: 'codex-config' } }));
|
|
604
|
+
}
|
|
605
|
+
return { statuses, resources };
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/** A server is recorded only when its table now holds DoFlow's rendering. One discovery found as
|
|
609
|
+
* the user's at plan time is reported `not-managed` and never recorded, even when its bytes match. */
|
|
610
|
+
function verifyMcpResources({ native, configText, context, options, sourceVersion, recoveryRef }: any) {
|
|
611
|
+
const statuses = []; const resources = [];
|
|
612
|
+
const catalog = nativeMcpCatalog(native.mcpCatalog);
|
|
613
|
+
const userDefined = new Set(options.discovery?.mcpUserDefined || []);
|
|
614
|
+
for (const id of native.selectedMcp || []) {
|
|
615
|
+
const definition = catalog.serverDefs[id];
|
|
616
|
+
const table = definition ? serverTableText(configText, id) : null;
|
|
617
|
+
const present = table !== null && configFingerprint(table) === configFingerprint(renderServer(id, definition));
|
|
618
|
+
const status = userDefined.has(id) ? 'not-managed' : (present ? 'managed' : 'missing');
|
|
619
|
+
statuses.push({ assetId: assetIdFor(options, 'mcp'), capability: 'mcp', status, identity: id,
|
|
620
|
+
ownershipIdentity: ownershipIdentity('mcp', id), target: context.paths.configFile });
|
|
621
|
+
if (status === 'managed') resources.push(lifecycleResource({ scope: options.scope, assetId: assetIdFor(options, 'mcp'), kind: 'mcp-server', identity: id,
|
|
622
|
+
target: context.paths.configFile, fingerprint: configFingerprint(renderServer(id, definition)), sourceVersion, recoveryRef,
|
|
623
|
+
projection: { renderer: 'codex-mcp' } }));
|
|
624
|
+
}
|
|
625
|
+
return { statuses, resources };
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function verifyAgentResources({ native, context, options, sourceVersion, recoveryRef }: any) {
|
|
629
|
+
const statuses: any[] = []; const resources: any[] = [];
|
|
630
|
+
if (!native.agentsSourceDir) return { statuses, resources };
|
|
631
|
+
for (const agent of discoverCodexAgents(native.agentsSourceDir)) {
|
|
632
|
+
const target = path.join(agentDirectory(context), agent.fileName);
|
|
633
|
+
const present = fs.existsSync(target) && fs.readFileSync(target, 'utf8') === agent.source;
|
|
634
|
+
const identity = `agent:${agent.name}`;
|
|
635
|
+
statuses.push({ assetId: assetIdFor(options, 'agents'), capability: 'agents', status: present ? 'managed' : 'missing', identity,
|
|
636
|
+
ownershipIdentity: ownershipIdentity('agents', identity), target });
|
|
637
|
+
if (present) resources.push(lifecycleResource({ scope: options.scope, assetId: assetIdFor(options, 'agents'), kind: 'custom-agent', identity: `agent:${agent.name}`,
|
|
638
|
+
target, fingerprint: agent.fingerprint, sourceVersion, recoveryRef, projection: { renderer: 'codex-agent' } }));
|
|
639
|
+
}
|
|
640
|
+
return { statuses, resources };
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function verifyHooksResource({ native, context, options, sourceVersion, recoveryRef }: any) {
|
|
644
|
+
const statuses: any[] = []; const resources: any[] = []; const conflicts: any[] = [];
|
|
645
|
+
if (!native.hooksSourceFile && !native.hooksConfig) return { statuses, resources, conflicts };
|
|
646
|
+
const hooks = planCodexHooks({ config: native.hooksConfig, sourceFile: native.hooksSourceFile, sourceHooksDir: native.hooksSourceDir,
|
|
647
|
+
trusted: native.hooksTrusted, destinationContext: context });
|
|
648
|
+
const status = hooks.ok && hooks.status === 'unchanged' ? 'managed' : (hooks.ok ? 'missing' : 'conflict');
|
|
649
|
+
statuses.push({ assetId: assetIdFor(options, 'hooks'), capability: 'hooks', status, identity: 'hooks.json',
|
|
650
|
+
ownershipIdentity: ownershipIdentity('hooks', 'hooks.json'), target: hooks.destination });
|
|
651
|
+
if (!hooks.ok) conflicts.push(...hooks.errors);
|
|
652
|
+
if (status === 'managed') resources.push(lifecycleResource({ scope: options.scope, assetId: assetIdFor(options, 'hooks'), kind: 'hooks-file', identity: 'hooks.json',
|
|
653
|
+
target: hooks.destination, fingerprint: sha256(fs.readFileSync(hooks.destination, 'utf8')), sourceVersion, recoveryRef, projection: { renderer: 'codex-hooks' } }));
|
|
654
|
+
return { statuses, resources, conflicts };
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function verifyAssetRenderStatuses({ registry, assets }: any) {
|
|
658
|
+
return assets.map((asset: any) => render({ registry, asset })).filter((rendered: any) => rendered.status !== 'renderable');
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function verify(options: any = {}) {
|
|
662
|
+
const { registry, assets = [], plan: proposed } = options;
|
|
663
|
+
const context = nativeContext(options);
|
|
664
|
+
const native = projectedNativeOptions(options);
|
|
665
|
+
const sourceVersion = options.sourceVersion ?? options.context?.sourceVersion;
|
|
666
|
+
const recoveryRef = options.recoveryRef;
|
|
667
|
+
let configText = '';
|
|
668
|
+
if (fs.existsSync(context.paths.configFile)) configText = fs.readFileSync(context.paths.configFile, 'utf8');
|
|
669
|
+
let parsed;
|
|
670
|
+
const conflicts = [];
|
|
671
|
+
try { parsed = parseToml(configText); } catch (error: any) { conflicts.push(`Malformed Codex config: ${error.message}`); }
|
|
672
|
+
|
|
673
|
+
const shared = { context, options, sourceVersion, recoveryRef };
|
|
674
|
+
const config = verifyConfigResources({ native, parsed, ...shared });
|
|
675
|
+
const mcp = verifyMcpResources({ native, configText, ...shared });
|
|
676
|
+
const agents = verifyAgentResources({ native, ...shared });
|
|
677
|
+
const hooks = verifyHooksResource({ native, ...shared });
|
|
678
|
+
const copyTree = verifyCopyTreeAssets({ assets, context, repoRoot: options.context?.repoRoot, sourceVersion, plannedWrites: options.context?.plannedWrites });
|
|
679
|
+
const instructions = verifyInstructionsAsset({ assets, context, repoRoot: options.context?.repoRoot });
|
|
680
|
+
|
|
681
|
+
const statuses = [
|
|
682
|
+
...config.statuses, ...mcp.statuses, ...agents.statuses, ...hooks.statuses,
|
|
683
|
+
...verifyAssetRenderStatuses({ registry, assets }),
|
|
684
|
+
...copyTree.statuses, ...instructions.statuses,
|
|
685
|
+
];
|
|
686
|
+
const resources = [...config.resources, ...mcp.resources, ...agents.resources, ...hooks.resources, ...copyTree.resources, ...instructions.resources];
|
|
687
|
+
conflicts.push(...hooks.conflicts, ...copyTree.conflicts);
|
|
688
|
+
const componentFailures = Object.values<any>(proposed?.components || {}).filter((component) => !component.ok);
|
|
689
|
+
for (const component of componentFailures) conflicts.push(...(component.conflicts || component.errors || []));
|
|
690
|
+
|
|
691
|
+
return { ok: conflicts.length === 0 && statuses.every((status) => status.status !== 'missing' && status.status !== 'conflict'), resources, statuses, conflicts };
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return { normalizeContext, nativeContext, projectedNativeOptions, nativeManagedResources, ownershipIdentity, discover, render, plan, apply, remove, verify };
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const singleton = createCodexAdapter();
|
|
698
|
+
|
|
699
|
+
module.exports = {
|
|
700
|
+
HARNESS,
|
|
701
|
+
normalizeContext: singleton.normalizeContext, nativeContext: singleton.nativeContext,
|
|
702
|
+
projectedNativeOptions: singleton.projectedNativeOptions, nativeManagedResources: singleton.nativeManagedResources,
|
|
703
|
+
ownershipIdentity: singleton.ownershipIdentity,
|
|
704
|
+
discover: singleton.discover, render: singleton.render, plan: singleton.plan, apply: singleton.apply,
|
|
705
|
+
remove: singleton.remove, verify: singleton.verify,
|
|
706
|
+
createCodexAdapter,
|
|
707
|
+
};
|