session-orchestrator 3.16.0
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/.claude-plugin/marketplace.json +29 -0
- package/.claude-plugin/plugin.json +18 -0
- package/.codex-plugin/agents/explorer.toml +14 -0
- package/.codex-plugin/agents/session-reviewer.toml +23 -0
- package/.codex-plugin/agents/wave-worker.toml +15 -0
- package/.codex-plugin/config.toml +20 -0
- package/.codex-plugin/plugin.json +37 -0
- package/.cursor/rules/000-session-orchestrator.mdc +73 -0
- package/.cursor/rules/010-session-workflow.mdc +170 -0
- package/.cursor/rules/020-quality-gates.mdc +128 -0
- package/.cursor/rules/030-wave-execution.mdc +216 -0
- package/.cursor/rules/040-discovery.mdc +242 -0
- package/.cursor/rules/050-plan.mdc +235 -0
- package/.cursor/rules/060-evolve.mdc +232 -0
- package/.cursor/rules/070-gitlab-ops.mdc +246 -0
- package/.cursor/rules/080-ecosystem-health.mdc +145 -0
- package/.mcp.json +8 -0
- package/CHANGELOG.md +1544 -0
- package/LICENSE +21 -0
- package/NOTICE +64 -0
- package/README.md +242 -0
- package/SECURITY.md +90 -0
- package/agents/AGENTS.md +136 -0
- package/agents/analyst.md +99 -0
- package/agents/architect-reviewer.md +93 -0
- package/agents/code-implementer.md +106 -0
- package/agents/db-specialist.md +104 -0
- package/agents/dialectic-deriver.md +139 -0
- package/agents/docs-writer.md +113 -0
- package/agents/eval-judge.md +146 -0
- package/agents/memory-proposal-collector.md +297 -0
- package/agents/qa-strategist.md +102 -0
- package/agents/schemas/analyst.schema.json +46 -0
- package/agents/schemas/architect-reviewer.schema.json +50 -0
- package/agents/schemas/code-implementer.schema.json +61 -0
- package/agents/schemas/db-specialist.schema.json +80 -0
- package/agents/schemas/docs-writer.schema.json +56 -0
- package/agents/schemas/persona-panel-sidecar.schema.json +245 -0
- package/agents/schemas/qa-strategist.schema.json +46 -0
- package/agents/schemas/security-reviewer.schema.json +86 -0
- package/agents/schemas/session-reviewer.schema.json +69 -0
- package/agents/schemas/test-writer.schema.json +69 -0
- package/agents/schemas/ui-developer.schema.json +90 -0
- package/agents/schemas/ux-evaluator.schema.json +51 -0
- package/agents/security-reviewer.md +236 -0
- package/agents/session-reviewer.md +201 -0
- package/agents/skill-applied-judge.md +122 -0
- package/agents/test-writer.md +123 -0
- package/agents/ui-developer.md +109 -0
- package/agents/ux-evaluator.md +161 -0
- package/assets/icon.svg +11 -0
- package/assets/og-card.png +0 -0
- package/assets/og-card.svg +47 -0
- package/commands/autopilot-multi.md +74 -0
- package/commands/autopilot.md +80 -0
- package/commands/bootstrap.md +56 -0
- package/commands/brainstorm.md +48 -0
- package/commands/close.md +24 -0
- package/commands/debug.md +36 -0
- package/commands/discovery.md +32 -0
- package/commands/dispatcher.md +59 -0
- package/commands/eval.md +28 -0
- package/commands/evolve.md +10 -0
- package/commands/go.md +41 -0
- package/commands/grill.md +45 -0
- package/commands/harness-audit.md +26 -0
- package/commands/memory-cleanup.md +25 -0
- package/commands/persona-panel.md +121 -0
- package/commands/plan.md +15 -0
- package/commands/portfolio.md +97 -0
- package/commands/reconcile.md +23 -0
- package/commands/repo-audit.md +24 -0
- package/commands/session.md +30 -0
- package/commands/spinout.md +15 -0
- package/commands/sunset-review.md +27 -0
- package/commands/templates-ack.md +96 -0
- package/commands/test.md +97 -0
- package/docs/README.md +105 -0
- package/docs/USER-GUIDE.md +1403 -0
- package/docs/ci-setup.md +81 -0
- package/docs/codex-setup.md +142 -0
- package/docs/components.md +74 -0
- package/docs/cursor-setup.md +104 -0
- package/docs/events-schema.md +81 -0
- package/docs/migration-v3.md +148 -0
- package/docs/owner-config-schema.md +154 -0
- package/docs/persona-panel.md +433 -0
- package/docs/pi-setup.md +115 -0
- package/docs/plugin-architecture-v3.md +296 -0
- package/docs/pm-skills-marketplace.md +114 -0
- package/docs/policy-cache-validation-2026-04-28.md +118 -0
- package/docs/rule-authoring.md +316 -0
- package/docs/session-config-reference.md +1439 -0
- package/docs/session-config-template.md +961 -0
- package/docs/vault-docs-architecture.md +297 -0
- package/hooks/_lib/lock-bootstrap.mjs +272 -0
- package/hooks/_lib/lock-reconcile.mjs +93 -0
- package/hooks/_lib/profile-gate.mjs +95 -0
- package/hooks/_lib/transcript-history.mjs +211 -0
- package/hooks/agent-teams-h3-test.sh +362 -0
- package/hooks/config-protection.mjs +0 -0
- package/hooks/cwd-change-restore.mjs +131 -0
- package/hooks/enforce-commands.mjs +179 -0
- package/hooks/enforce-scope.mjs +273 -0
- package/hooks/hooks-codex.json +60 -0
- package/hooks/hooks-cursor.json +15 -0
- package/hooks/hooks-pi.json +115 -0
- package/hooks/hooks.json +215 -0
- package/hooks/loop-guard.mjs +260 -0
- package/hooks/on-session-end.mjs +217 -0
- package/hooks/on-session-start.mjs +660 -0
- package/hooks/on-stop.mjs +294 -0
- package/hooks/operator-steer.mjs +64 -0
- package/hooks/post-edit-validate.mjs +225 -0
- package/hooks/post-subagent-discovery-validator.mjs +398 -0
- package/hooks/post-tool-batch-wave-signal.mjs +328 -0
- package/hooks/post-tool-failure-corrective-context.mjs +248 -0
- package/hooks/post-tooluse-frontend-slop.mjs +184 -0
- package/hooks/pre-bash-destructive-guard.mjs +515 -0
- package/hooks/pre-bash-memory-propose-audit.mjs +206 -0
- package/hooks/pre-bash-staging-fence.mjs +223 -0
- package/hooks/pre-bash-templates-first.mjs +404 -0
- package/hooks/run-node.sh +72 -0
- package/hooks/skill-invocation-telemetry.mjs +99 -0
- package/hooks/subagent-telemetry.mjs +249 -0
- package/hooks/wave-scope-commit-guard.mjs +191 -0
- package/monitors/monitors.json +14 -0
- package/output-styles/finding-report.md +48 -0
- package/output-styles/session-report.md +53 -0
- package/output-styles/wave-summary.md +38 -0
- package/package.json +94 -0
- package/pi/extensions/session-orchestrator.ts +25 -0
- package/pi/prompts/autopilot-multi.md +12 -0
- package/pi/prompts/autopilot.md +12 -0
- package/pi/prompts/bootstrap.md +12 -0
- package/pi/prompts/brainstorm.md +12 -0
- package/pi/prompts/close.md +11 -0
- package/pi/prompts/debug.md +12 -0
- package/pi/prompts/discovery.md +12 -0
- package/pi/prompts/dispatcher.md +12 -0
- package/pi/prompts/eval.md +12 -0
- package/pi/prompts/evolve.md +12 -0
- package/pi/prompts/go.md +12 -0
- package/pi/prompts/grill.md +12 -0
- package/pi/prompts/harness-audit.md +12 -0
- package/pi/prompts/memory-cleanup.md +12 -0
- package/pi/prompts/persona-panel.md +12 -0
- package/pi/prompts/plan.md +12 -0
- package/pi/prompts/portfolio.md +12 -0
- package/pi/prompts/reconcile.md +12 -0
- package/pi/prompts/repo-audit.md +12 -0
- package/pi/prompts/session.md +12 -0
- package/pi/prompts/spinout.md +12 -0
- package/pi/prompts/sunset-review.md +12 -0
- package/pi/prompts/templates-ack.md +12 -0
- package/pi/prompts/test.md +12 -0
- package/rules/_index.md +51 -0
- package/rules/always-on/commit-discipline.md +26 -0
- package/rules/always-on/npm-quality-gates.md +26 -0
- package/rules/always-on/parallel-sessions.md +43 -0
- package/rules/opt-in-domain/prompt-caching.md +270 -0
- package/rules/opt-in-stack/backend-data.md +188 -0
- package/rules/opt-in-stack/backend.md +390 -0
- package/rules/opt-in-stack/frontend.md +98 -0
- package/rules/opt-in-stack/security-web.md +194 -0
- package/rules/opt-in-stack/swift.md +65 -0
- package/scripts/archive-closed-prds.mjs +416 -0
- package/scripts/autopilot-multi.mjs +802 -0
- package/scripts/autopilot.mjs +383 -0
- package/scripts/backfill-abandoned-sessions.mjs +265 -0
- package/scripts/backfill-learnings-expires.mjs +196 -0
- package/scripts/backfill-learnings.mjs +203 -0
- package/scripts/backfill-sessions.mjs +282 -0
- package/scripts/check-doc-consistency.sh +279 -0
- package/scripts/check-package-manager.mjs +445 -0
- package/scripts/ci/assert-vitest-green.mjs +267 -0
- package/scripts/codex-install.mjs +435 -0
- package/scripts/compute-grounding-injection.sh +186 -0
- package/scripts/cursor-install.mjs +113 -0
- package/scripts/dialectic-deriver.mjs +573 -0
- package/scripts/emit-event.mjs +160 -0
- package/scripts/emit-session.mjs +212 -0
- package/scripts/eval-session.mjs +262 -0
- package/scripts/export-hw-learnings.mjs +437 -0
- package/scripts/gc-stale-worktrees.mjs +666 -0
- package/scripts/generate-pi-prompts.mjs +127 -0
- package/scripts/harness-audit.mjs +287 -0
- package/scripts/lib/agent-frontmatter.mjs +266 -0
- package/scripts/lib/agent-output-schema.mjs +166 -0
- package/scripts/lib/agent-status.mjs +303 -0
- package/scripts/lib/ajv-loader.mjs +34 -0
- package/scripts/lib/auto-dialectic.mjs +382 -0
- package/scripts/lib/auto-dream.mjs +471 -0
- package/scripts/lib/autonomy/suitability.mjs +212 -0
- package/scripts/lib/autopilot/dep-graph.mjs +417 -0
- package/scripts/lib/autopilot/durable-telemetry.mjs +121 -0
- package/scripts/lib/autopilot/flags.mjs +104 -0
- package/scripts/lib/autopilot/kill-switches.mjs +174 -0
- package/scripts/lib/autopilot/loop.mjs +320 -0
- package/scripts/lib/autopilot/mr-draft.mjs +520 -0
- package/scripts/lib/autopilot/multi-killswitch.mjs +184 -0
- package/scripts/lib/autopilot/recent-runs.mjs +106 -0
- package/scripts/lib/autopilot/stall-sampler.mjs +97 -0
- package/scripts/lib/autopilot/telemetry.mjs +224 -0
- package/scripts/lib/autopilot/worktree-pipeline.mjs +605 -0
- package/scripts/lib/autopilot-telemetry.mjs +11 -0
- package/scripts/lib/autopilot.mjs +39 -0
- package/scripts/lib/backlog-scan.mjs +179 -0
- package/scripts/lib/bootstrap-lock-freshness.mjs +260 -0
- package/scripts/lib/bootstrap-lock-refresh.mjs +186 -0
- package/scripts/lib/build-live-signals.mjs +150 -0
- package/scripts/lib/ci-status-banner.mjs +425 -0
- package/scripts/lib/claude-md-budget-lint.mjs +246 -0
- package/scripts/lib/cli-flags.mjs +158 -0
- package/scripts/lib/codex/plugin-contract.mjs +610 -0
- package/scripts/lib/cold-start-detector.mjs +240 -0
- package/scripts/lib/command-blocker.mjs +458 -0
- package/scripts/lib/common.mjs +333 -0
- package/scripts/lib/config/auto-dream.mjs +77 -0
- package/scripts/lib/config/block-header.mjs +94 -0
- package/scripts/lib/config/broken-window.mjs +114 -0
- package/scripts/lib/config/coercers.mjs +248 -0
- package/scripts/lib/config/cold-start.mjs +92 -0
- package/scripts/lib/config/config-protection.mjs +120 -0
- package/scripts/lib/config/cross-repo.mjs +104 -0
- package/scripts/lib/config/custom-phases.mjs +213 -0
- package/scripts/lib/config/dialectic.mjs +92 -0
- package/scripts/lib/config/discovery-validator.mjs +75 -0
- package/scripts/lib/config/dispatcher-autonomy-capture.mjs +240 -0
- package/scripts/lib/config/dispatcher-autonomy.mjs +152 -0
- package/scripts/lib/config/docs-orchestrator.mjs +90 -0
- package/scripts/lib/config/docs-staleness.mjs +96 -0
- package/scripts/lib/config/drift-check.mjs +155 -0
- package/scripts/lib/config/eval.mjs +130 -0
- package/scripts/lib/config/events-rotation.mjs +74 -0
- package/scripts/lib/config/evolve.mjs +308 -0
- package/scripts/lib/config/frontend-slop-hook.mjs +104 -0
- package/scripts/lib/config/gitlab-portfolio.mjs +150 -0
- package/scripts/lib/config/handover-gate.mjs +106 -0
- package/scripts/lib/config/host-paths.mjs +76 -0
- package/scripts/lib/config/io.mjs +54 -0
- package/scripts/lib/config/loop-guard.mjs +117 -0
- package/scripts/lib/config/memory.mjs +150 -0
- package/scripts/lib/config/persona-gate-wave.mjs +258 -0
- package/scripts/lib/config/reconcile.mjs +205 -0
- package/scripts/lib/config/section-extractor.mjs +100 -0
- package/scripts/lib/config/skill-evolution.mjs +112 -0
- package/scripts/lib/config/slopcheck.mjs +99 -0
- package/scripts/lib/config/state-md-lock.mjs +83 -0
- package/scripts/lib/config/templates-first.mjs +94 -0
- package/scripts/lib/config/test.mjs +113 -0
- package/scripts/lib/config/vault-integration.mjs +201 -0
- package/scripts/lib/config/vault-mirror-quality.mjs +99 -0
- package/scripts/lib/config/vault-staleness.mjs +84 -0
- package/scripts/lib/config/vault-sync.mjs +96 -0
- package/scripts/lib/config/verification-auto-fix.mjs +84 -0
- package/scripts/lib/config/wave-reviewers.mjs +133 -0
- package/scripts/lib/config-schema.mjs +345 -0
- package/scripts/lib/config.mjs +474 -0
- package/scripts/lib/convergence-monitor.mjs +389 -0
- package/scripts/lib/coordinator-snapshot.mjs +371 -0
- package/scripts/lib/crypto-digest-utils.mjs +91 -0
- package/scripts/lib/discovery/helpers.mjs +127 -0
- package/scripts/lib/discovery/triage-state.mjs +279 -0
- package/scripts/lib/dispatcher/cli.mjs +257 -0
- package/scripts/lib/dispatcher/enumerate.mjs +243 -0
- package/scripts/lib/dispatcher/rank.mjs +363 -0
- package/scripts/lib/ecosystem-health.mjs +224 -0
- package/scripts/lib/ecosystem-wizard/ci-detector.mjs +18 -0
- package/scripts/lib/ecosystem-wizard/config-parser.mjs +54 -0
- package/scripts/lib/ecosystem-wizard/config-writer.mjs +287 -0
- package/scripts/lib/ecosystem-wizard/package-manager-detector.mjs +42 -0
- package/scripts/lib/ecosystem-wizard/wizard-prompt.mjs +246 -0
- package/scripts/lib/ecosystem-wizard.mjs +48 -0
- package/scripts/lib/env-check.mjs +89 -0
- package/scripts/lib/eval/engine.mjs +605 -0
- package/scripts/lib/eval/judge.mjs +433 -0
- package/scripts/lib/eval/report.mjs +367 -0
- package/scripts/lib/eval/schema.mjs +618 -0
- package/scripts/lib/eval/session-resolve.mjs +137 -0
- package/scripts/lib/eval/sink.mjs +77 -0
- package/scripts/lib/events-rotation.mjs +86 -0
- package/scripts/lib/events-schema.mjs +81 -0
- package/scripts/lib/events.mjs +80 -0
- package/scripts/lib/evolve/autonomy-verdict.mjs +461 -0
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +293 -0
- package/scripts/lib/exclusivity-matrix.mjs +68 -0
- package/scripts/lib/fetch-baseline.mjs +311 -0
- package/scripts/lib/file-lock.mjs +512 -0
- package/scripts/lib/frontend-detect/detect.mjs +138 -0
- package/scripts/lib/frontend-detect/rules.mjs +295 -0
- package/scripts/lib/frontmatter-guard.mjs +241 -0
- package/scripts/lib/gates/echo-stub-detect.mjs +39 -0
- package/scripts/lib/gates/gate-baseline.mjs +42 -0
- package/scripts/lib/gates/gate-full.mjs +85 -0
- package/scripts/lib/gates/gate-helpers.mjs +231 -0
- package/scripts/lib/gates/gate-incremental.mjs +76 -0
- package/scripts/lib/gates/gate-per-file.mjs +55 -0
- package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +447 -0
- package/scripts/lib/gitlab-portfolio/aggregator.mjs +383 -0
- package/scripts/lib/gitlab-portfolio/cli.mjs +428 -0
- package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +289 -0
- package/scripts/lib/gitlab-portfolio/vcs-detect.mjs +182 -0
- package/scripts/lib/handover-gate.mjs +222 -0
- package/scripts/lib/hardening.mjs +43 -0
- package/scripts/lib/hardware-pattern-detector.mjs +238 -0
- package/scripts/lib/harness-audit/categories/category1.mjs +123 -0
- package/scripts/lib/harness-audit/categories/category2.mjs +145 -0
- package/scripts/lib/harness-audit/categories/category3.mjs +143 -0
- package/scripts/lib/harness-audit/categories/category4.mjs +202 -0
- package/scripts/lib/harness-audit/categories/category5.mjs +152 -0
- package/scripts/lib/harness-audit/categories/category6.mjs +211 -0
- package/scripts/lib/harness-audit/categories/category7.mjs +125 -0
- package/scripts/lib/harness-audit/categories/category8.mjs +328 -0
- package/scripts/lib/harness-audit/categories/category9.mjs +294 -0
- package/scripts/lib/harness-audit/categories/helpers.mjs +165 -0
- package/scripts/lib/harness-audit/categories.mjs +19 -0
- package/scripts/lib/historical-guard.mjs +15 -0
- package/scripts/lib/host-identity.mjs +262 -0
- package/scripts/lib/instruction-budget-guard.mjs +332 -0
- package/scripts/lib/io.mjs +304 -0
- package/scripts/lib/issue-close-strip-labels.mjs +161 -0
- package/scripts/lib/language-mappers/README.md +57 -0
- package/scripts/lib/language-mappers/index.mjs +165 -0
- package/scripts/lib/language-mappers/markdown.mjs +149 -0
- package/scripts/lib/language-mappers/python.mjs +249 -0
- package/scripts/lib/language-mappers/swift.mjs +201 -0
- package/scripts/lib/language-mappers/typescript.mjs +433 -0
- package/scripts/lib/learnings/expiry-sweep.mjs +164 -0
- package/scripts/lib/learnings/filters.mjs +43 -0
- package/scripts/lib/learnings/io.mjs +255 -0
- package/scripts/lib/learnings/schema.mjs +518 -0
- package/scripts/lib/learnings/surface.mjs +207 -0
- package/scripts/lib/learnings.mjs +42 -0
- package/scripts/lib/lock-reaper.mjs +648 -0
- package/scripts/lib/locks/index.mjs +31 -0
- package/scripts/lib/locks/lock-body.mjs +62 -0
- package/scripts/lib/locks/staging-fence-lock.mjs +267 -0
- package/scripts/lib/locks/state-md-lock.mjs +351 -0
- package/scripts/lib/loop-readiness-banner.mjs +144 -0
- package/scripts/lib/memory-banner.mjs +478 -0
- package/scripts/lib/memory-cleanup/worktree-sweep.mjs +108 -0
- package/scripts/lib/memory-cleanup-stamp.mjs +56 -0
- package/scripts/lib/memory-paths.mjs +31 -0
- package/scripts/lib/memory-proposals/collector.mjs +334 -0
- package/scripts/lib/memory-proposals/schema.mjs +289 -0
- package/scripts/lib/memory-proposals/sink.mjs +507 -0
- package/scripts/lib/memory-proposals/store.mjs +441 -0
- package/scripts/lib/mission-status-schema.mjs +114 -0
- package/scripts/lib/mode-selector/alternatives.mjs +64 -0
- package/scripts/lib/mode-selector/constants.mjs +29 -0
- package/scripts/lib/mode-selector/context-pressure.mjs +157 -0
- package/scripts/lib/mode-selector/rationale.mjs +55 -0
- package/scripts/lib/mode-selector/scoring.mjs +221 -0
- package/scripts/lib/mode-selector-accuracy.mjs +121 -0
- package/scripts/lib/mode-selector.mjs +160 -0
- package/scripts/lib/multi-provider-build/providers.mjs +64 -0
- package/scripts/lib/multi-provider-build/templating.mjs +130 -0
- package/scripts/lib/named-baseline-resolver.mjs +233 -0
- package/scripts/lib/named-vault-resolver.mjs +433 -0
- package/scripts/lib/owner-config/coerce.mjs +29 -0
- package/scripts/lib/owner-config/constants.mjs +21 -0
- package/scripts/lib/owner-config/defaults.mjs +50 -0
- package/scripts/lib/owner-config/error.mjs +19 -0
- package/scripts/lib/owner-config/index.mjs +13 -0
- package/scripts/lib/owner-config/merge.mjs +52 -0
- package/scripts/lib/owner-config/validate.mjs +259 -0
- package/scripts/lib/owner-config-banner.mjs +126 -0
- package/scripts/lib/owner-config-loader.mjs +159 -0
- package/scripts/lib/owner-config.example.yaml +72 -0
- package/scripts/lib/owner-config.mjs +28 -0
- package/scripts/lib/owner-interview.mjs +243 -0
- package/scripts/lib/owner-yaml.mjs +571 -0
- package/scripts/lib/package-manager.mjs +160 -0
- package/scripts/lib/path-utils.mjs +217 -0
- package/scripts/lib/peer-cards/merger.mjs +310 -0
- package/scripts/lib/peer-cards/reader.mjs +125 -0
- package/scripts/lib/peer-cards/schema.mjs +230 -0
- package/scripts/lib/peer-cards/staleness-banner.mjs +86 -0
- package/scripts/lib/peer-cards/writer.mjs +138 -0
- package/scripts/lib/peer-discovery.mjs +200 -0
- package/scripts/lib/persona-panel/catalog-loader.mjs +577 -0
- package/scripts/lib/persona-panel/consolidator.mjs +370 -0
- package/scripts/lib/persona-panel/persona-runner.mjs +375 -0
- package/scripts/lib/persona-panel/threshold.mjs +130 -0
- package/scripts/lib/pi-hook-bridge.mjs +328 -0
- package/scripts/lib/platform.mjs +266 -0
- package/scripts/lib/playwright-driver/runner.mjs +297 -0
- package/scripts/lib/plugin-root.mjs +210 -0
- package/scripts/lib/pre-dispatch-check.mjs +126 -0
- package/scripts/lib/product-repo-detect.mjs +121 -0
- package/scripts/lib/profiles/registry.mjs +176 -0
- package/scripts/lib/profiles/schema.mjs +209 -0
- package/scripts/lib/qg-command-drift-banner.mjs +88 -0
- package/scripts/lib/quality-gate/diagnostics.mjs +92 -0
- package/scripts/lib/quality-gate.mjs +536 -0
- package/scripts/lib/quality-gates-cache.mjs +228 -0
- package/scripts/lib/quality-gates-policy.mjs +95 -0
- package/scripts/lib/recommendations-v0.mjs +156 -0
- package/scripts/lib/reconcile/eligibility.mjs +203 -0
- package/scripts/lib/reconcile/emitter.mjs +244 -0
- package/scripts/lib/reconcile/engine.mjs +412 -0
- package/scripts/lib/reconcile/idempotency.mjs +239 -0
- package/scripts/lib/reconcile/renderer.mjs +211 -0
- package/scripts/lib/reconcile/writer.mjs +293 -0
- package/scripts/lib/reconcile-nudge-banner.mjs +284 -0
- package/scripts/lib/resource-probe/evaluate.mjs +190 -0
- package/scripts/lib/resource-probe/parsers.mjs +181 -0
- package/scripts/lib/resource-probe/probe-platform.mjs +300 -0
- package/scripts/lib/resource-probe.mjs +95 -0
- package/scripts/lib/rule-loader.mjs +552 -0
- package/scripts/lib/rules-sync.mjs +439 -0
- package/scripts/lib/scope-gate.mjs +496 -0
- package/scripts/lib/session-close-backfill.mjs +539 -0
- package/scripts/lib/session-discovery.mjs +256 -0
- package/scripts/lib/session-end/phase-skip.mjs +357 -0
- package/scripts/lib/session-end/worktree-cleanup.mjs +112 -0
- package/scripts/lib/session-id.mjs +362 -0
- package/scripts/lib/session-lock.mjs +703 -0
- package/scripts/lib/session-registry.mjs +355 -0
- package/scripts/lib/session-schema/aliases.mjs +71 -0
- package/scripts/lib/session-schema/constants.mjs +115 -0
- package/scripts/lib/session-schema/normalizer.mjs +66 -0
- package/scripts/lib/session-schema/timestamps.mjs +64 -0
- package/scripts/lib/session-schema/validator.mjs +453 -0
- package/scripts/lib/session-schema.mjs +71 -0
- package/scripts/lib/session-token-rollup.mjs +137 -0
- package/scripts/lib/sessions-staleness-banner.mjs +247 -0
- package/scripts/lib/skill-evolution/blast-radius-classifier.mjs +114 -0
- package/scripts/lib/skill-evolution/candidate-intake.mjs +270 -0
- package/scripts/lib/skill-evolution/config-validation-gate.mjs +279 -0
- package/scripts/lib/skill-evolution/engine.mjs +719 -0
- package/scripts/lib/skill-evolution/idempotency.mjs +279 -0
- package/scripts/lib/skill-evolution/mr-opener.mjs +507 -0
- package/scripts/lib/skill-health/join.mjs +181 -0
- package/scripts/lib/skill-health/score.mjs +123 -0
- package/scripts/lib/skill-invocations-schema.mjs +214 -0
- package/scripts/lib/skill-judge.mjs +348 -0
- package/scripts/lib/skill-judgments-schema.mjs +264 -0
- package/scripts/lib/slopcheck.mjs +501 -0
- package/scripts/lib/soul-resolve.mjs +118 -0
- package/scripts/lib/spiral-carryover.mjs +495 -0
- package/scripts/lib/state-md/body-sections.mjs +851 -0
- package/scripts/lib/state-md/frontmatter-mutators.mjs +453 -0
- package/scripts/lib/state-md/mission-status.mjs +247 -0
- package/scripts/lib/state-md/recommendations.mjs +57 -0
- package/scripts/lib/state-md/yaml-parser.mjs +234 -0
- package/scripts/lib/state-md-peer-guard.mjs +232 -0
- package/scripts/lib/state-md.mjs +53 -0
- package/scripts/lib/subagents-schema.mjs +309 -0
- package/scripts/lib/sunset/walker.mjs +1192 -0
- package/scripts/lib/test-runner/artifact-paths.mjs +94 -0
- package/scripts/lib/test-runner/fingerprint.mjs +33 -0
- package/scripts/lib/test-runner/issue-reconcile.mjs +770 -0
- package/scripts/lib/tmux-layout/layouts.mjs +224 -0
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +100 -0
- package/scripts/lib/tmux-layout/telemetry.mjs +88 -0
- package/scripts/lib/tmux-layout/tmux-shell.mjs +82 -0
- package/scripts/lib/tmux-layout/vcs-detector.mjs +88 -0
- package/scripts/lib/validate/check-agents.mjs +457 -0
- package/scripts/lib/validate/check-codex-plugin.mjs +37 -0
- package/scripts/lib/validate/check-commands.mjs +148 -0
- package/scripts/lib/validate/check-component-paths.mjs +112 -0
- package/scripts/lib/validate/check-dead-bridge.mjs +180 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +258 -0
- package/scripts/lib/validate/check-json-files.mjs +116 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +1011 -0
- package/scripts/lib/validate/check-path-utils-canary.mjs +175 -0
- package/scripts/lib/validate/check-peekaboo-driver-canary.mjs +201 -0
- package/scripts/lib/validate/check-pi-package.mjs +110 -0
- package/scripts/lib/validate/check-pi-prompts.mjs +43 -0
- package/scripts/lib/validate/check-playwright-mcp-canary.mjs +154 -0
- package/scripts/lib/validate/check-plugin-json.mjs +96 -0
- package/scripts/lib/validate/check-plugin-monitors.mjs +206 -0
- package/scripts/lib/validate/check-plugin-schema.mjs +137 -0
- package/scripts/lib/validate/check-rules.mjs +143 -0
- package/scripts/lib/validate/check-session-plan-routing.mjs +154 -0
- package/scripts/lib/validate/check-test-fixture-shapes.mjs +280 -0
- package/scripts/lib/validate/check-unicode-safety.mjs +533 -0
- package/scripts/lib/validate/confidential-names.mjs +169 -0
- package/scripts/lib/validate/dead-bridge-corpus.mjs +141 -0
- package/scripts/lib/validate/dead-bridge-detectors.mjs +568 -0
- package/scripts/lib/validate/tier-inference.mjs +100 -0
- package/scripts/lib/validate-vendored-rules.mjs +519 -0
- package/scripts/lib/vault-archive.mjs +404 -0
- package/scripts/lib/vault-backfill/glab.mjs +164 -0
- package/scripts/lib/vault-backfill/manifest.mjs +75 -0
- package/scripts/lib/vault-backfill/template.mjs +130 -0
- package/scripts/lib/vault-consolidate-fs.mjs +331 -0
- package/scripts/lib/vault-migration-rules.mjs +155 -0
- package/scripts/lib/vault-mirror/auto-commit.mjs +203 -0
- package/scripts/lib/vault-mirror/namespace.mjs +152 -0
- package/scripts/lib/vault-mirror/process.mjs +567 -0
- package/scripts/lib/vault-mirror/pseudonym-map.mjs +164 -0
- package/scripts/lib/vault-mirror/render-learnings.mjs +201 -0
- package/scripts/lib/vault-mirror/render-sessions.mjs +367 -0
- package/scripts/lib/vault-mirror/render.mjs +8 -0
- package/scripts/lib/vault-mirror/utils.mjs +217 -0
- package/scripts/lib/vault-relocation-rules.mjs +555 -0
- package/scripts/lib/vault-repo-backfill.mjs +235 -0
- package/scripts/lib/vault-staleness-banner.mjs +142 -0
- package/scripts/lib/vault-status/board-writer.mjs +769 -0
- package/scripts/lib/vault-status/narrative-mirror.mjs +544 -0
- package/scripts/lib/vault-sync-baseline.mjs +152 -0
- package/scripts/lib/wave-context.mjs +29 -0
- package/scripts/lib/wave-executor/pool.mjs +248 -0
- package/scripts/lib/wave-resource-gate.mjs +204 -0
- package/scripts/lib/wave-sizing.mjs +75 -0
- package/scripts/lib/webhook-url.mjs +105 -0
- package/scripts/lib/workspace.mjs +198 -0
- package/scripts/lib/worktree/constants.mjs +35 -0
- package/scripts/lib/worktree/index.mjs +17 -0
- package/scripts/lib/worktree/lifecycle.mjs +287 -0
- package/scripts/lib/worktree/listing.mjs +118 -0
- package/scripts/lib/worktree/meta.mjs +64 -0
- package/scripts/lib/worktree-freshness.mjs +313 -0
- package/scripts/lib/worktree.mjs +15 -0
- package/scripts/lifecycle-sim-v6.mjs +347 -0
- package/scripts/lock-reaper.mjs +185 -0
- package/scripts/mcp-server.sh +241 -0
- package/scripts/measure-policy-cache-effectiveness.mjs +427 -0
- package/scripts/memory-propose.mjs +464 -0
- package/scripts/migrate-cold-start-seed.mjs +404 -0
- package/scripts/migrate-learnings-jsonl.mjs +189 -0
- package/scripts/migrate-legacy-learnings.sh +61 -0
- package/scripts/migrate-sessions-jsonl.mjs +448 -0
- package/scripts/migrate-subagents-jsonl.mjs +196 -0
- package/scripts/migrate-vault-paths.mjs +796 -0
- package/scripts/parse-config.mjs +149 -0
- package/scripts/pi-install.mjs +117 -0
- package/scripts/print-applicable-rules.mjs +247 -0
- package/scripts/promote-vault-strict.mjs +496 -0
- package/scripts/relocate-vault-corpus.mjs +1178 -0
- package/scripts/run-migrate-v2-cross-repo.mjs +385 -0
- package/scripts/run-quality-gate.mjs +216 -0
- package/scripts/spikes/h3-agent-teams/preflight.sh +53 -0
- package/scripts/spikes/h3-agent-teams/run-h3.sh +112 -0
- package/scripts/spikes/h3-agent-teams/setup.sh +137 -0
- package/scripts/spikes/h3-agent-teams/toggle.sh +38 -0
- package/scripts/sweep-expired-learnings.mjs +135 -0
- package/scripts/sync-vault-schema.mjs +376 -0
- package/scripts/tests/fixtures/fetch-baseline/sample-rule.md +8 -0
- package/scripts/tmux-layout.mjs +245 -0
- package/scripts/token-audit.sh +191 -0
- package/scripts/typecheck.mjs +42 -0
- package/scripts/upload-social-preview.mjs +316 -0
- package/scripts/validate-config.mjs +46 -0
- package/scripts/validate-plugin-manifests.mjs +163 -0
- package/scripts/validate-plugin.mjs +264 -0
- package/scripts/validate-wave-scope.mjs +289 -0
- package/scripts/vault-backfill.mjs +404 -0
- package/scripts/vault-consolidate.mjs +596 -0
- package/scripts/vault-integration-watcher.mjs +394 -0
- package/scripts/vault-mirror.mjs +430 -0
- package/skills/_shared/bootstrap-gate.md +111 -0
- package/skills/_shared/config-reading.md +226 -0
- package/skills/_shared/instruction-file-resolution.md +79 -0
- package/skills/_shared/model-selection.md +64 -0
- package/skills/_shared/monitor-patterns.md +300 -0
- package/skills/_shared/parallel-aware-auq.md +121 -0
- package/skills/_shared/parallel-aware-preamble.md +185 -0
- package/skills/_shared/platform-tools.md +96 -0
- package/skills/_shared/state-ownership.md +221 -0
- package/skills/architecture/DEEPENING.md +37 -0
- package/skills/architecture/INTERFACE-DESIGN.md +44 -0
- package/skills/architecture/LANGUAGE.md +53 -0
- package/skills/architecture/SKILL.md +92 -0
- package/skills/autopilot/SKILL.md +419 -0
- package/skills/bootstrap/SKILL.md +592 -0
- package/skills/bootstrap/STATE.md.template +24 -0
- package/skills/bootstrap/_shared-template.md +243 -0
- package/skills/bootstrap/deep-template.md +659 -0
- package/skills/bootstrap/fast-template.md +251 -0
- package/skills/bootstrap/intensity-heuristic.md +80 -0
- package/skills/bootstrap/public-fallback.md +342 -0
- package/skills/bootstrap/standard-template.md +736 -0
- package/skills/bootstrap/templates/agents/project-code-review.md +18 -0
- package/skills/bootstrap/templates/agents/project-discovery.md +18 -0
- package/skills/bootstrap/templates/agents/project-quality-gate.md +18 -0
- package/skills/brainstorm/SKILL.md +268 -0
- package/skills/brainstorm/soul.md +49 -0
- package/skills/claude-md-drift-check/SKILL.md +186 -0
- package/skills/claude-md-drift-check/checker.mjs +1380 -0
- package/skills/claude-md-drift-check/checker.sh +37 -0
- package/skills/claude-md-drift-check/package.json +16 -0
- package/skills/convergence-monitoring/README.md +39 -0
- package/skills/convergence-monitoring/SIGNALS.md +246 -0
- package/skills/convergence-monitoring/SKILL.md +285 -0
- package/skills/daily/SKILL.md +222 -0
- package/skills/daily/generate.sh +92 -0
- package/skills/daily/templates/daily.md.tpl +36 -0
- package/skills/debug/SKILL.md +188 -0
- package/skills/debug/soul.md +35 -0
- package/skills/discovery/SKILL.md +567 -0
- package/skills/discovery/issue-templates.md +237 -0
- package/skills/discovery/probes/docs-staleness.mjs +195 -0
- package/skills/discovery/probes/frontend-slop.mjs +186 -0
- package/skills/discovery/probes/ssot-code-diff.mjs +310 -0
- package/skills/discovery/probes/supply-chain-slopcheck.mjs +440 -0
- package/skills/discovery/probes/vault-narrative-staleness.mjs +355 -0
- package/skills/discovery/probes/vault-staleness.mjs +272 -0
- package/skills/discovery/probes-arch.md +252 -0
- package/skills/discovery/probes-audit.md +95 -0
- package/skills/discovery/probes-code.md +329 -0
- package/skills/discovery/probes-docs.md +76 -0
- package/skills/discovery/probes-feature.md +150 -0
- package/skills/discovery/probes-infra.md +138 -0
- package/skills/discovery/probes-intro.md +25 -0
- package/skills/discovery/probes-session.md +495 -0
- package/skills/discovery/probes-supply-chain.md +94 -0
- package/skills/discovery/probes-ui.md +147 -0
- package/skills/discovery/probes-vault.md +64 -0
- package/skills/discovery/slop-patterns.md +115 -0
- package/skills/dispatcher/SKILL.md +173 -0
- package/skills/docs-orchestrator/SKILL.md +362 -0
- package/skills/docs-orchestrator/audience-mapping.md +140 -0
- package/skills/domain-model/ADR-FORMAT.md +47 -0
- package/skills/domain-model/CONTEXT-FORMAT.md +77 -0
- package/skills/domain-model/SKILL.md +85 -0
- package/skills/ecosystem-health/SKILL.md +119 -0
- package/skills/ecosystem-health/wizard.md +193 -0
- package/skills/eval/SKILL.md +293 -0
- package/skills/eval/rubric-v1.md +218 -0
- package/skills/evolve/SKILL.md +546 -0
- package/skills/frontmatter-guard/SKILL.md +126 -0
- package/skills/gitlab-ops/SKILL.md +368 -0
- package/skills/gitlab-portfolio/SKILL.md +196 -0
- package/skills/grill/SKILL.md +185 -0
- package/skills/grill/soul.md +55 -0
- package/skills/hook-development/SKILL.md +413 -0
- package/skills/mcp-builder/SKILL.md +260 -0
- package/skills/memory-cleanup/SKILL.md +310 -0
- package/skills/mode-selector/SKILL.md +226 -0
- package/skills/peekaboo-driver/SKILL.md +237 -0
- package/skills/peekaboo-driver/soul.md +32 -0
- package/skills/persona-panel/SKILL.md +365 -0
- package/skills/persona-panel/persona-format.md +205 -0
- package/skills/persona-panel/presets/designer-lens.md +87 -0
- package/skills/persona-panel/presets/engineer-lens.md +88 -0
- package/skills/persona-panel/presets/pm-lens.md +86 -0
- package/skills/plan/SKILL.md +496 -0
- package/skills/plan/mode-feature.md +141 -0
- package/skills/plan/mode-new.md +297 -0
- package/skills/plan/mode-retro.md +271 -0
- package/skills/plan/prd-feature-template.md +132 -0
- package/skills/plan/prd-full-template.md +151 -0
- package/skills/plan/prd-reviewer-prompt.md +103 -0
- package/skills/plan/retro-template.md +75 -0
- package/skills/plan/soul.md +62 -0
- package/skills/playwright-driver/SKILL.md +226 -0
- package/skills/playwright-driver/soul.md +30 -0
- package/skills/quality-gates/SKILL.md +212 -0
- package/skills/reconcile/SKILL.md +324 -0
- package/skills/repo-audit/SKILL.md +272 -0
- package/skills/session-end/SKILL.md +1044 -0
- package/skills/session-end/discovery-scan.md +37 -0
- package/skills/session-end/drift-operations.md +97 -0
- package/skills/session-end/learning-patterns.md +78 -0
- package/skills/session-end/metrics-collection.md +175 -0
- package/skills/session-end/phase-3-2-docs-verification.md +148 -0
- package/skills/session-end/phase-3-6-tail.md +344 -0
- package/skills/session-end/phase-3-7a-recommendations.md +86 -0
- package/skills/session-end/plan-verification.md +288 -0
- package/skills/session-end/session-metrics-write.md +223 -0
- package/skills/session-end/vault-operations.md +50 -0
- package/skills/session-end/verification-checklist.md +20 -0
- package/skills/session-plan/SKILL.md +554 -0
- package/skills/session-plan/wave-template.md +37 -0
- package/skills/session-start/SKILL.md +1043 -0
- package/skills/session-start/phase-2-5-docs-planning.md +119 -0
- package/skills/session-start/phase-4-5-resource-health.md +49 -0
- package/skills/session-start/phase-7-1-premise-check.md +47 -0
- package/skills/session-start/phase-7-5-mode-selector.md +237 -0
- package/skills/session-start/phase-8-5-express-path.md +61 -0
- package/skills/session-start/presentation-format.md +81 -0
- package/skills/session-start/soul.md +57 -0
- package/skills/skill-creator/SKILL.md +168 -0
- package/skills/spinout/SKILL.md +76 -0
- package/skills/sunset-review/SKILL.md +96 -0
- package/skills/test-runner/SKILL.md +362 -0
- package/skills/test-runner/rubric-v1.md +388 -0
- package/skills/test-runner/soul.md +46 -0
- package/skills/tmux-layout/SKILL.md +104 -0
- package/skills/ubiquitous-language/SKILL.md +97 -0
- package/skills/using-orchestrator/SKILL.md +144 -0
- package/skills/vault-mirror/SKILL.md +234 -0
- package/skills/vault-sync/SKILL.md +319 -0
- package/skills/vault-sync/package-lock.json +40 -0
- package/skills/vault-sync/package.json +11 -0
- package/skills/vault-sync/tests/fixtures/archive-test-vault/90-archive/bad-archived.md +8 -0
- package/skills/vault-sync/tests/fixtures/archive-test-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/archive-test-vault/live-note.md +8 -0
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/bad-type.md +8 -0
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/good-note.md +8 -0
- package/skills/vault-sync/tests/fixtures/clean-vault/.obsidian/config.md +8 -0
- package/skills/vault-sync/tests/fixtures/clean-vault/01-projects/foo/projects-baseline.md +10 -0
- package/skills/vault-sync/tests/fixtures/clean-vault/03-daily/daily-2026-04-13.md +8 -0
- package/skills/vault-sync/tests/fixtures/clean-vault/README.md +3 -0
- package/skills/vault-sync/tests/fixtures/clean-vault/hello-world.md +11 -0
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/has-dangling.md +9 -0
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/real-target.md +8 -0
- package/skills/vault-sync/tests/fixtures/empty-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/missing-field-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/missing-field-vault/missing-id.md +7 -0
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/03-daily/daily-2026-04-13.md +9 -0
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/nested-tags-note.md +11 -0
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/README.md +3 -0
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_MOC.md +3 -0
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/_MOC.md +11 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/hello-world.md +11 -0
- package/skills/vault-sync/tests/schema-drift.test.mjs +133 -0
- package/skills/vault-sync/validator.mjs +658 -0
- package/skills/vault-sync/validator.sh +55 -0
- package/skills/wave-executor/SKILL.md +496 -0
- package/skills/wave-executor/circuit-breaker.md +169 -0
- package/skills/wave-executor/wave-loop.md +1043 -0
- package/skills/write-executable-plan/SKILL.md +237 -0
- package/skills/write-executable-plan/plan-template.md +154 -0
- package/templates/_minimal/CLAUDE.md.tmpl +41 -0
- package/templates/_minimal/README.md.tmpl +15 -0
- package/templates/_minimal/gitignore.tmpl +47 -0
- package/templates/_shared/harte-regeln.md +16 -0
- package/templates/_shared/loop.md +90 -0
- package/templates/_shared/rules/parallel-sessions.md +77 -0
- package/templates/nextjs-minimal/README.md +30 -0
- package/templates/nextjs-minimal/app/layout.tsx +18 -0
- package/templates/nextjs-minimal/app/page.tsx +7 -0
- package/templates/nextjs-minimal/eslint.config.mjs +16 -0
- package/templates/nextjs-minimal/next.config.mjs +4 -0
- package/templates/nextjs-minimal/package.json +27 -0
- package/templates/nextjs-minimal/tsconfig.json +23 -0
- package/templates/node-minimal/README.md +33 -0
- package/templates/node-minimal/eslint.config.mjs +10 -0
- package/templates/node-minimal/package.json +21 -0
- package/templates/node-minimal/src/index.ts +1 -0
- package/templates/node-minimal/tests/sanity.test.ts +5 -0
- package/templates/node-minimal/tsconfig.json +17 -0
- package/templates/personas/README.md +150 -0
- package/templates/personas/accounting-compliance.v1.md +120 -0
- package/templates/personas/accounting-tax-advisor.v1.md +116 -0
- package/templates/personas/buyer-p1-cto.v1.md +125 -0
- package/templates/personas/buyer-p2-kanzlei.v1.md +134 -0
- package/templates/personas/buyer-p3-build.v1.md +130 -0
- package/templates/personas/buyer-p4-tech-veto.v1.md +130 -0
- package/templates/personas/buyer-p5-solo.v1.md +132 -0
- package/templates/personas/buyer-p6-ld.v1.md +130 -0
- package/templates/personas/klima-ai-expert.v1.md +114 -0
- package/templates/personas/klima-physicist.v1.md +117 -0
- package/templates/python-uv/README.md +28 -0
- package/templates/python-uv/pyproject.toml +32 -0
- package/templates/python-uv/src/__PROJECT_NAME__/__init__.py +0 -0
- package/templates/python-uv/src/__PROJECT_NAME__/main.py +6 -0
- package/templates/python-uv/tests/test_sanity.py +2 -0
- package/templates/static-html/README.md +19 -0
- package/templates/static-html/index.html +15 -0
- package/templates/static-html/script.js +1 -0
- package/templates/static-html/styles.css +26 -0
|
@@ -0,0 +1,770 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* test-runner/issue-reconcile.mjs — Reconcile findings with the open issue tracker.
|
|
3
|
+
*
|
|
4
|
+
* Uses execFile (NOT shell) per ADR-364 §C5. Binary allowlist + arg-boundary
|
|
5
|
+
* validation prevent shell-injection. Fingerprint-based dedup prevents
|
|
6
|
+
* duplicate issues.
|
|
7
|
+
*
|
|
8
|
+
* Exports:
|
|
9
|
+
* reconcileFinding({finding, existingFingerprints, dryRun, execFile})
|
|
10
|
+
* → Promise<{action: 'create'|'noop', iid?, command?}>
|
|
11
|
+
*
|
|
12
|
+
* listExistingFindings({project, label, maxBuffer, execFile})
|
|
13
|
+
* → Promise<{ok: true, issues: Array<{iid, title, body}>, fingerprints: Set<string>}
|
|
14
|
+
* | {ok: false, error}>
|
|
15
|
+
*
|
|
16
|
+
* createFinding({project, fingerprint, title, body, labels, dryRun, maxBuffer, execFile})
|
|
17
|
+
* → Promise<{ok: true, action: 'create', iid?, command?} | {ok: false, error}>
|
|
18
|
+
*
|
|
19
|
+
* updateFinding({project, iid, comment, dryRun, maxBuffer, execFile})
|
|
20
|
+
* → Promise<{ok: true, action: 'comment', command?} | {ok: false, error}>
|
|
21
|
+
*
|
|
22
|
+
* triageDecision(finding, candidates)
|
|
23
|
+
* → {action: 'create'|'update'|'ignore', target?, reason: string, confidence: number}
|
|
24
|
+
*
|
|
25
|
+
* ReconcileError
|
|
26
|
+
*
|
|
27
|
+
* Security:
|
|
28
|
+
* - #388 (SEC-IR-MED-1): sentinel injection hardening — sanitizeRecommendation()
|
|
29
|
+
* strips **Fingerprint:** literals from free-text fields before they are
|
|
30
|
+
* embedded in the body (before the authoritative sentinel line is appended).
|
|
31
|
+
* - #389 (SEC-IR-LOW-1): maxBuffer set to 4 MB on every execFile call;
|
|
32
|
+
* body > 65536 bytes is rejected with BODY_TOO_LARGE.
|
|
33
|
+
* - ADR-364 §C5 / HIGH: glabPath parameter removed. All call sites use
|
|
34
|
+
* RECONCILE_ALLOWLISTED_BINS.glab. Tests inject behaviour via opts.execFile
|
|
35
|
+
* (same DI seam as mr-draft.mjs) — never via caller-supplied binary paths.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { execFile as execFileCb } from 'node:child_process';
|
|
39
|
+
import { promisify } from 'node:util';
|
|
40
|
+
import { fingerprintFinding } from './fingerprint.mjs';
|
|
41
|
+
|
|
42
|
+
const realExecFile = promisify(execFileCb);
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Constants (#389)
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
/** Default maxBuffer for all execFile calls — 4 MB (#389 SEC-IR-LOW-1). */
|
|
49
|
+
const DEFAULT_MAX_BUFFER = 4 * 1024 * 1024;
|
|
50
|
+
|
|
51
|
+
/** Maximum allowed issue body length in bytes (#389 SEC-IR-LOW-1). */
|
|
52
|
+
const MAX_BODY_BYTES = 65536;
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Binary allowlist (ADR-364 C5 + SEC-014)
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Bare binary names only — resolved at runtime via PATH by Node's execve.
|
|
58
|
+
// Reject any string containing '/' or '..' to prevent path traversal.
|
|
59
|
+
const RECONCILE_ALLOWLISTED_BINS = Object.freeze({
|
|
60
|
+
glab: 'glab',
|
|
61
|
+
gh: 'gh',
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// CLI-argument-boundary rejection regex (ADR-364 §C5 / SEC-PD-MED-1)
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Reject only characters that corrupt CLI arg semantics when passed to execFile
|
|
68
|
+
// with shell: false. Narrowed to newline + CR + null byte — shell metacharacters
|
|
69
|
+
// are irrelevant because shell: false prevents shell interpretation.
|
|
70
|
+
const ARG_BOUNDARY_DANGEROUS = /[\n\r\0]/;
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// ReconcileError
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Typed error for reconciliation failures.
|
|
78
|
+
* Codes: 'VALIDATION' | 'EXEC_FAILURE' | 'BINARY_NOT_FOUND' | 'BODY_TOO_LARGE'
|
|
79
|
+
*/
|
|
80
|
+
export class ReconcileError extends Error {
|
|
81
|
+
constructor(message, code) {
|
|
82
|
+
super(message);
|
|
83
|
+
this.name = 'ReconcileError';
|
|
84
|
+
this.code = code;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Input validation
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
const VALID_SEVERITIES = new Set(['critical', 'high', 'medium', 'low']);
|
|
93
|
+
const REQUIRED_STRING_FIELDS = ['scope', 'checkId', 'locator', 'severity', 'title', 'description'];
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Validate a finding object, throwing ReconcileError(VALIDATION) on failure.
|
|
97
|
+
* @param {object} finding
|
|
98
|
+
*/
|
|
99
|
+
function validateFinding(finding) {
|
|
100
|
+
if (!finding || typeof finding !== 'object') {
|
|
101
|
+
throw new ReconcileError('finding must be an object', 'VALIDATION');
|
|
102
|
+
}
|
|
103
|
+
for (const key of REQUIRED_STRING_FIELDS) {
|
|
104
|
+
const v = finding[key];
|
|
105
|
+
if (typeof v !== 'string') {
|
|
106
|
+
throw new ReconcileError(`finding.${key} must be a string`, 'VALIDATION');
|
|
107
|
+
}
|
|
108
|
+
if (ARG_BOUNDARY_DANGEROUS.test(v)) {
|
|
109
|
+
throw new ReconcileError(
|
|
110
|
+
`finding.${key} contains forbidden characters (newline/CR/null byte)`,
|
|
111
|
+
'VALIDATION',
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (!VALID_SEVERITIES.has(finding.severity)) {
|
|
116
|
+
throw new ReconcileError(
|
|
117
|
+
`finding.severity must be one of: ${[...VALID_SEVERITIES].join(', ')}`,
|
|
118
|
+
'VALIDATION',
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Sentinel sanitization (#388 SEC-IR-MED-1)
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Sanitize free-text recommendation fields before embedding in the issue body.
|
|
129
|
+
* Replaces any literal `**Fingerprint:**` with `__Fingerprint__` to prevent
|
|
130
|
+
* a crafted recommendation from spoofing the authoritative fingerprint sentinel
|
|
131
|
+
* line that is appended by buildIssueBody().
|
|
132
|
+
*
|
|
133
|
+
* Context: glab parses issue bodies as Markdown. If `recommendation` contained
|
|
134
|
+
* `**Fingerprint:** <attacker-hash>`, a grep for the fingerprint sentinel in
|
|
135
|
+
* listExistingFindings() would match the spoofed value instead of the real one,
|
|
136
|
+
* bypassing dedup. Replacing the literal prevents the attack without altering
|
|
137
|
+
* the semantic meaning of the recommendation text.
|
|
138
|
+
*
|
|
139
|
+
* @param {string|undefined|null} text
|
|
140
|
+
* @returns {string|undefined|null}
|
|
141
|
+
*/
|
|
142
|
+
function sanitizeRecommendation(text) {
|
|
143
|
+
if (!text || typeof text !== 'string') return text;
|
|
144
|
+
// gi flag: case-insensitive match so **fingerprint:** and **FINGERPRINT:** variants
|
|
145
|
+
// are also neutralized. Sanitizer is intentionally broader than the case-sensitive
|
|
146
|
+
// extractor regex — this is the correct asymmetry (#388 SEC-IR-MED-1).
|
|
147
|
+
return text.replace(/\*\*Fingerprint:\*\*/gi, '__Fingerprint__');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// Body-length validation (#389 SEC-IR-LOW-1)
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Validate that a body string does not exceed MAX_BODY_BYTES.
|
|
156
|
+
* Returns {ok: false, error} on violation; callers propagate this directly.
|
|
157
|
+
*
|
|
158
|
+
* @param {string} body
|
|
159
|
+
* @returns {{ok: false, error: {code: string, message: string}}|null}
|
|
160
|
+
*/
|
|
161
|
+
function checkBodyLength(body) {
|
|
162
|
+
const byteLength = Buffer.byteLength(body, 'utf8');
|
|
163
|
+
if (byteLength > MAX_BODY_BYTES) {
|
|
164
|
+
return {
|
|
165
|
+
ok: false,
|
|
166
|
+
error: {
|
|
167
|
+
code: 'BODY_TOO_LARGE',
|
|
168
|
+
message: `Body exceeds maximum allowed size of ${MAX_BODY_BYTES} bytes (got ${byteLength} bytes)`,
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// Issue body builder
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Build the issue description body from a finding and its fingerprint.
|
|
181
|
+
* Newlines within the body are intentional and safe — execFile passes
|
|
182
|
+
* --description as a single argv element, not through a shell.
|
|
183
|
+
*
|
|
184
|
+
* Applies sanitizeRecommendation() to the recommendation field (#388) before
|
|
185
|
+
* embedding it, so the authoritative `**Fingerprint:** \`<fp>\`` sentinel
|
|
186
|
+
* line cannot be spoofed by attacker-controlled recommendation text.
|
|
187
|
+
*
|
|
188
|
+
* @param {object} finding
|
|
189
|
+
* @param {string} fp - 16-char hex fingerprint
|
|
190
|
+
* @returns {string}
|
|
191
|
+
*/
|
|
192
|
+
function buildIssueBody(finding, fp) {
|
|
193
|
+
const safeRecommendation = sanitizeRecommendation(finding.recommendation);
|
|
194
|
+
const lines = [
|
|
195
|
+
finding.description,
|
|
196
|
+
'',
|
|
197
|
+
safeRecommendation ? `**Recommendation:** ${safeRecommendation}` : null,
|
|
198
|
+
'',
|
|
199
|
+
`**Fingerprint:** \`${fp}\``,
|
|
200
|
+
`**Severity:** ${finding.severity}`,
|
|
201
|
+
`**Check:** ${finding.checkId}`,
|
|
202
|
+
`**Locator:** \`${finding.locator}\``,
|
|
203
|
+
];
|
|
204
|
+
return lines.filter((line) => line !== null).join('\n');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
// Parse glab stdout for the created issue IID
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Parse the issue IID from glab's stdout.
|
|
213
|
+
* glab issue create prints a URL like:
|
|
214
|
+
* https://gitlab.example.com/group/project/-/issues/123
|
|
215
|
+
* We extract the trailing integer.
|
|
216
|
+
*
|
|
217
|
+
* @param {string} stdout
|
|
218
|
+
* @returns {number|null}
|
|
219
|
+
*/
|
|
220
|
+
function parseIidFromGlabOutput(stdout) {
|
|
221
|
+
if (typeof stdout !== 'string') return null;
|
|
222
|
+
const match = stdout.match(/\/issues\/(\d+)/);
|
|
223
|
+
if (match) return parseInt(match[1], 10);
|
|
224
|
+
// Fallback: any trailing integer on the last non-empty line
|
|
225
|
+
const trail = stdout.trim().match(/(\d+)\s*$/);
|
|
226
|
+
return trail ? parseInt(trail[1], 10) : null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
// Extract fingerprints from issue bodies
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Extract the 16-hex fingerprint from an issue body string.
|
|
235
|
+
* Matches the authoritative sentinel line: `**Fingerprint:** \`<16-hex>\``
|
|
236
|
+
*
|
|
237
|
+
* @param {string} body
|
|
238
|
+
* @returns {string|null}
|
|
239
|
+
*/
|
|
240
|
+
function extractFingerprintFromBody(body) {
|
|
241
|
+
if (typeof body !== 'string') return null;
|
|
242
|
+
const match = body.match(/\*\*Fingerprint:\*\*\s*`([0-9a-f]{16})`/);
|
|
243
|
+
return match ? match[1] : null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
// Levenshtein distance (pure) — used by triageDecision
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Compute the Levenshtein edit distance between two strings.
|
|
252
|
+
* Pure function; no external dependencies.
|
|
253
|
+
*
|
|
254
|
+
* @param {string} a
|
|
255
|
+
* @param {string} b
|
|
256
|
+
* @returns {number}
|
|
257
|
+
*/
|
|
258
|
+
function levenshtein(a, b) {
|
|
259
|
+
if (typeof a !== 'string' || typeof b !== 'string') return Infinity;
|
|
260
|
+
if (a === b) return 0;
|
|
261
|
+
const la = a.length;
|
|
262
|
+
const lb = b.length;
|
|
263
|
+
if (la === 0) return lb;
|
|
264
|
+
if (lb === 0) return la;
|
|
265
|
+
|
|
266
|
+
// Two-row DP to keep memory O(min(m,n)).
|
|
267
|
+
let prev = Array.from({ length: lb + 1 }, (_, i) => i);
|
|
268
|
+
let curr = new Array(lb + 1);
|
|
269
|
+
|
|
270
|
+
for (let i = 1; i <= la; i++) {
|
|
271
|
+
curr[0] = i;
|
|
272
|
+
for (let j = 1; j <= lb; j++) {
|
|
273
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
274
|
+
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
275
|
+
}
|
|
276
|
+
[prev, curr] = [curr, prev];
|
|
277
|
+
}
|
|
278
|
+
return prev[lb];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
// reconcileFinding (Track A — backwards-compatible original export)
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Reconcile a finding against existing issues.
|
|
287
|
+
*
|
|
288
|
+
* If the fingerprint is already in `existingFingerprints`, return `{action: 'noop'}`.
|
|
289
|
+
* Otherwise, create a new issue via glab (or return the dry-run command).
|
|
290
|
+
*
|
|
291
|
+
* @param {object} opts
|
|
292
|
+
* @param {object} opts.finding - finding object with scope/checkId/locator/severity/title/description
|
|
293
|
+
* @param {Set<string>} opts.existingFingerprints - fingerprints of issues already filed
|
|
294
|
+
* @param {boolean} [opts.dryRun] - if true, return command without spawning
|
|
295
|
+
* @param {Function} [opts.execFile] - DI seam for testing; defaults to node:child_process execFile
|
|
296
|
+
* @returns {Promise<{action: 'create'|'noop', iid?: number, command?: string[]}>}
|
|
297
|
+
*/
|
|
298
|
+
export async function reconcileFinding({
|
|
299
|
+
finding,
|
|
300
|
+
existingFingerprints,
|
|
301
|
+
dryRun = false,
|
|
302
|
+
execFile: execFileOpt,
|
|
303
|
+
}) {
|
|
304
|
+
// Resolve execFile — DI seam for tests; never caller-supplied binary path (ADR-364 §C5 HIGH fix).
|
|
305
|
+
const execFileFn = typeof execFileOpt === 'function' ? execFileOpt : realExecFile;
|
|
306
|
+
// Binary is always the allowlisted value — never caller-supplied.
|
|
307
|
+
const bin = RECONCILE_ALLOWLISTED_BINS.glab;
|
|
308
|
+
|
|
309
|
+
validateFinding(finding);
|
|
310
|
+
|
|
311
|
+
if (!(existingFingerprints instanceof Set)) {
|
|
312
|
+
throw new ReconcileError('existingFingerprints must be a Set', 'VALIDATION');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const fp = fingerprintFinding({
|
|
316
|
+
scope: finding.scope,
|
|
317
|
+
checkId: finding.checkId,
|
|
318
|
+
locator: finding.locator,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
if (existingFingerprints.has(fp)) {
|
|
322
|
+
return { action: 'noop' };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const title = `[Test] ${finding.title}`;
|
|
326
|
+
// title is derived from finding.title which was already validated above,
|
|
327
|
+
// but we recheck the assembled value for completeness.
|
|
328
|
+
if (ARG_BOUNDARY_DANGEROUS.test(title)) {
|
|
329
|
+
throw new ReconcileError('finding.title produces unsafe issue title', 'VALIDATION');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const body = buildIssueBody(finding, fp);
|
|
333
|
+
// Body may contain newlines (intentional markdown structure) but must not
|
|
334
|
+
// contain null bytes, which would corrupt argv.
|
|
335
|
+
if (body.includes('\0')) {
|
|
336
|
+
throw new ReconcileError('issue body contains null byte', 'VALIDATION');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// #389 MED-1: body-length cap in reconcileFinding backwards-compat path.
|
|
340
|
+
const bodyLengthError = checkBodyLength(body);
|
|
341
|
+
if (bodyLengthError) {
|
|
342
|
+
throw new ReconcileError(bodyLengthError.error.message, bodyLengthError.error.code);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const labels = `from:test-runner,severity:${finding.severity},status:ready,type:bug`;
|
|
346
|
+
const cmd = [
|
|
347
|
+
'issue',
|
|
348
|
+
'create',
|
|
349
|
+
'--title',
|
|
350
|
+
title,
|
|
351
|
+
'--label',
|
|
352
|
+
labels,
|
|
353
|
+
'--description',
|
|
354
|
+
body,
|
|
355
|
+
];
|
|
356
|
+
|
|
357
|
+
if (dryRun) {
|
|
358
|
+
return { action: 'create', command: cmd };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
const { stdout } = await execFileFn(bin, cmd, {
|
|
363
|
+
shell: false,
|
|
364
|
+
timeout: 10_000,
|
|
365
|
+
maxBuffer: DEFAULT_MAX_BUFFER,
|
|
366
|
+
});
|
|
367
|
+
const iid = parseIidFromGlabOutput(stdout);
|
|
368
|
+
return { action: 'create', iid, command: cmd };
|
|
369
|
+
} catch (err) {
|
|
370
|
+
if (err.code === 'ENOENT') {
|
|
371
|
+
throw new ReconcileError(`Binary '${bin}' not found in PATH`, 'BINARY_NOT_FOUND');
|
|
372
|
+
}
|
|
373
|
+
throw new ReconcileError(`execFile failed: ${err.message}`, 'EXEC_FAILURE');
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ---------------------------------------------------------------------------
|
|
378
|
+
// listExistingFindings — query glab for open test-runner findings (#384)
|
|
379
|
+
// ---------------------------------------------------------------------------
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* List existing test-runner findings from the issue tracker.
|
|
383
|
+
*
|
|
384
|
+
* Calls `glab issue list --label <label> --output json` and extracts
|
|
385
|
+
* fingerprints from issue bodies for downstream dedup.
|
|
386
|
+
*
|
|
387
|
+
* @param {object} [opts]
|
|
388
|
+
* @param {string} [opts.project] - GitLab project path (passed via --repo if provided)
|
|
389
|
+
* @param {string} [opts.label='from:test-runner'] - label filter for the query
|
|
390
|
+
* @param {number} [opts.maxBuffer=4194304] - maxBuffer for execFile (4 MB default, #389)
|
|
391
|
+
* @param {Function} [opts.execFile] - DI seam for testing; defaults to node:child_process execFile
|
|
392
|
+
* @returns {Promise<
|
|
393
|
+
* {ok: true, issues: Array<{iid: number, title: string, body: string}>, fingerprints: Set<string>}
|
|
394
|
+
* | {ok: false, error: {code: string, message: string}}
|
|
395
|
+
* >}
|
|
396
|
+
*/
|
|
397
|
+
export async function listExistingFindings({
|
|
398
|
+
project,
|
|
399
|
+
label = 'from:test-runner',
|
|
400
|
+
maxBuffer = DEFAULT_MAX_BUFFER,
|
|
401
|
+
execFile: execFileOpt,
|
|
402
|
+
} = {}) {
|
|
403
|
+
// Binary is always the allowlisted value — never caller-supplied (ADR-364 §C5 HIGH fix).
|
|
404
|
+
const bin = RECONCILE_ALLOWLISTED_BINS.glab;
|
|
405
|
+
const execFileFn = typeof execFileOpt === 'function' ? execFileOpt : realExecFile;
|
|
406
|
+
|
|
407
|
+
// Validate label arg boundary
|
|
408
|
+
if (ARG_BOUNDARY_DANGEROUS.test(label)) {
|
|
409
|
+
return {
|
|
410
|
+
ok: false,
|
|
411
|
+
error: { code: 'VALIDATION', message: 'label contains forbidden characters' },
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const args = ['issue', 'list', '--label', label, '--output', 'json'];
|
|
416
|
+
|
|
417
|
+
// Optionally scope to a specific project (--repo flag)
|
|
418
|
+
if (project !== undefined) {
|
|
419
|
+
if (ARG_BOUNDARY_DANGEROUS.test(project)) {
|
|
420
|
+
return {
|
|
421
|
+
ok: false,
|
|
422
|
+
error: { code: 'VALIDATION', message: 'project contains forbidden characters' },
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
args.push('--repo', project);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
let stdout;
|
|
429
|
+
try {
|
|
430
|
+
const result = await execFileFn(bin, args, {
|
|
431
|
+
shell: false,
|
|
432
|
+
timeout: 15_000,
|
|
433
|
+
maxBuffer,
|
|
434
|
+
});
|
|
435
|
+
stdout = result.stdout;
|
|
436
|
+
} catch (err) {
|
|
437
|
+
if (err.code === 'ENOENT') {
|
|
438
|
+
return {
|
|
439
|
+
ok: false,
|
|
440
|
+
error: {
|
|
441
|
+
code: 'BINARY_NOT_FOUND',
|
|
442
|
+
message: `Binary '${bin}' not found in PATH`,
|
|
443
|
+
},
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
return {
|
|
447
|
+
ok: false,
|
|
448
|
+
error: {
|
|
449
|
+
code: 'EXEC_FAILURE',
|
|
450
|
+
message: `execFile failed: ${err.message}`,
|
|
451
|
+
},
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
let parsed;
|
|
456
|
+
try {
|
|
457
|
+
parsed = JSON.parse(stdout);
|
|
458
|
+
} catch {
|
|
459
|
+
return {
|
|
460
|
+
ok: false,
|
|
461
|
+
error: {
|
|
462
|
+
code: 'PARSE_ERROR',
|
|
463
|
+
message: 'Failed to parse glab JSON output',
|
|
464
|
+
},
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (!Array.isArray(parsed)) {
|
|
469
|
+
return {
|
|
470
|
+
ok: false,
|
|
471
|
+
error: {
|
|
472
|
+
code: 'PARSE_ERROR',
|
|
473
|
+
message: 'Expected glab output to be a JSON array',
|
|
474
|
+
},
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const issues = parsed.map((issue) => ({
|
|
479
|
+
iid: issue.iid ?? issue.number ?? null,
|
|
480
|
+
title: issue.title ?? '',
|
|
481
|
+
body: issue.description ?? issue.body ?? '',
|
|
482
|
+
}));
|
|
483
|
+
|
|
484
|
+
// Build fingerprint dedup set from issue bodies
|
|
485
|
+
const fingerprints = new Set();
|
|
486
|
+
for (const issue of issues) {
|
|
487
|
+
const fp = extractFingerprintFromBody(issue.body);
|
|
488
|
+
if (fp !== null) {
|
|
489
|
+
fingerprints.add(fp);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return { ok: true, issues, fingerprints };
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// ---------------------------------------------------------------------------
|
|
497
|
+
// createFinding — create a new GitLab issue (#384)
|
|
498
|
+
// ---------------------------------------------------------------------------
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Create a new finding as a GitLab issue.
|
|
502
|
+
*
|
|
503
|
+
* @param {object} opts
|
|
504
|
+
* @param {string} [opts.project] - GitLab project path (--repo)
|
|
505
|
+
* @param {string} opts.fingerprint - 16-hex fingerprint (appended as sentinel)
|
|
506
|
+
* @param {string} opts.title - issue title (no [Test] prefix added here — caller decides)
|
|
507
|
+
* @param {string} opts.body - issue description body; must not exceed 65536 bytes (#389)
|
|
508
|
+
* @param {string} [opts.labels='from:test-runner'] - comma-separated label string
|
|
509
|
+
* @param {boolean} [opts.dryRun=false] - if true, return command without spawning
|
|
510
|
+
* @param {number} [opts.maxBuffer=4194304] - maxBuffer for execFile (4 MB, #389)
|
|
511
|
+
* @param {Function} [opts.execFile] - DI seam for testing; defaults to node:child_process execFile
|
|
512
|
+
* @returns {Promise<
|
|
513
|
+
* {ok: true, action: 'create', iid?: number, command?: string[]}
|
|
514
|
+
* | {ok: false, error: {code: string, message: string}}
|
|
515
|
+
* >}
|
|
516
|
+
*/
|
|
517
|
+
export async function createFinding({
|
|
518
|
+
project,
|
|
519
|
+
fingerprint,
|
|
520
|
+
title,
|
|
521
|
+
body,
|
|
522
|
+
labels = 'from:test-runner',
|
|
523
|
+
dryRun = false,
|
|
524
|
+
maxBuffer = DEFAULT_MAX_BUFFER,
|
|
525
|
+
execFile: execFileOpt,
|
|
526
|
+
}) {
|
|
527
|
+
// --- Input validation ---
|
|
528
|
+
if (typeof title !== 'string' || title.length === 0) {
|
|
529
|
+
return { ok: false, error: { code: 'VALIDATION', message: 'title must be a non-empty string' } };
|
|
530
|
+
}
|
|
531
|
+
if (ARG_BOUNDARY_DANGEROUS.test(title)) {
|
|
532
|
+
return {
|
|
533
|
+
ok: false,
|
|
534
|
+
error: { code: 'VALIDATION', message: 'title contains forbidden characters (newline/CR/null byte)' },
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
if (typeof body !== 'string') {
|
|
538
|
+
return { ok: false, error: { code: 'VALIDATION', message: 'body must be a string' } };
|
|
539
|
+
}
|
|
540
|
+
if (body.includes('\0')) {
|
|
541
|
+
return { ok: false, error: { code: 'VALIDATION', message: 'body contains null byte' } };
|
|
542
|
+
}
|
|
543
|
+
if (typeof fingerprint !== 'string' || fingerprint.length === 0) {
|
|
544
|
+
return {
|
|
545
|
+
ok: false,
|
|
546
|
+
error: { code: 'VALIDATION', message: 'fingerprint must be a non-empty string' },
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
if (typeof labels !== 'string') {
|
|
550
|
+
return { ok: false, error: { code: 'VALIDATION', message: 'labels must be a string' } };
|
|
551
|
+
}
|
|
552
|
+
if (ARG_BOUNDARY_DANGEROUS.test(labels)) {
|
|
553
|
+
return {
|
|
554
|
+
ok: false,
|
|
555
|
+
error: { code: 'VALIDATION', message: 'labels contains forbidden characters' },
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// #389: body-length cap
|
|
560
|
+
const bodyLengthError = checkBodyLength(body);
|
|
561
|
+
if (bodyLengthError) return bodyLengthError;
|
|
562
|
+
|
|
563
|
+
const args = ['issue', 'create', '--title', title, '--label', labels, '--description', body];
|
|
564
|
+
|
|
565
|
+
if (project !== undefined) {
|
|
566
|
+
if (ARG_BOUNDARY_DANGEROUS.test(project)) {
|
|
567
|
+
return {
|
|
568
|
+
ok: false,
|
|
569
|
+
error: { code: 'VALIDATION', message: 'project contains forbidden characters' },
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
args.push('--repo', project);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (dryRun) {
|
|
576
|
+
return { ok: true, action: 'create', command: args };
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Binary is always the allowlisted value — never caller-supplied (ADR-364 §C5 HIGH fix).
|
|
580
|
+
const bin = RECONCILE_ALLOWLISTED_BINS.glab;
|
|
581
|
+
const execFileFn = typeof execFileOpt === 'function' ? execFileOpt : realExecFile;
|
|
582
|
+
|
|
583
|
+
try {
|
|
584
|
+
const { stdout } = await execFileFn(bin, args, {
|
|
585
|
+
shell: false,
|
|
586
|
+
timeout: 10_000,
|
|
587
|
+
maxBuffer,
|
|
588
|
+
});
|
|
589
|
+
const iid = parseIidFromGlabOutput(stdout);
|
|
590
|
+
return { ok: true, action: 'create', iid, command: args };
|
|
591
|
+
} catch (err) {
|
|
592
|
+
if (err.code === 'ENOENT') {
|
|
593
|
+
return {
|
|
594
|
+
ok: false,
|
|
595
|
+
error: {
|
|
596
|
+
code: 'BINARY_NOT_FOUND',
|
|
597
|
+
message: `Binary '${bin}' not found in PATH`,
|
|
598
|
+
},
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
return {
|
|
602
|
+
ok: false,
|
|
603
|
+
error: {
|
|
604
|
+
code: 'EXEC_FAILURE',
|
|
605
|
+
message: `execFile failed: ${err.message}`,
|
|
606
|
+
},
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// ---------------------------------------------------------------------------
|
|
612
|
+
// updateFinding — add a comment to an existing issue (#384)
|
|
613
|
+
// ---------------------------------------------------------------------------
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Add a comment to an existing finding issue.
|
|
617
|
+
*
|
|
618
|
+
* @param {object} opts
|
|
619
|
+
* @param {string} [opts.project] - GitLab project path (--repo)
|
|
620
|
+
* @param {number} opts.iid - issue IID to comment on
|
|
621
|
+
* @param {string} opts.comment - comment body; must not exceed 65536 bytes (#389)
|
|
622
|
+
* @param {boolean} [opts.dryRun=false] - if true, return command without spawning
|
|
623
|
+
* @param {number} [opts.maxBuffer=4194304] - maxBuffer for execFile (4 MB, #389)
|
|
624
|
+
* @param {Function} [opts.execFile] - DI seam for testing; defaults to node:child_process execFile
|
|
625
|
+
* @returns {Promise<
|
|
626
|
+
* {ok: true, action: 'comment', command?: string[]}
|
|
627
|
+
* | {ok: false, error: {code: string, message: string}}
|
|
628
|
+
* >}
|
|
629
|
+
*/
|
|
630
|
+
export async function updateFinding({
|
|
631
|
+
project,
|
|
632
|
+
iid,
|
|
633
|
+
comment,
|
|
634
|
+
dryRun = false,
|
|
635
|
+
maxBuffer = DEFAULT_MAX_BUFFER,
|
|
636
|
+
execFile: execFileOpt,
|
|
637
|
+
}) {
|
|
638
|
+
// --- Input validation ---
|
|
639
|
+
if (!Number.isInteger(iid) || iid < 1) {
|
|
640
|
+
return { ok: false, error: { code: 'VALIDATION', message: 'iid must be a positive integer' } };
|
|
641
|
+
}
|
|
642
|
+
if (typeof comment !== 'string' || comment.length === 0) {
|
|
643
|
+
return {
|
|
644
|
+
ok: false,
|
|
645
|
+
error: { code: 'VALIDATION', message: 'comment must be a non-empty string' },
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
if (comment.includes('\0')) {
|
|
649
|
+
return { ok: false, error: { code: 'VALIDATION', message: 'comment contains null byte' } };
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// #389: body-length cap (applies to comment body as well)
|
|
653
|
+
const commentLengthError = checkBodyLength(comment);
|
|
654
|
+
if (commentLengthError) return commentLengthError;
|
|
655
|
+
|
|
656
|
+
// glab issue note --message <comment> <iid>
|
|
657
|
+
// iid is a number — safe to convert to string; no arg-boundary concerns
|
|
658
|
+
const args = ['issue', 'note', String(iid), '--message', comment];
|
|
659
|
+
|
|
660
|
+
if (project !== undefined) {
|
|
661
|
+
if (ARG_BOUNDARY_DANGEROUS.test(project)) {
|
|
662
|
+
return {
|
|
663
|
+
ok: false,
|
|
664
|
+
error: { code: 'VALIDATION', message: 'project contains forbidden characters' },
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
args.push('--repo', project);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (dryRun) {
|
|
671
|
+
return { ok: true, action: 'comment', command: args };
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Binary is always the allowlisted value — never caller-supplied (ADR-364 §C5 HIGH fix).
|
|
675
|
+
const bin = RECONCILE_ALLOWLISTED_BINS.glab;
|
|
676
|
+
const execFileFn = typeof execFileOpt === 'function' ? execFileOpt : realExecFile;
|
|
677
|
+
|
|
678
|
+
try {
|
|
679
|
+
await execFileFn(bin, args, {
|
|
680
|
+
shell: false,
|
|
681
|
+
timeout: 10_000,
|
|
682
|
+
maxBuffer,
|
|
683
|
+
});
|
|
684
|
+
return { ok: true, action: 'comment', command: args };
|
|
685
|
+
} catch (err) {
|
|
686
|
+
if (err.code === 'ENOENT') {
|
|
687
|
+
return {
|
|
688
|
+
ok: false,
|
|
689
|
+
error: {
|
|
690
|
+
code: 'BINARY_NOT_FOUND',
|
|
691
|
+
message: `Binary '${bin}' not found in PATH`,
|
|
692
|
+
},
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
return {
|
|
696
|
+
ok: false,
|
|
697
|
+
error: {
|
|
698
|
+
code: 'EXEC_FAILURE',
|
|
699
|
+
message: `execFile failed: ${err.message}`,
|
|
700
|
+
},
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// ---------------------------------------------------------------------------
|
|
706
|
+
// triageDecision — pure decision function (#384)
|
|
707
|
+
// ---------------------------------------------------------------------------
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Pure triage decision: determine the action for a finding given a list of
|
|
711
|
+
* existing issue candidates.
|
|
712
|
+
*
|
|
713
|
+
* Decision logic (in priority order):
|
|
714
|
+
* 1. Any candidate body contains `**Fingerprint:** \`<finding.fingerprint>\``
|
|
715
|
+
* → { action: 'ignore', target: <iid>, reason: 'fingerprint exact match', confidence: 1.0 }
|
|
716
|
+
* 2. Any candidate title has Levenshtein distance ≤ 2 from finding.title
|
|
717
|
+
* → { action: 'update', target: <iid>, reason: 'fuzzy title match', confidence: 0.7 }
|
|
718
|
+
* 3. Otherwise
|
|
719
|
+
* → { action: 'create', reason: 'no match', confidence: 1.0 }
|
|
720
|
+
*
|
|
721
|
+
* This function is intentionally pure (no execFile, fs, or fetch) so it can be
|
|
722
|
+
* tested deterministically without DI.
|
|
723
|
+
*
|
|
724
|
+
* @param {{ fingerprint: string, title: string }} finding
|
|
725
|
+
* @param {Array<{iid: number, title: string, body: string}>} candidates
|
|
726
|
+
* @returns {{ action: 'create'|'update'|'ignore', target?: number, reason: string, confidence: number }}
|
|
727
|
+
*/
|
|
728
|
+
export function triageDecision(finding, candidates) {
|
|
729
|
+
if (!finding || typeof finding !== 'object') {
|
|
730
|
+
return { action: 'create', reason: 'invalid finding', confidence: 1.0 };
|
|
731
|
+
}
|
|
732
|
+
if (!Array.isArray(candidates)) {
|
|
733
|
+
return { action: 'create', reason: 'no match', confidence: 1.0 };
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const { fingerprint, title } = finding;
|
|
737
|
+
|
|
738
|
+
// Pass 1: fingerprint exact match (highest priority)
|
|
739
|
+
if (typeof fingerprint === 'string' && fingerprint.length > 0) {
|
|
740
|
+
for (const candidate of candidates) {
|
|
741
|
+
const candidateFp = extractFingerprintFromBody(candidate.body ?? '');
|
|
742
|
+
if (candidateFp === fingerprint) {
|
|
743
|
+
return {
|
|
744
|
+
action: 'ignore',
|
|
745
|
+
target: candidate.iid,
|
|
746
|
+
reason: 'fingerprint exact match',
|
|
747
|
+
confidence: 1.0,
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// Pass 2: fuzzy title match (Levenshtein ≤ 2)
|
|
754
|
+
if (typeof title === 'string' && title.length > 0) {
|
|
755
|
+
for (const candidate of candidates) {
|
|
756
|
+
const dist = levenshtein(title, candidate.title ?? '');
|
|
757
|
+
if (dist <= 2) {
|
|
758
|
+
return {
|
|
759
|
+
action: 'update',
|
|
760
|
+
target: candidate.iid,
|
|
761
|
+
reason: 'fuzzy title match',
|
|
762
|
+
confidence: 0.7,
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Pass 3: no match
|
|
769
|
+
return { action: 'create', reason: 'no match', confidence: 1.0 };
|
|
770
|
+
}
|