@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,550 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
4
|
+
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const fs = require('node:fs');
|
|
7
|
+
const { TextDecoder } = require('node:util');
|
|
8
|
+
const { WorkflowEngine, CALLER_ROLES } = require('./workflow-engine.ts');
|
|
9
|
+
const { finishRuntime } = require('./cli-result.ts');
|
|
10
|
+
const { REPO_ROOT } = require('../helper/repo-root.ts');
|
|
11
|
+
const { loadAdviceRegistry } = require('./advice/registry.ts');
|
|
12
|
+
const { createAdviceClient } = require('./advice/client.ts');
|
|
13
|
+
const { materializeKind } = require('./advice/kinds.ts');
|
|
14
|
+
const { appendOutcome, findPick } = require('./advice/record.ts');
|
|
15
|
+
const { strengthWords } = require('./advice/strength.ts');
|
|
16
|
+
const { loadStorageRegistry } = require('./storage/registry.ts');
|
|
17
|
+
const { resolveRoots } = require('./storage/roots.ts');
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validates a task class proposed by the model against the classes the registry declares.
|
|
22
|
+
*
|
|
23
|
+
* The split is deliberate (design C11): the model proposes, the runtime validates. The classifier
|
|
24
|
+
* contains no heuristics of its own — it never reads the request text and never guesses. Its only
|
|
25
|
+
* job is to turn a proposal into either a resolved workflow or an explicit rejection carrying the
|
|
26
|
+
* valid set, and to record which of those happened so the decision is traceable.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const CLASSIFICATION_OUTCOMES = Object.freeze({
|
|
30
|
+
ACCEPTED: 'ACCEPTED',
|
|
31
|
+
REJECTED: 'REJECTED',
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const REJECTION_REASONS = Object.freeze({
|
|
35
|
+
/** No class was proposed at all, or one was proposed under the wrong key. */
|
|
36
|
+
MISSING_CLASS: 'missing-class',
|
|
37
|
+
/** A class was proposed but is not declared in workflows.json. */
|
|
38
|
+
UNKNOWN_CLASS: 'unknown-class',
|
|
39
|
+
/** The class exists, but no stage of it names the calling skill. */
|
|
40
|
+
CALLER_NOT_A_STAGE: 'caller-not-a-stage',
|
|
41
|
+
/** The calling skill is not in the registry's caller list, so fit is unanswerable. */
|
|
42
|
+
UNKNOWN_CALLER: 'unknown-calling-skill',
|
|
43
|
+
/** The calling skill is declared to run outside class routing entirely. */
|
|
44
|
+
CALLER_OUTSIDE_WORKFLOWS: 'caller-outside-workflows',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* What the fit check concluded, reported on every decision.
|
|
49
|
+
*
|
|
50
|
+
* `NOT_EVALUATED` is the load-bearing one. A gate that cannot judge and says nothing is
|
|
51
|
+
* indistinguishable from a gate that judged and passed — the failure shape this whole check exists
|
|
52
|
+
* to remove — so an unjudgeable fit is named as unjudged in both the field and the message rather
|
|
53
|
+
* than folded into the ACCEPTED that membership earned.
|
|
54
|
+
*/
|
|
55
|
+
const FIT_STATES = Object.freeze({
|
|
56
|
+
HOSTED: 'HOSTED',
|
|
57
|
+
NOT_HOSTED: 'NOT_HOSTED',
|
|
58
|
+
NOT_APPLICABLE: 'NOT_APPLICABLE',
|
|
59
|
+
NOT_EVALUATED: 'NOT_EVALUATED',
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/** Reasons for the fit states that are not rejections. A rejection's `fit.reason` is the same
|
|
63
|
+
* string as `decision.reason`, so a reader never reconciles two names for one fact. */
|
|
64
|
+
const FIT_REASONS = Object.freeze({
|
|
65
|
+
CLASS_NOT_RESOLVED: 'class-not-resolved',
|
|
66
|
+
NO_CALLER: 'no-calling-skill-supplied',
|
|
67
|
+
CALLER_KEY_NEAR_MISS: 'caller-key-near-miss',
|
|
68
|
+
CALLER_IS_ROUTER: 'caller-is-a-router',
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
/** Keys a caller plausibly reaches for instead of `taskClass`. Naming them back in the rejection
|
|
72
|
+
* message is the whole fix for the `taskProfile.taskId` / `id` defect family: a near-miss key is
|
|
73
|
+
* reported as a near-miss, never read as if it were the right one and never ignored into a
|
|
74
|
+
* default. */
|
|
75
|
+
const NEAR_MISS_KEYS = ['class', 'taskType', 'type', 'proposedClass', 'workflow', 'workflowClass'];
|
|
76
|
+
|
|
77
|
+
/** The same treatment for the caller's identity: a proposal that names its caller under one of
|
|
78
|
+
* these has said who is asking, and reading it as "nobody asked" would turn a near miss into a
|
|
79
|
+
* silent skip of the fit check. */
|
|
80
|
+
const CALLER_NEAR_MISS_KEYS = ['caller', 'callerSkill', 'skill', 'skillId', 'invokedBy', 'calledBy'];
|
|
81
|
+
|
|
82
|
+
function isPlainObject(value: any) {
|
|
83
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Reads a proposal into its parts without normalizing the class itself.
|
|
88
|
+
*
|
|
89
|
+
* Case and whitespace are preserved on purpose: `'Feature '` is a rejection with a suggestion, not
|
|
90
|
+
* a silent match. Trimming here would be a small coercion that hides a real formatting bug in the
|
|
91
|
+
* caller's output.
|
|
92
|
+
*/
|
|
93
|
+
function readProposal(proposal: any) {
|
|
94
|
+
if (proposal === null || proposal === undefined) {
|
|
95
|
+
return {
|
|
96
|
+
taskClass: null, rationale: null, proposedBy: null, callingSkill: null,
|
|
97
|
+
nearMissKeys: [], nearMissCallerKeys: [],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (typeof proposal === 'string') {
|
|
101
|
+
return {
|
|
102
|
+
taskClass: proposal, rationale: null, proposedBy: null, callingSkill: null,
|
|
103
|
+
nearMissKeys: [], nearMissCallerKeys: [],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (!isPlainObject(proposal)) {
|
|
107
|
+
throw new TypeError(
|
|
108
|
+
`A task-class proposal must be a string or an object, received ${typeof proposal}`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
const hasKey = Object.prototype.hasOwnProperty.call(proposal, 'taskClass');
|
|
112
|
+
const callingSkill = typeof proposal.callingSkill === 'string' && proposal.callingSkill.trim() !== ''
|
|
113
|
+
? proposal.callingSkill
|
|
114
|
+
: null;
|
|
115
|
+
return {
|
|
116
|
+
taskClass: hasKey ? proposal.taskClass : null,
|
|
117
|
+
callingSkill,
|
|
118
|
+
rationale: typeof proposal.rationale === 'string' ? proposal.rationale : null,
|
|
119
|
+
// Not defaulted to 'model': attributing a proposal to an actor we did not observe would put a
|
|
120
|
+
// fabricated fact into the trace.
|
|
121
|
+
proposedBy: typeof proposal.proposedBy === 'string' ? proposal.proposedBy : null,
|
|
122
|
+
signals: Array.isArray(proposal.signals) ? proposal.signals.slice() : [],
|
|
123
|
+
nearMissKeys: hasKey
|
|
124
|
+
? []
|
|
125
|
+
: NEAR_MISS_KEYS.filter((key) => Object.prototype.hasOwnProperty.call(proposal, key)),
|
|
126
|
+
nearMissCallerKeys: callingSkill
|
|
127
|
+
? []
|
|
128
|
+
: CALLER_NEAR_MISS_KEYS.filter((key) => Object.prototype.hasOwnProperty.call(proposal, key)),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Candidates a rejected id plausibly meant — a class id against the class list, or a caller id
|
|
134
|
+
* against the caller list. Suggestion only: nothing acts on it, so a wrong suggestion costs the
|
|
135
|
+
* user a second look rather than the wrong workflow.
|
|
136
|
+
*
|
|
137
|
+
* Matching is substring in both directions, so a short id matches generously — `do` would match
|
|
138
|
+
* every `do-*` skill. That is acceptable for a hint nobody acts on; it is why the messages present
|
|
139
|
+
* the result as "Did you mean" rather than as a shortlist.
|
|
140
|
+
*/
|
|
141
|
+
function suggestFrom(proposed: any, candidates: any) {
|
|
142
|
+
if (typeof proposed !== 'string' || proposed.trim() === '') return [];
|
|
143
|
+
const needle = proposed.trim().toLowerCase();
|
|
144
|
+
return candidates.filter((candidate: any) => {
|
|
145
|
+
const other = candidate.toLowerCase();
|
|
146
|
+
return other === needle || other.includes(needle) || needle.includes(other);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Judges whether the proposed class has a stage the calling skill can occupy.
|
|
152
|
+
*
|
|
153
|
+
* Structural only: it asks whether some stage of the class names this skill, and consults the
|
|
154
|
+
* registry's declared caller roles for the two cases where stage membership is the wrong question.
|
|
155
|
+
* It never reads the request. When it cannot judge — no caller named, or a caller the registry does
|
|
156
|
+
* not declare — it says so rather than passing.
|
|
157
|
+
*
|
|
158
|
+
* @param {WorkflowEngine} engine
|
|
159
|
+
* @param {string} taskClass a class already known to exist
|
|
160
|
+
* @param {string|null} callingSkill
|
|
161
|
+
* @param {Array<string>} nearMissCallerKeys
|
|
162
|
+
* @returns {{state: string, reason: string|null, callingSkill: string|null, callerRole: string|null,
|
|
163
|
+
* hostedStageIds: Array<string>, hostingClasses: Array<{taskClass: string, stageIds: Array<string>}>,
|
|
164
|
+
* callerSuggestions: Array<string>}} frozen
|
|
165
|
+
*/
|
|
166
|
+
function judgeFit(engine: any, taskClass: string, callingSkill: any, nearMissCallerKeys: any) {
|
|
167
|
+
const unevaluated = (reason: any, extra: any = {}) => Object.freeze({
|
|
168
|
+
state: FIT_STATES.NOT_EVALUATED,
|
|
169
|
+
reason,
|
|
170
|
+
callingSkill,
|
|
171
|
+
callerRole: null,
|
|
172
|
+
hostedStageIds: [],
|
|
173
|
+
hostingClasses: [],
|
|
174
|
+
callerSuggestions: [],
|
|
175
|
+
...extra,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
if (callingSkill === null) {
|
|
179
|
+
return nearMissCallerKeys.length > 0
|
|
180
|
+
? unevaluated(FIT_REASONS.CALLER_KEY_NEAR_MISS)
|
|
181
|
+
: unevaluated(FIT_REASONS.NO_CALLER);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// A caller id the registry does not declare is a rejection, never a NOT_EVALUATED pass: reading
|
|
185
|
+
// `do-diagnos` as "no caller supplied" would let a one-character typo switch the gate off, which
|
|
186
|
+
// is the silently-passing shape this check exists to remove.
|
|
187
|
+
if (!engine.hasCaller(callingSkill)) {
|
|
188
|
+
return unevaluated(REJECTION_REASONS.UNKNOWN_CALLER, {
|
|
189
|
+
callerSuggestions: suggestFrom(callingSkill, engine.listCallers()),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const callerRole = engine.callerRole(callingSkill);
|
|
194
|
+
// Derived from the stage lists rather than declared, so it cannot drift from them. On HOSTED it
|
|
195
|
+
// includes the accepted class: it is the caller's complete map, not a list of alternatives.
|
|
196
|
+
const hostingClasses = engine.classesHosting(callingSkill);
|
|
197
|
+
const decided = (state: any, reason: any, hostedStageIds: any = []) => Object.freeze({
|
|
198
|
+
state, reason, callingSkill, callerRole, hostedStageIds, hostingClasses, callerSuggestions: [],
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
if (callerRole === CALLER_ROLES.ROUTER) {
|
|
202
|
+
return decided(FIT_STATES.NOT_APPLICABLE, FIT_REASONS.CALLER_IS_ROUTER);
|
|
203
|
+
}
|
|
204
|
+
if (callerRole === CALLER_ROLES.STANDALONE) {
|
|
205
|
+
return decided(FIT_STATES.NOT_HOSTED, REJECTION_REASONS.CALLER_OUTSIDE_WORKFLOWS);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const hostedStageIds = engine.stagesHosting(taskClass, callingSkill);
|
|
209
|
+
return hostedStageIds.length > 0
|
|
210
|
+
? decided(FIT_STATES.HOSTED, null, hostedStageIds)
|
|
211
|
+
: decided(FIT_STATES.NOT_HOSTED, REJECTION_REASONS.CALLER_NOT_A_STAGE);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** `bug (root-cause), refactor (architecture-mapping)` — the caller's hosting map, as prose. */
|
|
215
|
+
function renderHostingClasses(hostingClasses: any) {
|
|
216
|
+
return hostingClasses
|
|
217
|
+
.map(({ taskClass, stageIds }: any) => `${taskClass} (${stageIds.join(', ')})`)
|
|
218
|
+
.join(', ');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* The sentence appended to an ACCEPTED decision's message. Always present, including — especially —
|
|
223
|
+
* when fit was not evaluated: an operator must be able to read the blind spot off the message
|
|
224
|
+
* without inspecting the JSON.
|
|
225
|
+
*/
|
|
226
|
+
function describeAcceptedFit(fit: any, nearMissCallerKeys: any) {
|
|
227
|
+
if (fit.state === FIT_STATES.HOSTED) {
|
|
228
|
+
return ` It hosts '${fit.callingSkill}' at stage(s): ${fit.hostedStageIds.join(', ')}.`;
|
|
229
|
+
}
|
|
230
|
+
if (fit.state === FIT_STATES.NOT_APPLICABLE) {
|
|
231
|
+
return ` Fit is not applicable: '${fit.callingSkill}' is declared a router, so it occupies no `
|
|
232
|
+
+ 'stage. The skill this work is handed to must classify under its own identity.';
|
|
233
|
+
}
|
|
234
|
+
if (fit.reason === FIT_REASONS.CALLER_KEY_NEAR_MISS) {
|
|
235
|
+
return ' Fit was NOT evaluated: the proposal carries '
|
|
236
|
+
+ `${nearMissCallerKeys.map((key: any) => `'${key}'`).join(', ')}; the calling skill must be given `
|
|
237
|
+
+ 'as `callingSkill`. Membership is all this decision establishes.';
|
|
238
|
+
}
|
|
239
|
+
return ' Fit was NOT evaluated: no calling skill was named, so this decision establishes that the '
|
|
240
|
+
+ 'class exists and nothing about whether its workflow can host the caller. Pass '
|
|
241
|
+
+ '--calling-skill <skill-id> to have fit checked.';
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* The message for a rejection the fit check produced. Each one names the remedy: a rejection nobody
|
|
246
|
+
* can act on is a stop rather than a fix.
|
|
247
|
+
*/
|
|
248
|
+
function describeFitRejection(fit: any, taskClass: any, workflow: any, engine: any) {
|
|
249
|
+
if (fit.reason === REJECTION_REASONS.UNKNOWN_CALLER) {
|
|
250
|
+
const hint = fit.callerSuggestions.length > 0
|
|
251
|
+
? ` Did you mean: ${fit.callerSuggestions.join(', ')}?`
|
|
252
|
+
: '';
|
|
253
|
+
return `Calling skill '${fit.callingSkill}' is not declared in the workflow registry's caller `
|
|
254
|
+
+ 'list, so fit could not be judged and the proposal is refused rather than accepted on a '
|
|
255
|
+
+ `membership check alone. Declared callers: ${engine.listCallers().join(', ')}.${hint}`;
|
|
256
|
+
}
|
|
257
|
+
if (fit.reason === REJECTION_REASONS.CALLER_OUTSIDE_WORKFLOWS) {
|
|
258
|
+
return `'${fit.callingSkill}' runs outside class routing — no workflow declares a stage for it `
|
|
259
|
+
+ `— so no task class can host it. ${engine.callerReason(fit.callingSkill)} Do not propose a `
|
|
260
|
+
+ 'class for this work.';
|
|
261
|
+
}
|
|
262
|
+
const head = `Task class '${taskClass}' is declared, but its workflow has no stage for `
|
|
263
|
+
+ `'${fit.callingSkill}'`;
|
|
264
|
+
if (fit.hostingClasses.length === 0) {
|
|
265
|
+
return `${head}, and no declared class names it at any stage. That is a registry defect, not a `
|
|
266
|
+
+ 'proposal you can fix: report it rather than choosing another class.';
|
|
267
|
+
}
|
|
268
|
+
return `${head}: ${taskClass} runs ${workflow.stageIds.join(' → ')} `
|
|
269
|
+
+ `(skills: ${workflow.stageSkills.join(', ')}). Classes with a stage for `
|
|
270
|
+
+ `'${fit.callingSkill}': ${renderHostingClasses(fit.hostingClasses)}. Propose one of those, or `
|
|
271
|
+
+ `hand this work to the skill '${taskClass}' names for the stage you meant.`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
class TaskClassifier {
|
|
275
|
+
declare engine: any;
|
|
276
|
+
declare now: any;
|
|
277
|
+
/**
|
|
278
|
+
* @param {Object} [options]
|
|
279
|
+
* @param {WorkflowEngine} [options.engine] Pre-built engine; otherwise one is built from `options`.
|
|
280
|
+
* @param {Function} [options.now] Clock returning an ISO timestamp, for deterministic tests.
|
|
281
|
+
*/
|
|
282
|
+
constructor(options: any = {}) {
|
|
283
|
+
this.engine = options.engine || new WorkflowEngine(options);
|
|
284
|
+
this.now = options.now || (() => new Date().toISOString());
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** @returns {Array<string>} */
|
|
288
|
+
listClasses() {
|
|
289
|
+
return this.engine.listClasses();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** @returns {Array<Object>} class summaries, for presenting the choice to a model or a user. */
|
|
293
|
+
listClassOptions() {
|
|
294
|
+
return this.engine.listClassOptions();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Validates a proposed class and resolves its workflow.
|
|
299
|
+
*
|
|
300
|
+
* @param {string|Object|null} proposal Either a class id or
|
|
301
|
+
* `{ taskClass, rationale, proposedBy, signals, callingSkill }`.
|
|
302
|
+
* @param {Object} [context] Free-form trace context (task id, request summary) echoed on the decision.
|
|
303
|
+
* @returns {Object} decision — `outcome` is ACCEPTED with a `workflow`, or REJECTED with
|
|
304
|
+
* `reason`, `validClasses` and a null `workflow`. Never coerced to a default class. Every
|
|
305
|
+
* decision carries `fit`, which reports whether the class's workflow has a stage the calling
|
|
306
|
+
* skill can occupy — including `NOT_EVALUATED` when no caller was named, since an unchecked fit
|
|
307
|
+
* must never read as a checked one.
|
|
308
|
+
*/
|
|
309
|
+
classify(proposal: any, context: any = {}) {
|
|
310
|
+
const validClasses = this.listClasses();
|
|
311
|
+
const parsed = readProposal(proposal);
|
|
312
|
+
const base = {
|
|
313
|
+
proposedClass: parsed.taskClass,
|
|
314
|
+
proposedBy: parsed.proposedBy,
|
|
315
|
+
callingSkill: parsed.callingSkill,
|
|
316
|
+
rationale: parsed.rationale,
|
|
317
|
+
signals: parsed.signals || [],
|
|
318
|
+
validClasses,
|
|
319
|
+
// Not merged into `suggestions`: skills present that array to the user as "choose one of
|
|
320
|
+
// these classes", and a skill id in it would be offered as a task class.
|
|
321
|
+
callerSuggestions: [],
|
|
322
|
+
// Fit against a class that does not resolve is not a question that has an answer, so the two
|
|
323
|
+
// rejections below inherit this rather than claiming anything about the caller.
|
|
324
|
+
fit: Object.freeze({
|
|
325
|
+
state: FIT_STATES.NOT_EVALUATED,
|
|
326
|
+
reason: FIT_REASONS.CLASS_NOT_RESOLVED,
|
|
327
|
+
callingSkill: parsed.callingSkill,
|
|
328
|
+
callerRole: null,
|
|
329
|
+
hostedStageIds: [],
|
|
330
|
+
hostingClasses: [],
|
|
331
|
+
callerSuggestions: [],
|
|
332
|
+
}),
|
|
333
|
+
context: isPlainObject(context) ? { ...context } : {},
|
|
334
|
+
decidedAt: this.now(),
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
if (typeof parsed.taskClass !== 'string' || parsed.taskClass.trim() === '') {
|
|
338
|
+
const nearMiss = parsed.nearMissKeys.length > 0
|
|
339
|
+
? ` The proposal carries ${parsed.nearMissKeys.map((k) => `'${k}'`).join(', ')}; `
|
|
340
|
+
+ 'the class must be given as `taskClass`.'
|
|
341
|
+
: '';
|
|
342
|
+
return {
|
|
343
|
+
...base,
|
|
344
|
+
outcome: CLASSIFICATION_OUTCOMES.REJECTED,
|
|
345
|
+
taskClass: null,
|
|
346
|
+
workflow: null,
|
|
347
|
+
reason: REJECTION_REASONS.MISSING_CLASS,
|
|
348
|
+
suggestions: [],
|
|
349
|
+
message: `No task class was proposed, so no workflow can be selected.${nearMiss} `
|
|
350
|
+
+ `Propose one of: ${validClasses.join(', ')}.`,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (!this.engine.hasClass(parsed.taskClass)) {
|
|
355
|
+
const suggestions = suggestFrom(parsed.taskClass, validClasses);
|
|
356
|
+
const hint = suggestions.length > 0 ? ` Did you mean: ${suggestions.join(', ')}?` : '';
|
|
357
|
+
return {
|
|
358
|
+
...base,
|
|
359
|
+
outcome: CLASSIFICATION_OUTCOMES.REJECTED,
|
|
360
|
+
taskClass: null,
|
|
361
|
+
workflow: null,
|
|
362
|
+
reason: REJECTION_REASONS.UNKNOWN_CLASS,
|
|
363
|
+
suggestions,
|
|
364
|
+
message: `Proposed task class '${parsed.taskClass}' is not declared in the workflow `
|
|
365
|
+
+ `registry. Valid classes: ${validClasses.join(', ')}.${hint}`,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const fit = judgeFit(
|
|
370
|
+
this.engine, parsed.taskClass, parsed.callingSkill, parsed.nearMissCallerKeys,
|
|
371
|
+
);
|
|
372
|
+
const workflow = this.engine.resolveWorkflow(parsed.taskClass);
|
|
373
|
+
|
|
374
|
+
// A class that exists but has no stage for the caller is still a rejection: accepting it would
|
|
375
|
+
// put the caller's work under a workflow with nowhere to record it, which is the defect the fit
|
|
376
|
+
// check was added for. An unknown caller rejects too — see judgeFit.
|
|
377
|
+
if (fit.state === FIT_STATES.NOT_HOSTED || fit.reason === REJECTION_REASONS.UNKNOWN_CALLER) {
|
|
378
|
+
return {
|
|
379
|
+
...base,
|
|
380
|
+
outcome: CLASSIFICATION_OUTCOMES.REJECTED,
|
|
381
|
+
taskClass: null,
|
|
382
|
+
workflow: null,
|
|
383
|
+
reason: fit.reason,
|
|
384
|
+
suggestions: [],
|
|
385
|
+
callerSuggestions: fit.callerSuggestions,
|
|
386
|
+
fit,
|
|
387
|
+
message: describeFitRejection(fit, parsed.taskClass, workflow, this.engine),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
...base,
|
|
393
|
+
outcome: CLASSIFICATION_OUTCOMES.ACCEPTED,
|
|
394
|
+
taskClass: parsed.taskClass,
|
|
395
|
+
workflow,
|
|
396
|
+
reason: null,
|
|
397
|
+
suggestions: [],
|
|
398
|
+
fit,
|
|
399
|
+
message: `Task class '${parsed.taskClass}' selects the ${workflow.name} workflow: `
|
|
400
|
+
+ `${workflow.stageIds.join(' → ')}.`
|
|
401
|
+
+ describeAcceptedFit(fit, parsed.nearMissCallerKeys),
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Same validation, but a rejection throws instead of returning.
|
|
407
|
+
*
|
|
408
|
+
* For call sites that cannot meaningfully continue without a workflow — the alternative there is
|
|
409
|
+
* an ignored `outcome` field and a downstream null dereference far from the real cause.
|
|
410
|
+
* @param {string|Object} proposal
|
|
411
|
+
* @param {Object} [context]
|
|
412
|
+
* @returns {Object} the resolved workflow
|
|
413
|
+
*/
|
|
414
|
+
resolve(proposal: any, context: any = {}) {
|
|
415
|
+
const decision = this.classify(proposal, context);
|
|
416
|
+
if (decision.outcome !== CLASSIFICATION_OUTCOMES.ACCEPTED) {
|
|
417
|
+
throw new Error(decision.message);
|
|
418
|
+
}
|
|
419
|
+
return decision.workflow;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Handles `doflow classify` — validate a proposed task class against the workflow registry.
|
|
425
|
+
*
|
|
426
|
+
* Prints the classifier's decision and, when requested, an advisory second opinion that never
|
|
427
|
+
* changes the verdict. A REJECTED decision keeps `taskClass: null` and exits non-zero; it is never
|
|
428
|
+
* coerced to `feature`, which is the whole reason this verb exists rather than callers reading a
|
|
429
|
+
* class out of a string themselves.
|
|
430
|
+
*
|
|
431
|
+
* @param {Object} options
|
|
432
|
+
* @param {string|null} options.taskClass the proposed class
|
|
433
|
+
* @param {string} [options.rationale]
|
|
434
|
+
* @param {string} [options.proposedBy]
|
|
435
|
+
* @param {string} [options.callingSkill] the skill asking, against which fit is judged
|
|
436
|
+
* @param {string} [options.request] request text for a second opinion
|
|
437
|
+
* @param {string} [options.requestFile] file containing request text for a second opinion
|
|
438
|
+
* @param {boolean} [options.json=false]
|
|
439
|
+
* @returns {number|Promise<number>} exit code; advisory calls are asynchronous
|
|
440
|
+
*/
|
|
441
|
+
function handleClassifyCommand({ taskClass, rationale, proposedBy, callingSkill, request, requestFile, taskId, projectRoot, json = false }: any = {}) {
|
|
442
|
+
const classifier = new TaskClassifier({ repoRoot: REPO_ROOT });
|
|
443
|
+
const decision = classifier.classify({ taskClass, rationale, proposedBy, callingSkill });
|
|
444
|
+
const exitCode = decision.outcome === CLASSIFICATION_OUTCOMES.ACCEPTED
|
|
445
|
+
? 0 : decision.reason === REJECTION_REASONS.MISSING_CLASS ? 2 : 1;
|
|
446
|
+
const hasRequest = request !== undefined || requestFile !== undefined;
|
|
447
|
+
if (!hasRequest) {
|
|
448
|
+
if (json) console.log(JSON.stringify({ ...decision, secondOpinion: null }, null, 2));
|
|
449
|
+
else printClassifyDecision(decision, null);
|
|
450
|
+
return finishRuntime(exitCode);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return addSecondOpinion();
|
|
454
|
+
|
|
455
|
+
async function addSecondOpinion() {
|
|
456
|
+
let state = request;
|
|
457
|
+
let fileError = null;
|
|
458
|
+
if (requestFile !== undefined) {
|
|
459
|
+
try {
|
|
460
|
+
state = new TextDecoder('utf-8', { fatal: true }).decode(fs.readFileSync(requestFile));
|
|
461
|
+
if (!state.length) throw new Error('file is empty');
|
|
462
|
+
} catch (error: any) {
|
|
463
|
+
fileError = error;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
let opinion;
|
|
467
|
+
if (fileError) {
|
|
468
|
+
opinion = emptySecondOpinion('unavailable', 'config-invalid');
|
|
469
|
+
} else {
|
|
470
|
+
try {
|
|
471
|
+
const advice = loadAdviceRegistry({ repoRoot: REPO_ROOT });
|
|
472
|
+
const storage = loadStorageRegistry({ repoRoot: REPO_ROOT });
|
|
473
|
+
const roots = resolveRoots({ registry: storage, projectRoot: path.resolve(projectRoot || process.cwd()) });
|
|
474
|
+
const source = (name: any) => JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'core/registry', name), 'utf8'));
|
|
475
|
+
const sources = { workflows: source('workflows.json') };
|
|
476
|
+
const built = materializeKind('task-class', { adviceRegistry: advice.ok ? advice.registry : null, sources });
|
|
477
|
+
const client = createAdviceClient({ adviceRegistry: advice.ok ? advice.registry : null, roots, sources });
|
|
478
|
+
const result = advice.ok && built.ok
|
|
479
|
+
? await client.ask({
|
|
480
|
+
questionSet: { state, questions: [built.question] }, kind: 'task-class',
|
|
481
|
+
staticAnswer: taskClass ?? null, caller: 'classify', taskId: taskId ?? null, record: true,
|
|
482
|
+
})
|
|
483
|
+
: { status: 'unavailable', reason: 'config-invalid', answers: [] };
|
|
484
|
+
opinion = secondOpinion(result, taskClass);
|
|
485
|
+
if (decision.outcome === CLASSIFICATION_OUTCOMES.ACCEPTED && opinion.status === 'answered' && opinion.pickId) {
|
|
486
|
+
const found = findPick({ pickId: opinion.pickId, roots });
|
|
487
|
+
if (found) appendOutcome({
|
|
488
|
+
pickId: opinion.pickId, actual: decision.taskClass, source: 'classify-verdict', roots, store: found.store,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
} catch {
|
|
492
|
+
opinion = emptySecondOpinion('unavailable', 'config-invalid');
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (json) console.log(JSON.stringify({ ...decision, secondOpinion: opinion }, null, 2));
|
|
496
|
+
else printClassifyDecision(decision, opinion);
|
|
497
|
+
return finishRuntime(exitCode);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function emptySecondOpinion(status: any, reason: any) {
|
|
502
|
+
return { status, reason, pick: null, strength: null, between: null, agrees: null, proposed: null,
|
|
503
|
+
pickId: null, probabilities: null, confidence: null, model: null };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function secondOpinion(result: any, proposed: any) {
|
|
507
|
+
const answer = result.answers?.[0];
|
|
508
|
+
if (result.status !== 'answered' || !answer) return emptySecondOpinion(result.status, result.reason);
|
|
509
|
+
return {
|
|
510
|
+
status: 'answered', reason: null, pick: answer.pick, strength: answer.strength.word,
|
|
511
|
+
between: answer.strength.between, agrees: answer.agrees, proposed: proposed ?? null,
|
|
512
|
+
pickId: answer.pickId, probabilities: answer.probabilities, confidence: answer.confidence, model: result.model,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function printClassifyDecision(decision: any, opinion: any) {
|
|
517
|
+
console.log(`\nDoFlow Task Classification [${decision.outcome}]:`);
|
|
518
|
+
console.log('═'.repeat(78));
|
|
519
|
+
console.log(decision.message);
|
|
520
|
+
// Printed unconditionally, including when it says NOT_EVALUATED: an operator has to be able to
|
|
521
|
+
// see that fit was never checked without reading --json.
|
|
522
|
+
console.log(`Fit: ${decision.fit.state}${decision.fit.reason ? ` (${decision.fit.reason})` : ''}`);
|
|
523
|
+
if (opinion?.status === 'answered' && opinion.proposed !== null) {
|
|
524
|
+
const strength = strengthWords({ word: opinion.strength, between: opinion.between });
|
|
525
|
+
if (opinion.agrees) console.log(`Second opinion: agrees with '${opinion.proposed}' (${strength}).`);
|
|
526
|
+
else console.log(`Second opinion: '${opinion.pick}' (${strength}), differs from the proposed '${opinion.proposed}'. Validation alone decides the class.`);
|
|
527
|
+
}
|
|
528
|
+
if (decision.fit.state === 'NOT_HOSTED' && decision.fit.hostingClasses.length > 0) {
|
|
529
|
+
console.log(`Classes that host ${decision.fit.callingSkill}: `
|
|
530
|
+
+ decision.fit.hostingClasses.map((h: any) => `${h.taskClass} (${h.stageIds.join(', ')})`).join(', '));
|
|
531
|
+
}
|
|
532
|
+
if (decision.workflow) {
|
|
533
|
+
console.log('─'.repeat(78));
|
|
534
|
+
console.log(`Workflow: ${decision.workflow.name} (${decision.workflow.stageIds.length} stage(s))`);
|
|
535
|
+
console.log(`Stages: ${decision.workflow.stageIds.join(' → ')}`);
|
|
536
|
+
console.log(`Implementation stages: ${decision.workflow.hasImplementationStage ? decision.workflow.implementationStageIds.join(', ') : 'none'}`);
|
|
537
|
+
} else {
|
|
538
|
+
console.log(`Valid classes: ${decision.validClasses.join(', ')}`);
|
|
539
|
+
}
|
|
540
|
+
console.log('═'.repeat(78) + '\n');
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
module.exports = {
|
|
544
|
+
TaskClassifier,
|
|
545
|
+
CLASSIFICATION_OUTCOMES,
|
|
546
|
+
REJECTION_REASONS,
|
|
547
|
+
FIT_STATES,
|
|
548
|
+
FIT_REASONS,
|
|
549
|
+
handleClassifyCommand,
|
|
550
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
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 path = require('node:path');
|
|
7
|
+
const { resolveActiveFeature } = require('./feature-resolve.ts');
|
|
8
|
+
|
|
9
|
+
// Which feature a task record belongs to (feature 045, IC-001/IC-002).
|
|
10
|
+
//
|
|
11
|
+
// Task ids are short and repeat across features (`A.1` exists in every feature that has a Phase A),
|
|
12
|
+
// and the stores key records by the bare id. A task in one feature therefore read the evidence,
|
|
13
|
+
// claims and outcome another feature had written under the same id. A feature that has a decision
|
|
14
|
+
// register now keeps its records in a subdirectory named for the feature; a feature without one,
|
|
15
|
+
// and a task id that is the feature's own slug, keep the flat layout, so no existing record is
|
|
16
|
+
// moved, renamed or rewritten (FR-007) and a namespaced read never falls back to the flat file.
|
|
17
|
+
|
|
18
|
+
const SAFE_SLUG = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
19
|
+
const SLUG_RULE = 'use letters, digits, dot, underscore or dash, start with a letter or digit, and no ".."';
|
|
20
|
+
|
|
21
|
+
/** The one shape check for a feature slug (IC-009); do-paths.sh applies the same rule in bash. */
|
|
22
|
+
function isSafeSlug(slug: any): boolean {
|
|
23
|
+
return typeof slug === 'string' && SAFE_SLUG.test(slug) && !slug.includes('..');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The refusal for a slug that fails the shape check, in the shape do-paths.sh prints, or null when
|
|
28
|
+
* the slug is fine or was not given.
|
|
29
|
+
* @param {*} slug
|
|
30
|
+
* @returns {{error: 'invalid-slug', message: string, hint: string}|null}
|
|
31
|
+
*/
|
|
32
|
+
function invalidSlugRefusal(slug: any): { error: 'invalid-slug'; message: string; hint: string } | null {
|
|
33
|
+
if (typeof slug !== 'string' || slug === '' || isSafeSlug(slug)) return null;
|
|
34
|
+
return { error: 'invalid-slug', message: `slug "${slug}" is not a valid feature slug: ${SLUG_RULE}`, hint: SLUG_RULE };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** (projectRoot, slug) -> { slug, hasRegister } | { reason }. One resolver spawn per key per process. */
|
|
38
|
+
const featureCache = new Map<string, any>();
|
|
39
|
+
|
|
40
|
+
/** A slug the CLI named explicitly (`--slug`), applied to every store call in this process that was
|
|
41
|
+
* not given its own. The dispatcher sets it once so the verbs whose handlers build their ledger
|
|
42
|
+
* internally (readiness, evidence) route the same way as the ones that take `slug` directly. */
|
|
43
|
+
let defaultSlug: string | null = null;
|
|
44
|
+
|
|
45
|
+
function setDefaultSlug(slug: any): void {
|
|
46
|
+
defaultSlug = typeof slug === 'string' && slug !== '' ? slug : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function clearTaskScopeCache(): void {
|
|
50
|
+
featureCache.clear();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The resolver reports the git top level, which is the project root or one of its ancestors, and
|
|
54
|
+
* the feature folders live directly under it — so no `agent-docs/doflow` on the way up means there
|
|
55
|
+
* is nothing to resolve and the spawn is skipped. */
|
|
56
|
+
function hasFeatureFolderAbove(projectRoot: string): boolean {
|
|
57
|
+
let dir = path.resolve(projectRoot);
|
|
58
|
+
for (;;) {
|
|
59
|
+
try {
|
|
60
|
+
if (fs.statSync(path.join(dir, 'agent-docs', 'doflow')).isDirectory()) return true;
|
|
61
|
+
} catch { /* not here */ }
|
|
62
|
+
const parent = path.dirname(dir);
|
|
63
|
+
if (parent === dir) return false;
|
|
64
|
+
dir = parent;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function featureFor(projectRoot: string, slug: string | null | undefined): any {
|
|
69
|
+
const key = `${path.resolve(projectRoot)}\0${slug || ''}`;
|
|
70
|
+
if (featureCache.has(key)) return featureCache.get(key);
|
|
71
|
+
let value: any = { reason: 'no-feature' };
|
|
72
|
+
try {
|
|
73
|
+
if (hasFeatureFolderAbove(projectRoot)) {
|
|
74
|
+
const found = resolveActiveFeature({ projectRoot, slug: slug || null });
|
|
75
|
+
if (!found.error && found.paths && isSafeSlug(found.paths.feature_slug)) {
|
|
76
|
+
value = { slug: found.paths.feature_slug, hasRegister: found.paths.has_decisions === true, exists: fs.existsSync(found.featureDir) };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} catch { /* a resolver failure is "no feature", never an exception */ }
|
|
80
|
+
featureCache.set(key, value);
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {Object} options
|
|
86
|
+
* @param {string} options.projectRoot
|
|
87
|
+
* @param {string} options.taskId
|
|
88
|
+
* @param {string|null} [options.slug] explicit feature; otherwise the `--slug` default, then the branch
|
|
89
|
+
* @returns {{namespace: string|null, slug: string|null, reason: string}}
|
|
90
|
+
* `reason` is `namespaced` when a namespace applies, else `no-feature`, `no-register` or
|
|
91
|
+
* `feature-level-id`.
|
|
92
|
+
*/
|
|
93
|
+
function resolveTaskScope({ projectRoot, taskId, slug = null }: { projectRoot: string; taskId: string; slug?: string | null }): { namespace: string | null; slug: string | null; reason: string } {
|
|
94
|
+
const feature = featureFor(projectRoot, slug || defaultSlug);
|
|
95
|
+
if (feature.reason) return { namespace: null, slug: null, reason: feature.reason };
|
|
96
|
+
if (!feature.hasRegister) return { namespace: null, slug: feature.slug, reason: 'no-register' };
|
|
97
|
+
if (taskId === feature.slug) return { namespace: null, slug: feature.slug, reason: 'feature-level-id' };
|
|
98
|
+
return { namespace: feature.slug, slug: feature.slug, reason: 'namespaced' };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Whether an explicit slug, already known to be well-formed, names no feature folder here. The
|
|
103
|
+
* records then go to the shared task store, which is today's behaviour and is worth one line to the
|
|
104
|
+
* caller because the slug they typed did nothing.
|
|
105
|
+
* @param {Object} options
|
|
106
|
+
* @param {string} options.projectRoot
|
|
107
|
+
* @param {string} options.slug
|
|
108
|
+
* @returns {boolean}
|
|
109
|
+
*/
|
|
110
|
+
function slugNamesNoFeature({ projectRoot, slug }: { projectRoot: string; slug: string }): boolean {
|
|
111
|
+
const feature = featureFor(projectRoot, slug);
|
|
112
|
+
return Boolean(feature.reason) || feature.exists === false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The directory a task's record file for one store lives in (IC-002).
|
|
117
|
+
* @param {Object} options
|
|
118
|
+
* @param {string} options.projectRoot
|
|
119
|
+
* @param {string} options.store state subdirectory: evidence | research | outcome | retrieval
|
|
120
|
+
* @param {string} options.taskId
|
|
121
|
+
* @param {string|null} [options.slug]
|
|
122
|
+
* @returns {string}
|
|
123
|
+
*/
|
|
124
|
+
function taskStoreDir({ projectRoot, store, taskId, slug = null }: { projectRoot: string; store: string; taskId: string; slug?: string | null }): string {
|
|
125
|
+
const base = path.join(projectRoot, '.doflow', 'state', store);
|
|
126
|
+
const { namespace } = resolveTaskScope({ projectRoot, taskId, slug });
|
|
127
|
+
return namespace ? path.join(base, namespace) : base;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = { resolveTaskScope, taskStoreDir, setDefaultSlug, clearTaskScopeCache, isSafeSlug, invalidSlugRefusal, slugNamesNoFeature };
|