@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,626 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
4
|
+
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const os = require('node:os');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
const {
|
|
9
|
+
SESSION_GAP_MS,
|
|
10
|
+
groupSessions,
|
|
11
|
+
buildTrace,
|
|
12
|
+
buildStats,
|
|
13
|
+
buildDiscover,
|
|
14
|
+
PROVIDER,
|
|
15
|
+
} = require('./views.ts');
|
|
16
|
+
const {
|
|
17
|
+
shortTime,
|
|
18
|
+
formatMs,
|
|
19
|
+
finish,
|
|
20
|
+
integrityNote,
|
|
21
|
+
printEmptyLedgerNotice,
|
|
22
|
+
DISCOVER_MARK,
|
|
23
|
+
} = require('./render.ts');
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Task-run telemetry — the JavaScript port of
|
|
27
|
+
* `core/shared/scripts/doflow/evaluation/telemetry.py` (plan task B.3, component C9).
|
|
28
|
+
*
|
|
29
|
+
* Records a task's span: the events that happened inside it, how long it took, how many tokens it
|
|
30
|
+
* consumed. Ported from observed behaviour (plan decision D3): the event record shape, the trace
|
|
31
|
+
* key names, the six-decimal cost rounding and the default per-token rates are unchanged.
|
|
32
|
+
*
|
|
33
|
+
* Plan task C.3 added the second half of this file: the date-partitioned run ledger (FR-011) and
|
|
34
|
+
* its three inspection views (FR-012). `TaskRunTelemetry` captures one *task's* span in memory;
|
|
35
|
+
* `RunLedger` is the on-disk record of *dispatched runs*, written by `doflow-run` and by any
|
|
36
|
+
* runtime call that names the capability it used.
|
|
37
|
+
*
|
|
38
|
+
* Three defects are fixed rather than reproduced, each explained at its site:
|
|
39
|
+
* 1. Duration came from `time.time()`, a wall clock that an NTP step or a DST change can move
|
|
40
|
+
* backwards mid-run, producing negative or inflated durations.
|
|
41
|
+
* 2. `estimated_cost_usd` was computed from two hard-coded rates regardless of which model the
|
|
42
|
+
* trace named, so the number was presented as measured cost for models it did not price.
|
|
43
|
+
* 3. `export_trace` returned the live `events` array, so a caller mutating the exported trace
|
|
44
|
+
* silently rewrote the recorder's history.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The Python's literal rates, kept as the default so an existing caller sees the same number.
|
|
49
|
+
* They price one specific model; a trace naming a different one gets a cost only if the caller
|
|
50
|
+
* supplies its rates, because inventing a price for an unpriced model is inventing a metric.
|
|
51
|
+
*/
|
|
52
|
+
const DEFAULT_TOKEN_RATES: Readonly<{ inputPerToken: number; outputPerToken: number }> = Object.freeze({
|
|
53
|
+
inputPerToken: 0.000003,
|
|
54
|
+
outputPerToken: 0.000015,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const COST_DECIMALS = 6;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {number} value
|
|
61
|
+
* @returns {number} rounded to six decimals, matching the Python's `round(x, 6)`
|
|
62
|
+
*/
|
|
63
|
+
function roundCost(value: number): number {
|
|
64
|
+
const factor = 10 ** COST_DECIMALS;
|
|
65
|
+
return Math.round(value * factor) / factor;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
class TaskRunTelemetry {
|
|
69
|
+
taskId: string;
|
|
70
|
+
agent: string;
|
|
71
|
+
model: string;
|
|
72
|
+
rates: { inputPerToken: number; outputPerToken: number } | null;
|
|
73
|
+
events: any[];
|
|
74
|
+
clock: () => number;
|
|
75
|
+
startedAtMs: number;
|
|
76
|
+
startedAt: string;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {string} taskId
|
|
80
|
+
* @param {Object|string} [options] a string is accepted for parity with the Python's positional
|
|
81
|
+
* `agent` argument
|
|
82
|
+
* @param {string} [options.agent='codex']
|
|
83
|
+
* @param {string} [options.model='default']
|
|
84
|
+
* @param {{inputPerToken: number, outputPerToken: number}|null} [options.rates]
|
|
85
|
+
* @param {Function} [options.clock] injection seam for tests; returns elapsed milliseconds
|
|
86
|
+
*/
|
|
87
|
+
constructor(taskId: string, options: any = {}) {
|
|
88
|
+
const opts = typeof options === 'string' ? { agent: options } : options;
|
|
89
|
+
this.taskId = taskId;
|
|
90
|
+
this.agent = opts.agent || 'codex';
|
|
91
|
+
this.model = opts.model || 'default';
|
|
92
|
+
this.rates = opts.rates === undefined ? DEFAULT_TOKEN_RATES : opts.rates;
|
|
93
|
+
this.events = [];
|
|
94
|
+
|
|
95
|
+
// Defect fix (1): a monotonic source. `Date.now()` (like Python's `time.time()`) can step
|
|
96
|
+
// backwards, and a negative duration in a benchmark record is worse than no duration.
|
|
97
|
+
this.clock = opts.clock || (() => Number(process.hrtime.bigint() / 1000000n));
|
|
98
|
+
this.startedAtMs = this.clock();
|
|
99
|
+
this.startedAt = new Date().toISOString();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Appends a structured event to the span.
|
|
104
|
+
* @param {string} eventType
|
|
105
|
+
* @param {Object} [details={}]
|
|
106
|
+
* @returns {Object} the recorded event
|
|
107
|
+
*/
|
|
108
|
+
recordEvent(eventType: string, details: any = {}): any {
|
|
109
|
+
const event = {
|
|
110
|
+
timestamp: new Date().toISOString(),
|
|
111
|
+
type: eventType,
|
|
112
|
+
details,
|
|
113
|
+
};
|
|
114
|
+
this.events.push(event);
|
|
115
|
+
return event;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Milliseconds elapsed since the recorder was constructed.
|
|
120
|
+
* @returns {number}
|
|
121
|
+
*/
|
|
122
|
+
elapsedMs(): number {
|
|
123
|
+
return Math.max(0, this.clock() - this.startedAtMs);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Compiles the run's trace record.
|
|
128
|
+
* @param {boolean} success
|
|
129
|
+
* @param {number} [inputTokens=0]
|
|
130
|
+
* @param {number} [outputTokens=0]
|
|
131
|
+
* @returns {Object} trace record
|
|
132
|
+
*/
|
|
133
|
+
exportTrace(success: boolean, inputTokens: number = 0, outputTokens: number = 0): any {
|
|
134
|
+
// Defect fix (2): cost is only reported when rates are known for this run. The default rates
|
|
135
|
+
// reproduce the Python's number exactly; a caller that clears them (`rates: null`) gets `null`
|
|
136
|
+
// rather than a confident figure derived from another model's price list.
|
|
137
|
+
const estimatedCostUsd = this.rates
|
|
138
|
+
? roundCost(inputTokens * this.rates.inputPerToken + outputTokens * this.rates.outputPerToken)
|
|
139
|
+
: null;
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
taskId: this.taskId,
|
|
143
|
+
agent: this.agent,
|
|
144
|
+
model: this.model,
|
|
145
|
+
success: Boolean(success),
|
|
146
|
+
startedAt: this.startedAt,
|
|
147
|
+
durationMs: this.elapsedMs(),
|
|
148
|
+
inputTokens,
|
|
149
|
+
outputTokens,
|
|
150
|
+
estimatedCostUsd,
|
|
151
|
+
eventCount: this.events.length,
|
|
152
|
+
// Defect fix (3): a copy. The Python handed out its internal list, so an exported trace and
|
|
153
|
+
// the recorder aliased the same array.
|
|
154
|
+
events: this.events.map((e) => ({ ...e })),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------------- run ledger
|
|
160
|
+
//
|
|
161
|
+
// The ledger is one JSONL file per UTC date under `<config>/state/runs/`, appended to by
|
|
162
|
+
// `core/shared/scripts/doflow/bin/doflow-run` for every verb it dispatches. This module owns the
|
|
163
|
+
// reading side plus the writing side for capability-level events the shell dispatcher cannot see
|
|
164
|
+
// (which capability was requested, which provider answered, how much it returned).
|
|
165
|
+
//
|
|
166
|
+
// Two rules constrain everything below.
|
|
167
|
+
// NFR-004 — records are metadata. The dispatcher deliberately records an argument *count* and
|
|
168
|
+
// never an argument value; `sanitizeRunEvent` keeps that discipline for anything written here by
|
|
169
|
+
// allowing a closed set of fields, validating every string against a token pattern that cannot
|
|
170
|
+
// express a path, a URL, a quote or a space, and dropping everything else by name.
|
|
171
|
+
// FR-011 note — tracing must never change what the traced command returned. Every function here
|
|
172
|
+
// answers rather than throws: a missing directory, an unreadable file and a corrupt line are all
|
|
173
|
+
// normal states of a best-effort ledger.
|
|
174
|
+
|
|
175
|
+
const RUNS_DIRNAME = 'runs';
|
|
176
|
+
const LEDGER_FILE_RE = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The complete writable field set. Anything not listed here never reaches the ledger.
|
|
180
|
+
* `wire` is the on-disk key: snake_case, matching the five fields `doflow-run` already writes, so
|
|
181
|
+
* one file never mixes two naming conventions.
|
|
182
|
+
*/
|
|
183
|
+
const RUN_FIELDS: Readonly<Array<{ key: string; wire: string; type: string }>> = Object.freeze([
|
|
184
|
+
{ key: 'verb', wire: 'verb', type: 'token' },
|
|
185
|
+
{ key: 'kind', wire: 'kind', type: 'token' },
|
|
186
|
+
{ key: 'capability', wire: 'capability', type: 'token' },
|
|
187
|
+
{ key: 'provider', wire: 'provider', type: 'token' },
|
|
188
|
+
{ key: 'outcome', wire: 'outcome', type: 'token' },
|
|
189
|
+
{ key: 'stage', wire: 'stage', type: 'token' },
|
|
190
|
+
{ key: 'workflow', wire: 'workflow', type: 'token' },
|
|
191
|
+
{ key: 'taskClass', wire: 'task_class', type: 'token' },
|
|
192
|
+
{ key: 'session', wire: 'session', type: 'token' },
|
|
193
|
+
{ key: 'exitCode', wire: 'exit_code', type: 'int' },
|
|
194
|
+
{ key: 'durationMs', wire: 'duration_ms', type: 'uint' },
|
|
195
|
+
{ key: 'argCount', wire: 'arg_count', type: 'uint' },
|
|
196
|
+
{ key: 'resultCount', wire: 'result_count', type: 'uint' },
|
|
197
|
+
{ key: 'inputBytes', wire: 'input_bytes', type: 'uint' },
|
|
198
|
+
{ key: 'outputBytes', wire: 'output_bytes', type: 'uint' },
|
|
199
|
+
{ key: 'retryCount', wire: 'retry_count', type: 'uint' },
|
|
200
|
+
]);
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Identifiers only. No space, no quote, no slash, no dot-dot — a capability id (`code.exact-search`)
|
|
204
|
+
* and a provider id (`semble.search`) fit; a file path, a query string, a command line, a URL and
|
|
205
|
+
* anything with a secret's usual punctuation do not. This is the mechanical half of NFR-004: a
|
|
206
|
+
* caller cannot smuggle content through a field that only accepts an identifier.
|
|
207
|
+
*/
|
|
208
|
+
const SAFE_TOKEN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/;
|
|
209
|
+
|
|
210
|
+
/** Timestamps are written by us or by the dispatcher's `date -u`; anything else is not a timestamp. */
|
|
211
|
+
const ISO_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?$/;
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The neutral-state runs directory for an install root (`<root>/.doflow`).
|
|
215
|
+
* @param {string} configDir an install root, e.g. `<project>/.doflow` or `~/.doflow`
|
|
216
|
+
* @returns {string}
|
|
217
|
+
*/
|
|
218
|
+
function runsDir(configDir: string): string {
|
|
219
|
+
return path.join(configDir, 'state', RUNS_DIRNAME);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Mirrors the dispatcher's own `resolve_config_dir` (design §4.1) so a view reads the same ledger
|
|
224
|
+
* the dispatcher wrote. Resolving from `process.cwd()` alone would miss every run recorded by a
|
|
225
|
+
* dispatcher invoked from a subdirectory of the project, and report an empty ledger for a project
|
|
226
|
+
* that has been traced all day.
|
|
227
|
+
* @param {Object} [options]
|
|
228
|
+
* @param {string} [options.start] directory to walk up from (default: cwd)
|
|
229
|
+
* @param {boolean} [options.global=false] force the home-scoped install
|
|
230
|
+
* @param {Object} [options.env=process.env]
|
|
231
|
+
* @param {string} [options.homeDir]
|
|
232
|
+
* @param {Object} [options.fsImpl=fs]
|
|
233
|
+
* @param {string} [options.containing] accept a project `.doflow` only when it holds this
|
|
234
|
+
* subdirectory, so a project with state but no guidance tree falls through to the home install
|
|
235
|
+
* @returns {{dir: string, configDir: string, origin: string, searched: string[]}}
|
|
236
|
+
*/
|
|
237
|
+
function resolveRunsLocation({ start, global: forceGlobal = false, env = process.env, homeDir = os.homedir(), fsImpl = fs, containing = '' }: any = {}): any {
|
|
238
|
+
const searched: string[] = [];
|
|
239
|
+
const home = path.resolve(homeDir || '.');
|
|
240
|
+
if (forceGlobal) {
|
|
241
|
+
const configDir = path.join(home, '.doflow');
|
|
242
|
+
return { dir: runsDir(configDir), configDir, origin: 'global', searched: [configDir] };
|
|
243
|
+
}
|
|
244
|
+
if (env.DOFLOW_CONFIG_DIR) {
|
|
245
|
+
const configDir = path.resolve(env.DOFLOW_CONFIG_DIR);
|
|
246
|
+
return { dir: runsDir(configDir), configDir, origin: 'DOFLOW_CONFIG_DIR', searched: [configDir] };
|
|
247
|
+
}
|
|
248
|
+
let dir = path.resolve(start || process.cwd());
|
|
249
|
+
for (;;) {
|
|
250
|
+
const candidate = path.join(dir, '.doflow');
|
|
251
|
+
searched.push(candidate);
|
|
252
|
+
let found = false;
|
|
253
|
+
try {
|
|
254
|
+
found = fsImpl.statSync(path.join(candidate, containing)).isDirectory();
|
|
255
|
+
} catch {
|
|
256
|
+
found = false; // Not readable is the same as not there for a lookup that must not throw.
|
|
257
|
+
}
|
|
258
|
+
if (found) return { dir: runsDir(candidate), configDir: candidate, origin: 'project', searched };
|
|
259
|
+
const parent = path.dirname(dir);
|
|
260
|
+
if (parent === dir) break;
|
|
261
|
+
dir = parent;
|
|
262
|
+
}
|
|
263
|
+
const configDir = path.join(home, '.doflow');
|
|
264
|
+
searched.push(configDir);
|
|
265
|
+
return { dir: runsDir(configDir), configDir, origin: 'global-fallback', searched };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Reduces an arbitrary object to a writable ledger record.
|
|
270
|
+
* @param {Object} input
|
|
271
|
+
* @param {Object} [options]
|
|
272
|
+
* @param {Date} [options.now]
|
|
273
|
+
* @returns {{record: Object|null, dropped: string[], reason?: string}} `dropped` names the rejected
|
|
274
|
+
* fields and never carries their values — a rejection report that echoed the value would leak
|
|
275
|
+
* exactly the content the rejection exists to keep out.
|
|
276
|
+
*/
|
|
277
|
+
function sanitizeRunEvent(input: any, { now = new Date() } = {}): any {
|
|
278
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
279
|
+
return { record: null, dropped: [], reason: 'event must be an object' };
|
|
280
|
+
}
|
|
281
|
+
const byKey = new Map(RUN_FIELDS.map((field) => [field.key, field]));
|
|
282
|
+
const byWire = new Map(RUN_FIELDS.map((field) => [field.wire, field]));
|
|
283
|
+
const record: any = {};
|
|
284
|
+
const dropped: string[] = [];
|
|
285
|
+
|
|
286
|
+
const timestamp = typeof input.timestamp === 'string' && ISO_TIMESTAMP.test(input.timestamp)
|
|
287
|
+
? input.timestamp
|
|
288
|
+
: now.toISOString();
|
|
289
|
+
if (input.timestamp !== undefined && timestamp !== input.timestamp) dropped.push('timestamp');
|
|
290
|
+
record.timestamp = timestamp;
|
|
291
|
+
|
|
292
|
+
for (const [name, value] of Object.entries(input)) {
|
|
293
|
+
if (name === 'timestamp') continue;
|
|
294
|
+
const field = byKey.get(name) || byWire.get(name);
|
|
295
|
+
if (!field) { dropped.push(name); continue; }
|
|
296
|
+
if (value === undefined || value === null) continue;
|
|
297
|
+
if (field.type === 'token') {
|
|
298
|
+
if (typeof value !== 'string' || !SAFE_TOKEN.test(value)) { dropped.push(name); continue; }
|
|
299
|
+
record[field.wire] = value;
|
|
300
|
+
} else {
|
|
301
|
+
const numeric = typeof value === 'number' ? value : Number(value);
|
|
302
|
+
if (!Number.isFinite(numeric)) { dropped.push(name); continue; }
|
|
303
|
+
const truncated = Math.trunc(numeric);
|
|
304
|
+
if (field.type === 'uint' && truncated < 0) { dropped.push(name); continue; }
|
|
305
|
+
record[field.wire] = truncated;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// A record with no verb cannot be attributed to anything, and every view groups by verb.
|
|
310
|
+
if (!record.verb) return { record: null, dropped, reason: 'event has no usable verb' };
|
|
311
|
+
return { record, dropped };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Reads one on-disk line into the canonical camelCase shape the views work in. Accepts both the
|
|
316
|
+
* dispatcher's snake_case keys and the camelCase a JavaScript caller would write.
|
|
317
|
+
* @param {*} raw parsed JSON value
|
|
318
|
+
* @returns {Object|null} null when the line is not a usable record
|
|
319
|
+
*/
|
|
320
|
+
function normalizeRunRecord(raw: any): any | null {
|
|
321
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
322
|
+
const out: any = { timestamp: typeof raw.timestamp === 'string' ? raw.timestamp : null };
|
|
323
|
+
for (const field of RUN_FIELDS) {
|
|
324
|
+
const value = raw[field.wire] !== undefined ? raw[field.wire] : raw[field.key];
|
|
325
|
+
if (value === undefined || value === null) continue;
|
|
326
|
+
if (field.type === 'token') {
|
|
327
|
+
if (typeof value === 'string' && value !== '') out[field.key] = value;
|
|
328
|
+
} else {
|
|
329
|
+
const numeric = typeof value === 'number' ? value : Number(value);
|
|
330
|
+
if (Number.isFinite(numeric)) out[field.key] = Math.trunc(numeric);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (!out.verb) return null;
|
|
334
|
+
const parsed = out.timestamp ? Date.parse(out.timestamp) : NaN;
|
|
335
|
+
out.at = Number.isNaN(parsed) ? null : parsed;
|
|
336
|
+
return out;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
class RunLedger {
|
|
340
|
+
fsImpl: any;
|
|
341
|
+
clock: () => Date;
|
|
342
|
+
dir: string;
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* @param {Object} [options]
|
|
346
|
+
* @param {string} [options.dir] the runs directory; resolved from `configDir`/cwd when omitted
|
|
347
|
+
* @param {string} [options.configDir] an install root (`<project>/.doflow`)
|
|
348
|
+
* @param {Object} [options.fsImpl=fs]
|
|
349
|
+
* @param {Function} [options.clock] returns the current Date; injection seam for tests
|
|
350
|
+
*/
|
|
351
|
+
constructor({ dir, configDir, fsImpl = fs, clock = () => new Date() }: any = {}) {
|
|
352
|
+
this.fsImpl = fsImpl;
|
|
353
|
+
this.clock = clock;
|
|
354
|
+
if (dir) this.dir = path.resolve(dir);
|
|
355
|
+
else if (configDir) this.dir = runsDir(path.resolve(configDir));
|
|
356
|
+
else this.dir = resolveRunsLocation({ fsImpl }).dir;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Appends one metadata record. Always succeeds: an unwritable state directory degrades
|
|
361
|
+
* observability, never the run being observed.
|
|
362
|
+
* @param {Object} event
|
|
363
|
+
* @returns {{written: boolean, file?: string, record?: Object, dropped: string[], reason?: string}}
|
|
364
|
+
*/
|
|
365
|
+
append(event: any): any {
|
|
366
|
+
const { record, dropped, reason } = sanitizeRunEvent(event, { now: this.clock() });
|
|
367
|
+
if (!record) return { written: false, dropped, reason };
|
|
368
|
+
try {
|
|
369
|
+
this.fsImpl.mkdirSync(this.dir, { recursive: true });
|
|
370
|
+
const file = path.join(this.dir, `${record.timestamp.slice(0, 10)}.jsonl`);
|
|
371
|
+
this.fsImpl.appendFileSync(file, `${JSON.stringify(record)}\n`, 'utf8');
|
|
372
|
+
return { written: true, file, record, dropped };
|
|
373
|
+
} catch (error: any) {
|
|
374
|
+
return { written: false, record, dropped, reason: error.message };
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Reads the ledger. A missing directory, an unreadable partition and a corrupt line are reported
|
|
380
|
+
* as counts on the result rather than raised — but they *are* reported, so a view can say "3
|
|
381
|
+
* lines were unreadable" instead of quietly analysing a truncated history.
|
|
382
|
+
* @param {Object} [options]
|
|
383
|
+
* @param {number} [options.days] keep only partitions dated within the last N calendar days
|
|
384
|
+
* @returns {{dir: string, exists: boolean, files: string[], records: Object[], malformedLines: number, unreadableFiles: string[], days: number|null}}
|
|
385
|
+
*/
|
|
386
|
+
read({ days = null }: any = {}): any {
|
|
387
|
+
const result: any = {
|
|
388
|
+
dir: this.dir,
|
|
389
|
+
exists: false,
|
|
390
|
+
files: [],
|
|
391
|
+
records: [],
|
|
392
|
+
malformedLines: 0,
|
|
393
|
+
unreadableFiles: [],
|
|
394
|
+
days: days && days > 0 ? days : null,
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
let entries: string[];
|
|
398
|
+
try {
|
|
399
|
+
entries = this.fsImpl.readdirSync(this.dir);
|
|
400
|
+
} catch {
|
|
401
|
+
return result; // No ledger yet is the normal state of a fresh install, not a failure.
|
|
402
|
+
}
|
|
403
|
+
result.exists = true;
|
|
404
|
+
|
|
405
|
+
let names: string[] = entries.filter((name) => LEDGER_FILE_RE.test(name)).sort();
|
|
406
|
+
if (result.days) {
|
|
407
|
+
// Calendar days, not "the last N files": a quiet week must not silently widen the window.
|
|
408
|
+
const cutoff = new Date(this.clock().getTime() - (result.days - 1) * 24 * 60 * 60 * 1000)
|
|
409
|
+
.toISOString().slice(0, 10);
|
|
410
|
+
names = names.filter((name) => name.slice(0, 10) >= cutoff);
|
|
411
|
+
}
|
|
412
|
+
result.files = names;
|
|
413
|
+
|
|
414
|
+
for (const name of names) {
|
|
415
|
+
let text: string;
|
|
416
|
+
try {
|
|
417
|
+
text = this.fsImpl.readFileSync(path.join(this.dir, name), 'utf8');
|
|
418
|
+
} catch {
|
|
419
|
+
result.unreadableFiles.push(name);
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
for (const line of text.split('\n')) {
|
|
423
|
+
if (line.trim() === '') continue;
|
|
424
|
+
let parsed: any;
|
|
425
|
+
try {
|
|
426
|
+
parsed = JSON.parse(line);
|
|
427
|
+
} catch {
|
|
428
|
+
// A partially written line is expected: the dispatcher appends from concurrent shells.
|
|
429
|
+
result.malformedLines += 1;
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
const record = normalizeRunRecord(parsed);
|
|
433
|
+
if (record) result.records.push(record);
|
|
434
|
+
else result.malformedLines += 1;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Undated records sort last rather than being dropped: they still count toward usage.
|
|
439
|
+
result.records.sort((a: any, b: any) => (a.at ?? Number.MAX_SAFE_INTEGER) - (b.at ?? Number.MAX_SAFE_INTEGER));
|
|
440
|
+
return result;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// buildTrace, buildStats and buildDiscover (the three views, plus the helpers they share) now
|
|
445
|
+
// live in ./trace-views.js; shortTime/formatMs/finish/integrityNote/printEmptyLedgerNotice (CLI
|
|
446
|
+
// rendering) now live in ./trace-render.js. Both are required at the top of this file and used,
|
|
447
|
+
// unchanged, by the handle*Command functions below.
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Handles `doflow trace` — the trajectory of the current or most recent workflow.
|
|
451
|
+
* @param {Object} options
|
|
452
|
+
* @param {boolean} [options.json=false]
|
|
453
|
+
* @param {number} [options.days]
|
|
454
|
+
* @param {boolean} [options.global=false]
|
|
455
|
+
* @param {string} [options.projectRoot]
|
|
456
|
+
* @returns {number} process exit code
|
|
457
|
+
*/
|
|
458
|
+
function handleTraceCommand({ json = false, days = null, global: useGlobal = false, projectRoot }: any = {}): number {
|
|
459
|
+
const location = resolveRunsLocation({ start: projectRoot, global: useGlobal });
|
|
460
|
+
const view = buildTrace(new RunLedger({ dir: location.dir }).read({ days }));
|
|
461
|
+
view.origin = location.origin;
|
|
462
|
+
|
|
463
|
+
if (json) {
|
|
464
|
+
console.log(JSON.stringify(view, null, 2));
|
|
465
|
+
return finish(0);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
console.log('\nDoFlow Run Trace — most recent workflow');
|
|
469
|
+
console.log('═'.repeat(78));
|
|
470
|
+
if (!view.trajectory) {
|
|
471
|
+
printEmptyLedgerNotice(view.ledger, view.reason);
|
|
472
|
+
console.log('═'.repeat(78) + '\n');
|
|
473
|
+
return finish(0);
|
|
474
|
+
}
|
|
475
|
+
const t = view.trajectory;
|
|
476
|
+
console.log(`Started: ${t.startedAt}`);
|
|
477
|
+
console.log(`Ended: ${t.endedAt}`);
|
|
478
|
+
console.log(`Steps: ${t.stepCount} (${t.failedSteps} failed) · dispatched ${formatMs(t.dispatchedMs)} of ${formatMs(t.wallDurationMs)} wall`);
|
|
479
|
+
console.log(`Boundary: ${view.grouping === 'session-field' ? 'recorded session id' : `inferred from a ${view.gapMinutes}-minute idle gap`}${view.priorSessions ? ` · ${view.priorSessions} earlier session(s) in this window` : ''}`);
|
|
480
|
+
console.log('─'.repeat(78));
|
|
481
|
+
console.log('#'.padEnd(5) + 'TIME'.padEnd(16) + 'VERB'.padEnd(18) + 'OUTCOME'.padEnd(10) + 'TOOK'.padEnd(9) + 'CAPABILITY');
|
|
482
|
+
for (const step of t.steps) {
|
|
483
|
+
console.log(
|
|
484
|
+
String(step.n).padEnd(5) +
|
|
485
|
+
shortTime(step.timestamp).padEnd(16) +
|
|
486
|
+
step.verb.padEnd(18) +
|
|
487
|
+
String(step.outcome ?? 'unknown').padEnd(10) +
|
|
488
|
+
formatMs(step.durationMs).padEnd(9) +
|
|
489
|
+
(step.capability ?? (step.provider ?? '-'))
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
const note = integrityNote(view.ledger);
|
|
493
|
+
if (note) {
|
|
494
|
+
console.log('─'.repeat(78));
|
|
495
|
+
console.log(note);
|
|
496
|
+
}
|
|
497
|
+
console.log('═'.repeat(78) + '\n');
|
|
498
|
+
return finish(0);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Handles `doflow stats` — aggregate local usage.
|
|
503
|
+
* @param {Object} options
|
|
504
|
+
* @returns {number} process exit code
|
|
505
|
+
*/
|
|
506
|
+
function handleStatsCommand({ json = false, days = null, global: useGlobal = false, projectRoot }: any = {}): number {
|
|
507
|
+
const location = resolveRunsLocation({ start: projectRoot, global: useGlobal });
|
|
508
|
+
const view = buildStats(new RunLedger({ dir: location.dir }).read({ days }));
|
|
509
|
+
view.origin = location.origin;
|
|
510
|
+
|
|
511
|
+
if (json) {
|
|
512
|
+
console.log(JSON.stringify(view, null, 2));
|
|
513
|
+
return finish(0);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
console.log('\nDoFlow Run Statistics');
|
|
517
|
+
console.log('═'.repeat(78));
|
|
518
|
+
if (!view.recordCount) {
|
|
519
|
+
printEmptyLedgerNotice(view.ledger, view.ledger.exists ? 'the ledger holds no readable run records' : 'no ledger has been written yet at this location');
|
|
520
|
+
console.log('═'.repeat(78) + '\n');
|
|
521
|
+
return finish(0);
|
|
522
|
+
}
|
|
523
|
+
console.log(`Records: ${view.recordCount} across ${view.span.activeDays} day partition(s)${view.ledger.windowDays ? ` (last ${view.ledger.windowDays} days)` : ''}`);
|
|
524
|
+
console.log(`Window: ${view.span.first} → ${view.span.last}`);
|
|
525
|
+
console.log(`Outcomes: ${view.outcomes.succeeded} ok · ${view.outcomes.failed} failed · ${view.outcomes.unrecorded} exit code not recorded`);
|
|
526
|
+
console.log(`Duration: p50 ${formatMs(view.durations.p50Ms)} · p95 ${formatMs(view.durations.p95Ms)} · max ${formatMs(view.durations.maxMs)} (${view.durations.measured} measured)`);
|
|
527
|
+
console.log('─'.repeat(78));
|
|
528
|
+
console.log('VERB'.padEnd(20) + 'RUNS'.padEnd(8) + 'FAILED'.padEnd(9) + 'P50'.padEnd(9) + 'P95');
|
|
529
|
+
for (const row of view.byVerb) {
|
|
530
|
+
console.log(row.key.padEnd(20) + String(row.count).padEnd(8) + String(row.failures).padEnd(9) + formatMs(row.p50Ms).padEnd(9) + formatMs(row.p95Ms));
|
|
531
|
+
}
|
|
532
|
+
if (view.byCapability.length || view.byProvider.length) {
|
|
533
|
+
console.log('─'.repeat(78));
|
|
534
|
+
console.log('CAPABILITY'.padEnd(30) + 'RUNS'.padEnd(8) + 'FAILED');
|
|
535
|
+
for (const row of view.byCapability) {
|
|
536
|
+
console.log(row.key.padEnd(30) + String(row.count).padEnd(8) + String(row.failures));
|
|
537
|
+
}
|
|
538
|
+
console.log('PROVIDER'.padEnd(30) + 'RUNS'.padEnd(8) + 'FAILED');
|
|
539
|
+
for (const row of view.byProvider) {
|
|
540
|
+
console.log(row.key.padEnd(30) + String(row.count).padEnd(8) + String(row.failures));
|
|
541
|
+
}
|
|
542
|
+
} else {
|
|
543
|
+
console.log('─'.repeat(78));
|
|
544
|
+
console.log('No record in this window names a capability or provider — these are written by');
|
|
545
|
+
console.log('runtime calls that resolve one, not by the shell dispatcher.');
|
|
546
|
+
}
|
|
547
|
+
const note = integrityNote(view.ledger);
|
|
548
|
+
if (note) console.log(note);
|
|
549
|
+
console.log('═'.repeat(78) + '\n');
|
|
550
|
+
return finish(0);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Handles `doflow discover` — missed opportunities.
|
|
555
|
+
* @param {Object} options
|
|
556
|
+
* @param {Object} [options.providerHealth] injected for tests; probed from `health.ts` otherwise
|
|
557
|
+
* @returns {number} process exit code — 1 when there is a finding to act on
|
|
558
|
+
*/
|
|
559
|
+
function handleDiscoverCommand({ json = false, days = null, global: useGlobal = false, projectRoot, providerHealth }: any = {}): number {
|
|
560
|
+
const location = resolveRunsLocation({ start: projectRoot, global: useGlobal });
|
|
561
|
+
const read = new RunLedger({ dir: location.dir }).read({ days });
|
|
562
|
+
|
|
563
|
+
// Required lazily and defensively: a discover run must still answer if provider probing is
|
|
564
|
+
// unavailable for any reason — it simply reports the analyses it could not settle.
|
|
565
|
+
let health = providerHealth;
|
|
566
|
+
if (health === undefined) {
|
|
567
|
+
try {
|
|
568
|
+
health = require('../health.ts').probeProviders({ ids: Object.values(PROVIDER) });
|
|
569
|
+
} catch {
|
|
570
|
+
health = null;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const view = buildDiscover(read, { providerHealth: health });
|
|
575
|
+
view.origin = location.origin;
|
|
576
|
+
|
|
577
|
+
if (json) {
|
|
578
|
+
console.log(JSON.stringify(view, null, 2));
|
|
579
|
+
return finish(view.findings.length ? 1 : 0);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
console.log('\nDoFlow Missed Opportunities');
|
|
583
|
+
console.log('═'.repeat(78));
|
|
584
|
+
console.log(`Ledger: ${view.ledger.dir}`);
|
|
585
|
+
console.log(`Analysed: ${view.ledger.recordCount} record(s), ${view.sessionsAnalyzed} session(s)${view.ledger.windowDays ? ` (last ${view.ledger.windowDays} days)` : ''}`);
|
|
586
|
+
console.log(`Verdict: ${view.verdict}`);
|
|
587
|
+
const note = integrityNote(view.ledger);
|
|
588
|
+
if (note) console.log(note.replace('Note:', 'Integrity:'));
|
|
589
|
+
console.log('─'.repeat(78));
|
|
590
|
+
for (const analysis of view.analyses) {
|
|
591
|
+
console.log(`${DISCOVER_MARK[analysis.status].padEnd(12)}${analysis.title}`);
|
|
592
|
+
console.log(` ${analysis.detail}`);
|
|
593
|
+
if (analysis.observed && analysis.observed.length) {
|
|
594
|
+
for (const item of analysis.observed.slice(0, 5)) {
|
|
595
|
+
console.log(` · ${JSON.stringify(item)}`);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if (analysis.recommendation) console.log(` → ${analysis.recommendation}`);
|
|
599
|
+
console.log('');
|
|
600
|
+
}
|
|
601
|
+
if (view.verdict === 'insufficient-data') {
|
|
602
|
+
console.log('No analysis could be settled from this ledger. That is a statement about the data,');
|
|
603
|
+
console.log('not a clean bill of health.');
|
|
604
|
+
}
|
|
605
|
+
console.log('═'.repeat(78) + '\n');
|
|
606
|
+
return finish(view.findings.length ? 1 : 0);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
module.exports = {
|
|
610
|
+
TaskRunTelemetry,
|
|
611
|
+
DEFAULT_TOKEN_RATES,
|
|
612
|
+
RunLedger,
|
|
613
|
+
runsDir,
|
|
614
|
+
resolveRunsLocation,
|
|
615
|
+
sanitizeRunEvent,
|
|
616
|
+
normalizeRunRecord,
|
|
617
|
+
groupSessions,
|
|
618
|
+
buildTrace,
|
|
619
|
+
buildStats,
|
|
620
|
+
buildDiscover,
|
|
621
|
+
handleTraceCommand,
|
|
622
|
+
handleStatsCommand,
|
|
623
|
+
handleDiscoverCommand,
|
|
624
|
+
SESSION_GAP_MS,
|
|
625
|
+
RUN_FIELDS,
|
|
626
|
+
};
|