@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,208 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
4
|
+
|
|
5
|
+
// Codex custom-agent projection. Agent contracts are independent TOML documents, so unlike
|
|
6
|
+
// config.toml there is no safe key-level merge: a same-named destination is changed only when
|
|
7
|
+
// the manifest proves DoFlow owns the exact bytes currently on disk.
|
|
8
|
+
const crypto = require('node:crypto');
|
|
9
|
+
const fs = require('node:fs');
|
|
10
|
+
const path = require('node:path');
|
|
11
|
+
const { atomicWrite } = require('./config.ts');
|
|
12
|
+
|
|
13
|
+
const AGENT_DIRECTORY = 'agents';
|
|
14
|
+
const AGENT_KIND = 'custom-agent';
|
|
15
|
+
const REQUIRED_FIELDS = ['name', 'description', 'developer_instructions'];
|
|
16
|
+
const READ_ONLY_ROLES = new Set([
|
|
17
|
+
'backend-architect', 'deep-research-agent', 'devops-architect', 'frontend-architect',
|
|
18
|
+
'quality-engineer', 'requirements-analyst', 'root-cause-analyst', 'security-engineer',
|
|
19
|
+
'system-architect', 'technical-writer',
|
|
20
|
+
]);
|
|
21
|
+
const IMPLEMENTATION_ROLES = new Set([
|
|
22
|
+
'performance-engineer', 'pm-agent', 'python-expert', 'refactoring-expert',
|
|
23
|
+
]);
|
|
24
|
+
const ALLOWED_FIELDS = new Set([...REQUIRED_FIELDS, 'sandbox_mode']);
|
|
25
|
+
|
|
26
|
+
function fingerprint(content: string): string {
|
|
27
|
+
return `sha256:${crypto.createHash('sha256').update(content).digest('hex')}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function assertAgentName(name: any, label: string = 'agent name'): string {
|
|
31
|
+
if (typeof name !== 'string' || !/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
32
|
+
throw new Error(`${label} must be a lowercase kebab-case identifier`);
|
|
33
|
+
}
|
|
34
|
+
return name;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function filenameFor(name: string): string {
|
|
38
|
+
assertAgentName(name);
|
|
39
|
+
return `${name}.toml`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function expectedName(fileName: any): string {
|
|
43
|
+
if (typeof fileName !== 'string' || path.basename(fileName) !== fileName || !fileName.endsWith('.toml')) {
|
|
44
|
+
throw new Error('Agent filename must be a direct .toml filename');
|
|
45
|
+
}
|
|
46
|
+
return assertAgentName(fileName.slice(0, -'.toml'.length), 'Agent filename');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parseQuoted(value: string, field: string, line: number): string {
|
|
50
|
+
if (!/^"(?:[^"\\\n]|\\.)*"$/.test(value)) throw new Error(`${field} must be a quoted TOML string on line ${line}`);
|
|
51
|
+
try { return JSON.parse(value); } catch { throw new Error(`${field} has an invalid quoted value on line ${line}`); }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Parse the intentionally small TOML contract supported by Codex agent definitions. */
|
|
55
|
+
function parseAgentToml(source: any, { fileName }: { fileName?: string } = {}): Record<string, string> {
|
|
56
|
+
if (typeof source !== 'string') throw new Error('Agent TOML source must be text');
|
|
57
|
+
const values: Record<string, string> = {};
|
|
58
|
+
const lines = source.split(/\r?\n/);
|
|
59
|
+
for (let index = 0; index < lines.length; index++) {
|
|
60
|
+
const line = lines[index];
|
|
61
|
+
if (!line.trim() || /^\s*#/.test(line)) continue;
|
|
62
|
+
const triple = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*"""\s*$/);
|
|
63
|
+
if (triple) {
|
|
64
|
+
const field = triple[1];
|
|
65
|
+
if (!ALLOWED_FIELDS.has(field)) throw new Error(`Unsupported agent field '${field}'`);
|
|
66
|
+
if (Object.hasOwn(values, field)) throw new Error(`Duplicate agent field '${field}'`);
|
|
67
|
+
const body: string[] = [];
|
|
68
|
+
let closed = false;
|
|
69
|
+
while (++index < lines.length) {
|
|
70
|
+
if (/^"""\s*$/.test(lines[index])) { closed = true; break; }
|
|
71
|
+
body.push(lines[index]);
|
|
72
|
+
}
|
|
73
|
+
if (!closed) throw new Error(`${field} has an unterminated multiline string`);
|
|
74
|
+
values[field] = body.join('\n').trim();
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const assignment = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/);
|
|
78
|
+
if (!assignment) throw new Error(`Malformed agent TOML on line ${index + 1}`);
|
|
79
|
+
const [, field, raw] = assignment;
|
|
80
|
+
if (!ALLOWED_FIELDS.has(field)) throw new Error(`Unsupported agent field '${field}'`);
|
|
81
|
+
if (Object.hasOwn(values, field)) throw new Error(`Duplicate agent field '${field}'`);
|
|
82
|
+
values[field] = parseQuoted(raw, field, index + 1);
|
|
83
|
+
}
|
|
84
|
+
if (fileName) values._fileName = fileName;
|
|
85
|
+
return values;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function validateAgentContract(source: any, { fileName }: { fileName?: string } = {}): any {
|
|
89
|
+
const agent = parseAgentToml(source, { fileName });
|
|
90
|
+
for (const field of REQUIRED_FIELDS) {
|
|
91
|
+
if (typeof agent[field] !== 'string' || !agent[field].trim()) throw new Error(`Agent is missing required '${field}'`);
|
|
92
|
+
}
|
|
93
|
+
assertAgentName(agent.name);
|
|
94
|
+
if (fileName && agent.name !== expectedName(fileName)) throw new Error(`Agent name '${agent.name}' must match filename '${fileName}'`);
|
|
95
|
+
if (agent.description.trim().length < 20) throw new Error(`Agent '${agent.name}' needs a descriptive description`);
|
|
96
|
+
if (agent.developer_instructions.trim().length < 100) throw new Error(`Agent '${agent.name}' needs substantive developer instructions`);
|
|
97
|
+
if (agent.sandbox_mode !== undefined && !['read-only', 'workspace-write', 'danger-full-access'].includes(agent.sandbox_mode)) {
|
|
98
|
+
throw new Error(`Agent '${agent.name}' has an unsupported sandbox_mode`);
|
|
99
|
+
}
|
|
100
|
+
if (READ_ONLY_ROLES.has(agent.name) && agent.sandbox_mode !== 'read-only') {
|
|
101
|
+
throw new Error(`Analysis role '${agent.name}' must use sandbox_mode = "read-only"`);
|
|
102
|
+
}
|
|
103
|
+
if (IMPLEMENTATION_ROLES.has(agent.name) && agent.sandbox_mode !== undefined) {
|
|
104
|
+
throw new Error(`Implementation role '${agent.name}' must inherit its parent sandbox`);
|
|
105
|
+
}
|
|
106
|
+
return Object.freeze({ ...agent, fileName: filenameFor(agent.name) });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function agentDirectory({ scope, codexDir, projectRoot, paths }: { scope?: string, codexDir?: string, projectRoot?: string, paths?: any } = {}): string {
|
|
110
|
+
// Preferred: the declared path resolved by the adapter (harnesses.json "paths".agentsDirectory).
|
|
111
|
+
if (paths?.agentsDirectory) return paths.agentsDirectory;
|
|
112
|
+
// Legacy derivation for direct callers passing raw scope inputs; kept byte-compatible with the
|
|
113
|
+
// declaration the lifecycle path always supplies.
|
|
114
|
+
if (scope === 'global') {
|
|
115
|
+
if (!codexDir) throw new Error('codexDir is required for global Codex agents');
|
|
116
|
+
return path.join(codexDir, AGENT_DIRECTORY);
|
|
117
|
+
}
|
|
118
|
+
if (scope === 'project') {
|
|
119
|
+
if (!projectRoot) throw new Error('projectRoot is required for project Codex agents');
|
|
120
|
+
return path.join(projectRoot, '.codex', AGENT_DIRECTORY);
|
|
121
|
+
}
|
|
122
|
+
throw new Error(`Unsupported Codex agent scope: ${scope}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function destinationFor(directory: string, name: string): string {
|
|
126
|
+
const file = path.resolve(directory, filenameFor(name));
|
|
127
|
+
const root = `${path.resolve(directory)}${path.sep}`;
|
|
128
|
+
if (!file.startsWith(root)) throw new Error(`Refusing to write agent '${name}' outside the Codex agents directory`);
|
|
129
|
+
return file;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function discoverCodexAgents(sourceDir: any, fsImpl: any = fs): any[] {
|
|
133
|
+
if (!sourceDir) throw new Error('sourceDir is required for Codex agents');
|
|
134
|
+
const directory = path.resolve(sourceDir);
|
|
135
|
+
if (!fsImpl.existsSync(directory)) throw new Error(`Codex agent source directory does not exist: ${sourceDir}`);
|
|
136
|
+
const agents: any[] = [];
|
|
137
|
+
for (const entry of fsImpl.readdirSync(directory, { withFileTypes: true }).sort((a: any, b: any) => a.name.localeCompare(b.name))) {
|
|
138
|
+
if (!entry.name.endsWith('.toml')) continue;
|
|
139
|
+
if (!entry.isFile()) throw new Error(`Codex agent source '${entry.name}' must be a regular file`);
|
|
140
|
+
const name = expectedName(entry.name);
|
|
141
|
+
const source = fsImpl.readFileSync(path.join(directory, entry.name), 'utf8');
|
|
142
|
+
agents.push({ ...validateAgentContract(source, { fileName: entry.name }), name, source, fingerprint: fingerprint(source) });
|
|
143
|
+
}
|
|
144
|
+
if (!agents.length) throw new Error(`No Codex agent TOML contracts found in ${sourceDir}`);
|
|
145
|
+
return agents;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function isOwnedResource(resource: any, scope: any): boolean {
|
|
149
|
+
return resource && resource.target === 'codex' && resource.scope === scope && resource.kind === AGENT_KIND && typeof resource.identity === 'string';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function resourceFor(agent: any, scope: any, sourceVersion: any): any {
|
|
153
|
+
return {
|
|
154
|
+
target: 'codex', scope, kind: AGENT_KIND, identity: `agent:${agent.name}`,
|
|
155
|
+
sourceVersion, fingerprint: agent.fingerprint, selection: true,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Plan all writes before performing any, preserving unknown or user-modified same-name agents. */
|
|
160
|
+
/**
|
|
161
|
+
* @param {Object} options
|
|
162
|
+
* @param {boolean} [options.adopt=false] accept an agent file that exists with no ledger record,
|
|
163
|
+
* recording ownership instead of refusing. See planCodexConfig for why this is not `force`: a
|
|
164
|
+
* record that disagrees with the file on disk stays a conflict regardless of `adopt`.
|
|
165
|
+
*/
|
|
166
|
+
function planCodexAgents({ scope, codexDir, projectRoot, sourceDir, sourceVersion = 'unknown', managedResources = [], adopt = false, fsImpl = fs }: any): any {
|
|
167
|
+
const directory = agentDirectory({ scope, codexDir, projectRoot });
|
|
168
|
+
const agents = discoverCodexAgents(sourceDir, fsImpl);
|
|
169
|
+
const owned = new Map<string, any>(managedResources.filter((resource: any) => isOwnedResource(resource, scope)).map((resource: any) => [resource.identity, resource]));
|
|
170
|
+
const changes: any[] = [];
|
|
171
|
+
const conflicts: string[] = [];
|
|
172
|
+
const nextManagedResources = managedResources.filter((resource: any) => !isOwnedResource(resource, scope));
|
|
173
|
+
for (const agent of agents) {
|
|
174
|
+
const file = destinationFor(directory, agent.name);
|
|
175
|
+
const identity = `agent:${agent.name}`;
|
|
176
|
+
const record = owned.get(identity);
|
|
177
|
+
const current = fsImpl.existsSync(file) ? fsImpl.readFileSync(file, 'utf8') : null;
|
|
178
|
+
// The chain is written against an explicit `unowned` rather than gating the first branch on
|
|
179
|
+
// `adopt`, because falling through with `record` undefined would read `record.fingerprint` off
|
|
180
|
+
// nothing. The `record &&` guard on the drift branch is what makes the adopt path safe.
|
|
181
|
+
const unowned = current !== null && !record;
|
|
182
|
+
if (unowned && !adopt) conflicts.push(`Agent '${agent.name}' exists but is not owned by DoFlow`);
|
|
183
|
+
else if (current !== null && record && record.fingerprint !== fingerprint(current)) conflicts.push(`Agent '${agent.name}' was modified outside DoFlow`);
|
|
184
|
+
else if (current === null || current !== agent.source) changes.push({ type: current === null ? 'create' : 'update', identity, file, content: agent.source });
|
|
185
|
+
nextManagedResources.push(resourceFor(agent, scope, sourceVersion));
|
|
186
|
+
}
|
|
187
|
+
if (conflicts.length) return { ok: false, status: 'conflict', directory, agents, changes: [], conflicts, managedResources };
|
|
188
|
+
return {
|
|
189
|
+
ok: true, status: changes.length ? 'change' : 'unchanged', directory, agents, changes, conflicts: [],
|
|
190
|
+
managedResources: nextManagedResources,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function applyCodexAgents(plan: any, { dryRun = false, fsImpl = fs }: { dryRun?: boolean, fsImpl?: any } = {}): any {
|
|
195
|
+
if (!plan.ok || dryRun || plan.status === 'unchanged') return { ...plan, applied: false };
|
|
196
|
+
for (const change of plan.changes) atomicWrite(change.file, change.content, fsImpl);
|
|
197
|
+
return { ...plan, applied: true };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function reconcileCodexAgents(options: any): any {
|
|
201
|
+
return applyCodexAgents(planCodexAgents(options), options);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
module.exports = {
|
|
205
|
+
AGENT_DIRECTORY, AGENT_KIND, READ_ONLY_ROLES, IMPLEMENTATION_ROLES, fingerprint,
|
|
206
|
+
parseAgentToml, validateAgentContract, agentDirectory, destinationFor, discoverCodexAgents,
|
|
207
|
+
planCodexAgents, applyCodexAgents, reconcileCodexAgents,
|
|
208
|
+
};
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
4
|
+
|
|
5
|
+
// Codex config.toml reconciliation. This module deliberately manages individual,
|
|
6
|
+
// manifest-backed keys rather than serialising a whole TOML document. The latter would either
|
|
7
|
+
// require a TOML dependency or, more importantly, risk reformatting/deleting a user's settings.
|
|
8
|
+
const crypto = require('node:crypto');
|
|
9
|
+
const fs = require('node:fs');
|
|
10
|
+
const path = require('node:path');
|
|
11
|
+
const { parseToml, stripComment, tableViolations } = require('../../helper/toml.ts');
|
|
12
|
+
|
|
13
|
+
const CONFIG_NAME = 'config.toml';
|
|
14
|
+
const CONFIG_KIND = 'configuration-entry';
|
|
15
|
+
|
|
16
|
+
function configPath({ scope, codexDir, projectRoot, paths }: { scope?: string, codexDir?: string, projectRoot?: string, paths?: any } = {}): string {
|
|
17
|
+
// Preferred: the declared path resolved by the adapter (harnesses.json "paths".configFile).
|
|
18
|
+
if (paths?.configFile) return paths.configFile;
|
|
19
|
+
// Legacy derivation for direct callers passing raw scope inputs (tests, reconcileCodexConfig);
|
|
20
|
+
// kept byte-compatible with the declaration the lifecycle path always supplies.
|
|
21
|
+
if (scope === 'global') {
|
|
22
|
+
if (!codexDir) throw new Error('codexDir is required for global Codex configuration');
|
|
23
|
+
return path.join(codexDir, CONFIG_NAME);
|
|
24
|
+
}
|
|
25
|
+
if (scope === 'project') {
|
|
26
|
+
if (!projectRoot) throw new Error('projectRoot is required for project Codex configuration');
|
|
27
|
+
return path.join(projectRoot, '.codex', CONFIG_NAME);
|
|
28
|
+
}
|
|
29
|
+
throw new Error(`Unsupported Codex configuration scope: ${scope}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function fingerprint(value: any): string {
|
|
33
|
+
return `sha256:${crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex')}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function commentSuffix(line: string): string {
|
|
37
|
+
const withoutComment = stripComment(line);
|
|
38
|
+
return line.slice(withoutComment.length);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function renderValue(value: any): string {
|
|
42
|
+
if (typeof value === 'boolean' || typeof value === 'number') return String(value);
|
|
43
|
+
if (typeof value === 'string') return JSON.stringify(value);
|
|
44
|
+
throw new Error('Managed Codex config values must be boolean, number, or string');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normaliseDesired(resources: any): Map<string, any> {
|
|
48
|
+
const result = new Map<string, any>();
|
|
49
|
+
for (const resource of resources || []) {
|
|
50
|
+
if (!resource || (resource.kind && resource.kind !== CONFIG_KIND && resource.kind !== 'config-entry')) continue;
|
|
51
|
+
const identity = resource.identity ?? resource.key;
|
|
52
|
+
if (!identity || !/^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)+$/.test(identity)) {
|
|
53
|
+
throw new Error('A managed Codex configuration resource requires a dotted identity');
|
|
54
|
+
}
|
|
55
|
+
if (!Object.hasOwn(resource, 'value')) throw new Error(`Managed Codex configuration '${identity}' requires a value`);
|
|
56
|
+
renderValue(resource.value);
|
|
57
|
+
if (result.has(identity)) throw new Error(`Duplicate desired Codex configuration resource '${identity}'`);
|
|
58
|
+
result.set(identity, { ...resource, identity, kind: resource.kind ?? CONFIG_KIND });
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The comment on a line DoFlow added to a table the user opened with no entry: it is how removal
|
|
64
|
+
* knows to keep the user's header and restore the file's exact bytes. */
|
|
65
|
+
function addedMarker(table: string): string { return ` # DoFlow added this to your [${table}] table`; }
|
|
66
|
+
|
|
67
|
+
/** Where a new entry `<table>.<key>` goes in the parsed file: after the last entry of its table;
|
|
68
|
+
* on the line right after the table's empty header, marked; as a root dotted line after the last
|
|
69
|
+
* root dotted line of that table; refused (`valueEntry`) when the table or a parent of it is set as
|
|
70
|
+
* a value, which TOML cannot extend; otherwise in a new table (no `at`). */
|
|
71
|
+
function placeEntry(identity: string, value: any, parsed: any): any {
|
|
72
|
+
const parts = identity.split('.');
|
|
73
|
+
const table = parts.slice(0, -1).join('.');
|
|
74
|
+
const rendered = renderValue(value);
|
|
75
|
+
const assignment = `${parts.at(-1)} = ${rendered}`;
|
|
76
|
+
const entries: any[] = [...parsed.entries];
|
|
77
|
+
const after = (items: any[]) => Math.max(...items.map(([, entry]: any) => entry.line)) + 1;
|
|
78
|
+
const inTable = entries.filter(([, entry]: any) => entry.table === table);
|
|
79
|
+
if (inTable.length) return { table, at: after(inTable), line: assignment };
|
|
80
|
+
const header = parsed.headers.find((item: any) => item.table === table);
|
|
81
|
+
if (header) return { table, at: header.line + 1, line: `${assignment}${addedMarker(table)}` };
|
|
82
|
+
const dotted = entries.filter(([key, entry]: any) => entry.table === '' && key.startsWith(`${table}.`));
|
|
83
|
+
if (dotted.length) return { table, at: after(dotted), line: `${identity} = ${rendered}` };
|
|
84
|
+
const valueEntry = entries.find(([key]: any) => key === table || table.startsWith(`${key}.`));
|
|
85
|
+
if (valueEntry) return { table, valueEntry };
|
|
86
|
+
return { table, line: assignment };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Whether a managed entry's line is one placeEntry put into the user's own table: marked, or
|
|
90
|
+
* written as a dotted key outside its table. */
|
|
91
|
+
function placedLine(identity: string, entry: any, lines: any[]): boolean {
|
|
92
|
+
const table = identity.split('.').slice(0, -1).join('.');
|
|
93
|
+
return entry.table !== table || lines[entry.line].trimEnd().endsWith(addedMarker(table));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** A conflict when `content` breaks a TOML table rule that `original` did not already break, so a
|
|
97
|
+
* file that was valid is never written invalid; null otherwise. */
|
|
98
|
+
function newViolation({ file, original, content, identities }: { file: string, original: string, content: string, identities: string[] }): string | null {
|
|
99
|
+
const refused = (reason: string) => `${file}: DoFlow cannot write ${identities.map((identity) => `'${identity}'`).join(', ')} without making the file invalid TOML (${reason}). Nothing was written.`;
|
|
100
|
+
const before = tableViolations(original);
|
|
101
|
+
let after;
|
|
102
|
+
try { after = tableViolations(content); } catch (error: any) { return refused(error.message); }
|
|
103
|
+
const added = after.find((item: any) => !before.some((known: any) => known.kind === item.kind && known.table === item.table));
|
|
104
|
+
return added ? refused(`${added.kind} '${added.table}' on line ${added.line}`) : null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isOwnedRecord(resource: any, scope: any): boolean {
|
|
108
|
+
return resource && resource.target === 'codex' && resource.scope === scope &&
|
|
109
|
+
(resource.kind === CONFIG_KIND || resource.kind === 'config-entry') && typeof resource.identity === 'string';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @param {Object} options
|
|
114
|
+
* @param {boolean} [options.adopt=false] accept a config entry that exists with no ledger record,
|
|
115
|
+
* recording ownership of it instead of refusing. Distinct from `force`, which overrides a record
|
|
116
|
+
* that *disagrees* with the file: adoption only ever covers the no-record case, and a resource
|
|
117
|
+
* whose record contradicts its bytes stays a conflict here whatever `adopt` says. Without this,
|
|
118
|
+
* entries written before the neutral ledger existed could never be re-adopted — the condition
|
|
119
|
+
* blocking the install was the same condition an install had to run to clear.
|
|
120
|
+
*/
|
|
121
|
+
function planCodexConfig({ file, scope, managedResources = [], desiredResources = [], adopt = false }: { file: string, scope?: any, managedResources?: any[], desiredResources?: any[], adopt?: boolean }): any {
|
|
122
|
+
const original = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
|
|
123
|
+
let parsed;
|
|
124
|
+
try { parsed = parseToml(original); } catch (error: any) {
|
|
125
|
+
return { ok: false, status: 'malformed', file, original, changes: [], conflicts: [error.message], managedResources };
|
|
126
|
+
}
|
|
127
|
+
let desired;
|
|
128
|
+
try { desired = normaliseDesired(desiredResources); } catch (error: any) {
|
|
129
|
+
return { ok: false, status: 'invalid-request', file, original, changes: [], conflicts: [error.message], managedResources };
|
|
130
|
+
}
|
|
131
|
+
const owned = new Map<string, any>(managedResources.filter((resource: any) => isOwnedRecord(resource, scope)).map((resource: any) => [resource.identity, resource]));
|
|
132
|
+
const changes: any[] = [];
|
|
133
|
+
const conflicts: string[] = [];
|
|
134
|
+
const placements = new Map<string, any>();
|
|
135
|
+
const all = new Set([...desired.keys(), ...owned.keys()]);
|
|
136
|
+
for (const identity of all) {
|
|
137
|
+
const entry = parsed.entries.get(identity);
|
|
138
|
+
const record = owned.get(identity);
|
|
139
|
+
const wanted = desired.get(identity);
|
|
140
|
+
if (!record && entry && !adopt) {
|
|
141
|
+
conflicts.push(`'${identity}' exists but is not owned by DoFlow`);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
// Adopting: no `continue`, so the entry goes through ordinary planning below. Ownership is
|
|
145
|
+
// recorded either way — `nextManagedResources` is rebuilt from `desired` regardless of what was
|
|
146
|
+
// owned before — so adoption needs nothing beyond not refusing here.
|
|
147
|
+
if (record && entry && record.fingerprint !== fingerprint(entry.value)) {
|
|
148
|
+
conflicts.push(`'${identity}' was modified outside DoFlow`);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (wanted && (!entry || fingerprint(entry.value) !== fingerprint(wanted.value))) {
|
|
152
|
+
changes.push({ type: entry ? 'update' : 'create', identity, value: wanted.value, line: entry?.line });
|
|
153
|
+
if (!entry) {
|
|
154
|
+
const placement = placeEntry(identity, wanted.value, parsed);
|
|
155
|
+
if (placement.valueEntry) {
|
|
156
|
+
const [prefix, held] = placement.valueEntry;
|
|
157
|
+
conflicts.push(`${file}: '${prefix}' is set as a value on line ${held.line + 1}, so DoFlow cannot add '${identity}' to it. Write it as a [${prefix}] table and run the command again. Nothing was written.`);
|
|
158
|
+
}
|
|
159
|
+
placements.set(identity, placement);
|
|
160
|
+
}
|
|
161
|
+
} else if (!wanted && entry) {
|
|
162
|
+
changes.push({ type: 'remove', identity, line: entry.line });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (conflicts.length) return { ok: false, status: 'conflict', file, original, changes: [], conflicts, managedResources };
|
|
166
|
+
const nextLines = [...parsed.lines];
|
|
167
|
+
for (const change of changes.filter((change: any) => change.type === 'update')) {
|
|
168
|
+
const key = change.identity.split('.').at(-1);
|
|
169
|
+
const originalLine = nextLines[change.line];
|
|
170
|
+
const indentation = originalLine.match(/^\s*/)![0];
|
|
171
|
+
// A user comment beside an owned value is still user content. Retain it even though the
|
|
172
|
+
// owned scalar changes, so reconciliation does not silently erase unrelated explanation.
|
|
173
|
+
const suffix = commentSuffix(originalLine);
|
|
174
|
+
nextLines[change.line] = `${indentation}${key} = ${renderValue(change.value)}${suffix && !/^\s/.test(suffix) ? ' ' : ''}${suffix}`;
|
|
175
|
+
}
|
|
176
|
+
// A line DoFlow placed in the user's own table (marker or dotted form) is deleted outright and its
|
|
177
|
+
// header kept, so the file returns to its exact bytes. Any other removal leaves a blank line.
|
|
178
|
+
const removals = changes.filter((change: any) => change.type === 'remove');
|
|
179
|
+
const placedInUserTable = removals.filter((change: any) => placedLine(change.identity, parsed.entries.get(change.identity), parsed.lines));
|
|
180
|
+
for (const change of removals) nextLines[change.line] = placedInUserTable.includes(change) ? null : '';
|
|
181
|
+
dropEmptiedTables(nextLines, changes.filter((change: any) => !placedInUserTable.includes(change)), parsed.headers);
|
|
182
|
+
// A new key must land INSIDE its own table. Appending at end-of-file only happens to be
|
|
183
|
+
// correct when that table is the file's last one — otherwise the key silently joins whichever
|
|
184
|
+
// table trails the file, so `features.hooks` written after an `[mcp_servers.x]` block becomes
|
|
185
|
+
// `mcp_servers.x.hooks`. placeEntry chooses the spot; genuinely new tables are appended once
|
|
186
|
+
// each, with all of their keys grouped under a single header.
|
|
187
|
+
const insertions: { at: number, line: string }[] = [];
|
|
188
|
+
const newTables = new Map<string, string[]>();
|
|
189
|
+
for (const change of changes.filter((change: any) => change.type === 'create')) {
|
|
190
|
+
const placement = placements.get(change.identity);
|
|
191
|
+
if (placement.at !== undefined) insertions.push({ at: placement.at, line: placement.line });
|
|
192
|
+
else newTables.set(placement.table, [...(newTables.get(placement.table) || []), placement.line]);
|
|
193
|
+
}
|
|
194
|
+
// Descending, so an earlier insertion never shifts the index of one still pending.
|
|
195
|
+
for (const insertion of insertions.sort((a, b) => b.at - a.at)) nextLines.splice(insertion.at, 0, insertion.line);
|
|
196
|
+
// Dropped lines were kept as null placeholders so the insertion indices above stayed valid.
|
|
197
|
+
for (let index = nextLines.length - 1; index >= 0; index--) if (nextLines[index] === null) nextLines.splice(index, 1);
|
|
198
|
+
for (const [table, lines] of newTables) {
|
|
199
|
+
// `''.split(/\r?\n/)` is [`''`]; drop that synthetic line so a brand-new file does
|
|
200
|
+
// not start with an unintended blank line.
|
|
201
|
+
if (nextLines.length === 1 && nextLines[0] === '') nextLines.pop();
|
|
202
|
+
if (nextLines.length && nextLines.at(-1) !== '') nextLines.push('');
|
|
203
|
+
nextLines.push(`[${table}]`, ...lines);
|
|
204
|
+
}
|
|
205
|
+
let content = nextLines.join('\n');
|
|
206
|
+
if (content && !content.endsWith('\n')) content += '\n';
|
|
207
|
+
const invalid = newViolation({ file, original, content, identities: changes.map((change: any) => change.identity) });
|
|
208
|
+
if (invalid) return { ok: false, status: 'conflict', file, original, changes: [], conflicts: [invalid], managedResources };
|
|
209
|
+
const nextManagedResources = managedResources.filter((resource: any) => !isOwnedRecord(resource, scope) || desired.has(resource.identity));
|
|
210
|
+
for (const resource of desired.values()) {
|
|
211
|
+
const index = nextManagedResources.findIndex((item: any) => isOwnedRecord(item, scope) && item.identity === resource.identity);
|
|
212
|
+
const updated = { ...resource, target: 'codex', scope, kind: CONFIG_KIND, fingerprint: fingerprint(resource.value) };
|
|
213
|
+
if (index >= 0) nextManagedResources[index] = updated;
|
|
214
|
+
else nextManagedResources.push(updated);
|
|
215
|
+
}
|
|
216
|
+
return { ok: true, status: changes.length ? 'change' : 'unchanged', file, original, content, changes, conflicts: [], managedResources: nextManagedResources };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** A table whose every key this plan removed, with nothing else under its header (no user key or
|
|
220
|
+
* comment), is DoFlow's own leftover: its header and removed lines become null placeholders. When
|
|
221
|
+
* it was the file's last table, the blank line the table append puts before a new header goes too.
|
|
222
|
+
* A table that also receives a created key, or whose header line carries a comment, is kept.
|
|
223
|
+
* Headers come from the parser's own records, never from re-reading lines. */
|
|
224
|
+
function dropEmptiedTables(lines: any[], changes: any[], headerRecords: any[]): void {
|
|
225
|
+
const tableOf = (identity: string) => identity.split('.').slice(0, -1).join('.');
|
|
226
|
+
const removedTables = new Map<number, string>(changes.filter((change) => change.type === 'remove').map((change): [number, string] => [change.line, tableOf(change.identity)]));
|
|
227
|
+
const createdTables = new Set<string>(changes.filter((change) => change.type === 'create').map((change) => tableOf(change.identity)));
|
|
228
|
+
const headers: number[] = headerRecords.map((header) => header.line);
|
|
229
|
+
headers.forEach((start, position) => {
|
|
230
|
+
const end = headers[position + 1] ?? lines.length;
|
|
231
|
+
if (stripComment(lines[start]) !== lines[start]) return;
|
|
232
|
+
const body = lines.slice(start + 1, end).map((_: any, offset: number) => start + 1 + offset);
|
|
233
|
+
const removedHere = body.filter((index) => removedTables.has(index));
|
|
234
|
+
if (!removedHere.length || removedHere.some((index) => createdTables.has(removedTables.get(index) as string))) return;
|
|
235
|
+
if (body.some((index) => !removedTables.has(index) && lines[index] !== null && lines[index].trim() !== '')) return;
|
|
236
|
+
lines[start] = null;
|
|
237
|
+
for (const index of removedHere) lines[index] = null;
|
|
238
|
+
if (end === lines.length && start > 0 && lines[start - 1] === '') lines[start - 1] = null;
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Replace a file through a sibling temporary file, keeping the existing file's permission bits so
|
|
243
|
+
* a config kept private stays so. A file this creates gets the process's default mode. */
|
|
244
|
+
function atomicWrite(file: string, content: string, fsImpl: any = fs): void {
|
|
245
|
+
fsImpl.mkdirSync(path.dirname(file), { recursive: true });
|
|
246
|
+
const mode = fsImpl.existsSync(file) ? fsImpl.statSync(file).mode & 0o7777 : null;
|
|
247
|
+
const temporary = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${Date.now()}.tmp`);
|
|
248
|
+
try {
|
|
249
|
+
fsImpl.writeFileSync(temporary, content, { flag: 'wx', ...(mode === null ? {} : { mode }) });
|
|
250
|
+
if (mode !== null) fsImpl.chmodSync(temporary, mode);
|
|
251
|
+
fsImpl.renameSync(temporary, file);
|
|
252
|
+
} finally {
|
|
253
|
+
if (fsImpl.existsSync(temporary)) fsImpl.unlinkSync(temporary);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function applyCodexConfig(plan: any, { dryRun = false, fsImpl = fs }: { dryRun?: boolean, fsImpl?: any } = {}): any {
|
|
258
|
+
if (!plan.ok) return { ...plan, applied: false };
|
|
259
|
+
if (dryRun || plan.status === 'unchanged') return { ...plan, applied: false };
|
|
260
|
+
// A removal that leaves nothing behind deletes the file rather than leaving it empty.
|
|
261
|
+
if (plan.content === '') { if (fsImpl.existsSync(plan.file)) fsImpl.unlinkSync(plan.file); }
|
|
262
|
+
else atomicWrite(plan.file, plan.content, fsImpl);
|
|
263
|
+
return { ...plan, applied: true };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function reconcileCodexConfig(options: any): any {
|
|
267
|
+
const file = options.file ?? configPath(options);
|
|
268
|
+
const plan = planCodexConfig({ ...options, file });
|
|
269
|
+
return applyCodexConfig(plan, options);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
module.exports = { CONFIG_NAME, CONFIG_KIND, configPath, fingerprint, parseToml, newViolation, planCodexConfig, applyCodexConfig, reconcileCodexConfig, atomicWrite };
|