@khoavu882/doflow 2.0.0-beta.5 → 2.0.1
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/README.md +16 -16
- package/bin/doflow.js +22 -12
- package/core/.antigravity-plugin/plugin.json +1 -2
- package/core/.claude-plugin/plugin.json +1 -1
- package/core/.codex-plugin/plugin.json +1 -1
- package/core/.plugin/plugin.json +1 -1
- package/core/harnesses/claude/hooks/lib.sh +43 -24
- package/core/harnesses/claude/hooks/mcp-tool-guard.sh +4 -1
- package/core/harnesses/claude/hooks/permission-audit.sh +3 -0
- package/core/harnesses/claude/hooks/post-compact.sh +35 -32
- package/core/harnesses/claude/hooks/post-edit-lint.sh +2 -0
- package/core/harnesses/claude/hooks/post-shell-observe.sh +48 -0
- package/core/harnesses/claude/hooks/post-tool-failure.sh +3 -0
- package/core/harnesses/claude/hooks/pre-compact.sh +23 -27
- package/core/harnesses/claude/hooks/pre-implement-gate.sh +4 -1
- package/core/harnesses/claude/hooks/session-end.sh +23 -10
- package/core/harnesses/claude/hooks/skill-config-audit.sh +3 -0
- package/core/harnesses/claude/settings/settings.json +26 -9
- package/core/harnesses/claude/statusline/doflow-statusline.sh +1 -0
- package/core/harnesses/codex/agents/core-implementer.toml +1 -0
- package/core/harnesses/codex/agents/delivery-lead.toml +10 -0
- package/core/harnesses/codex/agents/quality-guardian.toml +1 -1
- package/core/harnesses/codex/agents/research-writer.toml +1 -1
- package/core/harnesses/codex/agents/reviewer.toml +10 -0
- package/core/harnesses/codex/agents/spec-analyst.toml +1 -1
- package/core/harnesses/codex/agents/system-architect.toml +1 -1
- package/core/harnesses/codex/hooks/hooks.json +0 -10
- package/core/harnesses/codex/hooks/lib.sh +44 -25
- package/core/harnesses/codex/hooks/mcp-tool-guard.sh +4 -1
- package/core/harnesses/codex/hooks/post-edit-lint.impl.sh +3 -1
- package/core/harnesses/codex/hooks/pre-compact.impl.sh +29 -26
- package/core/harnesses/codex/hooks/pre-implement-gate.sh +4 -1
- package/core/harnesses/codex/hooks/session-end.impl.sh +23 -10
- package/core/harnesses/codex/hooks/subagent-audit.sh +1 -0
- package/core/harnesses/copilot/hooks/doflow-lib.sh +94 -0
- package/core/harnesses/copilot/hooks/doflow-mcp-tool-guard.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-pre-implement-gate.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-session-start.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-stop-check.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-subagent-start.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-subagent-stop.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow.json +51 -0
- package/core/harnesses/kiro/agents/doflow.json +1 -11
- package/core/harnesses/kiro/hooks/doflow.json +0 -10
- package/core/harnesses/kiro/hooks/lib.sh +26 -6
- package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +8 -3
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +8 -3
- package/core/harnesses/kiro/hooks/session-start.sh +1 -0
- package/core/harnesses/kiro/hooks/stop-check.sh +1 -0
- package/core/harnesses/pi/extensions/doflow-guard.js +11 -2
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +1 -2
- package/core/harnesses/shared/hooks/adapters/antigravity.js +1 -1
- package/core/harnesses/shared/hooks/bridge/bridge.sh +126 -23
- package/core/harnesses/shared/hooks/bridge/envelope.jq +87 -18
- package/core/harnesses/shared/hooks/bridge/observe-model.sh +75 -0
- package/core/harnesses/shared/hooks/bridge/respond.sh +16 -2
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +49 -16
- package/core/harnesses/shared/hooks/policies/compact-text.sh +46 -0
- package/core/harnesses/shared/hooks/policies/contract.sh +3 -0
- package/core/harnesses/shared/hooks/policies/deny-json.sh +20 -7
- package/core/harnesses/shared/hooks/policies/emission-rules.sh +37 -0
- package/core/harnesses/shared/hooks/policies/emission.jq +209 -0
- package/core/harnesses/shared/hooks/policies/emission.sh +383 -0
- package/core/harnesses/shared/hooks/policies/identity.sh +177 -0
- package/core/harnesses/shared/hooks/policies/lib.sh +63 -40
- package/core/harnesses/shared/hooks/policies/mcp-policy.conf +1 -1
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +7 -8
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +132 -57
- package/core/harnesses/shared/hooks/policies/session-context.sh +30 -1
- package/core/harnesses/shared/hooks/policies/stop-check.sh +27 -15
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +6 -3
- package/core/harnesses/shared/hooks/policies/summary-store.sh +347 -0
- package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +60 -155
- package/core/harnesses/shared/hooks/stream-hook-runner.js +58 -20
- package/core/harnesses/shared/locator/doflow-run +2 -3
- package/core/registry/advice.json +3 -3
- package/core/registry/assets.json +55 -82
- package/core/registry/capabilities.json +12 -0
- package/core/registry/contracts.json +58 -31
- package/core/registry/contribution-points.json +35 -0
- package/core/registry/gates.json +32 -0
- package/core/registry/guidance-caps.json +40 -0
- package/core/registry/harnesses.json +1445 -366
- package/core/registry/intake.json +53 -0
- package/core/registry/lifecycle.json +599 -996
- package/core/registry/mcp.json +5 -1
- package/core/registry/models.json +7 -1
- package/core/registry/package-templates/README.md +19 -0
- package/core/registry/package-templates/agent/files/agent.md +9 -0
- package/core/registry/package-templates/agent/template.json +4 -0
- package/core/registry/package-templates/capability-provider/files/provider.json +9 -0
- package/core/registry/package-templates/capability-provider/template.json +11 -0
- package/core/registry/package-templates/debate-preset/files/preset.json +26 -0
- package/core/registry/package-templates/debate-preset/template.json +4 -0
- package/core/registry/package-templates/intake-category/files/category.json +8 -0
- package/core/registry/package-templates/intake-category/template.json +4 -0
- package/core/registry/package-templates/review-preset/files/preset.json +30 -0
- package/core/registry/package-templates/review-preset/template.json +4 -0
- package/core/registry/package-templates/skill/files/SKILL.md +8 -0
- package/core/registry/package-templates/skill/template.json +4 -0
- package/core/registry/package-templates/worker/files/worker.json +92 -0
- package/core/registry/package-templates/worker/template.json +11 -0
- package/core/registry/packages/doflow/doflow-package.json +15 -0
- package/core/registry/packages/doflow/presets/debate-3.json +26 -0
- package/core/registry/protocols.json +61 -0
- package/core/registry/routes.json +17 -0
- package/core/registry/rules.json +431 -0
- package/core/registry/settings.json +272 -0
- package/core/registry/storage.json +687 -31
- package/core/registry/telemetry.json +236 -0
- package/core/registry/transcripts.json +142 -0
- package/core/registry/verification.json +76 -6
- package/core/registry/workflows.json +55 -0
- package/core/shared/agent-specs/core-implementer.md +3 -0
- package/core/shared/agent-specs/delivery-lead.md +68 -0
- package/core/shared/agent-specs/quality-guardian.md +5 -1
- package/core/shared/agent-specs/research-writer.md +10 -3
- package/core/shared/agent-specs/reviewer.md +53 -0
- package/core/shared/agent-specs/spec-analyst.md +5 -6
- package/core/shared/agent-specs/system-architect.md +5 -1
- package/core/shared/guidance/DOFLOW_CORE.md +1 -5
- package/core/shared/guidance/modes/MODE_DeepResearch.md +1 -1
- package/core/shared/guidance/modes/MODE_Orchestration.md +4 -3
- package/core/shared/guidance/modes/MODE_Task_Management.md +14 -14
- package/core/shared/guidance/pointers/codex.md +1 -4
- package/core/shared/guidance/references/ARTIFACT_FORMAT.md +30 -16
- package/core/shared/guidance/references/ARTIFACT_VOICE.md +3 -2
- package/core/shared/guidance/references/CONSTITUTION_BASE.md +17 -15
- package/core/shared/guidance/references/DIAGRAM_GUIDE.md +1 -1
- package/core/shared/guidance/references/DOFLOW_CHAIN.md +17 -19
- package/core/shared/guidance/references/EVIDENCE_LEDGER.md +7 -8
- package/core/shared/guidance/references/MODEL_SELECTION.md +5 -3
- package/core/shared/guidance/references/ORCHESTRATION.md +3 -2
- package/core/shared/guidance/references/WORKFLOW_HANDOFF.md +19 -1
- package/core/shared/guidance/rules/RULES.md +45 -0
- package/core/shared/scripts/doflow/bash/do-args.sh +37 -0
- package/core/shared/scripts/doflow/bash/do-exec-paths.sh +4 -0
- package/core/shared/scripts/doflow/bash/do-git-state.sh +15 -4
- package/core/shared/scripts/doflow/bash/do-parallel-check.sh +4 -0
- package/core/shared/scripts/doflow/bash/do-paths.sh +40 -2
- package/core/shared/scripts/doflow/bash/do-prereqs.sh +2 -1
- package/core/shared/scripts/doflow/bash/do-review-package.sh +4 -0
- package/core/shared/scripts/doflow/bash/do-task-brief.sh +58 -16
- package/core/shared/scripts/doflow/bash/render-audit.sh +26 -2
- package/core/shared/scripts/doflow/bash/render-diagrams.sh +1 -0
- package/core/shared/scripts/doflow/bash/render-puml.sh +2 -1
- package/core/shared/scripts/doflow/bash/validate-artifacts.sh +49 -5
- package/core/shared/scripts/doflow/bin/doflow-run +152 -34
- package/core/shared/skills/do/SKILL.md +14 -12
- package/core/shared/skills/do/references/advice_calls.md +26 -0
- package/core/shared/skills/do/references/pm_routing.md +1 -2
- package/core/shared/skills/do/references/program_mode.md +31 -0
- package/core/shared/skills/do-brainstorm/SKILL.md +45 -97
- package/core/shared/skills/do-brainstorm/references/handoff_branches.md +36 -0
- package/core/shared/skills/do-brainstorm/references/intent_seed.md +21 -0
- package/core/shared/skills/do-brainstorm/references/round_log.md +17 -0
- package/core/shared/skills/do-code-review/SKILL.md +48 -148
- package/core/shared/skills/do-code-review/content-types/markdown.md +24 -2
- package/core/shared/skills/do-code-review/references/tools.md +90 -0
- package/core/shared/skills/do-code-review/scripts/doc_quality_checker.py +549 -7
- package/core/shared/skills/do-design/SKILL.md +23 -20
- package/core/shared/skills/do-diagnose/SKILL.md +18 -6
- package/core/shared/skills/do-document/SKILL.md +12 -1
- package/core/shared/skills/do-document/references/deep_research.md +1 -2
- package/core/shared/skills/do-document/references/embedded-research.md +19 -7
- package/core/shared/skills/do-document/references/implementation-flow.md +5 -6
- package/core/shared/skills/do-execute-plan/SKILL.md +19 -60
- package/core/shared/skills/do-execute-plan/references/handoff_record.md +7 -2
- package/core/shared/skills/do-execute-plan/references/parallel_dispatch.md +13 -0
- package/core/shared/skills/do-execute-plan/references/readiness_gate.md +26 -1
- package/core/shared/skills/do-execute-plan/references/run_position.md +16 -2
- package/core/shared/skills/do-execute-plan/references/scaffold.md +17 -9
- package/core/shared/skills/do-execute-plan/references/state_checkpoint.md +11 -0
- package/core/shared/skills/do-execute-plan/references/task_attempts.md +78 -24
- package/core/shared/skills/do-flow/SKILL.md +10 -11
- package/core/shared/skills/do-flow/references/task_classes.md +5 -5
- package/core/shared/skills/do-git/SKILL.md +4 -0
- package/core/shared/skills/do-implement/SKILL.md +13 -14
- package/core/shared/skills/do-plan/SKILL.md +34 -20
- package/core/shared/skills/do-test/SKILL.md +7 -4
- package/core/shared/templates/doflow/constitution-template.md +9 -3
- package/core/shared/templates/doflow/data-model-template.md +1 -1
- package/core/shared/templates/doflow/design-template.md +113 -16
- package/core/shared/templates/doflow/plan-template.md +11 -10
- package/core/shared/templates/doflow/question-log-template.md +14 -2
- package/core/shared/templates/doflow/requirement-template.md +1 -1
- package/core/shared/workflows/do-flow-chain.md +2 -0
- package/core/worker-evidence/antigravity/checks.json +14 -0
- package/core/worker-evidence/codex/2026-10-18-cli-check.json +107 -0
- package/core/worker-evidence/codex/2026-10-18-live-run-edit.json +73 -0
- package/core/worker-evidence/codex/2026-10-18-live-run-read-only.json +62 -0
- package/core/worker-evidence/codex/2026-10-24-cli-check.json +126 -0
- package/core/worker-evidence/codex/checks.json +38 -0
- package/package.json +10 -6
- package/src/adapters/antigravity/index.js +143 -66
- package/src/adapters/claude/index.js +46 -5
- package/src/adapters/codex/agents.js +25 -15
- package/src/adapters/codex/hooks.js +39 -3
- package/src/adapters/codex/index.js +61 -13
- package/src/adapters/codex/native-projection.js +6 -2
- package/src/adapters/copilot/index.js +58 -23
- package/src/adapters/copy-tree.js +291 -46
- package/src/adapters/guidance-manifest.js +71 -0
- package/src/adapters/guidance-pointer.js +19 -0
- package/src/adapters/hook-commands.js +6 -10
- package/src/adapters/index.js +1 -0
- package/src/adapters/kiro/index.js +23 -12
- package/src/adapters/opencode/index.js +8 -8
- package/src/adapters/pi/hooks.js +2 -1
- package/src/adapters/pi/index.js +14 -7
- package/src/adapters/resolve-source.js +26 -2
- package/src/cli/commands/install.js +2 -1
- package/src/cli/commands/reconcile.js +53 -9
- package/src/cli/commands/remove.js +2 -2
- package/src/cli/commands/rollback.js +6 -3
- package/src/cli/commands/self-update.js +18 -9
- package/src/cli/commands/status.js +22 -3
- package/src/cli/commands/ui.js +99 -0
- package/src/cli/commands/update.js +2 -1
- package/src/cli/index.js +337 -32
- package/src/cli/runtime-commands.js +220 -60
- package/src/cli/shared.js +0 -2
- package/src/helper/git.js +1 -1
- package/src/helper/guardrails.js +1 -1
- package/src/helper/marker-merge.js +31 -13
- package/src/helper/package-ids.js +37 -0
- package/src/helper/semver.js +94 -0
- package/src/install/backup.js +105 -15
- package/src/install/context.js +1 -1
- package/src/install/targets.js +31 -16
- package/src/lifecycle/coverage.js +4 -16
- package/src/lifecycle/hook-tier.js +160 -0
- package/src/lifecycle/index.js +412 -78
- package/src/lifecycle/retired-harnesses.js +318 -0
- package/src/lifecycle/view.js +35 -15
- package/src/registry/index.js +551 -26
- package/src/registry/worker-ids.js +45 -0
- package/src/release/build-package.js +0 -3
- package/src/release/docs-blocks.js +472 -0
- package/src/release/preflight.js +425 -0
- package/src/release/rehearse.js +401 -0
- package/src/release/release-workflow.js +733 -0
- package/src/release/sync-plugin-versions.js +43 -7
- package/src/runtime/advice/bench-gate.js +25 -0
- package/src/runtime/advice/cli.js +59 -16
- package/src/runtime/advice/client.js +32 -9
- package/src/runtime/advice/config.js +98 -133
- package/src/runtime/advice/doctor.js +1 -1
- package/src/runtime/advice/elevation.js +97 -0
- package/src/runtime/advice/endpoint-guard.js +9 -3
- package/src/runtime/advice/mode.js +37 -0
- package/src/runtime/advice/record.js +18 -5
- package/src/runtime/advice/registry.js +4 -1
- package/src/runtime/advice/shadow-config.js +229 -0
- package/src/runtime/advice/shadow-dispatcher.js +276 -0
- package/src/runtime/brief-rules.js +81 -0
- package/src/runtime/brief-tools.js +111 -34
- package/src/runtime/candidate.js +20 -11
- package/src/runtime/capabilities/executor.js +450 -0
- package/src/runtime/capabilities/loopback.js +41 -0
- package/src/runtime/capabilities/sanitizer.js +88 -0
- package/src/runtime/capability-router.js +135 -3
- package/src/runtime/checkouts.js +73 -11
- package/src/runtime/claims.js +13 -7
- package/src/runtime/cli.js +196 -19
- package/src/runtime/config-home.js +42 -0
- package/src/runtime/context/arbitrate.js +767 -0
- package/src/runtime/context/delivery-store.js +4 -6
- package/src/runtime/context/emission-rules.js +118 -0
- package/src/runtime/context/identity.js +227 -0
- package/src/runtime/context/pointer.js +67 -12
- package/src/runtime/context/primary-command.js +117 -0
- package/src/runtime/context/primary.js +344 -0
- package/src/runtime/context/summary-store.js +655 -0
- package/src/runtime/context-pack.js +158 -79
- package/src/runtime/decision-register.js +13 -4
- package/src/runtime/dispatch/admission.js +392 -0
- package/src/runtime/dispatch/attempt-end.js +274 -0
- package/src/runtime/dispatch/command.js +628 -0
- package/src/runtime/dispatch/controls.js +904 -0
- package/src/runtime/dispatch/envelope.js +276 -0
- package/src/runtime/dispatch/grants.js +310 -0
- package/src/runtime/dispatch/integrate.js +617 -0
- package/src/runtime/dispatch/loop.js +1835 -0
- package/src/runtime/dispatch/store.js +352 -0
- package/src/runtime/dispatch/verify-run.js +201 -0
- package/src/runtime/evidence-ledger.js +52 -7
- package/src/runtime/failure/capture.js +3 -1
- package/src/runtime/failure/cli.js +25 -9
- package/src/runtime/failure/home.js +57 -30
- package/src/runtime/failure/store.js +7 -2
- package/src/runtime/feature-pointer.js +174 -0
- package/src/runtime/feature-resolve.js +13 -2
- package/src/runtime/fleet/command.js +77 -0
- package/src/runtime/fleet/projects.js +178 -0
- package/src/runtime/fleet/tree.js +383 -0
- package/src/runtime/freshness.js +33 -3
- package/src/runtime/git-memo.js +46 -0
- package/src/runtime/guidance/ablation.js +427 -0
- package/src/runtime/health.js +624 -18
- package/src/runtime/history-compactor.js +13 -3
- package/src/runtime/implementation-gate.js +281 -26
- package/src/runtime/intake/engine.js +677 -0
- package/src/runtime/intake/index.js +136 -0
- package/src/runtime/intake/manifest.js +140 -0
- package/src/runtime/intake/taxonomy.js +206 -0
- package/src/runtime/inventory/coverage.js +1 -1
- package/src/runtime/inventory/index.js +1 -1
- package/src/runtime/inventory/precedence.js +2 -2
- package/src/runtime/inventory/siblings.js +6 -4
- package/src/runtime/knowledge/code-corpus.js +277 -0
- package/src/runtime/knowledge/code-lexical.js +130 -0
- package/src/runtime/knowledge/code-retrieval.js +307 -0
- package/src/runtime/knowledge/code-validation.js +106 -0
- package/src/runtime/knowledge/gp3-gate.js +74 -0
- package/src/runtime/knowledge/graph-adapter.js +288 -0
- package/src/runtime/knowledge/graph-fallback.js +394 -0
- package/src/runtime/knowledge/hybrid.js +136 -8
- package/src/runtime/knowledge/retrieval.js +34 -1
- package/src/runtime/knowledge/rtk-compress.js +77 -0
- package/src/runtime/knowledge/semble-bridge.py +106 -0
- package/src/runtime/knowledge/semble-lifecycle.js +114 -0
- package/src/runtime/knowledge/semble-provider.js +365 -0
- package/src/runtime/lifecycle/cli.js +88 -10
- package/src/runtime/lifecycle/event-store.js +8 -6
- package/src/runtime/lifecycle/followup.js +38 -7
- package/src/runtime/lifecycle/overview.js +26 -2
- package/src/runtime/lifecycle/report-store.js +8 -10
- package/src/runtime/lifecycle/retention.js +95 -31
- package/src/runtime/lifecycle/root.js +9 -1
- package/src/runtime/lifecycle/status.js +34 -17
- package/src/runtime/lifecycle/store-upkeep.js +56 -7
- package/src/runtime/mask.js +134 -7
- package/src/runtime/mcp-consultations.js +40 -6
- package/src/runtime/model-router.js +79 -7
- package/src/runtime/models/catalogue.js +191 -0
- package/src/runtime/models/command.js +153 -0
- package/src/runtime/off-words.js +12 -0
- package/src/runtime/outcome.js +2 -2
- package/src/runtime/packages/command.js +1009 -0
- package/src/runtime/packages/contributions.js +116 -0
- package/src/runtime/packages/digest.js +132 -0
- package/src/runtime/packages/fetch.js +182 -0
- package/src/runtime/packages/grants.js +192 -0
- package/src/runtime/packages/index-file.js +134 -0
- package/src/runtime/packages/instruction.js +90 -0
- package/src/runtime/packages/manifest.js +331 -0
- package/src/runtime/packages/points.js +487 -0
- package/src/runtime/packages/programs.js +216 -0
- package/src/runtime/packages/scaffold.js +277 -0
- package/src/runtime/packages/source.js +90 -0
- package/src/runtime/packages/state.js +164 -0
- package/src/runtime/packages/store.js +449 -0
- package/src/runtime/packages/tar.js +106 -0
- package/src/runtime/packages/view.js +406 -0
- package/src/runtime/plan-check.js +59 -0
- package/src/runtime/plan-content.js +324 -0
- package/src/runtime/policy-init.js +118 -0
- package/src/runtime/profiles/doctor.js +96 -0
- package/src/runtime/profiles/resolve.js +244 -0
- package/src/runtime/profiles/rollback.js +254 -0
- package/src/runtime/profiles/sampler.js +41 -0
- package/src/runtime/readiness-record.js +16 -11
- package/src/runtime/readiness.js +46 -5
- package/src/runtime/research-request.js +50 -25
- package/src/runtime/retrieval-plan.js +10 -30
- package/src/runtime/retrieval-record.js +75 -5
- package/src/runtime/review/command.js +246 -0
- package/src/runtime/review/dominance.js +68 -0
- package/src/runtime/review/findings.js +434 -0
- package/src/runtime/review/identity.js +255 -0
- package/src/runtime/review/protocol-command.js +109 -0
- package/src/runtime/review/protocols.js +405 -0
- package/src/runtime/review/resolve-protocol.js +302 -0
- package/src/runtime/review/start.js +530 -0
- package/src/runtime/roadmap/attendance.js +137 -0
- package/src/runtime/roadmap/facts.js +479 -0
- package/src/runtime/roadmap/gates.js +273 -0
- package/src/runtime/roadmap/loop.js +1719 -0
- package/src/runtime/roadmap/merge.js +390 -0
- package/src/runtime/roadmap/rows.js +1122 -0
- package/src/runtime/roadmap/seed.js +1063 -0
- package/src/runtime/roadmap/worktree-cleanup.js +147 -0
- package/src/runtime/rules-report.js +336 -0
- package/src/runtime/scaffold/artifacts.js +60 -9
- package/src/runtime/scaffold/generate.js +21 -9
- package/src/runtime/settings/autopilot.js +106 -0
- package/src/runtime/settings/cli.js +280 -0
- package/src/runtime/settings/config-write.js +314 -0
- package/src/runtime/settings/files.js +271 -0
- package/src/runtime/settings/legacy.js +154 -0
- package/src/runtime/settings/lexical.js +56 -0
- package/src/runtime/settings/prepared-run.js +269 -0
- package/src/runtime/settings/project-layers.js +118 -0
- package/src/runtime/settings/refs.js +288 -0
- package/src/runtime/settings/registry.js +553 -0
- package/src/runtime/settings/resolve.js +713 -0
- package/src/runtime/settings/shared.js +37 -0
- package/src/runtime/settings/worker-billing.js +107 -0
- package/src/runtime/settings/write.js +269 -0
- package/src/runtime/slot-command.js +78 -0
- package/src/runtime/slots.js +671 -0
- package/src/runtime/source-version.js +1 -0
- package/src/runtime/state-root.js +217 -0
- package/src/runtime/storage/cli.js +12 -5
- package/src/runtime/storage/engine.js +9 -2
- package/src/runtime/storage/registry.js +14 -1
- package/src/runtime/storage/roots.js +66 -16
- package/src/runtime/storage/rules.js +82 -10
- package/src/runtime/storage/upkeep.js +10 -2
- package/src/runtime/storage/work-status.js +245 -26
- package/src/runtime/task-attempts.js +589 -69
- package/src/runtime/task-classifier.js +85 -14
- package/src/runtime/task-graph.js +373 -0
- package/src/runtime/task-run.js +778 -0
- package/src/runtime/task-scope.js +176 -55
- package/src/runtime/task-sources.js +147 -0
- package/src/runtime/task-state.js +373 -64
- package/src/runtime/team/admission.js +301 -0
- package/src/runtime/team/envelope.js +324 -0
- package/src/runtime/team/review-guard.js +156 -0
- package/src/runtime/trace/analyze.js +427 -0
- package/src/runtime/trace/baseline.js +770 -0
- package/src/runtime/trace/ledger.js +63 -140
- package/src/runtime/trace/otlp-export.js +568 -0
- package/src/runtime/trace/otlp-map.js +481 -0
- package/src/runtime/trace/spans.js +745 -0
- package/src/runtime/trace/telemetry.js +225 -0
- package/src/runtime/trace/views.js +1 -0
- package/src/runtime/transcripts/command.js +98 -0
- package/src/runtime/transcripts/cursor.js +326 -0
- package/src/runtime/transcripts/read.js +126 -0
- package/src/runtime/transcripts/readers/claude-jsonl.js +74 -0
- package/src/runtime/transcripts/readers/codex-rollout.js +70 -0
- package/src/runtime/transcripts/readers/copilot-events.js +90 -0
- package/src/runtime/transcripts/readers/kiro-jsonl.js +77 -0
- package/src/runtime/transcripts/readers/pi-jsonl.js +59 -0
- package/src/runtime/transcripts/registry.js +161 -0
- package/src/runtime/transcripts/stats.js +161 -0
- package/src/runtime/verb-view.js +267 -0
- package/src/runtime/verification/check-key.js +628 -0
- package/src/runtime/verification/contract-runner.js +546 -11
- package/src/runtime/verification/engine.js +499 -54
- package/src/runtime/verification/impact.js +617 -0
- package/src/runtime/verification/inflight.js +205 -0
- package/src/runtime/verification/registry.js +176 -0
- package/src/runtime/verification/reuse.js +184 -0
- package/src/runtime/verification/telemetry-emit.js +254 -0
- package/src/runtime/verification/test-selection.js +67 -7
- package/src/runtime/worker/active.js +90 -0
- package/src/runtime/worker/cancel.js +222 -0
- package/src/runtime/worker/catalogue.js +122 -0
- package/src/runtime/worker/command.js +677 -0
- package/src/runtime/worker/evidence.js +288 -0
- package/src/runtime/worker/launch.js +256 -0
- package/src/runtime/worker/models.js +137 -0
- package/src/runtime/worker/parsers.js +218 -0
- package/src/runtime/worker/probes/index.js +706 -0
- package/src/runtime/worker/process-tree.js +415 -0
- package/src/runtime/worker/quota.js +43 -0
- package/src/runtime/worker/readiness.js +713 -0
- package/src/runtime/worker/run-store.js +166 -0
- package/src/runtime/worker/supervisor.js +541 -0
- package/src/runtime/worker/worktree-check.js +252 -0
- package/src/runtime/workflow-engine.js +5 -1
- package/src/runtime/workflow-orchestrator.js +610 -67
- package/src/runtime/worktree.js +149 -22
- package/src/state/index.js +1 -1
- package/src/state/lockfile.js +64 -10
- package/src/state/run-lock.js +1 -1
- package/types/node-commonjs.d.ts +106 -7
- package/core/harnesses/claude/hooks/pre-bash-guard.sh +0 -10
- package/core/harnesses/codex/hooks/pre-bash-guard.sh +0 -10
- package/core/harnesses/gemini/hooks/hooks.json +0 -77
- package/core/harnesses/gemini/hooks/lib.sh +0 -302
- package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +0 -10
- package/core/harnesses/gemini/hooks/post-edit-lint.sh +0 -39
- package/core/harnesses/gemini/hooks/pre-bash-guard.sh +0 -10
- package/core/harnesses/gemini/hooks/pre-compact.sh +0 -38
- package/core/harnesses/gemini/hooks/pre-implement-gate.sh +0 -10
- package/core/harnesses/gemini/hooks/session-end.impl.sh +0 -89
- package/core/harnesses/gemini/hooks/session-end.sh +0 -5
- package/core/harnesses/gemini/hooks/session-start.sh +0 -7
- package/core/harnesses/gemini/settings/adapter-defaults.json +0 -4
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +0 -24
- package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +0 -91
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +0 -378
- package/core/shared/guidance/FLAGS.md +0 -25
- package/core/shared/guidance/rules/RULE_01_SAFETY.md +0 -25
- package/core/shared/guidance/rules/RULE_02_WORKFLOW.md +0 -23
- package/core/shared/guidance/rules/RULE_03_QUALITY.md +0 -14
- package/core/shared/guidance/rules/RULE_04_QUESTIONS.md +0 -46
- package/src/adapters/gemini/hooks.js +0 -251
- package/src/adapters/gemini/index.js +0 -429
|
@@ -91,7 +91,7 @@ def frontmatter_keys(lines: List[str]) -> List[str]:
|
|
|
91
91
|
def is_agent_spec(path: Path, lines: Optional[List[str]] = None) -> bool:
|
|
92
92
|
"""True for a specialist agent-spec file. `agent-specs` is this repo's own SOURCE-TREE
|
|
93
93
|
directory name — every harness projects it to a differently-named directory on install
|
|
94
|
-
(`agents/` for claude/codex/
|
|
94
|
+
(`agents/` for claude/codex/copilot/kiro; never `agent-specs/`), so a real installed
|
|
95
95
|
copy would silently never match a directory-name-only check. Since DoFlow is designed to be
|
|
96
96
|
installed anywhere, this checks the installed-shape name too, but path name alone is too
|
|
97
97
|
generic (an unrelated project can have its own unrelated `agents/` folder) — content shape
|
|
@@ -417,13 +417,524 @@ def check_staleness(path: Path, lines: List[str]) -> List[Dict]:
|
|
|
417
417
|
return findings
|
|
418
418
|
|
|
419
419
|
|
|
420
|
+
# --------------------------------------------------------------------------------------
|
|
421
|
+
# Check 6: uncited vendor claims (Feature 124, IC-004)
|
|
422
|
+
# --------------------------------------------------------------------------------------
|
|
423
|
+
|
|
424
|
+
_VENDOR_KW_RE = re.compile(
|
|
425
|
+
r"\b(library|libraries|framework|frameworks|runtime|runtimes|sdk|sdks|package|packages|dependency|dependencies|vendor|vendors|api|apis)\b",
|
|
426
|
+
re.IGNORECASE,
|
|
427
|
+
)
|
|
428
|
+
_BEHAVIOR_KW_RE = re.compile(
|
|
429
|
+
r"\b(returns|throws|behaves|defaults to|implements|requires|supports)\b",
|
|
430
|
+
re.IGNORECASE,
|
|
431
|
+
)
|
|
432
|
+
_CITATION_RE = re.compile(
|
|
433
|
+
r"\b(context7|c7|mcp__context7__)\b|https?://|\bUNVERIFIED\b",
|
|
434
|
+
re.IGNORECASE,
|
|
435
|
+
)
|
|
436
|
+
_SENTENCE_SPLIT_RE = re.compile(r"(?<=[.!?])\s+")
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def check_uncited_vendor_claims(path: Path, lines: List[str]) -> List[Dict]:
|
|
440
|
+
findings: List[Dict] = []
|
|
441
|
+
# Skip YAML frontmatter
|
|
442
|
+
fm_end = 0
|
|
443
|
+
if lines and lines[0].strip() == "---":
|
|
444
|
+
for i in range(1, len(lines)):
|
|
445
|
+
if lines[i].strip() == "---":
|
|
446
|
+
fm_end = i + 1
|
|
447
|
+
break
|
|
448
|
+
|
|
449
|
+
for start_idx, para_lines in _iter_paragraphs(lines):
|
|
450
|
+
if start_idx < fm_end:
|
|
451
|
+
continue
|
|
452
|
+
para_text = " ".join(para_lines)
|
|
453
|
+
offset = 0
|
|
454
|
+
for sent in _SENTENCE_SPLIT_RE.split(para_text):
|
|
455
|
+
sent_clean = sent.strip()
|
|
456
|
+
if not sent_clean:
|
|
457
|
+
offset += len(sent) + 1
|
|
458
|
+
continue
|
|
459
|
+
if _VENDOR_KW_RE.search(sent_clean) and _BEHAVIOR_KW_RE.search(sent_clean):
|
|
460
|
+
if not _CITATION_RE.search(sent_clean):
|
|
461
|
+
sent_pos = para_text.find(sent, offset)
|
|
462
|
+
if sent_pos == -1:
|
|
463
|
+
sent_pos = offset
|
|
464
|
+
line_no = _offset_to_line(start_idx, para_lines, sent_pos)
|
|
465
|
+
findings.append(make_finding(
|
|
466
|
+
path, line_no, "uncited_vendor_claim", "low",
|
|
467
|
+
"States library/vendor fact without citation: use context7 first, else an HTTPS URL, else tag as UNVERIFIED."
|
|
468
|
+
))
|
|
469
|
+
offset += len(sent) + 1
|
|
470
|
+
return findings
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
# --------------------------------------------------------------------------------------
|
|
474
|
+
# Check 7: voice tells (opt-in `--voice`; row 132)
|
|
475
|
+
# --------------------------------------------------------------------------------------
|
|
476
|
+
#
|
|
477
|
+
# Advisory and deterministic: it finds phrases the guidance tree's `ARTIFACT_VOICE.md` asks
|
|
478
|
+
# writers to avoid. Every finding quotes the match; there is no score, percentage or grade, and
|
|
479
|
+
# the check never changes this script's exit status. The word lists below are this repo's own.
|
|
480
|
+
#
|
|
481
|
+
# A tell is STRONG (reported on its own) or WEAK (a word or mark that is often fine alone: one
|
|
482
|
+
# dash, one bold word, "key"). A weak tell is reported only when at least two other tells share
|
|
483
|
+
# its paragraph, so a plain sentence with one dash stays silent.
|
|
484
|
+
|
|
485
|
+
_V_STRONG_WORDS = [
|
|
486
|
+
("overused_word", r"delv(?:e|es|ed|ing)|tapestry|pivotal|testament|intricate|interplay|"
|
|
487
|
+
r"foster(?:s|ed|ing)?|underscor(?:e|es|ed|ing)|showcas(?:e|es|ed|ing)|vibrant"),
|
|
488
|
+
("sales_word", r"boasts?|seamless(?:ly)?|cutting-edge|game-chang(?:er|ing)|groundbreaking|"
|
|
489
|
+
r"revolutionary|world-class|best-in-class|state-of-the-art|unparalleled"),
|
|
490
|
+
("chatbot_residue", r"i hope this helps|hope that helps|great question|good question|"
|
|
491
|
+
r"i'?d be happy to|happy to help|as an ai(?: language model)?|"
|
|
492
|
+
r"let me know if you|feel free to (?:ask|reach)|"
|
|
493
|
+
r"(?<![\w'])(?:certainly|absolutely|of course)(?=!)"),
|
|
494
|
+
("knowledge_limit", r"as of my (?:last|latest) (?:update|training)|as of my knowledge|"
|
|
495
|
+
r"knowledge cut-?off|my training data|up to my last (?:update|training)|"
|
|
496
|
+
r"i (?:don'?t|do not) have (?:access to )?real-time"),
|
|
497
|
+
("serves_as", r"serv(?:es|e|ing) as|stands? as|standing as"),
|
|
498
|
+
]
|
|
499
|
+
_V_WEAK_WORDS = [
|
|
500
|
+
("overused_word", r"actually|additionally|crucial|enhanc(?:e|es|ed|ing)|highlight(?:s|ed|ing)?|"
|
|
501
|
+
r"key|landscape|valuable"),
|
|
502
|
+
("sales_word", r"robust|powerful"),
|
|
503
|
+
]
|
|
504
|
+
_V_STRONG_RES = [(k, re.compile(r"\b(?:" + p + r")\b" if k != "chatbot_residue" else "(?:" + p + ")",
|
|
505
|
+
re.IGNORECASE)) for k, p in _V_STRONG_WORDS]
|
|
506
|
+
_V_WEAK_RES = [(k, re.compile(r"\b(?:" + p + r")\b", re.IGNORECASE)) for k, p in _V_WEAK_WORDS]
|
|
507
|
+
|
|
508
|
+
_V_DASH_RE = re.compile("[—–]")
|
|
509
|
+
_V_CURLY_RE = re.compile("[‘’“”]")
|
|
510
|
+
_V_BOLD_RE = re.compile(r"\*\*[^*\n]+?\*\*|__[^_\n]+?__")
|
|
511
|
+
_V_LIST_RE = re.compile(r"^\s*(?:[-*+]|\d+[.)])\s+")
|
|
512
|
+
_V_HEADING_LINE_RE = re.compile(r"^\s{0,3}(#{1,6})\s+(.*?)\s*#*\s*$")
|
|
513
|
+
_V_HEADING_ID_RE = re.compile(r"^(?:\d+(?:\.\d+)*\.?|[A-Z]{1,4}-?\d+:?)\s+")
|
|
514
|
+
_V_MINOR = {"a", "an", "the", "and", "or", "of", "to", "in", "on", "for", "with", "by", "at", "as",
|
|
515
|
+
"vs", "via", "nor", "but", "from", "into", "is", "are"}
|
|
516
|
+
|
|
517
|
+
_FENCE_OPEN_RE = re.compile(r"^ {0,3}(`{3,}|~{3,})")
|
|
518
|
+
_INLINE_CODE_RE = re.compile(r"(`+)(?:(?!\1).)+?\1", re.DOTALL)
|
|
519
|
+
_HTML_COMMENT_RE = re.compile(r"<!--.*?-->", re.DOTALL)
|
|
520
|
+
_REF_USE_RE = re.compile(r"(\]\[)[^\]\n]*(\])")
|
|
521
|
+
_AUTOLINK_RE = re.compile(r"<(?:https?|mailto):[^>\n]*>|https?://[^\s)>\]]+")
|
|
522
|
+
_REFDEF_RE = re.compile(r"^ {0,3}\[[^\]\n]+\]:[ \t]*\S.*$")
|
|
523
|
+
_THEMATIC_RE = re.compile(r"^ {0,3}([-*_])(?:[ \t]*\1){2,}[ \t]*$")
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def _blank(match: "re.Match") -> str:
|
|
527
|
+
return " " * len(match.group(0))
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def _spaces(text: str) -> str:
|
|
531
|
+
"""`text` with every character but newlines replaced by a space (offsets stay valid)."""
|
|
532
|
+
return re.sub(r"[^\n]", " ", text)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _mask_destinations(text: str) -> str:
|
|
536
|
+
"""Blank the `(...)` of every `](...)`, matching nested parentheses."""
|
|
537
|
+
out = list(text)
|
|
538
|
+
pos = 0
|
|
539
|
+
while True:
|
|
540
|
+
k = text.find("](", pos)
|
|
541
|
+
if k < 0:
|
|
542
|
+
return "".join(out)
|
|
543
|
+
depth, m = 1, k + 2
|
|
544
|
+
while m < len(text) and depth:
|
|
545
|
+
depth += {"(": 1, ")": -1}.get(text[m], 0)
|
|
546
|
+
m += 1
|
|
547
|
+
end = m if depth == 0 else k + 2
|
|
548
|
+
for q in range(k + 2, max(end - 1, k + 2)):
|
|
549
|
+
if text[q] != "\n":
|
|
550
|
+
out[q] = " "
|
|
551
|
+
pos = max(end, k + 2)
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _mask_inline(text: str) -> str:
|
|
555
|
+
"""Blank inline code (also over line breaks), HTML comments, link destinations and
|
|
556
|
+
identifiers, and autolinks in a block of joined lines."""
|
|
557
|
+
text = _INLINE_CODE_RE.sub(lambda m: _spaces(m.group(0)), text)
|
|
558
|
+
text = _HTML_COMMENT_RE.sub(lambda m: _spaces(m.group(0)), text)
|
|
559
|
+
text = _mask_destinations(text)
|
|
560
|
+
text = re.sub(r"\[[^\]\n]*\]\[\]", lambda m: _spaces(m.group(0)), text) # collapsed [text][]
|
|
561
|
+
text = _REF_USE_RE.sub(lambda m: m.group(1) + _spaces(m.group(0))[2:-1] + m.group(2), text)
|
|
562
|
+
return _AUTOLINK_RE.sub(lambda m: _spaces(m.group(0)), text)
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def mask_prose(lines: List[str]) -> List[str]:
|
|
566
|
+
"""Return `lines` with every region a voice check must not read replaced by spaces, so
|
|
567
|
+
offsets and line numbers stay valid. A line removed whole becomes ""; a line with only part
|
|
568
|
+
masked keeps its length, so a paragraph is never split by masking. Regions: YAML front
|
|
569
|
+
matter; fenced code (closed only by the same character, at least as long); indented code
|
|
570
|
+
outside a list; block quotes including lazy continuation lines; HTML comments; inline code
|
|
571
|
+
(over line breaks too); link destinations, reference identifiers and definitions; autolinks."""
|
|
572
|
+
out: List[str] = []
|
|
573
|
+
i = 0
|
|
574
|
+
if lines and lines[0].strip() == "---":
|
|
575
|
+
for j in range(1, len(lines)):
|
|
576
|
+
if lines[j].strip() == "---":
|
|
577
|
+
out = [""] * (j + 1)
|
|
578
|
+
i = j + 1
|
|
579
|
+
break
|
|
580
|
+
fence = None # (char, length) of the open fence
|
|
581
|
+
in_comment = in_quote = in_list = False
|
|
582
|
+
prev = "blank" # blank | code | para: what the previous line was
|
|
583
|
+
for line in lines[i:]:
|
|
584
|
+
stripped = line.strip()
|
|
585
|
+
if fence:
|
|
586
|
+
if re.match(r"^ {0,3}" + re.escape(fence[0]) + "{" + str(fence[1]) + r",}[ \t]*$", line):
|
|
587
|
+
fence = None
|
|
588
|
+
out.append("")
|
|
589
|
+
prev = "code"
|
|
590
|
+
continue
|
|
591
|
+
if in_comment:
|
|
592
|
+
in_comment = "-->" not in line
|
|
593
|
+
out.append("")
|
|
594
|
+
continue
|
|
595
|
+
fm = _FENCE_OPEN_RE.match(line)
|
|
596
|
+
if fm:
|
|
597
|
+
fence = (fm.group(1)[0], len(fm.group(1)))
|
|
598
|
+
out.append("")
|
|
599
|
+
in_quote = False
|
|
600
|
+
prev = "code"
|
|
601
|
+
continue
|
|
602
|
+
if stripped.startswith("<!--"):
|
|
603
|
+
in_comment = "-->" not in line
|
|
604
|
+
out.append("")
|
|
605
|
+
continue
|
|
606
|
+
if not stripped:
|
|
607
|
+
out.append("")
|
|
608
|
+
in_quote = False
|
|
609
|
+
prev = "blank"
|
|
610
|
+
continue
|
|
611
|
+
is_list = bool(_V_LIST_RE.match(line)) and not line.startswith((" ", "\t"))
|
|
612
|
+
if stripped.startswith(">"):
|
|
613
|
+
in_quote = True
|
|
614
|
+
out.append("")
|
|
615
|
+
prev = "para"
|
|
616
|
+
continue
|
|
617
|
+
if in_quote and not (_V_HEADING_LINE_RE.match(line) or is_list or _THEMATIC_RE.match(line)):
|
|
618
|
+
out.append("") # lazy continuation of a block quote
|
|
619
|
+
prev = "para"
|
|
620
|
+
continue
|
|
621
|
+
in_quote = False
|
|
622
|
+
indented = line.startswith((" ", "\t"))
|
|
623
|
+
if indented and not in_list and prev != "para":
|
|
624
|
+
out.append("")
|
|
625
|
+
prev = "code"
|
|
626
|
+
continue
|
|
627
|
+
if is_list:
|
|
628
|
+
in_list = True
|
|
629
|
+
elif not indented:
|
|
630
|
+
in_list = False
|
|
631
|
+
prev = "heading" if _V_HEADING_LINE_RE.match(line) else "para"
|
|
632
|
+
out.append(line)
|
|
633
|
+
|
|
634
|
+
# Second pass: inline masks, over each run of consecutive kept lines so a span may cross a break.
|
|
635
|
+
k = 0
|
|
636
|
+
while k < len(out):
|
|
637
|
+
if out[k] == "" or not lines[k].strip():
|
|
638
|
+
k += 1
|
|
639
|
+
continue
|
|
640
|
+
m = k
|
|
641
|
+
while m < len(out) and out[m] != "" and lines[m].strip():
|
|
642
|
+
m += 1
|
|
643
|
+
block = _mask_inline("\n".join(out[k:m])).split("\n")
|
|
644
|
+
for n, text in enumerate(block):
|
|
645
|
+
out[k + n] = _REFDEF_RE.sub(lambda r: " " * len(r.group(0)), text)
|
|
646
|
+
k = m
|
|
647
|
+
return out
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def _voice_paragraphs(masked: List[str], lines: List[str]) -> List[Tuple[int, List[str]]]:
|
|
651
|
+
"""Paragraphs of masked lines as (0-based start index, lines). A line that is empty in the
|
|
652
|
+
source, or removed whole by masking (""), ends a paragraph; so do a heading and each list
|
|
653
|
+
item. A line that only had inline code masked stays in its paragraph."""
|
|
654
|
+
paras: List[Tuple[int, List[str]]] = []
|
|
655
|
+
start, block = None, []
|
|
656
|
+
|
|
657
|
+
def flush():
|
|
658
|
+
nonlocal start, block
|
|
659
|
+
if block:
|
|
660
|
+
paras.append((start, block))
|
|
661
|
+
start, block = None, []
|
|
662
|
+
|
|
663
|
+
for i, line in enumerate(masked):
|
|
664
|
+
if line == "" or not lines[i].strip():
|
|
665
|
+
flush()
|
|
666
|
+
continue
|
|
667
|
+
if _V_HEADING_LINE_RE.match(line):
|
|
668
|
+
flush()
|
|
669
|
+
paras.append((i, [line]))
|
|
670
|
+
continue
|
|
671
|
+
if _V_LIST_RE.match(line):
|
|
672
|
+
flush()
|
|
673
|
+
if start is None:
|
|
674
|
+
start = i
|
|
675
|
+
block.append(line)
|
|
676
|
+
flush()
|
|
677
|
+
return paras
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
def _quote(text: str, limit: int = 60) -> str:
|
|
681
|
+
text = " ".join(text.split())
|
|
682
|
+
return text if len(text) <= limit else text[: limit - 3] + "..."
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def _is_title_case(heading: str) -> bool:
|
|
686
|
+
heading = _V_HEADING_ID_RE.sub("", heading) # "3.", "R1:" and "FR-001:" are identifiers, not words
|
|
687
|
+
words = re.findall(r"[A-Za-z][A-Za-z'-]*", heading)
|
|
688
|
+
rest = [w for w in words[1:] if w.lower() not in _V_MINOR and len(w) >= 3]
|
|
689
|
+
return len(rest) >= 2 and all(w[0].isupper() for w in rest)
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def check_voice(path: Path, lines: List[str]) -> List[Dict]:
|
|
693
|
+
findings: List[Dict] = []
|
|
694
|
+
masked = mask_prose(lines)
|
|
695
|
+
for start_idx, para in _voice_paragraphs(masked, lines):
|
|
696
|
+
strong: List[Tuple[int, str, str]] = [] # (1-based line, kind, quoted text)
|
|
697
|
+
weak: List[Tuple[int, str, str]] = []
|
|
698
|
+
dashes: List[Tuple[int, int]] = []
|
|
699
|
+
curly: List[Tuple[int, int]] = []
|
|
700
|
+
bold: List[Tuple[int, str]] = []
|
|
701
|
+
|
|
702
|
+
for off, line in enumerate(para):
|
|
703
|
+
ln = start_idx + off + 1
|
|
704
|
+
text = line
|
|
705
|
+
hm = _V_HEADING_LINE_RE.match(line)
|
|
706
|
+
if hm and _is_title_case(hm.group(2)):
|
|
707
|
+
strong.append((ln, "title_case_heading", _quote(hm.group(2))))
|
|
708
|
+
for kind, rx in _V_STRONG_RES:
|
|
709
|
+
for m in rx.finditer(text):
|
|
710
|
+
strong.append((ln, kind, _quote(m.group(0))))
|
|
711
|
+
for kind, rx in _V_WEAK_RES:
|
|
712
|
+
for m in rx.finditer(text):
|
|
713
|
+
weak.append((ln, kind, _quote(m.group(0))))
|
|
714
|
+
for m in _V_DASH_RE.finditer(text):
|
|
715
|
+
dashes.append((ln, m.start()))
|
|
716
|
+
for m in _V_CURLY_RE.finditer(text):
|
|
717
|
+
curly.append((ln, m.start()))
|
|
718
|
+
lead = _V_LIST_RE.match(text)
|
|
719
|
+
body_start = lead.end() if lead else len(text) - len(text.lstrip())
|
|
720
|
+
for m in _V_BOLD_RE.finditer(text):
|
|
721
|
+
inner = m.group(0)[2:-2].strip()
|
|
722
|
+
if m.start() <= body_start or inner.endswith(":"):
|
|
723
|
+
continue # a label or a defined term opening the item, not emphasis
|
|
724
|
+
bold.append((ln, m.group(0)))
|
|
725
|
+
|
|
726
|
+
def around(ln: int, col: int) -> str:
|
|
727
|
+
src = para[ln - start_idx - 1]
|
|
728
|
+
return _quote(src[max(0, col - 20): col + 20])
|
|
729
|
+
|
|
730
|
+
if len(dashes) >= 2:
|
|
731
|
+
strong.append((dashes[0][0], "dash_habit", around(*dashes[0])))
|
|
732
|
+
elif dashes:
|
|
733
|
+
weak.append((dashes[0][0], "dash_habit", around(*dashes[0])))
|
|
734
|
+
if curly:
|
|
735
|
+
weak.append((curly[0][0], "curly_quote", around(*curly[0])))
|
|
736
|
+
if len(bold) >= 3:
|
|
737
|
+
strong.append((bold[0][0], "bold_density", _quote(" ".join(b[1] for b in bold[:3]))))
|
|
738
|
+
elif bold:
|
|
739
|
+
weak.append((bold[0][0], "bold_density", _quote(bold[0][1])))
|
|
740
|
+
|
|
741
|
+
if not (para and _V_LIST_RE.match(para[0]) or para[0].lstrip().startswith("|")):
|
|
742
|
+
joined = " ".join(p.strip() for p in para)
|
|
743
|
+
sents = [s for s in _SENTENCE_SPLIT_RE.split(joined) if s.strip()]
|
|
744
|
+
openers = []
|
|
745
|
+
for s in sents:
|
|
746
|
+
om = re.match(r"[A-Za-z][A-Za-z'-]*", s.lstrip(" \"'(*_"))
|
|
747
|
+
openers.append(om.group(0) if om else None)
|
|
748
|
+
for i in range(len(openers) - 2):
|
|
749
|
+
a, b, c = openers[i:i + 3]
|
|
750
|
+
if a and b and c and len(a) >= 2 and a.lower() == b.lower() == c.lower():
|
|
751
|
+
pos = joined.find(sents[i])
|
|
752
|
+
strong.append((_offset_to_line(start_idx, para, max(pos, 0)),
|
|
753
|
+
"repeated_opener", f"{a} (opens 3 consecutive sentences)"))
|
|
754
|
+
break
|
|
755
|
+
|
|
756
|
+
tells = len(strong) + len(weak)
|
|
757
|
+
reported = list(strong) + (list(weak) if tells >= 3 else [])
|
|
758
|
+
for ln, kind, quoted in sorted(reported, key=lambda t: (t[0], t[1])):
|
|
759
|
+
is_weak = (ln, kind, quoted) in weak
|
|
760
|
+
findings.append(make_finding(
|
|
761
|
+
path, ln, f"voice_{kind}", "low",
|
|
762
|
+
f"{kind}: \"{quoted}\"" + (" (weak tell, shown because the paragraph has others)"
|
|
763
|
+
if is_weak else "")))
|
|
764
|
+
return findings
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
# --------------------------------------------------------------------------------------
|
|
768
|
+
# Facts diff: what a rewrite must not change (opt-in `--facts-diff BEFORE AFTER`; row 132)
|
|
769
|
+
# --------------------------------------------------------------------------------------
|
|
770
|
+
#
|
|
771
|
+
# Extracts numbers, capitalised names, dates, URLs and citation markers from two versions of a
|
|
772
|
+
# file. A fact only in AFTER is an error (the rewrite added it); a fact only in BEFORE is a
|
|
773
|
+
# warning (the rewrite dropped or changed it). Presence is compared, not counts. Dates are
|
|
774
|
+
# compared as ISO dates ("March 14, 2026" and "14 March 2026" are one fact). Lines wrapped
|
|
775
|
+
# inside a paragraph are joined first, so a name split over two lines is one name.
|
|
776
|
+
#
|
|
777
|
+
# Names are a heuristic. A run of capitalised words is a name. A single capitalised word that
|
|
778
|
+
# opens a sentence is a name only when the same word is capitalised mid-sentence in either
|
|
779
|
+
# version, or has an inner capital or digit ("PostgreSQL"): so a name that moves to or from a
|
|
780
|
+
# sentence start still matches itself, and "Run" turned into "Execute" is not an added fact.
|
|
781
|
+
# Words in `_F_STOP_START` are never names.
|
|
782
|
+
|
|
783
|
+
_MONTHS = (r"(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|"
|
|
784
|
+
r"Aug(?:ust)?|Sep(?:t(?:ember)?)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)\.?")
|
|
785
|
+
_MONTH_NUMBER = {m: n for n, m in enumerate(
|
|
786
|
+
"jan feb mar apr may jun jul aug sep oct nov dec".split(), 1)}
|
|
787
|
+
_F_URL_RE = re.compile(r"https?://[^\s)>\]\"'`]+")
|
|
788
|
+
_F_DATE_RE = re.compile(
|
|
789
|
+
rf"\b\d{{4}}-\d{{2}}-\d{{2}}\b|\b{_MONTHS}\s+\d{{1,2}}(?:st|nd|rd|th)?,?\s+\d{{4}}\b|"
|
|
790
|
+
rf"\b\d{{1,2}}(?:st|nd|rd|th)?\s+{_MONTHS}\s+\d{{4}}\b|\b{_MONTHS}\s+\d{{4}}\b")
|
|
791
|
+
_F_DATE_FORMS = [
|
|
792
|
+
(re.compile(r"(\d{4})-(\d{2})-(\d{2})"), lambda g: f"{g[0]}-{g[1]}-{g[2]}"),
|
|
793
|
+
(re.compile(rf"({_MONTHS})\s+(\d{{1,2}})(?:st|nd|rd|th)?,?\s+(\d{{4}})"),
|
|
794
|
+
lambda g: f"{g[2]}-{_month(g[0]):02d}-{int(g[1]):02d}"),
|
|
795
|
+
(re.compile(rf"(\d{{1,2}})(?:st|nd|rd|th)?\s+({_MONTHS})\s+(\d{{4}})"),
|
|
796
|
+
lambda g: f"{g[2]}-{_month(g[1]):02d}-{int(g[0]):02d}"),
|
|
797
|
+
(re.compile(rf"({_MONTHS})\s+(\d{{4}})"), lambda g: f"{g[1]}-{_month(g[0]):02d}"),
|
|
798
|
+
]
|
|
799
|
+
_F_CITE_RE = re.compile(
|
|
800
|
+
r"\[\^[^\]\s]+\]|\[@[^\]\s]+\]|\[\d+(?:\s*[,–-]\s*\d+)*\]|"
|
|
801
|
+
r"\([A-Z][A-Za-z-]+(?: et al\.| (?:and|&) [A-Z][A-Za-z-]+)?,? \d{4}[a-z]?\)")
|
|
802
|
+
_F_NUM_RE = re.compile(r"(?<![\w.])\d[\d,]*(?:\.\d+)*%?(?![\w])")
|
|
803
|
+
_F_STOP_START = {
|
|
804
|
+
"The", "A", "An", "This", "That", "These", "Those", "It", "Its", "We", "Our", "You", "Your", "In",
|
|
805
|
+
"On", "At", "For", "If", "When", "While", "Then", "There", "Here", "And", "But", "Or", "So",
|
|
806
|
+
"As", "By", "With", "Without", "To", "Of", "Each", "Every", "Some", "Any", "All", "No", "Not",
|
|
807
|
+
"See", "Note", "Use", "Run", "Read", "Set", "Add", "Make", "Keep", "Do", "Does", "Is", "Are",
|
|
808
|
+
"Was", "Were", "Be", "After", "Before", "Once", "Since", "Because", "However", "Also", "Both",
|
|
809
|
+
"Most", "Many", "One", "Two", "Three", "Only", "Even", "Still", "Yet", "Now", "Today",
|
|
810
|
+
}
|
|
811
|
+
_F_NAME_RUN_RE = re.compile(r"[A-Z][\w'-]*(?:[ \t]+[A-Z][\w'-]*)*")
|
|
812
|
+
_F_LAYOUT_PREFIX_RE = re.compile(r"[\s\-+*>|]*(?:\d+[.)])?[\s\-+*>|]*")
|
|
813
|
+
_F_BLOCK_START_RE = re.compile(r"^\s*(?:[-*+>|#]|\d+[.)]|`{3,}|~{3,})")
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def _month(name: str) -> int:
|
|
817
|
+
return _MONTH_NUMBER[name.lower()[:3]]
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
def _iso_date(text: str) -> str:
|
|
821
|
+
for rx, fmt in _F_DATE_FORMS:
|
|
822
|
+
m = rx.fullmatch(text)
|
|
823
|
+
if m:
|
|
824
|
+
return fmt(m.groups())
|
|
825
|
+
return text
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
def _join_wrapped(text: str) -> str:
|
|
829
|
+
"""Replace the line break inside a paragraph with a space (same length, so offsets hold)."""
|
|
830
|
+
lines = text.split("\n")
|
|
831
|
+
out = []
|
|
832
|
+
for idx, line in enumerate(lines):
|
|
833
|
+
out.append(line)
|
|
834
|
+
if idx < len(lines) - 1:
|
|
835
|
+
nxt = lines[idx + 1]
|
|
836
|
+
joinable = bool(line.strip() and nxt.strip() and not _F_BLOCK_START_RE.match(nxt)
|
|
837
|
+
and not line.lstrip().startswith(("|", "```", "~~~")))
|
|
838
|
+
out.append(" " if joinable else "\n")
|
|
839
|
+
return "".join(out)
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
def _extract_facts(text: str) -> Tuple[List[Tuple[str, str, int]], List[Tuple[str, int]]]:
|
|
843
|
+
"""Return (facts, initials). facts are (kind, normalised value, 1-based line); initials are
|
|
844
|
+
the single capitalised words that open a sentence, which `facts_diff` resolves."""
|
|
845
|
+
facts: List[Tuple[str, str, int]] = []
|
|
846
|
+
initials: List[Tuple[str, int]] = []
|
|
847
|
+
line_starts: List[int] = []
|
|
848
|
+
pos = 0
|
|
849
|
+
for line in text.split("\n"):
|
|
850
|
+
line_starts.append(pos)
|
|
851
|
+
pos += len(line) + 1
|
|
852
|
+
|
|
853
|
+
def line_of(offset: int) -> int:
|
|
854
|
+
lo, hi = 0, len(line_starts) - 1
|
|
855
|
+
while lo < hi:
|
|
856
|
+
mid = (lo + hi + 1) // 2
|
|
857
|
+
if line_starts[mid] <= offset:
|
|
858
|
+
lo = mid
|
|
859
|
+
else:
|
|
860
|
+
hi = mid - 1
|
|
861
|
+
return lo + 1
|
|
862
|
+
|
|
863
|
+
work = list(text)
|
|
864
|
+
|
|
865
|
+
def take(rx: "re.Pattern", kind: str, norm=lambda s: s):
|
|
866
|
+
for m in rx.finditer("".join(work)):
|
|
867
|
+
value = m.group(0)
|
|
868
|
+
if kind == "url":
|
|
869
|
+
value = value.rstrip(".,;:!?")
|
|
870
|
+
facts.append((kind, norm(value), line_of(m.start())))
|
|
871
|
+
for k in range(m.start(), m.end()):
|
|
872
|
+
if work[k] != "\n":
|
|
873
|
+
work[k] = " "
|
|
874
|
+
|
|
875
|
+
take(_F_URL_RE, "url")
|
|
876
|
+
take(_F_DATE_RE, "date", lambda s: _iso_date(" ".join(s.split())))
|
|
877
|
+
take(_F_CITE_RE, "citation", lambda s: re.sub(r"\s+", "", s))
|
|
878
|
+
# ordered-list markers are layout, not facts
|
|
879
|
+
body = re.sub(r"(?m)^(\s*)(\d+[.)])(\s)", lambda m: m.group(1) + " " * len(m.group(2)) + m.group(3),
|
|
880
|
+
"".join(work))
|
|
881
|
+
for m in _F_NUM_RE.finditer(body):
|
|
882
|
+
facts.append(("number", m.group(0).replace(",", ""), line_of(m.start())))
|
|
883
|
+
# headings are layout for names; blank them in place so later offsets keep their lines
|
|
884
|
+
name_body = "\n".join(" " * len(l) if re.match(r"^\s{0,3}#{1,6}\s", l) else l for l in body.split("\n"))
|
|
885
|
+
name_body = _join_wrapped(_INLINE_CODE_RE.sub(lambda m: _spaces(m.group(0)), name_body))
|
|
886
|
+
for m in _F_NAME_RUN_RE.finditer(name_body):
|
|
887
|
+
words = m.group(0).split()
|
|
888
|
+
while words and words[0] in _F_STOP_START:
|
|
889
|
+
words.pop(0)
|
|
890
|
+
if not words or (len(words) == 1 and len(words[0]) < 2):
|
|
891
|
+
continue
|
|
892
|
+
prefix = name_body[name_body.rfind("\n", 0, m.start()) + 1:m.start()]
|
|
893
|
+
opener = prefix.rstrip(" \t*_\"'([")
|
|
894
|
+
sentence_start = bool(_F_LAYOUT_PREFIX_RE.fullmatch(prefix)) or opener == "" or opener[-1] in ".!?:"
|
|
895
|
+
if len(words) == 1 and sentence_start and not re.search(r"[A-Z].*[A-Z]|[a-z]\d|\d", words[0]):
|
|
896
|
+
initials.append((words[0], line_of(m.start())))
|
|
897
|
+
continue
|
|
898
|
+
facts.append(("name", " ".join(words), line_of(m.start())))
|
|
899
|
+
return facts, initials
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def facts_diff(before: Path, after: Path) -> List[Dict]:
|
|
903
|
+
"""Compare the facts of two versions. Added facts are errors, dropped ones warnings."""
|
|
904
|
+
b, b_initials = _extract_facts(read_file_content(before))
|
|
905
|
+
a, a_initials = _extract_facts(read_file_content(after))
|
|
906
|
+
confirmed = {v for k, v, _ in b + a if k == "name"}
|
|
907
|
+
b += [("name", w, ln) for w, ln in b_initials if w in confirmed]
|
|
908
|
+
a += [("name", w, ln) for w, ln in a_initials if w in confirmed]
|
|
909
|
+
b_keys = {(k, v) for k, v, _ in b}
|
|
910
|
+
a_keys = {(k, v) for k, v, _ in a}
|
|
911
|
+
findings: List[Dict] = []
|
|
912
|
+
done = set()
|
|
913
|
+
for kind, value, line in a:
|
|
914
|
+
if (kind, value) not in b_keys and (kind, value) not in done:
|
|
915
|
+
done.add((kind, value))
|
|
916
|
+
findings.append(make_finding(
|
|
917
|
+
after, line, f"fact_added_{kind}", "error",
|
|
918
|
+
f"{kind} \"{_quote(value)}\" is in the rewrite but not in the source: "
|
|
919
|
+
"remove it, or restore the source's version."))
|
|
920
|
+
for kind, value, line in b:
|
|
921
|
+
if (kind, value) not in a_keys and (kind, value) not in done:
|
|
922
|
+
done.add((kind, value))
|
|
923
|
+
findings.append(make_finding(
|
|
924
|
+
before, line, f"fact_dropped_{kind}", "warning",
|
|
925
|
+
f"{kind} \"{_quote(value)}\" is in the source but not in the rewrite: "
|
|
926
|
+
"confirm the removal was intended."))
|
|
927
|
+
return findings
|
|
928
|
+
|
|
929
|
+
|
|
420
930
|
# --------------------------------------------------------------------------------------
|
|
421
931
|
# File / directory orchestration
|
|
422
932
|
# --------------------------------------------------------------------------------------
|
|
423
933
|
|
|
424
|
-
def analyze_file(filepath: Path) -> List[Dict]:
|
|
934
|
+
def analyze_file(filepath: Path, voice: bool = False) -> List[Dict]:
|
|
425
935
|
"""Analyze a single markdown file. Returns [] for a non-.md file or an unreadable one —
|
|
426
|
-
the same quiet-skip behavior code_quality_checker.py's directory walk relies on.
|
|
936
|
+
the same quiet-skip behavior code_quality_checker.py's directory walk relies on. `voice`
|
|
937
|
+
adds the opt-in voice-tell check (Check 7); without it the output is unchanged."""
|
|
427
938
|
if filepath.suffix.lower() != ".md":
|
|
428
939
|
return []
|
|
429
940
|
content = read_file_content(filepath)
|
|
@@ -437,17 +948,20 @@ def analyze_file(filepath: Path) -> List[Dict]:
|
|
|
437
948
|
findings.extend(check_description(filepath, lines))
|
|
438
949
|
findings.extend(check_dangling_references(filepath, lines))
|
|
439
950
|
findings.extend(check_staleness(filepath, lines))
|
|
951
|
+
findings.extend(check_uncited_vendor_claims(filepath, lines))
|
|
952
|
+
if voice:
|
|
953
|
+
findings.extend(check_voice(filepath, lines))
|
|
440
954
|
return findings
|
|
441
955
|
|
|
442
956
|
|
|
443
|
-
def analyze_directory(dir_path: Path, recursive: bool = True) -> List[Dict]:
|
|
957
|
+
def analyze_directory(dir_path: Path, recursive: bool = True, voice: bool = False) -> List[Dict]:
|
|
444
958
|
"""Analyze every .md file under a directory."""
|
|
445
959
|
findings: List[Dict] = []
|
|
446
960
|
pattern = "**/*.md" if recursive else "*.md"
|
|
447
961
|
for filepath in sorted(dir_path.glob(pattern)):
|
|
448
962
|
if "node_modules" in str(filepath) or ".git" in str(filepath):
|
|
449
963
|
continue
|
|
450
|
-
findings.extend(analyze_file(filepath))
|
|
964
|
+
findings.extend(analyze_file(filepath, voice))
|
|
451
965
|
return findings
|
|
452
966
|
|
|
453
967
|
|
|
@@ -483,8 +997,21 @@ def main():
|
|
|
483
997
|
)
|
|
484
998
|
parser.add_argument(
|
|
485
999
|
"path",
|
|
1000
|
+
nargs="?",
|
|
486
1001
|
help="File or directory to analyze"
|
|
487
1002
|
)
|
|
1003
|
+
parser.add_argument(
|
|
1004
|
+
"--voice",
|
|
1005
|
+
action="store_true",
|
|
1006
|
+
help="Also report advisory voice tells (overused and sales words, chatbot residue, ...)"
|
|
1007
|
+
)
|
|
1008
|
+
parser.add_argument(
|
|
1009
|
+
"--facts-diff",
|
|
1010
|
+
nargs=2,
|
|
1011
|
+
metavar=("BEFORE", "AFTER"),
|
|
1012
|
+
help="Compare the numbers, names, dates, URLs and citations of two versions of a file: "
|
|
1013
|
+
"a fact only in AFTER is an error (exit 1), one only in BEFORE a warning"
|
|
1014
|
+
)
|
|
488
1015
|
parser.add_argument(
|
|
489
1016
|
"--recursive", "-r",
|
|
490
1017
|
action="store_true",
|
|
@@ -503,6 +1030,21 @@ def main():
|
|
|
503
1030
|
|
|
504
1031
|
args = parser.parse_args()
|
|
505
1032
|
|
|
1033
|
+
if args.facts_diff:
|
|
1034
|
+
before, after = (Path(p).resolve() for p in args.facts_diff)
|
|
1035
|
+
for p in (before, after):
|
|
1036
|
+
if not p.is_file():
|
|
1037
|
+
print(f"Error: Not a file: {p}", file=sys.stderr)
|
|
1038
|
+
sys.exit(1)
|
|
1039
|
+
findings = facts_diff(before, after)
|
|
1040
|
+
if args.json:
|
|
1041
|
+
print(json.dumps(findings, indent=2, default=str))
|
|
1042
|
+
else:
|
|
1043
|
+
print_report(findings, after)
|
|
1044
|
+
sys.exit(1 if any(f["severity"] == "error" for f in findings) else 0)
|
|
1045
|
+
|
|
1046
|
+
if not args.path:
|
|
1047
|
+
parser.error("a path is required unless --facts-diff is given")
|
|
506
1048
|
target = Path(args.path).resolve()
|
|
507
1049
|
|
|
508
1050
|
if not target.exists():
|
|
@@ -510,9 +1052,9 @@ def main():
|
|
|
510
1052
|
sys.exit(1)
|
|
511
1053
|
|
|
512
1054
|
if target.is_file():
|
|
513
|
-
findings = analyze_file(target)
|
|
1055
|
+
findings = analyze_file(target, args.voice)
|
|
514
1056
|
else:
|
|
515
|
-
findings = analyze_directory(target, args.recursive)
|
|
1057
|
+
findings = analyze_directory(target, args.recursive, args.voice)
|
|
516
1058
|
if not findings and not any(target.glob("**/*.md" if args.recursive else "*.md")):
|
|
517
1059
|
print(f"Warning: no .md files found under {display_path(target)}", file=sys.stderr)
|
|
518
1060
|
|