@kontourai/flow-agents 0.1.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/.githooks/pre-push +11 -0
- package/.github/workflows/ci.yml +210 -0
- package/.github/workflows/docs-pages.yml +52 -0
- package/.github/workflows/publish-npm.yml +104 -0
- package/AGENTS.md +26 -0
- package/CHANGELOG.md +66 -0
- package/CODE_OF_CONDUCT.md +25 -0
- package/CONTEXT.md +300 -0
- package/CONTRIBUTING.md +44 -0
- package/LICENSE +201 -0
- package/README.md +129 -0
- package/SECURITY.md +33 -0
- package/agent-cards/dev.json +19 -0
- package/agents/dev.json +127 -0
- package/agents/tool-code-reviewer.json +61 -0
- package/agents/tool-dependencies-updater.json +118 -0
- package/agents/tool-explore-config.json +92 -0
- package/agents/tool-explore-deps.json +92 -0
- package/agents/tool-explore-entry.json +92 -0
- package/agents/tool-explore-patterns.json +92 -0
- package/agents/tool-explore-structure.json +92 -0
- package/agents/tool-explore-tests.json +92 -0
- package/agents/tool-planner.json +57 -0
- package/agents/tool-playwright.json +145 -0
- package/agents/tool-security-reviewer.json +56 -0
- package/agents/tool-verifier.json +61 -0
- package/agents/tool-worker.json +58 -0
- package/build/src/cli/console-learning-projection.js +123 -0
- package/build/src/cli/docs-preview.js +39 -0
- package/build/src/cli/effective-backlog-settings.js +102 -0
- package/build/src/cli/export-bookmarks.js +38 -0
- package/build/src/cli/fixture-retirement-audit.js +140 -0
- package/build/src/cli/flow-kit.js +138 -0
- package/build/src/cli/import-bookmarks.js +50 -0
- package/build/src/cli/init.js +239 -0
- package/build/src/cli/instinct-cli.js +93 -0
- package/build/src/cli/promote-workflow-artifact.js +63 -0
- package/build/src/cli/publish-change-helper.js +154 -0
- package/build/src/cli/pull-work-provider.js +469 -0
- package/build/src/cli/runtime-adapter.js +23 -0
- package/build/src/cli/telemetry-doctor.js +221 -0
- package/build/src/cli/usage-feedback.js +443 -0
- package/build/src/cli/validate-hook-influence.js +152 -0
- package/build/src/cli/validate-source-tree.js +31 -0
- package/build/src/cli/validate-workflow-artifacts.js +486 -0
- package/build/src/cli/veritas-governance.js +262 -0
- package/build/src/cli/workflow-artifact-cleanup-audit.js +272 -0
- package/build/src/cli/workflow-sidecar.js +816 -0
- package/build/src/cli.js +89 -0
- package/build/src/flow-kit/validate.js +75 -0
- package/build/src/lib/args.js +45 -0
- package/build/src/lib/fs.js +62 -0
- package/build/src/lib/workflow-learning-projection.js +334 -0
- package/build/src/runtime-adapters.js +146 -0
- package/build/src/tools/build-universal-bundles.js +397 -0
- package/build/src/tools/common.js +56 -0
- package/build/src/tools/filter-installed-packs.js +132 -0
- package/build/src/tools/generate-context-map.js +198 -0
- package/build/src/tools/validate-package.js +64 -0
- package/build/src/tools/validate-source-tree.js +622 -0
- package/console.telemetry.json +176 -0
- package/context/base-rules.md +17 -0
- package/context/code-review-standards.md +62 -0
- package/context/coding-standards.md +42 -0
- package/context/common/orchestrators.md +12 -0
- package/context/common/subagents.md +28 -0
- package/context/contracts/artifact-contract.md +182 -0
- package/context/contracts/builder-kit-workflow-state-contract.md +319 -0
- package/context/contracts/delivery-contract.md +69 -0
- package/context/contracts/execution-contract.md +53 -0
- package/context/contracts/governance-adapter-contract.md +67 -0
- package/context/contracts/planning-contract.md +85 -0
- package/context/contracts/review-contract.md +104 -0
- package/context/contracts/sandbox-policy.md +52 -0
- package/context/contracts/verification-contract.md +134 -0
- package/context/contracts/work-item-contract.md +215 -0
- package/context/deferred/demo-mode.md +33 -0
- package/context/deferred/languages/go.md +31 -0
- package/context/deferred/languages/python.md +31 -0
- package/context/deferred/languages/typescript.md +34 -0
- package/context/deferred/parallelization.md +35 -0
- package/context/deferred/worktree-isolation.md +24 -0
- package/context/development-workflow.md +50 -0
- package/context/scripts/context-budget/budget-scan.sh +166 -0
- package/context/scripts/detect-tools.sh +3 -0
- package/context/scripts/discover-agents.sh +28 -0
- package/context/scripts/git-status.sh +49 -0
- package/context/scripts/hooks/config-protection.js +79 -0
- package/context/scripts/hooks/desktop-notify.sh +39 -0
- package/context/scripts/hooks/governance-audit.sh +135 -0
- package/context/scripts/hooks/lib/audit-transport.sh +40 -0
- package/context/scripts/hooks/lib/hook-flags.js +49 -0
- package/context/scripts/hooks/lib/patterns.sh +57 -0
- package/context/scripts/hooks/lib/resolve-formatter.js +80 -0
- package/context/scripts/hooks/post-edit-accumulator.js +66 -0
- package/context/scripts/hooks/pre-commit-quality.js +194 -0
- package/context/scripts/hooks/quality-gate.js +93 -0
- package/context/scripts/hooks/report-only-guard.js +21 -0
- package/context/scripts/hooks/run-hook.js +136 -0
- package/context/scripts/hooks/stop-format-typecheck.js +141 -0
- package/context/scripts/hooks/stop-goal-fit.js +337 -0
- package/context/scripts/hooks/workflow-steering.js +250 -0
- package/context/scripts/telemetry/console-presets.sh +14 -0
- package/context/scripts/telemetry/install-console-config.sh +214 -0
- package/context/scripts/telemetry/lib/config.sh +85 -0
- package/context/scripts/telemetry/lib/enrich.sh +115 -0
- package/context/scripts/telemetry/lib/redact.sh +22 -0
- package/context/scripts/telemetry/lib/session.sh +63 -0
- package/context/scripts/telemetry/lib/transport.sh +183 -0
- package/context/scripts/telemetry/lib/usage.sh +29 -0
- package/context/scripts/telemetry/sync-agents.sh +173 -0
- package/context/scripts/telemetry/telemetry.conf +23 -0
- package/context/scripts/telemetry/telemetry.sh +387 -0
- package/context/scripts/validate-package.sh +89 -0
- package/context/settings/backlog-provider-settings.json +54 -0
- package/context/templates/core/identity.md +26 -0
- package/context/templates/core/user.md +15 -0
- package/docs/_config.yml +15 -0
- package/docs/_layouts/default.html +87 -0
- package/docs/adr/0001-flow-agents-consumes-flow.md +77 -0
- package/docs/adr/0002-flow-kits-as-extension-unit.md +13 -0
- package/docs/adr/0003-flow-agents-coordinates-kits-and-adapters.md +13 -0
- package/docs/adr/0004-gates-expect-surface-claims.md +15 -0
- package/docs/adr/0005-kubernetes-inspired-resource-contracts.md +48 -0
- package/docs/adr/0006-typescript-first-source-policy.md +98 -0
- package/docs/agent-system-guidebook.md +391 -0
- package/docs/agent-usage-feedback-loop.md +351 -0
- package/docs/assets/favicon.svg +13 -0
- package/docs/assets/og-image.png +0 -0
- package/docs/assets/site.css +774 -0
- package/docs/assets/site.js +139 -0
- package/docs/configurable-workflow-routing.md +174 -0
- package/docs/context-map.md +145 -0
- package/docs/developer-architecture.md +145 -0
- package/docs/developer-hook-setup.md +61 -0
- package/docs/fixture-ownership.md +44 -0
- package/docs/flow-kit-repository-contract.md +180 -0
- package/docs/index.md +129 -0
- package/docs/kontour-resource-contract.md +358 -0
- package/docs/migrations.md +64 -0
- package/docs/north-star.md +322 -0
- package/docs/operating-layers.md +110 -0
- package/docs/repository-structure.md +132 -0
- package/docs/sandbox-policy.md +56 -0
- package/docs/skills-map.md +203 -0
- package/docs/standards-register.md +96 -0
- package/docs/veritas-integration.md +165 -0
- package/docs/work-item-adapters.md +72 -0
- package/docs/workflow-artifact-lifecycle.md +141 -0
- package/docs/workflow-eval-strategy.md +295 -0
- package/docs/workflow-shared-contracts.md +51 -0
- package/docs/workflow-usage-guide.md +443 -0
- package/evals/ARCHITECTURE.md +143 -0
- package/evals/CONVENTIONS.md +58 -0
- package/evals/README.md +128 -0
- package/evals/acceptance/run.sh +29 -0
- package/evals/acceptance/test_claude_harness.sh +242 -0
- package/evals/acceptance/test_codex_harness.sh +108 -0
- package/evals/acceptance/test_kiro_harness.sh +128 -0
- package/evals/cases/dev/404.html +97 -0
- package/evals/cases/dev/code-review.yaml +44 -0
- package/evals/cases/dev/dashboard.html +300 -0
- package/evals/cases/dev/deliver.yaml +66 -0
- package/evals/cases/dev/dependency-update.yaml +16 -0
- package/evals/cases/dev/explore.yaml +20 -0
- package/evals/cases/dev/index.html +370 -0
- package/evals/cases/dev/package-lock.json +28 -0
- package/evals/cases/dev/package.json +16 -0
- package/evals/cases/dev/plan-work.yaml +20 -0
- package/evals/cases/dev/promptfooconfig.yaml +666 -0
- package/evals/cases/dev/search-first.yaml +20 -0
- package/evals/cases/dev/tdd-workflow.yaml +48 -0
- package/evals/cases/dev/verify-work.yaml +44 -0
- package/evals/cases/dev/workflow.yaml +34 -0
- package/evals/ci/run-baseline.sh +283 -0
- package/evals/fixtures/backlog-provider-settings/global-default.json +44 -0
- package/evals/fixtures/backlog-provider-settings/project-override.json +53 -0
- package/evals/fixtures/builder-kit-workflow-state/baseline-freshness-resolution-hint.json +139 -0
- package/evals/fixtures/builder-kit-workflow-state/direct-primitive-stop.json +59 -0
- package/evals/fixtures/builder-kit-workflow-state/empty-board-route-shape.json +55 -0
- package/evals/fixtures/builder-kit-workflow-state/happy-path.json +71 -0
- package/evals/fixtures/builder-kit-workflow-state/mid-work-resume.json +80 -0
- package/evals/fixtures/builder-kit-workflow-state/missing-prestep-recovery.json +65 -0
- package/evals/fixtures/builder-kit-workflow-state/product-build-chaining.json +60 -0
- package/evals/fixtures/builder-kit-workflow-state/stale-continuation-requires-new-probe.json +57 -0
- package/evals/fixtures/console-learning-projection/artifacts/console-learning-correction/learning.json +50 -0
- package/evals/fixtures/console-learning-projection/artifacts/console-learning-open-route/learning.json +41 -0
- package/evals/fixtures/flow-kit-repository/invalid-absolute-path/kit.json +8 -0
- package/evals/fixtures/flow-kit-repository/invalid-asset-section/flows/review.flow.json +6 -0
- package/evals/fixtures/flow-kit-repository/invalid-asset-section/kit.json +11 -0
- package/evals/fixtures/flow-kit-repository/invalid-duplicate-flow/flows/review.flow.json +6 -0
- package/evals/fixtures/flow-kit-repository/invalid-duplicate-flow/kit.json +9 -0
- package/evals/fixtures/flow-kit-repository/invalid-id/flows/review.flow.json +6 -0
- package/evals/fixtures/flow-kit-repository/invalid-id/kit.json +8 -0
- package/evals/fixtures/flow-kit-repository/invalid-malformed-json/kit.json +8 -0
- package/evals/fixtures/flow-kit-repository/invalid-missing-flow/kit.json +8 -0
- package/evals/fixtures/flow-kit-repository/invalid-missing-id/flows/review.flow.json +6 -0
- package/evals/fixtures/flow-kit-repository/invalid-missing-id/kit.json +7 -0
- package/evals/fixtures/flow-kit-repository/invalid-missing-schema-version/flows/review.flow.json +6 -0
- package/evals/fixtures/flow-kit-repository/invalid-missing-schema-version/kit.json +7 -0
- package/evals/fixtures/flow-kit-repository/invalid-name/flows/review.flow.json +6 -0
- package/evals/fixtures/flow-kit-repository/invalid-name/kit.json +8 -0
- package/evals/fixtures/flow-kit-repository/invalid-schema-version/flows/review.flow.json +6 -0
- package/evals/fixtures/flow-kit-repository/invalid-schema-version/kit.json +8 -0
- package/evals/fixtures/flow-kit-repository/invalid-traversal/kit.json +8 -0
- package/evals/fixtures/flow-kit-repository/mixed-runtime-kit/adapters/example.json +3 -0
- package/evals/fixtures/flow-kit-repository/mixed-runtime-kit/assets/example.txt +1 -0
- package/evals/fixtures/flow-kit-repository/mixed-runtime-kit/docs/README.md +3 -0
- package/evals/fixtures/flow-kit-repository/mixed-runtime-kit/flows/runtime.flow.json +26 -0
- package/evals/fixtures/flow-kit-repository/mixed-runtime-kit/kit-evals/example.json +3 -0
- package/evals/fixtures/flow-kit-repository/mixed-runtime-kit/kit-skills/mixed/SKILL.md +3 -0
- package/evals/fixtures/flow-kit-repository/mixed-runtime-kit/kit.json +44 -0
- package/evals/fixtures/flow-kit-repository/valid-local-kit/docs/README.md +3 -0
- package/evals/fixtures/flow-kit-repository/valid-local-kit/flows/review.flow.json +26 -0
- package/evals/fixtures/flow-kit-repository/valid-local-kit/kit.json +20 -0
- package/evals/fixtures/hook-influence/cases.json +336 -0
- package/evals/fixtures/pull-work-provider/github-issues.json +170 -0
- package/evals/fixtures/pull-work-wip-shepherding/global-wip-informs.json +43 -0
- package/evals/fixtures/pull-work-wip-shepherding/personal-wip-blocks.json +42 -0
- package/evals/fixtures/surface-trust/accepted-claim-trust-report.json +31 -0
- package/evals/fixtures/surface-trust/artifact-absent.json +19 -0
- package/evals/fixtures/surface-trust/integrity-mismatch-trust-report.json +32 -0
- package/evals/fixtures/surface-trust/missing-authority-trust-report.json +27 -0
- package/evals/fixtures/surface-trust/provider-absent.json +19 -0
- package/evals/fixtures/surface-trust/rejected-claim-trust-report.json +30 -0
- package/evals/fixtures/surface-trust/stale-claim-trust-snapshot.json +31 -0
- package/evals/fixtures/usage-feedback/sample-full.jsonl +11 -0
- package/evals/fixtures/usage-feedback/sample-outcomes.jsonl +1 -0
- package/evals/fixtures/veritas-governance-adapter/fake-veritas-pass.sh +18 -0
- package/evals/fixtures/veritas-governance-adapter/fake-veritas-secret-fail.sh +10 -0
- package/evals/fixtures/veritas-governance-adapter/fake-veritas-unconfigured.sh +4 -0
- package/evals/integration/test_bundle_install.sh +541 -0
- package/evals/integration/test_console_learning_projection.sh +192 -0
- package/evals/integration/test_context_map.sh +65 -0
- package/evals/integration/test_effective_backlog_settings.sh +58 -0
- package/evals/integration/test_fixture_retirement_audit.sh +58 -0
- package/evals/integration/test_flow_agents_statusline.sh +93 -0
- package/evals/integration/test_flow_kit_repository.sh +90 -0
- package/evals/integration/test_goal_fit_hook.sh +482 -0
- package/evals/integration/test_hook_category_behaviors.sh +190 -0
- package/evals/integration/test_hook_influence_cases.sh +69 -0
- package/evals/integration/test_local_flow_kit_install.sh +145 -0
- package/evals/integration/test_publish_change_helper.sh +176 -0
- package/evals/integration/test_pull_work_provider.sh +140 -0
- package/evals/integration/test_runtime_adapter_activation.sh +106 -0
- package/evals/integration/test_telemetry.sh +485 -0
- package/evals/integration/test_telemetry_doctor.sh +193 -0
- package/evals/integration/test_usage_feedback_dashboard.sh +169 -0
- package/evals/integration/test_usage_feedback_global.sh +117 -0
- package/evals/integration/test_usage_feedback_import.sh +227 -0
- package/evals/integration/test_usage_feedback_outcomes.sh +165 -0
- package/evals/integration/test_usage_feedback_report.sh +263 -0
- package/evals/integration/test_veritas_governance_adapter.sh +235 -0
- package/evals/integration/test_workflow_artifact_cleanup_audit.sh +287 -0
- package/evals/integration/test_workflow_artifacts.sh +1247 -0
- package/evals/integration/test_workflow_sidecar_writer.sh +2112 -0
- package/evals/integration/test_workflow_steering_hook.sh +337 -0
- package/evals/lib/assertions/delegated-to.js +40 -0
- package/evals/lib/assertions/max-tool-calls.js +15 -0
- package/evals/lib/assertions/no-write-tools.js +27 -0
- package/evals/lib/assertions/pass-at-k.js +39 -0
- package/evals/lib/assertions/telemetry-utils.js +105 -0
- package/evals/lib/assertions/tool-called.js +39 -0
- package/evals/lib/assertions/verify-after-fix.js +61 -0
- package/evals/lib/claude-judge.sh +40 -0
- package/evals/lib/claude-provider.sh +74 -0
- package/evals/lib/codex-judge.sh +39 -0
- package/evals/lib/codex-provider.sh +81 -0
- package/evals/lib/eval-dev.sh +5 -0
- package/evals/lib/eval-judge.sh +22 -0
- package/evals/lib/eval-provider.sh +26 -0
- package/evals/lib/eval-report.sh +73 -0
- package/evals/lib/kiro-dev.sh +4 -0
- package/evals/lib/kiro-judge.sh +17 -0
- package/evals/lib/kiro-provider.sh +62 -0
- package/evals/lib/node.sh +111 -0
- package/evals/promptfooconfig.yaml +70 -0
- package/evals/run.sh +309 -0
- package/evals/static/test_evidence_refs.sh +141 -0
- package/evals/static/test_package.sh +407 -0
- package/evals/static/test_repo_hooks.sh +68 -0
- package/evals/static/test_universal_bundles.sh +274 -0
- package/evals/static/test_workflow_skills.sh +1207 -0
- package/install.sh +64 -0
- package/integrations/veritas/flow-agents.adapter.json +138 -0
- package/integrations/veritas/flow-agents.authority-settings.json +26 -0
- package/integrations/veritas/flow-agents.repo-standards.json +82 -0
- package/kits/builder/flows/build.flow.json +218 -0
- package/kits/builder/flows/shape.flow.json +127 -0
- package/kits/builder/kit.json +19 -0
- package/kits/catalog.json +11 -0
- package/package.json +130 -0
- package/packaging/README.md +60 -0
- package/packaging/manifest.json +173 -0
- package/packaging/packs.json +69 -0
- package/powers/dependency-checker/POWER.md +20 -0
- package/powers/dependency-checker/mcp.json +20 -0
- package/powers/playwright/POWER.md +25 -0
- package/powers/playwright/mcp.json +12 -0
- package/prompts/code-audit.md +123 -0
- package/prompts/kcommit.md +88 -0
- package/schemas/backlog-provider-settings.schema.json +138 -0
- package/schemas/workflow-acceptance.schema.json +216 -0
- package/schemas/workflow-critique.schema.json +113 -0
- package/schemas/workflow-evidence.schema.json +357 -0
- package/schemas/workflow-handoff.schema.json +52 -0
- package/schemas/workflow-learning.schema.json +223 -0
- package/schemas/workflow-release.schema.json +172 -0
- package/schemas/workflow-state.schema.json +80 -0
- package/scripts/README.md +111 -0
- package/scripts/build-universal-bundles.js +3 -0
- package/scripts/check-content-boundary.cjs +99 -0
- package/scripts/context-budget/budget-scan.sh +166 -0
- package/scripts/detect-tools.sh +3 -0
- package/scripts/discover-agents.sh +28 -0
- package/scripts/effective-backlog-settings.js +2 -0
- package/scripts/filter-installed-packs.js +2 -0
- package/scripts/flow-kit.js +2 -0
- package/scripts/generate-context-map.js +2 -0
- package/scripts/git-status.sh +49 -0
- package/scripts/hooks/claude-hook-adapter.js +174 -0
- package/scripts/hooks/claude-telemetry-hook.js +115 -0
- package/scripts/hooks/codex-hook-adapter.js +176 -0
- package/scripts/hooks/codex-telemetry-hook.js +95 -0
- package/scripts/hooks/config-protection.js +79 -0
- package/scripts/hooks/desktop-notify.sh +39 -0
- package/scripts/hooks/governance-audit.sh +135 -0
- package/scripts/hooks/lib/audit-transport.sh +40 -0
- package/scripts/hooks/lib/hook-flags.js +49 -0
- package/scripts/hooks/lib/patterns.sh +57 -0
- package/scripts/hooks/lib/resolve-formatter.js +80 -0
- package/scripts/hooks/post-edit-accumulator.js +66 -0
- package/scripts/hooks/pre-commit-quality.js +194 -0
- package/scripts/hooks/quality-gate.js +93 -0
- package/scripts/hooks/report-only-guard.js +21 -0
- package/scripts/hooks/run-hook.js +136 -0
- package/scripts/hooks/stop-format-typecheck.js +141 -0
- package/scripts/hooks/stop-goal-fit.js +337 -0
- package/scripts/hooks/workflow-steering.js +250 -0
- package/scripts/install-codex-home.sh +106 -0
- package/scripts/package.json +3 -0
- package/scripts/promote-workflow-artifact.js +2 -0
- package/scripts/publish-change-helper.js +2 -0
- package/scripts/pull-work-provider.js +2 -0
- package/scripts/setup-repo-hooks.sh +8 -0
- package/scripts/statusline/flow-agents-statusline.js +157 -0
- package/scripts/telemetry/console-presets.sh +14 -0
- package/scripts/telemetry/install-console-config.sh +214 -0
- package/scripts/telemetry/lib/config.sh +85 -0
- package/scripts/telemetry/lib/enrich.sh +115 -0
- package/scripts/telemetry/lib/redact.sh +22 -0
- package/scripts/telemetry/lib/session.sh +63 -0
- package/scripts/telemetry/lib/transport.sh +183 -0
- package/scripts/telemetry/lib/usage.sh +29 -0
- package/scripts/telemetry/sync-agents.sh +173 -0
- package/scripts/telemetry/telemetry.conf +23 -0
- package/scripts/telemetry/telemetry.sh +387 -0
- package/scripts/usage-feedback.js +2 -0
- package/scripts/validate-hook-influence-cases.js +2 -0
- package/scripts/validate-package.sh +89 -0
- package/scripts/validate-source-tree.js +9 -0
- package/skills/agentic-engineering/SKILL.md +62 -0
- package/skills/browser-test/SKILL.md +51 -0
- package/skills/builder-shape/SKILL.md +76 -0
- package/skills/context-budget/SKILL.md +40 -0
- package/skills/deliver/SKILL.md +241 -0
- package/skills/dependency-update/SKILL.md +68 -0
- package/skills/design-probe/SKILL.md +107 -0
- package/skills/eval-rebuild/SKILL.md +39 -0
- package/skills/evidence-gate/SKILL.md +186 -0
- package/skills/execute-plan/SKILL.md +110 -0
- package/skills/explore/SKILL.md +137 -0
- package/skills/feedback-loop/SKILL.md +87 -0
- package/skills/fix-bug/SKILL.md +133 -0
- package/skills/frontend-design/SKILL.md +80 -0
- package/skills/github-cli/SKILL.md +63 -0
- package/skills/idea-to-backlog/SKILL.md +267 -0
- package/skills/knowledge-capture/SKILL.md +55 -0
- package/skills/learning-review/SKILL.md +115 -0
- package/skills/pickup-probe/SKILL.md +114 -0
- package/skills/plan-work/SKILL.md +176 -0
- package/skills/pull-work/SKILL.md +309 -0
- package/skills/release-readiness/SKILL.md +121 -0
- package/skills/review-work/SKILL.md +161 -0
- package/skills/search-first/SKILL.md +66 -0
- package/skills/tdd-workflow/SKILL.md +140 -0
- package/skills/verify-work/SKILL.md +109 -0
- package/src/cli/console-learning-projection.ts +140 -0
- package/src/cli/effective-backlog-settings.ts +99 -0
- package/src/cli/fixture-retirement-audit.ts +154 -0
- package/src/cli/flow-kit.ts +139 -0
- package/src/cli/init.ts +248 -0
- package/src/cli/promote-workflow-artifact.ts +64 -0
- package/src/cli/publish-change-helper.ts +143 -0
- package/src/cli/pull-work-provider.ts +481 -0
- package/src/cli/runtime-adapter.ts +24 -0
- package/src/cli/telemetry-doctor.ts +243 -0
- package/src/cli/usage-feedback.ts +418 -0
- package/src/cli/validate-hook-influence.ts +119 -0
- package/src/cli/validate-source-tree.ts +30 -0
- package/src/cli/validate-workflow-artifacts.ts +411 -0
- package/src/cli/veritas-governance.ts +322 -0
- package/src/cli/workflow-artifact-cleanup-audit.ts +281 -0
- package/src/cli/workflow-sidecar.ts +676 -0
- package/src/cli.ts +95 -0
- package/src/flow-kit/validate.ts +74 -0
- package/src/lib/args.ts +43 -0
- package/src/lib/fs.ts +62 -0
- package/src/lib/workflow-learning-projection.ts +491 -0
- package/src/runtime-adapters.ts +154 -0
- package/src/tools/build-universal-bundles.ts +366 -0
- package/src/tools/common.ts +61 -0
- package/src/tools/filter-installed-packs.ts +129 -0
- package/src/tools/generate-context-map.ts +199 -0
- package/src/tools/validate-package.ts +57 -0
- package/src/tools/validate-source-tree.ts +488 -0
- package/tsconfig.json +19 -0
- package/veritas.claims.json +6 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as http from "node:http";
|
|
3
|
+
import * as https from "node:https";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { parseArgs, flagBool, flagString } from "../lib/args.js";
|
|
6
|
+
|
|
7
|
+
type Config = Record<string, string>;
|
|
8
|
+
|
|
9
|
+
type Reachability = {
|
|
10
|
+
checked: boolean;
|
|
11
|
+
ok: boolean | null;
|
|
12
|
+
statusCode?: number;
|
|
13
|
+
error?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type DoctorReport = {
|
|
17
|
+
ok: boolean;
|
|
18
|
+
destination: string;
|
|
19
|
+
telemetry: {
|
|
20
|
+
configFile: string;
|
|
21
|
+
configExists: boolean;
|
|
22
|
+
enabled: boolean;
|
|
23
|
+
dataDir: string;
|
|
24
|
+
sessionDir: string;
|
|
25
|
+
channels: Array<{ name: string; logFile?: string; endpointUrl?: string; active: boolean }>;
|
|
26
|
+
activeSinks: string[];
|
|
27
|
+
};
|
|
28
|
+
console: {
|
|
29
|
+
sink: "local-only" | "console";
|
|
30
|
+
url?: string;
|
|
31
|
+
endpointUrl?: string;
|
|
32
|
+
endpointAllowed: boolean;
|
|
33
|
+
tokenConfigured: boolean;
|
|
34
|
+
tenantConfigured: boolean;
|
|
35
|
+
reachability: Reachability;
|
|
36
|
+
};
|
|
37
|
+
warnings: string[];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const defaultChannels = "full";
|
|
41
|
+
const sensitiveQueryKeys = new Set(["token", "api_key", "apikey", "key", "secret", "password", "auth", "authorization", "access_token"]);
|
|
42
|
+
|
|
43
|
+
function usage(): void {
|
|
44
|
+
console.error(`usage: flow-agents telemetry-doctor [options]
|
|
45
|
+
|
|
46
|
+
Options:
|
|
47
|
+
--dest PATH Installed Flow Agents root. Defaults to current directory.
|
|
48
|
+
--json Emit machine-readable JSON.
|
|
49
|
+
--headless Do not prompt; suitable for CI.
|
|
50
|
+
--timeout-ms N Console reachability timeout. Defaults to 2000.
|
|
51
|
+
--allow-network Allow reachability checks to non-local HTTPS Console hosts.
|
|
52
|
+
`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readConfig(file: string): Config {
|
|
56
|
+
if (!fs.existsSync(file)) return {};
|
|
57
|
+
const config: Config = {};
|
|
58
|
+
for (const line of fs.readFileSync(file, "utf8").split(/\r?\n/)) {
|
|
59
|
+
const trimmed = line.trim();
|
|
60
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
61
|
+
const eq = trimmed.indexOf("=");
|
|
62
|
+
if (eq === -1) continue;
|
|
63
|
+
const key = trimmed.slice(0, eq).trim();
|
|
64
|
+
const value = trimmed.slice(eq + 1).trim();
|
|
65
|
+
if (key) config[key] = value;
|
|
66
|
+
}
|
|
67
|
+
return config;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function configValue(config: Config, envName: string, key: string, fallback = ""): string {
|
|
71
|
+
return process.env[envName] ?? config[key] ?? fallback;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function channelEnvName(channel: string, key: string): string {
|
|
75
|
+
return `TELEMETRY_CHANNEL_${channel.toUpperCase()}_${key.toUpperCase()}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function channelConfigValue(config: Config, channel: string, key: string, fallback = ""): string {
|
|
79
|
+
const envName = channelEnvName(channel, key);
|
|
80
|
+
return process.env[envName] ?? config[`channel.${channel}.${key}`] ?? fallback;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function telemetryDataDir(dest: string): string {
|
|
84
|
+
const configured = process.env.TELEMETRY_DATA_DIR;
|
|
85
|
+
return configured ? path.resolve(dest, configured) : path.resolve(dest, "..", ".telemetry");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function deriveConsoleEndpoint(consoleUrl: string, explicitEndpoint: string): string {
|
|
89
|
+
if (explicitEndpoint) return explicitEndpoint;
|
|
90
|
+
if (!consoleUrl) return "";
|
|
91
|
+
const base = consoleUrl.replace(/\/+$/, "");
|
|
92
|
+
if (base.endsWith("/api/telemetry/records")) return base;
|
|
93
|
+
if (base.endsWith("/api/telemetry")) return `${base}/records`;
|
|
94
|
+
return `${base}/api/telemetry/records`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function endpointAllowed(endpointUrl: string, allowNetwork = false): boolean {
|
|
98
|
+
if (!endpointUrl || endpointUrl.includes("\n") || endpointUrl.includes("\r") || endpointUrl.includes('"')) return false;
|
|
99
|
+
const url = parseUrl(endpointUrl);
|
|
100
|
+
if (!url) return false;
|
|
101
|
+
if (url.username || url.password) return false;
|
|
102
|
+
if (url.protocol === "https:") return allowNetwork || isLocalHostname(url.hostname);
|
|
103
|
+
return url.protocol === "http:" && isLocalHostname(url.hostname);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function activeSinks(enabled: boolean, channels: DoctorReport["telemetry"]["channels"], consoleEndpoint: string, allowed: boolean): string[] {
|
|
107
|
+
const sinks: string[] = [];
|
|
108
|
+
if (enabled && channels.some((channel) => channel.active && channel.logFile)) sinks.push("local-files");
|
|
109
|
+
if (enabled && channels.some((channel) => channel.active && channel.endpointUrl)) sinks.push("channel-endpoint");
|
|
110
|
+
if (enabled && consoleEndpoint && allowed) sinks.push("console");
|
|
111
|
+
return sinks;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function checkConsoleReachability(endpointUrl: string, timeoutMs: number, allowNetwork: boolean): Promise<Reachability> {
|
|
115
|
+
if (!endpointUrl) return Promise.resolve({ checked: false, ok: null });
|
|
116
|
+
if (!endpointAllowed(endpointUrl, allowNetwork)) return Promise.resolve({ checked: false, ok: null, error: "endpoint is not allowed" });
|
|
117
|
+
const url = parseUrl(endpointUrl);
|
|
118
|
+
if (!url) return Promise.resolve({ checked: false, ok: null, error: "endpoint URL is malformed" });
|
|
119
|
+
return new Promise((resolve) => {
|
|
120
|
+
let settled = false;
|
|
121
|
+
const client = url.protocol === "https:" ? https : http;
|
|
122
|
+
const req = client.request(url, { method: "HEAD", timeout: timeoutMs }, (res) => {
|
|
123
|
+
settled = true;
|
|
124
|
+
res.resume();
|
|
125
|
+
resolve({ checked: true, ok: Boolean(res.statusCode && res.statusCode < 500), statusCode: res.statusCode });
|
|
126
|
+
});
|
|
127
|
+
req.on("timeout", () => {
|
|
128
|
+
if (settled) return;
|
|
129
|
+
settled = true;
|
|
130
|
+
req.destroy();
|
|
131
|
+
resolve({ checked: true, ok: false, error: `timeout after ${timeoutMs}ms` });
|
|
132
|
+
});
|
|
133
|
+
req.on("error", (error) => {
|
|
134
|
+
if (settled) return;
|
|
135
|
+
settled = true;
|
|
136
|
+
resolve({ checked: true, ok: false, error: error.message });
|
|
137
|
+
});
|
|
138
|
+
req.end();
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function parseUrl(value: string): URL | null {
|
|
143
|
+
try {
|
|
144
|
+
return new URL(value);
|
|
145
|
+
} catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isLocalHostname(hostname: string): boolean {
|
|
151
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname.endsWith(".localhost");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function safeReportUrl(value: string): string | undefined {
|
|
155
|
+
if (!value) return undefined;
|
|
156
|
+
const parsed = parseUrl(value);
|
|
157
|
+
if (!parsed) return "[malformed-url]";
|
|
158
|
+
parsed.username = "";
|
|
159
|
+
parsed.password = "";
|
|
160
|
+
for (const key of Array.from(parsed.searchParams.keys())) {
|
|
161
|
+
if (sensitiveQueryKeys.has(key.toLowerCase())) parsed.searchParams.set(key, "[redacted]");
|
|
162
|
+
}
|
|
163
|
+
return parsed.toString();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function buildReport(argv: string[]): Promise<DoctorReport> {
|
|
167
|
+
const args = parseArgs(argv);
|
|
168
|
+
const allowNetwork = flagBool(args.flags, "allow-network");
|
|
169
|
+
const dest = path.resolve(flagString(args.flags, "dest", process.cwd()) ?? process.cwd());
|
|
170
|
+
const telemetryDir = path.join(dest, "scripts", "telemetry");
|
|
171
|
+
const configFile = path.join(telemetryDir, "telemetry.conf");
|
|
172
|
+
const config = readConfig(configFile);
|
|
173
|
+
const enabled = configValue(config, "TELEMETRY_ENABLED", "enabled", "true") !== "false";
|
|
174
|
+
const dataDir = telemetryDataDir(dest);
|
|
175
|
+
const sessionDir = path.resolve(configValue(config, "TELEMETRY_SESSION_DIR", "telemetry_session_dir", path.join(dataDir, "sessions")));
|
|
176
|
+
const channels = configValue(config, "TELEMETRY_CHANNELS", "channels", defaultChannels).split(",").map((channel) => channel.trim()).filter(Boolean);
|
|
177
|
+
const channelReports = channels.map((name) => {
|
|
178
|
+
const defaultLog = name === "full" ? path.join(dataDir, "full.jsonl") : name === "analytics" ? path.join(dataDir, "analytics.jsonl") : "";
|
|
179
|
+
const logFile = channelConfigValue(config, name, "log_file", defaultLog);
|
|
180
|
+
const endpointUrl = channelConfigValue(config, name, "endpoint_url");
|
|
181
|
+
return { name, logFile: logFile ? path.resolve(dest, logFile) : undefined, endpointUrl: safeReportUrl(endpointUrl), active: enabled };
|
|
182
|
+
});
|
|
183
|
+
const consoleUrl = process.env.CONSOLE_TELEMETRY_URL ?? process.env.CONSOLE_URL ?? config.console_telemetry_url ?? config.console_url ?? "";
|
|
184
|
+
const explicitEndpoint = process.env.CONSOLE_TELEMETRY_ENDPOINT_URL ?? config.console_telemetry_endpoint_url ?? "";
|
|
185
|
+
const endpointUrl = deriveConsoleEndpoint(consoleUrl, explicitEndpoint);
|
|
186
|
+
const allowed = endpointAllowed(endpointUrl, allowNetwork);
|
|
187
|
+
const timeoutMs = Number.parseInt(flagString(args.flags, "timeout-ms", "2000") ?? "2000", 10);
|
|
188
|
+
const reachability = await checkConsoleReachability(endpointUrl, Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 2000, allowNetwork);
|
|
189
|
+
const warnings = reportWarnings(configFile, endpointUrl, allowed, allowNetwork);
|
|
190
|
+
return {
|
|
191
|
+
ok: enabled && (!endpointUrl || (allowed && reachability.ok !== false)),
|
|
192
|
+
destination: dest,
|
|
193
|
+
telemetry: {
|
|
194
|
+
configFile,
|
|
195
|
+
configExists: fs.existsSync(configFile),
|
|
196
|
+
enabled,
|
|
197
|
+
dataDir,
|
|
198
|
+
sessionDir,
|
|
199
|
+
channels: channelReports,
|
|
200
|
+
activeSinks: activeSinks(enabled, channelReports, endpointUrl, allowed),
|
|
201
|
+
},
|
|
202
|
+
console: {
|
|
203
|
+
sink: endpointUrl ? "console" : "local-only",
|
|
204
|
+
url: safeReportUrl(consoleUrl),
|
|
205
|
+
endpointUrl: safeReportUrl(endpointUrl),
|
|
206
|
+
endpointAllowed: allowed,
|
|
207
|
+
tokenConfigured: Boolean(process.env.CONSOLE_TELEMETRY_TOKEN ?? process.env.CONSOLE_AUTH_TOKEN ?? config.console_telemetry_token),
|
|
208
|
+
tenantConfigured: Boolean(process.env.CONSOLE_TENANT_ID ?? config.console_tenant_id),
|
|
209
|
+
reachability,
|
|
210
|
+
},
|
|
211
|
+
warnings,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function reportWarnings(configFile: string, endpointUrl: string, allowed: boolean, allowNetwork: boolean): string[] {
|
|
216
|
+
const warnings: string[] = [];
|
|
217
|
+
if (!fs.existsSync(configFile)) warnings.push("telemetry.conf was not found under destination scripts/telemetry");
|
|
218
|
+
if (endpointUrl && !allowed) warnings.push(allowNetwork ? "Console endpoint is malformed or contains credentials" : "Console endpoint is not allowed without --allow-network; local http(s) endpoints are allowed by default");
|
|
219
|
+
return warnings;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function printText(report: DoctorReport): void {
|
|
223
|
+
console.log(`Telemetry doctor for ${report.destination}`);
|
|
224
|
+
console.log(`Config: ${report.telemetry.configFile} (${report.telemetry.configExists ? "found" : "missing"})`);
|
|
225
|
+
console.log(`Telemetry enabled: ${report.telemetry.enabled}`);
|
|
226
|
+
console.log(`Local telemetry dir: ${report.telemetry.dataDir}`);
|
|
227
|
+
console.log(`Active sinks: ${report.telemetry.activeSinks.length ? report.telemetry.activeSinks.join(", ") : "none"}`);
|
|
228
|
+
console.log(`Console endpoint: ${report.console.endpointUrl || "not configured"}`);
|
|
229
|
+
console.log(`Console reachability: ${report.console.reachability.checked ? (report.console.reachability.ok ? "ok" : `failed (${report.console.reachability.error ?? report.console.reachability.statusCode ?? "unknown"})`) : "not checked"}`);
|
|
230
|
+
for (const warning of report.warnings) console.log(`Warning: ${warning}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export async function main(argv: string[] = process.argv.slice(2)): Promise<number> {
|
|
234
|
+
const args = parseArgs(argv);
|
|
235
|
+
if (flagBool(args.flags, "help") || flagBool(args.flags, "h")) {
|
|
236
|
+
usage();
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
const report = await buildReport(argv);
|
|
240
|
+
if (flagBool(args.flags, "json")) console.log(JSON.stringify(report, null, 2));
|
|
241
|
+
else printText(report);
|
|
242
|
+
return report.ok ? 0 : 1;
|
|
243
|
+
}
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { parseArgs, flagBool, flagList, flagString } from "../lib/args.js";
|
|
5
|
+
|
|
6
|
+
const VALID_RESULTS = new Set(["success", "partial", "failure", "not_verified"]);
|
|
7
|
+
|
|
8
|
+
function telemetryDir(flags: Record<string, string | boolean | string[]>): string {
|
|
9
|
+
return path.resolve(flagString(flags, "telemetry-dir", process.env.TELEMETRY_DATA_DIR ?? ".telemetry") ?? ".telemetry");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function ensureSafeDir(dir: string): void {
|
|
13
|
+
let current = path.resolve(dir);
|
|
14
|
+
const missing: string[] = [];
|
|
15
|
+
while (!fs.existsSync(current)) {
|
|
16
|
+
missing.push(current);
|
|
17
|
+
current = path.dirname(current);
|
|
18
|
+
}
|
|
19
|
+
if (fs.lstatSync(current).isSymbolicLink()) throw new Error(`unsafe telemetry path contains symlink: ${current}`);
|
|
20
|
+
const rel = path.relative(current, dir).split(path.sep).filter(Boolean);
|
|
21
|
+
let cursor = current;
|
|
22
|
+
for (const part of rel) {
|
|
23
|
+
cursor = path.join(cursor, part);
|
|
24
|
+
if (fs.existsSync(cursor) && fs.lstatSync(cursor).isSymbolicLink()) throw new Error(`unsafe telemetry path contains symlink: ${cursor}`);
|
|
25
|
+
}
|
|
26
|
+
for (const item of missing.reverse()) fs.mkdirSync(item);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function appendJsonl(file: string, record: unknown): void {
|
|
30
|
+
try {
|
|
31
|
+
if (fs.lstatSync(file).isSymbolicLink()) throw new Error(`refusing to write symlinked file: ${file}`);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
34
|
+
}
|
|
35
|
+
fs.appendFileSync(file, `${JSON.stringify(record)}\n`, "utf8");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function readJsonl(file: string): Record<string, unknown>[] {
|
|
39
|
+
if (!fs.existsSync(file)) return [];
|
|
40
|
+
return fs.readFileSync(file, "utf8").split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line) as Record<string, unknown>);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writeJsonlUpsert(file: string, rows: Record<string, unknown>[], key: string): void {
|
|
44
|
+
const existing = new Map(readJsonl(file).map((row) => [String(row[key]), row]));
|
|
45
|
+
for (const row of rows) existing.set(String(row[key]), row);
|
|
46
|
+
fs.writeFileSync(file, Array.from(existing.values()).map((row) => JSON.stringify(row)).join("\n") + (existing.size ? "\n" : ""), "utf8");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function recordOutcome(argv: string[]): number {
|
|
50
|
+
const { flags } = parseArgs(argv);
|
|
51
|
+
const sessionId = flagString(flags, "session-id");
|
|
52
|
+
const result = flagString(flags, "result");
|
|
53
|
+
if (!sessionId) throw new Error("--session-id is required");
|
|
54
|
+
if (!result || !VALID_RESULTS.has(result)) throw new Error("--result must be success, partial, failure, or not_verified");
|
|
55
|
+
const dir = telemetryDir(flags);
|
|
56
|
+
ensureSafeDir(dir);
|
|
57
|
+
const record = {
|
|
58
|
+
schema_version: "1",
|
|
59
|
+
outcome_id: `${sessionId}:${flagString(flags, "task-slug", "outcome")}`,
|
|
60
|
+
recorded_at: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
61
|
+
session_id: sessionId,
|
|
62
|
+
runtime_session_id: flagString(flags, "runtime-session-id"),
|
|
63
|
+
runtime: flagString(flags, "runtime", "codex"),
|
|
64
|
+
repo: flagString(flags, "repo"),
|
|
65
|
+
agent: flagString(flags, "agent"),
|
|
66
|
+
profile_id: flagString(flags, "profile-id"),
|
|
67
|
+
prompt_id: flagString(flags, "prompt-id"),
|
|
68
|
+
prompt_variant: flagString(flags, "prompt-variant"),
|
|
69
|
+
skill_ids: flagList(flags, "skill-id"),
|
|
70
|
+
skill_variant: flagString(flags, "skill-variant"),
|
|
71
|
+
task_type: flagString(flags, "task-type"),
|
|
72
|
+
task_slug: flagString(flags, "task-slug"),
|
|
73
|
+
result,
|
|
74
|
+
quality_score: flagString(flags, "quality-score") ? Number(flagString(flags, "quality-score")) : null,
|
|
75
|
+
human_minutes_saved: flagString(flags, "human-minutes-saved") ? Number(flagString(flags, "human-minutes-saved")) : null,
|
|
76
|
+
rework_required: flagBool(flags, "rework-required"),
|
|
77
|
+
notes: flagString(flags, "notes"),
|
|
78
|
+
evidence: flagList(flags, "evidence"),
|
|
79
|
+
};
|
|
80
|
+
appendJsonl(path.join(dir, "outcomes.jsonl"), record);
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function normalize(input: Record<string, unknown>[], runtime: string, flags: Record<string, string | boolean | string[]>, fallbackSource = "flow-agents"): Record<string, unknown>[] {
|
|
85
|
+
const groups = new Map<string, Record<string, unknown>[]>();
|
|
86
|
+
for (const event of input) groups.set(String(event.session_id ?? "unknown"), [...(groups.get(String(event.session_id ?? "unknown")) ?? []), event]);
|
|
87
|
+
return Array.from(groups, ([sessionId, events]) => ({
|
|
88
|
+
schema_version: "1",
|
|
89
|
+
session_id: sessionId,
|
|
90
|
+
source_id: flagString(flags, "source-id") ?? String(events.find((event) => event.repo)?.repo ?? fallbackSource),
|
|
91
|
+
runtime,
|
|
92
|
+
repo: flagString(flags, "repo") ?? String(events.find((event) => event.repo)?.repo ?? ""),
|
|
93
|
+
repo_root: flagString(flags, "repo-root"),
|
|
94
|
+
agent: flagString(flags, "agent") ?? String((events.find((event) => event.agent) as { agent?: { name?: string } } | undefined)?.agent?.name ?? "dev"),
|
|
95
|
+
profile_id: flagString(flags, "profile-id") ?? String(events.find((event) => event.profile_id)?.profile_id ?? ""),
|
|
96
|
+
prompt_id: flagString(flags, "prompt-id") ?? String(events.find((event) => event.prompt_id)?.prompt_id ?? ""),
|
|
97
|
+
prompt_variant: flagString(flags, "prompt-variant") ?? String(events.find((event) => event.prompt_variant)?.prompt_variant ?? ""),
|
|
98
|
+
skill_ids: flagList(flags, "skill-id").length ? flagList(flags, "skill-id") : ((events.find((event) => Array.isArray(event.skill_ids))?.skill_ids as string[] | undefined) ?? []),
|
|
99
|
+
skill_variant: flagString(flags, "skill-variant") ?? String(events.find((event) => event.skill_variant)?.skill_variant ?? ""),
|
|
100
|
+
started_at: events[0]?.timestamp,
|
|
101
|
+
ended_at: events[events.length - 1]?.timestamp,
|
|
102
|
+
turns: events.filter((event) => String(event.event_type ?? "").includes("turn.user")).length,
|
|
103
|
+
tool_invocations: events.filter((event) => String(event.event_type ?? "") === "tool.invoke").length,
|
|
104
|
+
delegations: events.filter((event) => String(event.event_type ?? "").includes("delegate")).length,
|
|
105
|
+
permission_requests: events.filter((event) => String(event.event_type ?? "").includes("permission")).length,
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function importTelemetry(argv: string[], defaultRuntime?: string): number {
|
|
110
|
+
const { flags } = parseArgs(argv);
|
|
111
|
+
const runtime = defaultRuntime ?? flagString(flags, "runtime", "codex") ?? "codex";
|
|
112
|
+
const input = flagString(flags, "input-full-jsonl") ?? path.join(flagString(flags, "input-telemetry-dir") ?? "", "full.jsonl");
|
|
113
|
+
if (!input || !fs.existsSync(input)) throw new Error(`input telemetry file does not exist: ${input}`);
|
|
114
|
+
const dir = telemetryDir(flags);
|
|
115
|
+
ensureSafeDir(dir);
|
|
116
|
+
const inputDir = flagString(flags, "input-telemetry-dir");
|
|
117
|
+
const fallbackSource = inputDir ? path.basename(path.resolve(inputDir)) : "flow-agents";
|
|
118
|
+
writeJsonlUpsert(path.join(dir, "normalized-sessions.jsonl"), normalize(readJsonl(input), runtime, flags, fallbackSource), "session_id");
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function artifactOutcomes(artifactPath: string, flags: Record<string, string | boolean | string[]>): Record<string, unknown>[] {
|
|
123
|
+
const files: string[] = [];
|
|
124
|
+
const stat = fs.statSync(artifactPath);
|
|
125
|
+
if (stat.isDirectory()) {
|
|
126
|
+
for (const name of fs.readdirSync(artifactPath, { recursive: true }) as string[]) if (name.endsWith(".md")) files.push(path.join(artifactPath, name));
|
|
127
|
+
} else files.push(artifactPath);
|
|
128
|
+
return files.flatMap((file) => {
|
|
129
|
+
const text = fs.readFileSync(file, "utf8");
|
|
130
|
+
const status = text.match(/^status:\s*(.+)$/m)?.[1].trim() ?? "";
|
|
131
|
+
const type = text.match(/^type:\s*(.+)$/m)?.[1].trim() ?? "";
|
|
132
|
+
const terminal = ["delivered", "accepted", "done"].includes(status);
|
|
133
|
+
if (!terminal && !flagBool(flags, "include-open")) return [];
|
|
134
|
+
const title = text.split(/\r?\n/).find((line) => line.startsWith("# "))?.slice(2).trim() ?? path.basename(file, ".md");
|
|
135
|
+
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
136
|
+
return [{ schema_version: "1", outcome_id: `artifact:${file}`, recorded_at: new Date().toISOString(), session_id: slug, runtime: flagString(flags, "runtime", "codex"), repo: flagString(flags, "repo"), profile_id: flagString(flags, "profile-id"), prompt_id: flagString(flags, "prompt-id"), skill_ids: flagList(flags, "skill-id"), task_type: type || "deliver", task_slug: slug, result: terminal ? "success" : status === "failed" ? "failure" : "not_verified", quality_score: null, human_minutes_saved: null, rework_required: false, evidence: [file] }];
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function syncArtifacts(argv: string[]): number {
|
|
141
|
+
const { flags } = parseArgs(argv);
|
|
142
|
+
const dir = telemetryDir(flags);
|
|
143
|
+
ensureSafeDir(dir);
|
|
144
|
+
const artifacts = flagList(flags, "artifact-dir");
|
|
145
|
+
const records = (artifacts.length ? artifacts : [".flow-agents"]).flatMap((item) => fs.existsSync(item) ? artifactOutcomes(item, flags) : []);
|
|
146
|
+
writeJsonlUpsert(path.join(dir, "outcomes.jsonl"), records, "outcome_id");
|
|
147
|
+
if (!flagBool(flags, "quiet")) console.log(`synced ${records.length} artifact outcome(s) to ${path.join(dir, "outcomes.jsonl")}`);
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function rows(dir: string): { sessions: Record<string, unknown>[]; outcomes: Record<string, unknown>[] } {
|
|
152
|
+
const full = fs.existsSync(path.join(dir, "full.jsonl")) ? path.join(dir, "full.jsonl") : path.join(dir, "sample-full.jsonl");
|
|
153
|
+
const outcomes = fs.existsSync(path.join(dir, "outcomes.jsonl")) ? path.join(dir, "outcomes.jsonl") : path.join(dir, "sample-outcomes.jsonl");
|
|
154
|
+
return {
|
|
155
|
+
sessions: [
|
|
156
|
+
...readJsonl(path.join(dir, "normalized-sessions.jsonl")),
|
|
157
|
+
...readJsonl(path.join(dir, "sessions.jsonl")),
|
|
158
|
+
...normalize(readJsonl(full), "codex", {}, path.basename(path.resolve(dir))),
|
|
159
|
+
],
|
|
160
|
+
outcomes: readJsonl(outcomes),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function reportData(dirs: string[], groupBy?: string): Record<string, unknown> {
|
|
165
|
+
const sessions = dirs.flatMap((dir) => rows(dir).sessions);
|
|
166
|
+
const outcomes = dirs.flatMap((dir) => rows(dir).outcomes);
|
|
167
|
+
const outcomesBySession = new Map<string, Record<string, unknown>[]>();
|
|
168
|
+
for (const outcome of outcomes) {
|
|
169
|
+
const key = String(outcome.session_id ?? "unknown");
|
|
170
|
+
outcomesBySession.set(key, [...(outcomesBySession.get(key) ?? []), outcome]);
|
|
171
|
+
}
|
|
172
|
+
const success = outcomes.filter((outcome) => outcome.result === "success").length;
|
|
173
|
+
const groups = new Map<string, { sessions: number; outcomes: number; success: number; tools: number[]; rework: number }>();
|
|
174
|
+
const groupValue = (session: Record<string, unknown>, sessionOutcomes: Record<string, unknown>[]): string => {
|
|
175
|
+
if (!groupBy) return "all";
|
|
176
|
+
if (groupBy === "source") return String(session.source_id ?? "unknown");
|
|
177
|
+
if (groupBy === "skill_id") return Array.isArray(session.skill_ids) && session.skill_ids.length ? session.skill_ids.join(",") : "unknown";
|
|
178
|
+
if (groupBy === "task_type") return String(sessionOutcomes.find((outcome) => outcome.task_type)?.task_type ?? "unknown");
|
|
179
|
+
return String(session[groupBy] ?? "unknown");
|
|
180
|
+
};
|
|
181
|
+
for (const session of sessions) {
|
|
182
|
+
const sessionOutcomes = outcomesBySession.get(String(session.session_id ?? "unknown")) ?? [];
|
|
183
|
+
const key = cleanLabel(groupValue(session, sessionOutcomes));
|
|
184
|
+
const entry = groups.get(key) ?? { sessions: 0, outcomes: 0, success: 0, tools: [], rework: 0 };
|
|
185
|
+
entry.sessions += 1;
|
|
186
|
+
entry.outcomes += sessionOutcomes.length;
|
|
187
|
+
entry.success += sessionOutcomes.filter((outcome) => outcome.result === "success").length;
|
|
188
|
+
if (session.tool_invocations !== undefined && session.tool_invocations !== null) entry.tools.push(Number(session.tool_invocations));
|
|
189
|
+
entry.rework += sessionOutcomes.filter((outcome) => outcome.rework_required).length;
|
|
190
|
+
groups.set(key, entry);
|
|
191
|
+
}
|
|
192
|
+
const sessionIdsWithOutcomes = new Set(outcomes.map((outcome) => outcome.session_id));
|
|
193
|
+
const avgTools = sessions.length ? sessions.reduce((total, session) => total + Number(session.tool_invocations ?? 0), 0) / sessions.length : null;
|
|
194
|
+
const reworkCount = outcomes.filter((outcome) => outcome.rework_required).length;
|
|
195
|
+
return {
|
|
196
|
+
summary: {
|
|
197
|
+
sessions: sessions.length,
|
|
198
|
+
sessions_with_outcomes: sessionIdsWithOutcomes.size,
|
|
199
|
+
outcomes: outcomes.length,
|
|
200
|
+
success_rate: outcomes.length ? success / outcomes.length : null,
|
|
201
|
+
avg_tool_invocations: avgTools,
|
|
202
|
+
rework_rate: outcomes.length ? reworkCount / outcomes.length : null,
|
|
203
|
+
},
|
|
204
|
+
sources: Array.from(new Set(sessions.map((session) => String(session.source_id ?? "unknown")))).sort(),
|
|
205
|
+
groups: Array.from(groups, ([key, entry]) => ({
|
|
206
|
+
key,
|
|
207
|
+
group: key,
|
|
208
|
+
name: key,
|
|
209
|
+
sessions: entry.sessions,
|
|
210
|
+
outcomes: entry.outcomes,
|
|
211
|
+
success_rate: entry.outcomes ? entry.success / entry.outcomes : null,
|
|
212
|
+
avg_tool_invocations: entry.tools.length ? entry.tools.reduce((a, b) => a + b, 0) / entry.tools.length : null,
|
|
213
|
+
rework_rate: entry.outcomes ? entry.rework / entry.outcomes : null,
|
|
214
|
+
})).sort((a, b) => String(a.key).localeCompare(String(b.key))),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function reportPath(output: string | undefined, dir: string, force: boolean): string | undefined {
|
|
219
|
+
if (!output) return undefined;
|
|
220
|
+
const reports = path.join(dir, "reports");
|
|
221
|
+
if (fs.existsSync(reports) && fs.lstatSync(reports).isSymbolicLink()) throw new Error("reports directory is symlinked");
|
|
222
|
+
let relativeOutput = output.replace(new RegExp(`^${path.basename(dir)}/reports/`), "").replace(/^reports\//, "");
|
|
223
|
+
let target = path.isAbsolute(output) ? output : path.join(reports, relativeOutput);
|
|
224
|
+
target = path.resolve(target);
|
|
225
|
+
if (!target.startsWith(path.resolve(reports) + path.sep)) throw new Error("report output must be inside telemetry reports directory");
|
|
226
|
+
try {
|
|
227
|
+
if (fs.lstatSync(target).isSymbolicLink()) throw new Error("report output is symlinked");
|
|
228
|
+
} catch (error) {
|
|
229
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
230
|
+
}
|
|
231
|
+
if (fs.existsSync(target) && !force) throw new Error("report output exists; rerun with --force");
|
|
232
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
233
|
+
return target;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function html(content: string): string {
|
|
237
|
+
return `<!doctype html><html><body><h1>Usage Dashboard</h1><h2>What Needs Attention</h2><h2>Measurement state</h2><h2>Data Coverage</h2><h2>Outcome Mix</h2><h2>Missing Label Drilldown</h2><pre>${escapeHtml(content)}</pre></body></html>`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function cleanLabel(value: unknown): string {
|
|
241
|
+
return String(value ?? "unknown").replace(/[\r\n\t\x00-\x1f]+/g, " ").split(/\s+/).filter(Boolean).join(" ") || "unknown";
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function escapeHtml(value: unknown): string {
|
|
245
|
+
return String(value ?? "")
|
|
246
|
+
.replace(/&/g, "&")
|
|
247
|
+
.replace(/</g, "<")
|
|
248
|
+
.replace(/>/g, ">")
|
|
249
|
+
.replace(/"/g, """)
|
|
250
|
+
.replace(/'/g, "'");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function markdownCell(value: unknown): string {
|
|
254
|
+
return cleanLabel(value).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function fmtRate(value: unknown): string {
|
|
258
|
+
return typeof value === "number" ? `${(value * 100).toFixed(1)}%` : "n/a";
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function fmtNum(value: unknown): string {
|
|
262
|
+
return typeof value === "number" ? String(Number.isInteger(value) ? value : Number(value.toFixed(2))) : "n/a";
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function markdownReport(data: Record<string, unknown>, groupBy?: string): string {
|
|
266
|
+
const summary = data.summary as Record<string, unknown>;
|
|
267
|
+
const groups = data.groups as Record<string, unknown>[];
|
|
268
|
+
const lines = [
|
|
269
|
+
"# Agent Usage Feedback Report",
|
|
270
|
+
"",
|
|
271
|
+
"## Summary",
|
|
272
|
+
"",
|
|
273
|
+
`- Sessions: ${summary.sessions}`,
|
|
274
|
+
`- Sessions with outcomes: ${summary.sessions_with_outcomes}`,
|
|
275
|
+
`- Success rate: ${fmtRate(summary.success_rate)}`,
|
|
276
|
+
`- Avg tool invocations: ${fmtNum(summary.avg_tool_invocations)}`,
|
|
277
|
+
`- Rework rate: ${fmtRate(summary.rework_rate)}`,
|
|
278
|
+
];
|
|
279
|
+
if (groupBy) {
|
|
280
|
+
lines.push("", `## Groups by ${groupBy}`, "", "| Group | Sessions | Outcomes | Success rate | Avg tool invocations | Rework rate |", "| --- | ---: | ---: | ---: | ---: | ---: |");
|
|
281
|
+
for (const group of groups) {
|
|
282
|
+
lines.push(`| ${markdownCell(group.key)} | ${group.sessions} | ${group.outcomes} | ${fmtRate(group.success_rate)} | ${fmtNum(group.avg_tool_invocations)} | ${fmtRate(group.rework_rate)} |`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return `${lines.join("\n")}\n`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function report(argv: string[]): number {
|
|
289
|
+
const { flags } = parseArgs(argv);
|
|
290
|
+
const dirs = flagList(flags, "telemetry-dir").map((dir) => path.resolve(dir));
|
|
291
|
+
dirs.forEach(ensureSafeDir);
|
|
292
|
+
const data = reportData(dirs, flagString(flags, "group-by"));
|
|
293
|
+
const format = flagString(flags, "format", "markdown");
|
|
294
|
+
const out = reportPath(flagString(flags, "output"), dirs[0], flagBool(flags, "force"));
|
|
295
|
+
let body: string;
|
|
296
|
+
if (format === "json") body = JSON.stringify(data, null, 2);
|
|
297
|
+
else if (format === "html") body = html(JSON.stringify(data));
|
|
298
|
+
else body = markdownReport(data, flagString(flags, "group-by"));
|
|
299
|
+
if (out) fs.writeFileSync(out, body, "utf8");
|
|
300
|
+
else console.log(body);
|
|
301
|
+
return 0;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function dashboard(argv: string[]): number {
|
|
305
|
+
const { flags } = parseArgs(argv);
|
|
306
|
+
syncArtifacts(argv);
|
|
307
|
+
const dir = telemetryDir(flags);
|
|
308
|
+
const out = reportPath(flagString(flags, "output", "dashboard.html"), dir, flagBool(flags, "force"))!;
|
|
309
|
+
const outcomes = readJsonl(path.join(dir, "outcomes.jsonl"));
|
|
310
|
+
fs.writeFileSync(out, html(outcomes.map((o) => String(o.task_slug)).join("\n")), "utf8");
|
|
311
|
+
if (!flagBool(flags, "quiet")) console.log(`dashboard written to ${out}`);
|
|
312
|
+
return 0;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function registerProject(argv: string[]): number {
|
|
316
|
+
const { flags } = parseArgs(argv);
|
|
317
|
+
const globalDir = path.resolve(flagString(flags, "global-dir", path.join(os.homedir(), ".local", "share", "flow-agents", "usage-feedback")) ?? "");
|
|
318
|
+
ensureSafeDir(globalDir);
|
|
319
|
+
const repoRoot = path.resolve(flagString(flags, "repo-root", ".") ?? ".");
|
|
320
|
+
const name = flagString(flags, "name", path.basename(repoRoot)) ?? path.basename(repoRoot);
|
|
321
|
+
const record = { name, repo_root: repoRoot, artifact_dir: path.join(repoRoot, ".flow-agents"), input_telemetry_dir: path.join(repoRoot, ".telemetry"), runtime: flagString(flags, "runtime", "codex"), repo: flagString(flags, "repo", name), agent: flagString(flags, "agent"), profile_id: flagString(flags, "profile-id"), prompt_id: flagString(flags, "prompt-id"), prompt_variant: flagString(flags, "prompt-variant"), skill_ids: flagList(flags, "skill-id"), skill_variant: flagString(flags, "skill-variant") };
|
|
322
|
+
const registryFile = path.join(globalDir, "projects.json");
|
|
323
|
+
const existing = fs.existsSync(registryFile) ? JSON.parse(fs.readFileSync(registryFile, "utf8")) : { projects: [] };
|
|
324
|
+
const projects = Array.isArray(existing) ? existing : Array.isArray(existing.projects) ? existing.projects : [];
|
|
325
|
+
const index = projects.findIndex((project: Record<string, unknown>) => project.name === name || project.repo_root === repoRoot);
|
|
326
|
+
if (index >= 0) projects[index] = { ...projects[index], ...record };
|
|
327
|
+
else projects.push(record);
|
|
328
|
+
fs.writeFileSync(registryFile, `${JSON.stringify({ projects }, null, 2)}\n`, "utf8");
|
|
329
|
+
console.log(`registered ${record.name} in ${path.join(globalDir, "projects.json")}`);
|
|
330
|
+
return 0;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function loadProjects(globalDir: string): Record<string, unknown>[] {
|
|
334
|
+
const file = path.join(globalDir, "projects.json");
|
|
335
|
+
if (!fs.existsSync(file)) return [];
|
|
336
|
+
const data = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
337
|
+
return Array.isArray(data) ? data : Array.isArray(data.projects) ? data.projects : [];
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function syncProject(project: Record<string, unknown>, globalDir: string): void {
|
|
341
|
+
const name = String(project.name ?? project.repo ?? "project").replace(/[^a-zA-Z0-9_.-]+/g, "-") || "project";
|
|
342
|
+
const store = path.join(globalDir, "projects", name);
|
|
343
|
+
ensureSafeDir(store);
|
|
344
|
+
const artifactDir = String(project.artifact_dir ?? path.join(String(project.repo_root), ".flow-agents"));
|
|
345
|
+
const flags: Record<string, string | boolean | string[]> = {
|
|
346
|
+
"repo": String(project.repo ?? name),
|
|
347
|
+
"runtime": String(project.runtime ?? "codex"),
|
|
348
|
+
"agent": project.agent ? String(project.agent) : "",
|
|
349
|
+
"profile-id": project.profile_id ? String(project.profile_id) : "",
|
|
350
|
+
"prompt-id": project.prompt_id ? String(project.prompt_id) : "",
|
|
351
|
+
"prompt-variant": project.prompt_variant ? String(project.prompt_variant) : "",
|
|
352
|
+
"skill-id": Array.isArray(project.skill_ids) ? project.skill_ids.map(String) : [],
|
|
353
|
+
"skill-variant": project.skill_variant ? String(project.skill_variant) : "",
|
|
354
|
+
"include-open": true,
|
|
355
|
+
};
|
|
356
|
+
const outcomes = fs.existsSync(artifactDir) ? artifactOutcomes(artifactDir, flags) : [];
|
|
357
|
+
writeJsonlUpsert(path.join(store, "outcomes.jsonl"), outcomes, "outcome_id");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function discoverProjects(root: string): Record<string, unknown>[] {
|
|
361
|
+
if (!fs.existsSync(root)) return [];
|
|
362
|
+
const candidates = [root, ...fs.readdirSync(root).map((name) => path.join(root, name))];
|
|
363
|
+
return candidates.filter((candidate) => fs.existsSync(path.join(candidate, ".flow-agents"))).map((repoRoot) => {
|
|
364
|
+
const name = path.basename(repoRoot);
|
|
365
|
+
return { name, repo: name, repo_root: repoRoot, artifact_dir: path.join(repoRoot, ".flow-agents"), input_telemetry_dir: path.join(repoRoot, ".telemetry"), runtime: "codex", skill_ids: [] };
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function syncProjects(argv: string[]): number {
|
|
370
|
+
const { flags } = parseArgs(argv);
|
|
371
|
+
const globalDir = path.resolve(flagString(flags, "global-dir", path.join(os.homedir(), ".local", "share", "flow-agents", "usage-feedback")) ?? "");
|
|
372
|
+
ensureSafeDir(globalDir);
|
|
373
|
+
if (flagString(flags, "repo-root")) registerProject(argv);
|
|
374
|
+
for (const project of loadProjects(globalDir)) syncProject(project, globalDir);
|
|
375
|
+
return 0;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function globalDashboard(argv: string[]): number {
|
|
379
|
+
const { flags } = parseArgs(argv);
|
|
380
|
+
const globalDir = path.resolve(flagString(flags, "global-dir", path.join(os.homedir(), ".local", "share", "flow-agents", "usage-feedback")) ?? "");
|
|
381
|
+
ensureSafeDir(globalDir);
|
|
382
|
+
const discovered = flagList(flags, "discover").flatMap(discoverProjects);
|
|
383
|
+
if (discovered.length) {
|
|
384
|
+
const existing = loadProjects(globalDir);
|
|
385
|
+
const merged = [...existing];
|
|
386
|
+
for (const project of discovered) if (!merged.some((item) => item.name === project.name || item.repo_root === project.repo_root)) merged.push(project);
|
|
387
|
+
fs.writeFileSync(path.join(globalDir, "projects.json"), `${JSON.stringify({ projects: merged }, null, 2)}\n`, "utf8");
|
|
388
|
+
}
|
|
389
|
+
for (const project of loadProjects(globalDir)) syncProject(project, globalDir);
|
|
390
|
+
const dirs = fs.existsSync(path.join(globalDir, "projects")) ? fs.readdirSync(path.join(globalDir, "projects")).map((name) => path.join(globalDir, "projects", name)) : [];
|
|
391
|
+
const data = reportData(dirs, "repo");
|
|
392
|
+
const out = reportPath(flagString(flags, "output", "global-dashboard.html"), globalDir, flagBool(flags, "force"))!;
|
|
393
|
+
const projectNames = loadProjects(globalDir).map((project) => String(project.name ?? project.repo ?? "")).filter(Boolean).join("\n");
|
|
394
|
+
fs.writeFileSync(out, html(`${markdownReport(data, "repo")}\n${projectNames}`), "utf8");
|
|
395
|
+
return 0;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function main(argv = process.argv.slice(2)): number {
|
|
399
|
+
try {
|
|
400
|
+
const [command, ...rest] = argv;
|
|
401
|
+
if (command === "record-outcome") return recordOutcome(rest);
|
|
402
|
+
if (command === "import-codex") return importTelemetry(rest, "codex");
|
|
403
|
+
if (command === "import-telemetry") return importTelemetry(rest);
|
|
404
|
+
if (command === "sync-artifacts") return syncArtifacts(rest);
|
|
405
|
+
if (command === "report") return report(rest);
|
|
406
|
+
if (command === "dashboard") return dashboard(rest);
|
|
407
|
+
if (command === "register-project") return registerProject(rest);
|
|
408
|
+
if (command === "sync-projects") return syncProjects(rest);
|
|
409
|
+
if (command === "global-dashboard") return globalDashboard(rest);
|
|
410
|
+
console.error("usage-feedback command required");
|
|
411
|
+
return 2;
|
|
412
|
+
} catch (error) {
|
|
413
|
+
console.error((error as Error).message);
|
|
414
|
+
return 1;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (import.meta.url === `file://${process.argv[1]}`) process.exit(main());
|