@khoavu882/doflow 0.0.0-stage → 2.0.0-beta.4
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 +1061 -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 +48 -4
- package/src/adapters/antigravity/index.js +631 -0
- package/src/adapters/claude/index.js +548 -0
- package/src/adapters/claude/mcp.js +94 -0
- package/src/adapters/codex/agents.js +208 -0
- package/src/adapters/codex/config.js +272 -0
- package/src/adapters/codex/hooks.js +203 -0
- package/src/adapters/codex/index.js +707 -0
- package/src/adapters/codex/mcp.js +271 -0
- package/src/adapters/codex/native-projection.js +34 -0
- package/src/adapters/copilot/index.js +413 -0
- package/src/adapters/copy-tree.js +620 -0
- package/src/adapters/gemini/hooks.js +210 -0
- package/src/adapters/gemini/index.js +390 -0
- package/src/adapters/hook-commands.js +60 -0
- package/src/adapters/index.js +106 -0
- package/src/adapters/kiro/index.js +310 -0
- package/src/adapters/mcp-entries.js +331 -0
- package/src/adapters/opencode/index.js +466 -0
- package/src/adapters/pi/index.js +279 -0
- package/src/adapters/pi/json-members.js +185 -0
- package/src/adapters/pi/mcp.js +373 -0
- package/src/adapters/resolve-source.js +19 -0
- package/src/cli/commands/install.js +218 -0
- package/src/cli/commands/list-backups.js +21 -0
- package/src/cli/commands/reconcile.js +120 -0
- package/src/cli/commands/remove.js +82 -0
- package/src/cli/commands/rollback.js +172 -0
- package/src/cli/commands/self-update.js +33 -0
- package/src/cli/commands/status.js +138 -0
- package/src/cli/commands/tools.js +92 -0
- package/src/cli/commands/update.js +162 -0
- package/src/cli/index.js +568 -0
- package/src/cli/runtime-commands.js +281 -0
- package/src/cli/shared.js +218 -0
- package/src/helper/bash-helper.js +34 -0
- package/src/helper/git.js +24 -0
- package/src/helper/guardrails.js +49 -0
- package/src/helper/harness-paths.js +140 -0
- package/src/helper/marker-merge.js +116 -0
- package/src/helper/prompt.js +61 -0
- package/src/helper/repo-root.js +29 -0
- package/src/helper/settings-merge.js +151 -0
- package/src/helper/settings-scope.js +33 -0
- package/src/helper/toml.js +244 -0
- package/src/install/backup-set.js +149 -0
- package/src/install/backup.js +724 -0
- package/src/install/context.js +58 -0
- package/src/install/manifest.js +145 -0
- package/src/install/mcp.js +254 -0
- package/src/install/paths.js +84 -0
- package/src/install/targets.js +90 -0
- package/src/install/tool-lifecycle.js +151 -0
- package/src/lifecycle/bash-availability.js +25 -0
- package/src/lifecycle/index.js +812 -0
- package/src/lifecycle/mcp-index.js +28 -0
- package/src/lifecycle/policies.js +73 -0
- package/src/lifecycle/view.js +175 -0
- package/src/registry/index.js +611 -0
- package/src/release/build-package.js +91 -0
- package/src/release/changelog-section.js +70 -0
- package/src/release/sync-plugin-versions.js +66 -0
- package/src/runtime/advice/cli.js +283 -0
- package/src/runtime/advice/client.js +324 -0
- package/src/runtime/advice/config.js +189 -0
- package/src/runtime/advice/doctor.js +169 -0
- package/src/runtime/advice/endpoint-guard.js +134 -0
- package/src/runtime/advice/kinds.js +154 -0
- package/src/runtime/advice/providers/ollama-systemone.js +209 -0
- package/src/runtime/advice/record.js +166 -0
- package/src/runtime/advice/registry.js +170 -0
- package/src/runtime/advice/strength.js +66 -0
- package/src/runtime/brief-tools.js +342 -0
- package/src/runtime/c4-project.js +371 -0
- package/src/runtime/capability-router.js +484 -0
- package/src/runtime/checkouts.js +170 -0
- package/src/runtime/claims.js +468 -0
- package/src/runtime/cli-result.js +55 -0
- package/src/runtime/cli.js +678 -0
- package/src/runtime/command-detect.js +516 -0
- package/src/runtime/context-pack.js +492 -0
- package/src/runtime/decision-register.js +493 -0
- package/src/runtime/evidence-ledger.js +338 -0
- package/src/runtime/failure/capture.js +272 -0
- package/src/runtime/failure/classifier.js +68 -0
- package/src/runtime/failure/cli.js +178 -0
- package/src/runtime/failure/home.js +64 -0
- package/src/runtime/failure/store.js +226 -0
- package/src/runtime/feature-resolve.js +79 -0
- package/src/runtime/freshness.js +177 -0
- package/src/runtime/health.js +863 -0
- package/src/runtime/history-compactor.js +237 -0
- package/src/runtime/implementation-gate.js +321 -0
- package/src/runtime/inventory/coverage.js +218 -0
- package/src/runtime/inventory/identity.js +65 -0
- package/src/runtime/inventory/index.js +705 -0
- package/src/runtime/inventory/precedence.js +134 -0
- package/src/runtime/inventory/read-scopes.js +131 -0
- package/src/runtime/inventory/siblings.js +192 -0
- package/src/runtime/knowledge/chunker.js +124 -0
- package/src/runtime/knowledge/hybrid.js +149 -0
- package/src/runtime/knowledge/index-store.js +145 -0
- package/src/runtime/knowledge/retrieval.js +194 -0
- package/src/runtime/leak-scan.js +185 -0
- package/src/runtime/lifecycle/cli.js +276 -0
- package/src/runtime/lifecycle/event-store.js +340 -0
- package/src/runtime/lifecycle/fold.js +353 -0
- package/src/runtime/lifecycle/followup.js +432 -0
- package/src/runtime/lifecycle/goal.js +249 -0
- package/src/runtime/lifecycle/intent-writer.js +86 -0
- package/src/runtime/lifecycle/overview.js +292 -0
- package/src/runtime/lifecycle/release.js +241 -0
- package/src/runtime/lifecycle/report-store.js +221 -0
- package/src/runtime/lifecycle/retention.js +212 -0
- package/src/runtime/lifecycle/root.js +71 -0
- package/src/runtime/lifecycle/status.js +353 -0
- package/src/runtime/lifecycle/store-upkeep.js +393 -0
- package/src/runtime/locator-resolve.js +158 -0
- package/src/runtime/mask.js +210 -0
- package/src/runtime/mcp-consultations.js +412 -0
- package/src/runtime/mcp-reach.js +157 -0
- package/src/runtime/model-router.js +153 -0
- package/src/runtime/outcome.js +467 -0
- package/src/runtime/reach.js +148 -0
- package/src/runtime/readiness-record.js +150 -0
- package/src/runtime/readiness.js +435 -0
- package/src/runtime/recovery.js +282 -0
- package/src/runtime/research-request.js +231 -0
- package/src/runtime/retrieval-plan.js +578 -0
- package/src/runtime/scaffold/artifacts.js +217 -0
- package/src/runtime/scaffold/fingerprint.js +56 -0
- package/src/runtime/scaffold/generate.js +911 -0
- package/src/runtime/scaffold/languages.js +245 -0
- package/src/runtime/storage/cli.js +64 -0
- package/src/runtime/storage/engine.js +249 -0
- package/src/runtime/storage/registry.js +130 -0
- package/src/runtime/storage/roots.js +63 -0
- package/src/runtime/storage/rules.js +282 -0
- package/src/runtime/storage/upkeep.js +243 -0
- package/src/runtime/storage/work-status.js +326 -0
- package/src/runtime/task-attempts.js +415 -0
- package/src/runtime/task-classifier.js +550 -0
- package/src/runtime/task-scope.js +130 -0
- package/src/runtime/task-state.js +176 -0
- package/src/runtime/trace/indicators.js +235 -0
- package/src/runtime/trace/ledger.js +626 -0
- package/src/runtime/trace/render.js +76 -0
- package/src/runtime/trace/views.js +459 -0
- package/src/runtime/verification/contract-runner.js +341 -0
- package/src/runtime/verification/engine.js +1461 -0
- package/src/runtime/verification/registry.js +130 -0
- package/src/runtime/verification/scope-bound.js +418 -0
- package/src/runtime/verification/test-selection.js +172 -0
- package/src/runtime/workflow-engine.js +588 -0
- package/src/runtime/workflow-orchestrator.js +953 -0
- package/src/runtime/worktree.js +340 -0
- package/src/state/index.js +193 -0
- package/src/state/lockfile.js +136 -0
- package/src/state/run-lock.js +408 -0
- package/types/node-commonjs.d.ts +68 -0
- package/types/runtime.d.ts +91 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Ephemeral git worktrees for isolated task execution — the JavaScript port of
|
|
7
|
+
* `core/shared/scripts/doflow/sandbox/worktree_manager.py` (plan task B.3, pulled forward because
|
|
8
|
+
* A.3's baseline capture cannot run safely without it).
|
|
9
|
+
*
|
|
10
|
+
* Why this exists: bench cases invoke skills that write files, create branches, or
|
|
11
|
+
* commit. Running them in the repo that is executing the plan would mutate the very tree under
|
|
12
|
+
* test. Each run gets its own worktree so the blast radius of a misbehaving skill is one
|
|
13
|
+
* throwaway directory.
|
|
14
|
+
*
|
|
15
|
+
* Three defects in the Python original are fixed here rather than reproduced. Plan decision D3
|
|
16
|
+
* says port from observed behaviour rather than from the specification — that was about not losing
|
|
17
|
+
* capability (recovery's eleven failure classes), not about preserving bugs:
|
|
18
|
+
* 1. `create_worktree` returned early when the directory existed without checking the branch
|
|
19
|
+
* existed too, so a half-removed worktree resolved to an unusable path.
|
|
20
|
+
* 2. `collect_diff` ran `git diff HEAD~1`, which fails on a worktree with fewer than two commits
|
|
21
|
+
* and returned null — conflating "no changes" with "could not tell".
|
|
22
|
+
* 3. `remove_worktree` deleted the directory but left the `task/<id>` branch behind, so repeated
|
|
23
|
+
* runs accumulated branches indefinitely.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const fs = require('node:fs');
|
|
27
|
+
const path = require('node:path');
|
|
28
|
+
const crypto = require('node:crypto');
|
|
29
|
+
const { execFileSync } = require('node:child_process');
|
|
30
|
+
|
|
31
|
+
/** Worktree ids become branch names and directory names, so anything outside this set is a
|
|
32
|
+
* potential argument-injection or path-traversal vector. Rejecting loudly beats sanitising
|
|
33
|
+
* silently: a caller passing a bad id has a bug worth seeing. */
|
|
34
|
+
const SAFE_ID = /^[A-Za-z0-9._-]+$/;
|
|
35
|
+
|
|
36
|
+
/** Records the base commit a sandbox was cut from. Lives inside the worktree so it survives a
|
|
37
|
+
* process boundary; excluded from collected diffs since it is harness bookkeeping, not task work. */
|
|
38
|
+
const BASE_FILE = '.doflow-worktree-base';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Records which skill source a sandbox carries (plan task A.5). Written inside the worktree for the
|
|
42
|
+
* same reason as BASE_FILE: grading may run in a different process from execution, and a run that
|
|
43
|
+
* cannot say which skills it measured is not evidence of anything.
|
|
44
|
+
*/
|
|
45
|
+
const SKILL_SOURCE_FILE = '.doflow-skill-source.json';
|
|
46
|
+
|
|
47
|
+
/** Where the claude adapter lands skills inside an installed project. Not invented here — this is
|
|
48
|
+
* the adapter's own layout, which is the point: the sandbox holds a real projection, not a copy
|
|
49
|
+
* that can drift from one. */
|
|
50
|
+
const SANDBOX_SKILLS_DIR = path.join('.claude', 'skills');
|
|
51
|
+
|
|
52
|
+
/** Source of truth for skill content, relative to the repo root. */
|
|
53
|
+
const SOURCE_SKILLS_DIR = path.join('core', 'shared', 'skills');
|
|
54
|
+
|
|
55
|
+
function sha256File(file ) {
|
|
56
|
+
return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function git(args , cwd , { allowFailure = false } = {}) {
|
|
60
|
+
try {
|
|
61
|
+
return {
|
|
62
|
+
ok: true,
|
|
63
|
+
stdout: execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }),
|
|
64
|
+
};
|
|
65
|
+
} catch (err ) {
|
|
66
|
+
if (!allowFailure) throw err;
|
|
67
|
+
return { ok: false, stdout: '', stderr: err.stderr ? String(err.stderr) : String(err.message) };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
class WorktreeManager {
|
|
72
|
+
repoRoot ;
|
|
73
|
+
worktreesDir ;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @param {string} repoRoot repository whose worktrees are managed
|
|
77
|
+
* @param {string} [worktreesDir] override the sandbox root; defaults to `<repo>/.doflow/worktrees`,
|
|
78
|
+
* which is gitignored so sandboxes never appear as untracked noise in the parent tree.
|
|
79
|
+
*/
|
|
80
|
+
constructor(repoRoot , worktreesDir ) {
|
|
81
|
+
this.repoRoot = repoRoot;
|
|
82
|
+
this.worktreesDir = worktreesDir || path.join(repoRoot, '.doflow', 'worktrees');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
static assertSafeId(taskId ) {
|
|
86
|
+
if (!taskId || !SAFE_ID.test(taskId)) {
|
|
87
|
+
throw new Error(`unsafe worktree id ${JSON.stringify(taskId)} — allowed: letters, digits, dot, dash, underscore`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
pathFor(taskId ) {
|
|
92
|
+
WorktreeManager.assertSafeId(taskId);
|
|
93
|
+
return path.join(this.worktreesDir, taskId);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
branchFor(taskId ) {
|
|
97
|
+
WorktreeManager.assertSafeId(taskId);
|
|
98
|
+
return `task/${taskId}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Whether git itself considers this path a registered worktree — the directory existing on disk
|
|
102
|
+
* is not the same question, which is the bug fixed here. */
|
|
103
|
+
isRegistered(wtPath ) {
|
|
104
|
+
const res = git(['worktree', 'list', '--porcelain'], this.repoRoot, { allowFailure: true });
|
|
105
|
+
if (!res.ok) return false;
|
|
106
|
+
const resolved = path.resolve(wtPath);
|
|
107
|
+
return res.stdout
|
|
108
|
+
.split('\n')
|
|
109
|
+
.filter((l ) => l.startsWith('worktree '))
|
|
110
|
+
.some((l ) => path.resolve(l.slice('worktree '.length).trim()) === resolved);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Create an isolated worktree, reusing it only when git agrees it is registered. Returns the
|
|
115
|
+
* absolute path.
|
|
116
|
+
*/
|
|
117
|
+
create(taskId , baseRef = 'HEAD') {
|
|
118
|
+
const wtPath = this.pathFor(taskId);
|
|
119
|
+
const branch = this.branchFor(taskId);
|
|
120
|
+
|
|
121
|
+
if (this.isRegistered(wtPath)) return wtPath;
|
|
122
|
+
|
|
123
|
+
// A leftover directory that git does not know about blocks `worktree add`. Clear it, and
|
|
124
|
+
// prune git's stale administrative records, before trying.
|
|
125
|
+
if (fs.existsSync(wtPath)) fs.rmSync(wtPath, { recursive: true, force: true });
|
|
126
|
+
git(['worktree', 'prune'], this.repoRoot, { allowFailure: true });
|
|
127
|
+
|
|
128
|
+
fs.mkdirSync(this.worktreesDir, { recursive: true });
|
|
129
|
+
|
|
130
|
+
const branchExists = git(['rev-parse', '--verify', '--quiet', branch], this.repoRoot, { allowFailure: true }).ok;
|
|
131
|
+
const args = branchExists
|
|
132
|
+
? ['worktree', 'add', wtPath, branch]
|
|
133
|
+
: ['worktree', 'add', '-b', branch, wtPath, baseRef];
|
|
134
|
+
git(args, this.repoRoot);
|
|
135
|
+
|
|
136
|
+
// Persist the base as a resolved SHA. A symbolic ref is useless later: inside the worktree
|
|
137
|
+
// `HEAD` moves with every commit the task makes, so diffing against it reports nothing. The
|
|
138
|
+
// file also survives a process boundary, since grading may run separately from execution.
|
|
139
|
+
const baseSha = git(['rev-parse', baseRef], this.repoRoot).stdout.trim();
|
|
140
|
+
fs.writeFileSync(path.join(wtPath, BASE_FILE), `${baseSha}\n`);
|
|
141
|
+
return wtPath;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The resolved base SHA recorded at creation, or null when unavailable. */
|
|
145
|
+
baseOf(taskId ) {
|
|
146
|
+
const file = path.join(this.pathFor(taskId), BASE_FILE);
|
|
147
|
+
return fs.existsSync(file) ? fs.readFileSync(file, 'utf8').trim() : null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Materialise **this repo's** skills inside the sandbox and record exactly what landed there.
|
|
152
|
+
*
|
|
153
|
+
* Why this is not optional for an evaluation sandbox: Claude Code resolves a bare skill name from
|
|
154
|
+
* user scope (`~/.claude/skills/`) before project scope, and first match wins — so a run that
|
|
155
|
+
* invokes `/do-code-review` measures whatever is globally installed, not the tree under test.
|
|
156
|
+
* Verified against 2.1.226 (`lE()` is a first-match lookup over a list ordered policy → user →
|
|
157
|
+
* project) and observed live: a project-scope `do-git` shadow was ignored in favour of the
|
|
158
|
+
* installed copy. Twelve of the thirteen installed skills currently differ from this repo's
|
|
159
|
+
* source, so the difference is not hypothetical.
|
|
160
|
+
*
|
|
161
|
+
* Projecting here therefore does *not* win the name lookup — nothing can, in-process. What it
|
|
162
|
+
* buys is a known-good copy at a known path with recorded hashes, which is what makes the
|
|
163
|
+
* by-path dispatch contract (see `bench/README.md`) checkable rather than trusted.
|
|
164
|
+
*
|
|
165
|
+
* The real installer is used rather than a hand-copy so the sandbox exercises the actual
|
|
166
|
+
* projection, including the guidance, scripts and templates the skills reference — a SKILL.md
|
|
167
|
+
* copied alone would leave every `references/` path dangling and change the behaviour being
|
|
168
|
+
* measured. It costs ~0.15s per sandbox.
|
|
169
|
+
*
|
|
170
|
+
* Writes the manifest before validating, so a failed projection is still diagnosable, then throws
|
|
171
|
+
* if any skill is missing or does not match source.
|
|
172
|
+
*
|
|
173
|
+
* @returns {object} the manifest also written to `<worktree>/.doflow-skill-source.json`
|
|
174
|
+
*/
|
|
175
|
+
projectSkills(taskId , { harness = 'claude', cli = path.join('bin', 'doflow.js'), skillsRoot = SOURCE_SKILLS_DIR } = {}) {
|
|
176
|
+
const wtPath = this.pathFor(taskId);
|
|
177
|
+
if (!fs.existsSync(wtPath)) {
|
|
178
|
+
throw new Error(`no worktree at ${wtPath} — create it before projecting skills`);
|
|
179
|
+
}
|
|
180
|
+
const sourceRoot = path.join(this.repoRoot, skillsRoot);
|
|
181
|
+
if (!fs.existsSync(sourceRoot)) {
|
|
182
|
+
throw new Error(`no skill source at ${sourceRoot}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let installOutput ;
|
|
186
|
+
try {
|
|
187
|
+
installOutput = execFileSync(
|
|
188
|
+
process.execPath,
|
|
189
|
+
[path.join(this.repoRoot, cli), 'install', wtPath, '--force', '-t', harness],
|
|
190
|
+
{ cwd: this.repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] },
|
|
191
|
+
);
|
|
192
|
+
} catch (err ) {
|
|
193
|
+
throw new Error(`projecting skills into ${wtPath} failed: ${err.stderr || err.message}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const names = fs
|
|
197
|
+
.readdirSync(sourceRoot, { withFileTypes: true })
|
|
198
|
+
.filter((e ) => e.isDirectory() && fs.existsSync(path.join(sourceRoot, e.name, 'SKILL.md')))
|
|
199
|
+
.map((e ) => e.name)
|
|
200
|
+
.sort();
|
|
201
|
+
|
|
202
|
+
const skills = {};
|
|
203
|
+
const mismatches = [];
|
|
204
|
+
for (const name of names) {
|
|
205
|
+
const rel = path.join(SANDBOX_SKILLS_DIR, name, 'SKILL.md');
|
|
206
|
+
const dst = path.join(wtPath, rel);
|
|
207
|
+
const sourceSha256 = sha256File(path.join(sourceRoot, name, 'SKILL.md'));
|
|
208
|
+
const sha256 = fs.existsSync(dst) ? sha256File(dst) : null;
|
|
209
|
+
skills[name] = { path: rel, sha256, sourceSha256, matchesSource: sha256 === sourceSha256 };
|
|
210
|
+
if (sha256 !== sourceSha256) mismatches.push(name);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const manifest = {
|
|
214
|
+
generatedAt: new Date().toISOString(),
|
|
215
|
+
taskId,
|
|
216
|
+
repoRoot: this.repoRoot,
|
|
217
|
+
sourceRoot: skillsRoot,
|
|
218
|
+
sourceCommit: this.headCommit(),
|
|
219
|
+
harness,
|
|
220
|
+
projectedBy: `${cli} install <sandbox> --force -t ${harness}`,
|
|
221
|
+
skillsDir: path.join(wtPath, SANDBOX_SKILLS_DIR),
|
|
222
|
+
skillsDirRelative: SANDBOX_SKILLS_DIR,
|
|
223
|
+
skillCount: names.length,
|
|
224
|
+
mismatches,
|
|
225
|
+
skills,
|
|
226
|
+
installLog: (installOutput || '').trim().split('\n').slice(-3),
|
|
227
|
+
};
|
|
228
|
+
fs.writeFileSync(path.join(wtPath, SKILL_SOURCE_FILE), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
229
|
+
|
|
230
|
+
if (mismatches.length) {
|
|
231
|
+
throw new Error(
|
|
232
|
+
`sandbox ${taskId} does not carry this repo's skills (${mismatches.join(', ')}) — ` +
|
|
233
|
+
`a run against it would measure something else. See ${path.join(wtPath, SKILL_SOURCE_FILE)}`,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
return manifest;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** The skill-source manifest a sandbox recorded, or null when it was never projected. */
|
|
240
|
+
skillSourceOf(taskId ) {
|
|
241
|
+
const file = path.join(this.pathFor(taskId), SKILL_SOURCE_FILE);
|
|
242
|
+
if (!fs.existsSync(file)) return null;
|
|
243
|
+
try {
|
|
244
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
245
|
+
} catch {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Repo HEAD, or null outside a repository. Recorded for readability; the per-skill hashes are
|
|
251
|
+
* the actual provenance, and they stay correct on a dirty tree where a commit id does not. */
|
|
252
|
+
headCommit() {
|
|
253
|
+
const res = git(['rev-parse', 'HEAD'], this.repoRoot, { allowFailure: true });
|
|
254
|
+
return res.ok ? res.stdout.trim() : null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* One call for the shape an evaluation run needs: an isolated worktree that carries this repo's
|
|
259
|
+
* skills. Split from `create()` because non-evaluation callers want a bare worktree and should
|
|
260
|
+
* not pay for a projection they will not read.
|
|
261
|
+
*/
|
|
262
|
+
createSandbox(taskId , { baseRef = 'HEAD', ...opts } = {}) {
|
|
263
|
+
const wtPath = this.create(taskId, baseRef);
|
|
264
|
+
return { path: wtPath, manifest: this.projectSkills(taskId, opts) };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Remove the worktree and its branch. Idempotent: removing something absent is success. */
|
|
268
|
+
remove(taskId ) {
|
|
269
|
+
const wtPath = this.pathFor(taskId);
|
|
270
|
+
const branch = this.branchFor(taskId);
|
|
271
|
+
|
|
272
|
+
git(['worktree', 'remove', '--force', wtPath], this.repoRoot, { allowFailure: true });
|
|
273
|
+
if (fs.existsSync(wtPath)) fs.rmSync(wtPath, { recursive: true, force: true });
|
|
274
|
+
git(['worktree', 'prune'], this.repoRoot, { allowFailure: true });
|
|
275
|
+
// Deleting the branch is what keeps repeated bench runs from accumulating one branch per case.
|
|
276
|
+
git(['branch', '-D', branch], this.repoRoot, { allowFailure: true });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* What the task actually changed inside its sandbox: committed diff against the base ref plus
|
|
281
|
+
* anything still uncommitted. Returns `{ diff, untracked, error }` — `error` is set rather than
|
|
282
|
+
* collapsing a failure into an empty diff, so a caller can tell "nothing changed" from
|
|
283
|
+
* "could not tell", which the Python version could not.
|
|
284
|
+
*/
|
|
285
|
+
collectDiff(taskId , baseRef ) {
|
|
286
|
+
const wtPath = this.pathFor(taskId);
|
|
287
|
+
if (!fs.existsSync(wtPath)) {
|
|
288
|
+
return { diff: null, untracked: [], error: `no worktree at ${wtPath}` };
|
|
289
|
+
}
|
|
290
|
+
// Default to the SHA recorded at creation, never to `HEAD` — see create().
|
|
291
|
+
const base = baseRef || this.baseOf(taskId);
|
|
292
|
+
if (!base) {
|
|
293
|
+
return { diff: null, untracked: [], error: `no recorded base for ${taskId}; pass baseRef explicitly` };
|
|
294
|
+
}
|
|
295
|
+
const committed = git(['diff', `${base}...HEAD`], wtPath, { allowFailure: true });
|
|
296
|
+
const working = git(['diff', 'HEAD'], wtPath, { allowFailure: true });
|
|
297
|
+
const untracked = git(['ls-files', '--others', '--exclude-standard'], wtPath, { allowFailure: true });
|
|
298
|
+
|
|
299
|
+
if (!committed.ok && !working.ok) {
|
|
300
|
+
return { diff: null, untracked: [], error: committed.stderr || working.stderr };
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
diff: `${committed.ok ? committed.stdout : ''}${working.ok ? working.stdout : ''}`,
|
|
304
|
+
// Harness bookkeeping is not task work: the base pointer and the skill-source manifest are
|
|
305
|
+
// written by this module, not by the skill under test.
|
|
306
|
+
untracked: untracked.ok
|
|
307
|
+
? untracked.stdout.split('\n').filter((f ) => f && f !== BASE_FILE && f !== SKILL_SOURCE_FILE)
|
|
308
|
+
: [],
|
|
309
|
+
error: null,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Every sandbox this manager knows about — used to clean up after an interrupted run. */
|
|
314
|
+
list() {
|
|
315
|
+
if (!fs.existsSync(this.worktreesDir)) return [];
|
|
316
|
+
return fs
|
|
317
|
+
.readdirSync(this.worktreesDir, { withFileTypes: true })
|
|
318
|
+
.filter((e ) => e.isDirectory())
|
|
319
|
+
.map((e ) => e.name);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Run `fn(worktreePath)` in a fresh sandbox and always tear it down, even when `fn` throws. */
|
|
323
|
+
withWorktree(taskId , fn , baseRef = 'HEAD') {
|
|
324
|
+
const wtPath = this.create(taskId, baseRef);
|
|
325
|
+
try {
|
|
326
|
+
return fn(wtPath);
|
|
327
|
+
} finally {
|
|
328
|
+
this.remove(taskId);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
module.exports = {
|
|
334
|
+
WorktreeManager,
|
|
335
|
+
BASE_FILE,
|
|
336
|
+
SKILL_SOURCE_FILE,
|
|
337
|
+
SANDBOX_SKILLS_DIR,
|
|
338
|
+
SOURCE_SKILLS_DIR,
|
|
339
|
+
sha256File,
|
|
340
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
4
|
+
|
|
5
|
+
// Neutral DoFlow lifecycle state. This module intentionally has no knowledge of a harness's
|
|
6
|
+
// native directory: callers provide a project root or user home, and adapter-owned paths stay in
|
|
7
|
+
// adapter records. Legacy manifests are an import source only; they are never changed here.
|
|
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 { manifestPath, readManifest } = require('../install/manifest.js');
|
|
13
|
+
|
|
14
|
+
const STATE_VERSION = 1;
|
|
15
|
+
// Ledger document version. v1 ledgers (no tombstones) remain readable forever; writers produce
|
|
16
|
+
// v2, which adds the append-only tombstone log used by the reconciler to remember moved targets.
|
|
17
|
+
const LEDGER_VERSION = 2;
|
|
18
|
+
const LEDGER_FILE = 'ledger.json';
|
|
19
|
+
const RECOVERY_DIR = 'recovery';
|
|
20
|
+
|
|
21
|
+
function assertScope(scope ) {
|
|
22
|
+
if (scope !== 'project' && scope !== 'global') throw new Error(`Invalid state scope: '${scope}'`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Resolve DoFlow-owned state without deriving it from a harness directory. */
|
|
26
|
+
function stateRoot({ scope, projectRoot = '.', homeDir = os.homedir() } ) {
|
|
27
|
+
assertScope(scope);
|
|
28
|
+
return scope === 'project'
|
|
29
|
+
? path.join(path.resolve(projectRoot), '.doflow', 'state')
|
|
30
|
+
: path.join(path.resolve(homeDir), '.doflow', 'state');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function ledgerPath(root ) { return path.join(root, LEDGER_FILE); }
|
|
34
|
+
function recoveryRoot(root ) { return path.join(root, RECOVERY_DIR); }
|
|
35
|
+
|
|
36
|
+
function stable(value ) {
|
|
37
|
+
if (Array.isArray(value)) return value.map(stable);
|
|
38
|
+
if (!value || typeof value !== 'object') return value;
|
|
39
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable(value[key])]));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function fingerprint(value ) {
|
|
43
|
+
return crypto.createHash('sha256').update(JSON.stringify(stable(value))).digest('hex');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function atomicJsonWrite(file , value , { fsImpl = fs } = {}) {
|
|
47
|
+
fsImpl.mkdirSync(path.dirname(file), { recursive: true });
|
|
48
|
+
const tmp = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${Date.now()}.tmp`);
|
|
49
|
+
try {
|
|
50
|
+
fsImpl.writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', flag: 'wx' });
|
|
51
|
+
fsImpl.renameSync(tmp, file);
|
|
52
|
+
} finally {
|
|
53
|
+
if (fsImpl.existsSync(tmp)) fsImpl.rmSync(tmp, { force: true });
|
|
54
|
+
}
|
|
55
|
+
return file;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function defaultLedger({ scope, scopeRoot } ) {
|
|
59
|
+
assertScope(scope);
|
|
60
|
+
return {
|
|
61
|
+
version: LEDGER_VERSION,
|
|
62
|
+
scope,
|
|
63
|
+
scopeRoot: path.resolve(scopeRoot),
|
|
64
|
+
targets: {},
|
|
65
|
+
mcpSelections: {},
|
|
66
|
+
resources: [],
|
|
67
|
+
legacyImports: [],
|
|
68
|
+
lastRecoveryId: null,
|
|
69
|
+
guidanceVersion: null,
|
|
70
|
+
tombstones: [],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Upgrade a v1 ledger in memory to the current shape without touching disk. Idempotent. */
|
|
75
|
+
function upgradeLedger(ledger ) {
|
|
76
|
+
if (!ledger || typeof ledger !== 'object') throw new Error('Invalid neutral ledger: expected object');
|
|
77
|
+
if (ledger.version === LEDGER_VERSION) return ledger;
|
|
78
|
+
return { ...ledger, version: LEDGER_VERSION, tombstones: Array.isArray(ledger.tombstones) ? ledger.tombstones : [] };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function validateLedger(value ) {
|
|
82
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Invalid neutral ledger: expected object');
|
|
83
|
+
if (![STATE_VERSION, LEDGER_VERSION].includes(value.version)) throw new Error(`Unsupported neutral ledger version: '${value.version}'`);
|
|
84
|
+
if (value.version >= LEDGER_VERSION && value.tombstones !== undefined && !Array.isArray(value.tombstones)) {
|
|
85
|
+
throw new Error('Invalid neutral ledger: tombstones must be an array');
|
|
86
|
+
}
|
|
87
|
+
assertScope(value.scope);
|
|
88
|
+
if (typeof value.scopeRoot !== 'string' || !path.isAbsolute(value.scopeRoot)) throw new Error('Invalid neutral ledger: scopeRoot must be absolute');
|
|
89
|
+
for (const field of ['targets', 'mcpSelections']) if (!value[field] || typeof value[field] !== 'object' || Array.isArray(value[field])) throw new Error(`Invalid neutral ledger: ${field} must be an object`);
|
|
90
|
+
for (const field of ['resources', 'legacyImports']) if (!Array.isArray(value[field])) throw new Error(`Invalid neutral ledger: ${field} must be an array`);
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function readLedger(root , { fsImpl = fs } = {}) {
|
|
95
|
+
const file = ledgerPath(root);
|
|
96
|
+
if (!fsImpl.existsSync(file)) return null;
|
|
97
|
+
try { return validateLedger(JSON.parse(fsImpl.readFileSync(file, 'utf8'))); }
|
|
98
|
+
catch (error ) { throw new Error(`Cannot read neutral ledger '${file}': ${error.message}`); }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function writeLedger(root , ledger , options = {}) {
|
|
102
|
+
return atomicJsonWrite(ledgerPath(root), validateLedger(ledger), options);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function recoveryId(now = new Date()) {
|
|
106
|
+
return `recovery_${now.toISOString().replace(/[:.]/g, '-').replace(/Z$/, 'Z')}_${crypto.randomBytes(4).toString('hex')}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function assertSafeRecoveryId(id ) {
|
|
110
|
+
if (typeof id !== 'string' || !/^recovery_[A-Za-z0-9_-]+$/.test(id)) throw new Error(`Invalid recovery id: '${id}'`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Persist a journal record before native mutation; append-only records make interrupted runs observable. */
|
|
114
|
+
function writeRecoveryRecord(root , record , { now = new Date(), fsImpl = fs } = {}) {
|
|
115
|
+
const id = record?.id ?? recoveryId(now);
|
|
116
|
+
assertSafeRecoveryId(id);
|
|
117
|
+
const value = {
|
|
118
|
+
id,
|
|
119
|
+
version: STATE_VERSION,
|
|
120
|
+
status: record?.status ?? 'pending',
|
|
121
|
+
createdAt: record?.createdAt ?? now.toISOString(),
|
|
122
|
+
...record,
|
|
123
|
+
};
|
|
124
|
+
value.id = id; // re-asserted after the spread (a record carrying its own `id` key, even undefined, must not win)
|
|
125
|
+
if (!Array.isArray(value.changes)) value.changes = [];
|
|
126
|
+
return { id, file: atomicJsonWrite(path.join(recoveryRoot(root), `${id}.json`), value, { fsImpl }), record: value };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function readRecoveryRecord(root , id , { fsImpl = fs } = {}) {
|
|
130
|
+
assertSafeRecoveryId(id);
|
|
131
|
+
const file = path.join(recoveryRoot(root), `${id}.json`);
|
|
132
|
+
if (!fsImpl.existsSync(file)) return null;
|
|
133
|
+
try { return JSON.parse(fsImpl.readFileSync(file, 'utf8')); }
|
|
134
|
+
catch (error ) { throw new Error(`Cannot read recovery record '${file}': ${error.message}`); }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function ownershipKey(record ) {
|
|
138
|
+
return [record.harness, record.scope, record.assetId, record.target, record.ownershipIdentity].join('\u0000');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function importLegacyResource(resource , scope ) {
|
|
142
|
+
if (!resource || typeof resource !== 'object') return null;
|
|
143
|
+
if (typeof resource.target !== 'string' || typeof resource.kind !== 'string' || typeof resource.identity !== 'string') return null;
|
|
144
|
+
return {
|
|
145
|
+
harness: resource.target,
|
|
146
|
+
scope: resource.scope === 'project' || resource.scope === 'global' ? resource.scope : scope,
|
|
147
|
+
assetId: `legacy.${resource.kind}`,
|
|
148
|
+
target: resource.target,
|
|
149
|
+
ownershipIdentity: resource.identity,
|
|
150
|
+
sourceVersion: resource.sourceVersion ?? 'legacy',
|
|
151
|
+
fingerprint: resource.fingerprint ?? null,
|
|
152
|
+
projection: { legacyKind: resource.kind },
|
|
153
|
+
recoveryRef: resource.recoveryPoint ?? null,
|
|
154
|
+
selection: resource.selection ?? null,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Read-compatible, conservative legacy import. Only explicit `managed_resources` records are
|
|
160
|
+
* converted to ownership claims; a legacy tool entry alone never authorizes adoption of files.
|
|
161
|
+
*/
|
|
162
|
+
function migrateLegacyManifest({ root, scope, scopeRoot, claudeDir, now = new Date(), fsImpl = fs } ) {
|
|
163
|
+
assertScope(scope);
|
|
164
|
+
const legacyFile = manifestPath(claudeDir);
|
|
165
|
+
const current = readLedger(root, { fsImpl }) ?? defaultLedger({ scope, scopeRoot });
|
|
166
|
+
if (current.scope !== scope || current.scopeRoot !== path.resolve(scopeRoot)) throw new Error('Neutral ledger scope does not match requested migration scope');
|
|
167
|
+
if (current.legacyImports.some((item ) => item.path === legacyFile)) return { migrated: false, reason: 'already-imported', ledger: current, legacyFile };
|
|
168
|
+
if (!fsImpl.existsSync(legacyFile)) return { migrated: false, reason: 'legacy-missing', ledger: current, legacyFile };
|
|
169
|
+
|
|
170
|
+
const legacy = readManifest(claudeDir);
|
|
171
|
+
if (!legacy || legacy.operation === 'error') return { migrated: false, reason: 'legacy-invalid', ledger: current, legacyFile };
|
|
172
|
+
const resources = legacy.managedResources.map((resource ) => importLegacyResource(resource, scope)).filter(Boolean);
|
|
173
|
+
const existing = new Set(current.resources.map(ownershipKey));
|
|
174
|
+
for (const resource of resources) if (!existing.has(ownershipKey(resource))) current.resources.push(resource);
|
|
175
|
+
|
|
176
|
+
// The legacy manifest's MCP selection was owned by Claude's config flow. Do not infer an
|
|
177
|
+
// equivalent selection for Codex or Gemini merely because they happened to share the old file.
|
|
178
|
+
if (Array.isArray(legacy.mcpServers) && !Object.hasOwn(current.mcpSelections, 'claude')) current.mcpSelections.claude = [...legacy.mcpServers];
|
|
179
|
+
for (const [harness, status] of Object.entries(legacy.tools) ) {
|
|
180
|
+
if (status?.installed && !current.targets[harness]) current.targets[harness] = { installed: true, lastUpdated: status.last_updated ?? null, imported: true };
|
|
181
|
+
}
|
|
182
|
+
current.legacyImports.push({ path: legacyFile, importedAt: now.toISOString(), sourceFingerprint: fingerprint({ operation: legacy.operation, lastRun: legacy.lastRun, backupId: legacy.backupId, resources: legacy.managedResources }), lastBackupId: legacy.backupId });
|
|
183
|
+
writeLedger(root, current, { fsImpl });
|
|
184
|
+
return { migrated: true, reason: 'imported', ledger: current, legacyFile };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports = {
|
|
188
|
+
STATE_VERSION, LEDGER_VERSION, LEDGER_FILE, RECOVERY_DIR,
|
|
189
|
+
stateRoot, ledgerPath, recoveryRoot, atomicJsonWrite,
|
|
190
|
+
defaultLedger, upgradeLedger, validateLedger, readLedger, writeLedger,
|
|
191
|
+
recoveryId, writeRecoveryRecord, readRecoveryRecord,
|
|
192
|
+
ownershipKey, importLegacyResource, migrateLegacyManifest,
|
|
193
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
4
|
+
|
|
5
|
+
// doflow.lock — the resolved-projection lockfile. A ledger records what DoFlow OWNS;
|
|
6
|
+
// a lock records what DoFlow CHOSE. It pins, per scope: targeted harnesses, selected assets with
|
|
7
|
+
// their projection summaries, MCP selections, and the source version they were resolved from, so
|
|
8
|
+
// that installs become reproducible and drift becomes a reviewable diff instead of a surprise.
|
|
9
|
+
// Deliberately metadata-only: no file contents, no secrets — same posture as the neutral ledger.
|
|
10
|
+
const fs = require('node:fs');
|
|
11
|
+
const os = require('node:os');
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
const { atomicJsonWrite } = require('./index.js');
|
|
14
|
+
|
|
15
|
+
const LOCK_VERSION = 1;
|
|
16
|
+
const LOCK_FILE = 'doflow.lock';
|
|
17
|
+
|
|
18
|
+
function lockRoot({ scope, projectRoot = '.', homeDir = os.homedir() } ) {
|
|
19
|
+
if (scope !== 'project' && scope !== 'global') throw new Error(`Invalid lock scope: '${scope}'`);
|
|
20
|
+
return scope === 'project' ? path.resolve(projectRoot) : path.resolve(homeDir);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function lockPath(options ) { return path.join(lockRoot(options), '.doflow', LOCK_FILE); }
|
|
24
|
+
|
|
25
|
+
function stable(value ) {
|
|
26
|
+
if (Array.isArray(value)) return [...value].map(stable).sort((a , b ) => JSON.stringify(a) < JSON.stringify(b) ? -1 : 1);
|
|
27
|
+
if (!value || typeof value !== 'object') return value;
|
|
28
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable(value[key])]));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function defaultLock({ scope, scopeRoot } ) {
|
|
32
|
+
if (scope !== 'project' && scope !== 'global') throw new Error(`Invalid lock scope: '${scope}'`);
|
|
33
|
+
return {
|
|
34
|
+
version: LOCK_VERSION,
|
|
35
|
+
scope,
|
|
36
|
+
scopeRoot: path.resolve(scopeRoot),
|
|
37
|
+
generatedAt: null,
|
|
38
|
+
sourceVersion: null,
|
|
39
|
+
targets: [],
|
|
40
|
+
assets: [],
|
|
41
|
+
mcpSelections: {},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function validateLock(value ) {
|
|
46
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Invalid doflow.lock: expected object');
|
|
47
|
+
if (value.version !== LOCK_VERSION) throw new Error(`Unsupported doflow.lock version: '${value.version}'`);
|
|
48
|
+
if (value.scope !== 'project' && value.scope !== 'global') throw new Error(`Invalid doflow.lock scope: '${value.scope}'`);
|
|
49
|
+
if (typeof value.scopeRoot !== 'string' || !path.isAbsolute(value.scopeRoot)) throw new Error('Invalid doflow.lock: scopeRoot must be absolute');
|
|
50
|
+
for (const field of ['targets', 'assets']) {
|
|
51
|
+
if (!Array.isArray(value[field])) throw new Error(`Invalid doflow.lock: ${field} must be an array`);
|
|
52
|
+
}
|
|
53
|
+
if (!value.mcpSelections || typeof value.mcpSelections !== 'object' || Array.isArray(value.mcpSelections)) {
|
|
54
|
+
throw new Error('Invalid doflow.lock: mcpSelections must be an object');
|
|
55
|
+
}
|
|
56
|
+
for (const [harness, ids] of Object.entries(value.mcpSelections)) {
|
|
57
|
+
if (!Array.isArray(ids) || !ids.every((id ) => typeof id === 'string')) {
|
|
58
|
+
throw new Error(`Invalid doflow.lock: mcpSelections.${harness} must be an array of server ids`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function readLock(options , { fsImpl = fs } = {}) {
|
|
65
|
+
const file = lockPath(options);
|
|
66
|
+
if (!fsImpl.existsSync(file)) return null;
|
|
67
|
+
try { return validateLock(JSON.parse(fsImpl.readFileSync(file, 'utf8'))); }
|
|
68
|
+
catch (error ) { throw new Error(`Cannot read doflow.lock '${file}': ${error.message}`); }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function writeLock(options , lock , { fsImpl = fs } = {}) {
|
|
72
|
+
// Validate the caller's document first so a foreign version fails loudly here rather than
|
|
73
|
+
// being silently normalized into something no future reader can distinguish.
|
|
74
|
+
validateLock(lock);
|
|
75
|
+
return atomicJsonWrite(lockPath(options), stable({ ...lock, version: LOCK_VERSION }), { fsImpl });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The lock's MCP rows for the harnesses it targets. A row naming a harness outside `targets` is
|
|
79
|
+
* ignored by every reader; the next write drops it. */
|
|
80
|
+
function pinnedSelections(lock ) {
|
|
81
|
+
const targets = new Set((lock?.targets ?? []).map((entry ) => entry.harness));
|
|
82
|
+
return Object.fromEntries(Object.entries(lock?.mcpSelections ?? {}).filter(([harness]) => targets.has(harness)));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Compare two lock documents. Arrays are keyed by identity (assets by id, targets by harness,
|
|
86
|
+
* fallback to full JSON), mcpSelections by harness, where an absent row (`null`) and a recorded
|
|
87
|
+
* empty selection (`[]`) differ. Returns {added, removed, changed} per section plus a clean flag. */
|
|
88
|
+
function diffLocks(previousLock , nextLock ) {
|
|
89
|
+
// Compare in the order writeLock stores, so a document built in memory never differs from the
|
|
90
|
+
// file it was written to.
|
|
91
|
+
const previous = previousLock && stable(previousLock);
|
|
92
|
+
const next = nextLock && stable(nextLock);
|
|
93
|
+
const key = (entry ) => entry?.id ?? entry?.harness ?? JSON.stringify(entry);
|
|
94
|
+
const sectionDiff = (before , after ) => {
|
|
95
|
+
const beforeKeys = new Map ((before || []).map((entry ) => [key(entry), entry]));
|
|
96
|
+
const afterKeys = new Map ((after || []).map((entry ) => [key(entry), entry]));
|
|
97
|
+
const added = [...afterKeys].filter(([k]) => !beforeKeys.has(k)).map(([, v]) => v);
|
|
98
|
+
const removed = [...beforeKeys].filter(([k]) => !afterKeys.has(k)).map(([, v]) => v);
|
|
99
|
+
const changed = [...afterKeys]
|
|
100
|
+
.filter(([k, v]) => beforeKeys.has(k) && JSON.stringify(beforeKeys.get(k)) !== JSON.stringify(v))
|
|
101
|
+
.map(([, v]) => v);
|
|
102
|
+
return { added, removed, changed };
|
|
103
|
+
};
|
|
104
|
+
const selectionsBefore = pinnedSelections(previous);
|
|
105
|
+
const selectionsAfter = pinnedSelections(next);
|
|
106
|
+
const harnesses = new Set([...Object.keys(selectionsBefore), ...Object.keys(selectionsAfter)]);
|
|
107
|
+
const mcpChanged = [...harnesses]
|
|
108
|
+
.filter((harness) => JSON.stringify(selectionsBefore[harness] ?? null) !== JSON.stringify(selectionsAfter[harness] ?? null))
|
|
109
|
+
.map((harness) => ({ harness, before: selectionsBefore[harness] ?? null, after: selectionsAfter[harness] ?? null }));
|
|
110
|
+
const result = {
|
|
111
|
+
targets: sectionDiff(previous?.targets, next?.targets),
|
|
112
|
+
assets: sectionDiff(previous?.assets, next?.assets),
|
|
113
|
+
mcpSelections: { changed: mcpChanged },
|
|
114
|
+
meta: {
|
|
115
|
+
sourceVersion: (previous?.sourceVersion ?? null) !== (next?.sourceVersion ?? null)
|
|
116
|
+
? { before: previous?.sourceVersion ?? null, after: next?.sourceVersion ?? null } : null,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
result.clean = ['targets', 'assets'].every((name) => {
|
|
120
|
+
const section = result[name];
|
|
121
|
+
return section.added.length === 0 && section.removed.length === 0 && section.changed.length === 0;
|
|
122
|
+
}) && result.mcpSelections.changed.length === 0 && result.meta.sourceVersion === null;
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Delete the lock when nothing remains pinned. Returns true when a file actually went away. */
|
|
127
|
+
function removeLock(options , { fsImpl = fs } = {}) {
|
|
128
|
+
const file = lockPath(options);
|
|
129
|
+
if (!fsImpl.existsSync(file)) return false;
|
|
130
|
+
fsImpl.rmSync(file);
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = {
|
|
135
|
+
LOCK_VERSION, LOCK_FILE, lockRoot, lockPath, defaultLock, validateLock, readLock, writeLock, pinnedSelections, diffLocks, removeLock,
|
|
136
|
+
};
|