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,512 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* file-lock.mjs — shared POSIX file-lock primitive (issue #630).
|
|
3
|
+
*
|
|
4
|
+
* Consolidates the five self-contained tmp + linkSync lock implementations that
|
|
5
|
+
* had accreted across the codebase (agent-status.mjs, memory-proposals/store.mjs,
|
|
6
|
+
* and the state-lock / staging-fence / session-lock blocks of session-lock.mjs).
|
|
7
|
+
* Each copy independently re-implemented the same skeleton:
|
|
8
|
+
*
|
|
9
|
+
* 1. atomic create-or-fail via `linkSync(tmp, lock)` (POSIX mutex);
|
|
10
|
+
* 2. on EEXIST → read + parse the existing body;
|
|
11
|
+
* 3. same-host + dead-PID (or unparseable) → atomic override + WARN;
|
|
12
|
+
* 4. live holder OR cross-host → poll until a deadline;
|
|
13
|
+
* 5. owner-guarded release.
|
|
14
|
+
*
|
|
15
|
+
* This module is the single home for that skeleton. It is a PURE primitive:
|
|
16
|
+
* it imports ONLY scripts/lib/io.mjs (for writeJsonAtomicSync) and Node stdlib.
|
|
17
|
+
* It does NOT import session-lock.mjs — instead `isPidAliveOnHost` is MOVED here
|
|
18
|
+
* and re-exported from session-lock.mjs, so the dependency edge points
|
|
19
|
+
* file-lock → io, never the reverse (no import cycle).
|
|
20
|
+
*
|
|
21
|
+
* Behavior-preservation contract (issue #630 is a PURE dedup refactor):
|
|
22
|
+
* - Lock file paths, body shapes, timeouts, poll cadences, override channels
|
|
23
|
+
* (console.warn vs process.stderr.write), and result-object reasons are
|
|
24
|
+
* reproduced EXACTLY by the call-site options. The primitive carries no
|
|
25
|
+
* opinion of its own — every divergence between the five copies is a knob.
|
|
26
|
+
* - Cross-host locks are NEVER auto-overridden (PSA-003 hard invariant).
|
|
27
|
+
* - Overrides always go through writeJsonAtomicSync (tmp + renameSync).
|
|
28
|
+
*
|
|
29
|
+
* No external dependencies — Node 20+ stdlib + io.mjs only.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import fs from 'node:fs';
|
|
33
|
+
import os from 'node:os';
|
|
34
|
+
import path from 'node:path';
|
|
35
|
+
import crypto from 'node:crypto';
|
|
36
|
+
|
|
37
|
+
import { writeJsonAtomicSync } from './io.mjs';
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// PID liveness (moved here from session-lock.mjs in #630)
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check whether a PID corresponds to a live process on this host.
|
|
45
|
+
* Returns true when the process exists (even if we lack kill permission).
|
|
46
|
+
* Returns false when the process does not exist (ESRCH).
|
|
47
|
+
*
|
|
48
|
+
* @forensic
|
|
49
|
+
* This is a SAME-HOST PID liveness probe (POSIX `process.kill(pid, 0)`,
|
|
50
|
+
* signal-0). It is NOT the discovery-path liveness check. Since Epic #583
|
|
51
|
+
* (W1-D1/W1-D4) the discovery decision tree uses heartbeat-age via
|
|
52
|
+
* `isLockLive` (session-lock.mjs) instead, because the `pid` recorded on a
|
|
53
|
+
* session.lock is the *ephemeral hook subprocess* PID — that process exits
|
|
54
|
+
* ~500ms after the SessionStart hook returns, so a signal-0 probe of it reports
|
|
55
|
+
* "dead" while the semantic owner (the Claude harness) is still alive (the D2
|
|
56
|
+
* production defect). Cross-host callers MUST pass `null` for the liveness slot
|
|
57
|
+
* and never call this function. Remaining same-host callers use it only for the
|
|
58
|
+
* short-lived state-lock / staging-fence-lock / agent-status / memory-proposals
|
|
59
|
+
* stale-override path, where the recorded PID IS the live writer's PID.
|
|
60
|
+
*
|
|
61
|
+
* @param {number} pid Process ID to probe via the POSIX signal-0 trick.
|
|
62
|
+
* @returns {boolean} true when a process with the given PID exists; false
|
|
63
|
+
* when no such process exists or the probe failed.
|
|
64
|
+
*
|
|
65
|
+
* @remarks
|
|
66
|
+
* PID-recycle trade-off (#560 Q3 L2 — deep-2115 session-reviewer):
|
|
67
|
+
*
|
|
68
|
+
* - On Unix, `process.kill(pid, 0)` returns true if ANY process exists with
|
|
69
|
+
* that PID, including a recycled one. The kernel does not distinguish the
|
|
70
|
+
* original lock-holder from a fresh process that happens to have inherited
|
|
71
|
+
* the same numeric PID after the lock-holder's death.
|
|
72
|
+
* - On a long session where the lock-holder died abnormally AND the OS reused
|
|
73
|
+
* its PID before TTL expiry, this function returns `true` even though the
|
|
74
|
+
* original lock-holder is gone. The lock is then perceived as held by a
|
|
75
|
+
* "live" process that is actually unrelated to the original writer.
|
|
76
|
+
* - Impact: a stale lock waits the full TTL before being reclaimed by the
|
|
77
|
+
* timeout path. Fail-open posture means correctness is preserved — only
|
|
78
|
+
* operational latency is affected, and the missed race-detection is
|
|
79
|
+
* bounded to one incident per stale-lock event.
|
|
80
|
+
* - Trade-off accepted: the alternative would require recording a UUID or
|
|
81
|
+
* boot-nonce alongside the PID and comparing both fields on stale-detection.
|
|
82
|
+
* That adds complexity to the lock body, parse path, and every
|
|
83
|
+
* acquire/release branch for a single-missed-race-per-incident impact.
|
|
84
|
+
* Current trade-off is operationally sound at our scale.
|
|
85
|
+
*/
|
|
86
|
+
export function isPidAliveOnHost(pid) {
|
|
87
|
+
try {
|
|
88
|
+
process.kill(pid, 0);
|
|
89
|
+
return true;
|
|
90
|
+
} catch (err) {
|
|
91
|
+
if (err.code === 'ESRCH') return false;
|
|
92
|
+
if (err.code === 'EPERM') return true; // process exists, we just lack permission
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Internal helpers
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Parse a lock-file body. Returns null on any malformed input. The required
|
|
103
|
+
* fields are configurable so a single parser serves both the 3-field shape
|
|
104
|
+
* ({pid, host, acquiredAt}) used by agent-status / memory-proposals and the
|
|
105
|
+
* 4-field shape ({pid, host, acquiredAt, holder}) used by the state-lock /
|
|
106
|
+
* staging-fence locks. The primitive only ever inspects `pid` and `host`, so
|
|
107
|
+
* the minimal contract enforced here is `{ pid: number, host: string }`.
|
|
108
|
+
*
|
|
109
|
+
* @param {string} raw
|
|
110
|
+
* @returns {object|null}
|
|
111
|
+
*/
|
|
112
|
+
function parseBody(raw) {
|
|
113
|
+
try {
|
|
114
|
+
const obj = JSON.parse(raw);
|
|
115
|
+
if (
|
|
116
|
+
obj &&
|
|
117
|
+
typeof obj === 'object' &&
|
|
118
|
+
!Array.isArray(obj) &&
|
|
119
|
+
typeof obj.pid === 'number' &&
|
|
120
|
+
typeof obj.host === 'string'
|
|
121
|
+
) {
|
|
122
|
+
return obj;
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
} catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Resolve whether an existing lock body is stale relative to the requested
|
|
132
|
+
* stale-check policy. Cross-host bodies are NEVER stale here (PSA-003 — we
|
|
133
|
+
* cannot signal a remote process). Returns a `{ stale, reason }` pair.
|
|
134
|
+
*
|
|
135
|
+
* @param {object} existing — parsed lock body (non-null).
|
|
136
|
+
* @param {'pid'|'heartbeat'|'mtime'|'none'} staleCheck
|
|
137
|
+
* @param {number|undefined} staleMs — TTL for heartbeat/mtime checks.
|
|
138
|
+
* @param {string|undefined} lockPath — needed for mtime stat.
|
|
139
|
+
* @returns {{ stale: boolean, reason: string }}
|
|
140
|
+
*/
|
|
141
|
+
function isExistingStale(existing, staleCheck, staleMs, lockPath) {
|
|
142
|
+
const sameHost = existing.host === os.hostname();
|
|
143
|
+
if (!sameHost) return { stale: false, reason: 'cross-host' };
|
|
144
|
+
|
|
145
|
+
if (staleCheck === 'none') return { stale: false, reason: 'live' };
|
|
146
|
+
|
|
147
|
+
if (staleCheck === 'pid') {
|
|
148
|
+
const pidIsNumber = typeof existing.pid === 'number';
|
|
149
|
+
const dead = pidIsNumber && !isPidAliveOnHost(existing.pid);
|
|
150
|
+
return dead
|
|
151
|
+
? { stale: true, reason: `dead pid ${existing.pid}` }
|
|
152
|
+
: { stale: false, reason: 'live' };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (staleCheck === 'heartbeat') {
|
|
156
|
+
const ts = Date.parse(existing.acquiredAt ?? existing.lastHeartbeat ?? '');
|
|
157
|
+
if (!Number.isFinite(ts) || typeof staleMs !== 'number') {
|
|
158
|
+
return { stale: false, reason: 'live' };
|
|
159
|
+
}
|
|
160
|
+
const age = Date.now() - ts;
|
|
161
|
+
return age > staleMs
|
|
162
|
+
? { stale: true, reason: `heartbeat age ${age}ms > ${staleMs}ms` }
|
|
163
|
+
: { stale: false, reason: 'live' };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (staleCheck === 'mtime') {
|
|
167
|
+
if (typeof staleMs !== 'number' || !lockPath) {
|
|
168
|
+
return { stale: false, reason: 'live' };
|
|
169
|
+
}
|
|
170
|
+
let mtimeMs;
|
|
171
|
+
try {
|
|
172
|
+
mtimeMs = fs.statSync(lockPath).mtimeMs;
|
|
173
|
+
} catch {
|
|
174
|
+
return { stale: false, reason: 'live' };
|
|
175
|
+
}
|
|
176
|
+
const age = Date.now() - mtimeMs;
|
|
177
|
+
return age > staleMs
|
|
178
|
+
? { stale: true, reason: `mtime age ${Math.round(age)}ms > ${staleMs}ms` }
|
|
179
|
+
: { stale: false, reason: 'live' };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { stale: false, reason: 'live' };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Atomically create the lock file via tmp + linkSync (POSIX create-or-fail).
|
|
187
|
+
* The winner creates the hard link; every loser gets EEXIST.
|
|
188
|
+
*
|
|
189
|
+
* @param {string} lockPath
|
|
190
|
+
* @param {object} body
|
|
191
|
+
* @param {{ indent?: number|null, tmpPrefix: string }} fmt
|
|
192
|
+
* @returns {{ ok: true } | { ok: false, reason: 'exists' } | { ok: false, reason: 'fs-error', error: string }}
|
|
193
|
+
*/
|
|
194
|
+
function createExclusive(lockPath, body, fmt) {
|
|
195
|
+
const dir = path.dirname(lockPath);
|
|
196
|
+
let tmpFile;
|
|
197
|
+
try {
|
|
198
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
199
|
+
const suffix = crypto.randomBytes(8).toString('hex');
|
|
200
|
+
tmpFile = path.join(dir, `${fmt.tmpPrefix}.create.tmp.${suffix}`);
|
|
201
|
+
fs.writeFileSync(tmpFile, serializeBody(body, fmt.indent), 'utf8');
|
|
202
|
+
} catch (err) {
|
|
203
|
+
if (tmpFile) {
|
|
204
|
+
try { fs.unlinkSync(tmpFile); } catch { /* best-effort */ }
|
|
205
|
+
}
|
|
206
|
+
return { ok: false, reason: 'fs-error', error: err?.message ?? String(err) };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
fs.linkSync(tmpFile, lockPath);
|
|
211
|
+
return { ok: true };
|
|
212
|
+
} catch (err) {
|
|
213
|
+
if (err.code === 'EEXIST') return { ok: false, reason: 'exists' };
|
|
214
|
+
return { ok: false, reason: 'fs-error', error: err?.message ?? String(err) };
|
|
215
|
+
} finally {
|
|
216
|
+
try { fs.unlinkSync(tmpFile); } catch { /* best-effort */ }
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Serialize a lock body. `indent === null` → compact (no whitespace, no
|
|
222
|
+
* trailing newline) to reproduce agent-status / memory-proposals exactly.
|
|
223
|
+
* Numeric indent → pretty-printed with a trailing newline (state / staging).
|
|
224
|
+
*
|
|
225
|
+
* @param {object} body
|
|
226
|
+
* @param {number|null} indent
|
|
227
|
+
* @returns {string}
|
|
228
|
+
*/
|
|
229
|
+
function serializeBody(body, indent) {
|
|
230
|
+
if (indent === null || indent === undefined) {
|
|
231
|
+
return JSON.stringify(body);
|
|
232
|
+
}
|
|
233
|
+
return JSON.stringify(body, null, indent) + '\n';
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
// Exported primitive
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Attempt ONE exclusive lock acquisition pass.
|
|
242
|
+
*
|
|
243
|
+
* On EEXIST the existing body is read and parsed; if it is stale per the
|
|
244
|
+
* configured `staleCheck` policy (or unparseable), the lock is atomically
|
|
245
|
+
* overridden via writeJsonAtomicSync and a WARN is emitted. A live holder or a
|
|
246
|
+
* cross-host body returns `{ acquired: false, reason: 'held' }`.
|
|
247
|
+
*
|
|
248
|
+
* The `signalVanished` knob reproduces memory-proposals/store.mjs's distinct
|
|
249
|
+
* third state: when the lock file disappears between the EEXIST and the read
|
|
250
|
+
* (concurrent release race), `{ acquired: false, reason: 'vanished' }` is
|
|
251
|
+
* returned so the caller defers WITHOUT any override side-effect (preserves the
|
|
252
|
+
* C9 8-worker double-holder invariant). When `signalVanished` is false the
|
|
253
|
+
* ENOENT-on-read race collapses into `{ acquired: false, reason: 'held' }`
|
|
254
|
+
* (state-lock / staging-fence / agent-status behavior).
|
|
255
|
+
*
|
|
256
|
+
* @param {string} lockPath Absolute path to the lock file.
|
|
257
|
+
* @param {object} [opts]
|
|
258
|
+
* @param {'pid'|'heartbeat'|'mtime'|'none'} [opts.staleCheck='pid']
|
|
259
|
+
* @param {number} [opts.staleMs] — TTL for heartbeat/mtime staleCheck.
|
|
260
|
+
* @param {object} [opts.meta] — extra fields merged into the body.
|
|
261
|
+
* @param {string} [opts.holder] — holder label (added to body when set).
|
|
262
|
+
* @param {boolean} [opts.signalVanished=false]
|
|
263
|
+
* @param {number|null} [opts.indent] — body serialization: null=compact, number=pretty.
|
|
264
|
+
* @param {string} [opts.tmpPrefix] — tmp-file prefix for create + override.
|
|
265
|
+
* @param {(msg: string) => void} [opts.warn] — override-WARN sink (default console.warn).
|
|
266
|
+
* @param {(reason: string, lockPath: string, existing: object|null) => string} [opts.warnMessage]
|
|
267
|
+
* — build the WARN string from the stale reason, path, and the parsed
|
|
268
|
+
* existing body (null when the body was unparseable). The reason token is
|
|
269
|
+
* 'unparseable body' | 'dead pid <n>' | 'heartbeat age ...' | 'mtime age ...'.
|
|
270
|
+
* @returns {{ acquired: true, body: object }
|
|
271
|
+
* | { acquired: false, reason: 'held'|'vanished'|'fs-error', existing?: object|null, error?: string }}
|
|
272
|
+
*/
|
|
273
|
+
export function tryAcquireFileLock(lockPath, opts = {}) {
|
|
274
|
+
const {
|
|
275
|
+
staleCheck = 'pid',
|
|
276
|
+
staleMs,
|
|
277
|
+
meta,
|
|
278
|
+
holder,
|
|
279
|
+
signalVanished = false,
|
|
280
|
+
indent = null,
|
|
281
|
+
tmpPrefix = '.file.lock',
|
|
282
|
+
warn = (msg) => console.warn(msg),
|
|
283
|
+
warnMessage = (reason, lp) => `⚠ file-lock: overriding stale lock (${reason}) at ${lp}`,
|
|
284
|
+
} = opts;
|
|
285
|
+
|
|
286
|
+
// The stale-override path writes via writeJsonAtomicSync. The original copies
|
|
287
|
+
// diverged on override formatting: agent-status / state-lock / staging-fence
|
|
288
|
+
// override pretty-printed (indent 2) regardless of create format, while
|
|
289
|
+
// memory-proposals overrode compact. writeJsonAtomicSync always appends a
|
|
290
|
+
// trailing newline; with overrideIndent:null it emits `JSON.stringify(body)\n`
|
|
291
|
+
// (compact + newline). The trailing newline is parse-transparent (JSON.parse
|
|
292
|
+
// ignores it), so this is behavior-identical for every consumer.
|
|
293
|
+
const overrideIndent = opts.overrideIndent !== undefined ? opts.overrideIndent : (indent ?? 2);
|
|
294
|
+
|
|
295
|
+
const body = {
|
|
296
|
+
pid: process.pid,
|
|
297
|
+
host: os.hostname(),
|
|
298
|
+
acquiredAt: new Date().toISOString(),
|
|
299
|
+
...(meta && typeof meta === 'object' ? meta : {}),
|
|
300
|
+
};
|
|
301
|
+
if (typeof holder === 'string' && holder.length > 0) {
|
|
302
|
+
body.holder = holder;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const fmt = { indent, tmpPrefix };
|
|
306
|
+
|
|
307
|
+
let created;
|
|
308
|
+
try {
|
|
309
|
+
created = createExclusive(lockPath, body, fmt);
|
|
310
|
+
} catch (err) {
|
|
311
|
+
return { acquired: false, reason: 'fs-error', error: err?.message ?? String(err) };
|
|
312
|
+
}
|
|
313
|
+
if (created.ok) return { acquired: true, body };
|
|
314
|
+
if (created.reason === 'fs-error') {
|
|
315
|
+
return { acquired: false, reason: 'fs-error', error: created.error };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// reason === 'exists' — inspect the holder.
|
|
319
|
+
let raw;
|
|
320
|
+
try {
|
|
321
|
+
raw = fs.readFileSync(lockPath, 'utf8');
|
|
322
|
+
} catch (err) {
|
|
323
|
+
if (err.code === 'ENOENT') {
|
|
324
|
+
// Lock vanished between linkSync-EEXIST and read — a concurrent
|
|
325
|
+
// release-then-reacquire race. We lost; never override.
|
|
326
|
+
return signalVanished
|
|
327
|
+
? { acquired: false, reason: 'vanished' }
|
|
328
|
+
: { acquired: false, reason: 'held', existing: null };
|
|
329
|
+
}
|
|
330
|
+
return { acquired: false, reason: 'fs-error', error: err?.message ?? String(err) };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const existing = parseBody(raw);
|
|
334
|
+
|
|
335
|
+
// Unparseable contents → treat as stale and override (only safe move).
|
|
336
|
+
if (existing === null) {
|
|
337
|
+
warn(warnMessage('unparseable body', lockPath, null));
|
|
338
|
+
const replaced = writeJsonAtomicSync(lockPath, body, { tmpPrefix, indent: overrideIndent });
|
|
339
|
+
if (replaced.ok) return { acquired: true, body };
|
|
340
|
+
return { acquired: false, reason: 'fs-error', error: replaced.error };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const { stale, reason } = isExistingStale(existing, staleCheck, staleMs, lockPath);
|
|
344
|
+
if (stale) {
|
|
345
|
+
warn(warnMessage(reason, lockPath, existing));
|
|
346
|
+
const replaced = writeJsonAtomicSync(lockPath, body, { tmpPrefix, indent: overrideIndent });
|
|
347
|
+
if (replaced.ok) return { acquired: true, body };
|
|
348
|
+
return { acquired: false, reason: 'fs-error', error: replaced.error };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Live holder OR cross-host → caller polls.
|
|
352
|
+
return { acquired: false, reason: 'held', existing };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Release a lock file.
|
|
357
|
+
*
|
|
358
|
+
* With `ownerGuard: true` (default) the file is unlinked ONLY when we own it:
|
|
359
|
+
* the recorded `holder` matches `opts.holder` (when provided), else PID + host
|
|
360
|
+
* match. This reproduces the agent-status / state-lock / staging-fence owner
|
|
361
|
+
* guard (PSA-003: never delete a lock another holder owns).
|
|
362
|
+
*
|
|
363
|
+
* With `ownerGuard: false` the file is unlinked unconditionally, ENOENT
|
|
364
|
+
* ignored — reproducing memory-proposals/store.mjs's `releaseProposalsLock`.
|
|
365
|
+
*
|
|
366
|
+
* @param {string} lockPath
|
|
367
|
+
* @param {object} [opts]
|
|
368
|
+
* @param {string} [opts.holder] — expected holder for the owner guard.
|
|
369
|
+
* @param {boolean} [opts.ownerGuard=true]
|
|
370
|
+
* @param {(errToken: string) => void} [opts.warn] — sink for unexpected fs
|
|
371
|
+
* errors on the ownerGuard:false path. Receives the raw
|
|
372
|
+
* `err.code ?? err.message` token; the call-site formats the message.
|
|
373
|
+
* @returns {{ ok: true }
|
|
374
|
+
* | { ok: false, reason: 'not-found'|'not-owner'|'fs-error', error?: string }}
|
|
375
|
+
*/
|
|
376
|
+
export function releaseFileLock(lockPath, opts = {}) {
|
|
377
|
+
const { holder, ownerGuard = true, warn } = opts;
|
|
378
|
+
|
|
379
|
+
if (ownerGuard === false) {
|
|
380
|
+
// Unconditional unlink; ENOENT ignored. Other fs errors surfaced via warn.
|
|
381
|
+
// The warn callback receives the raw `err.code ?? err.message` token so the
|
|
382
|
+
// call-site controls the surrounding message format.
|
|
383
|
+
try {
|
|
384
|
+
fs.unlinkSync(lockPath);
|
|
385
|
+
return { ok: true };
|
|
386
|
+
} catch (err) {
|
|
387
|
+
if (err.code === 'ENOENT') return { ok: true };
|
|
388
|
+
if (typeof warn === 'function') warn(err.code ?? err.message);
|
|
389
|
+
return { ok: false, reason: 'fs-error', error: err?.message ?? String(err) };
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
let raw;
|
|
394
|
+
try {
|
|
395
|
+
raw = fs.readFileSync(lockPath, 'utf8');
|
|
396
|
+
} catch (err) {
|
|
397
|
+
if (err.code === 'ENOENT') return { ok: false, reason: 'not-found' };
|
|
398
|
+
return { ok: false, reason: 'fs-error', error: err?.message ?? String(err) };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const lock = parseBody(raw);
|
|
402
|
+
if (lock === null) {
|
|
403
|
+
// Unparseable — refuse to delete; some other process may be writing now.
|
|
404
|
+
return { ok: false, reason: 'not-owner' };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const expectedHolder = typeof holder === 'string' && holder.length > 0 ? holder : null;
|
|
408
|
+
const ownerMatch = expectedHolder !== null
|
|
409
|
+
? lock.holder === expectedHolder
|
|
410
|
+
: lock.pid === process.pid && lock.host === os.hostname();
|
|
411
|
+
|
|
412
|
+
if (!ownerMatch) return { ok: false, reason: 'not-owner' };
|
|
413
|
+
|
|
414
|
+
try {
|
|
415
|
+
fs.unlinkSync(lockPath);
|
|
416
|
+
return { ok: true };
|
|
417
|
+
} catch (err) {
|
|
418
|
+
if (err.code === 'ENOENT') return { ok: false, reason: 'not-found' };
|
|
419
|
+
return { ok: false, reason: 'fs-error', error: err?.message ?? String(err) };
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Synchronous busy-wait sleep via Atomics.wait on a throwaway buffer — a
|
|
425
|
+
* portable, CPU-cheap sleep used by the `sync` variant of withFileLock (the
|
|
426
|
+
* agent-status no-throw hot path that must not leak async timers). Falls back
|
|
427
|
+
* to a tight spin if SharedArrayBuffer is unavailable.
|
|
428
|
+
* @param {number} ms
|
|
429
|
+
*/
|
|
430
|
+
function sleepSync(ms) {
|
|
431
|
+
const end = Date.now() + ms;
|
|
432
|
+
try {
|
|
433
|
+
const sab = new Int32Array(new SharedArrayBuffer(4));
|
|
434
|
+
Atomics.wait(sab, 0, 0, ms);
|
|
435
|
+
} catch {
|
|
436
|
+
while (Date.now() < end) { /* spin */ }
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Async poll-sleep. Promise-returning so the loop does not block the event loop.
|
|
442
|
+
* @param {number} ms
|
|
443
|
+
*/
|
|
444
|
+
function delay(ms) {
|
|
445
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* High-level wrapper: acquire `lockPath` (polling until a deadline), run `fn`,
|
|
450
|
+
* and release in a finally. The acquire pass delegates to tryAcquireFileLock,
|
|
451
|
+
* so every stale-override / cross-host / vanished knob is honored here too.
|
|
452
|
+
*
|
|
453
|
+
* Returns the value of `fn()`. On acquire timeout or fs-error returns a
|
|
454
|
+
* structured failure (callers that want a throw should branch on it — the
|
|
455
|
+
* existing session-lock wrappers keep their own throw-translation). `fn` errors
|
|
456
|
+
* propagate after the lock is released.
|
|
457
|
+
*
|
|
458
|
+
* @param {string} lockPath
|
|
459
|
+
* @param {(body: object) => (T | Promise<T>)} fn — receives the acquired body.
|
|
460
|
+
* @param {object} [opts]
|
|
461
|
+
* @param {number} [opts.timeoutMs=10000]
|
|
462
|
+
* @param {number} [opts.pollMs=100]
|
|
463
|
+
* @param {boolean} [opts.sync=false] — true → synchronous busy-wait poll +
|
|
464
|
+
* synchronous fn (agent-status variant). false → async poll.
|
|
465
|
+
* @param {boolean} [opts.ownerGuard=true] — passed to releaseFileLock.
|
|
466
|
+
* @param {...*} [opts.acquireOpts] — remaining keys forwarded to tryAcquireFileLock.
|
|
467
|
+
* @returns {Promise<{ ok: true, value: T }
|
|
468
|
+
* | { ok: false, reason: 'timeout'|'fs-error', error?: string, existing?: object|null }>}
|
|
469
|
+
* @template T
|
|
470
|
+
*/
|
|
471
|
+
export async function withFileLock(lockPath, fn, opts = {}) {
|
|
472
|
+
const {
|
|
473
|
+
timeoutMs = 10000,
|
|
474
|
+
pollMs = 100,
|
|
475
|
+
sync = false,
|
|
476
|
+
ownerGuard = true,
|
|
477
|
+
...acquireOpts
|
|
478
|
+
} = opts;
|
|
479
|
+
|
|
480
|
+
const effectiveTimeout = typeof timeoutMs === 'number' && timeoutMs >= 0 ? timeoutMs : 10000;
|
|
481
|
+
const effectivePoll = typeof pollMs === 'number' && pollMs > 0 ? pollMs : 100;
|
|
482
|
+
const deadline = Date.now() + effectiveTimeout;
|
|
483
|
+
|
|
484
|
+
let acquired;
|
|
485
|
+
// First iteration runs unconditionally so timeoutMs:0 still attempts once.
|
|
486
|
+
for (;;) {
|
|
487
|
+
const attempt = tryAcquireFileLock(lockPath, acquireOpts);
|
|
488
|
+
if (attempt.acquired) {
|
|
489
|
+
acquired = attempt;
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
if (attempt.reason === 'fs-error') {
|
|
493
|
+
return { ok: false, reason: 'fs-error', error: attempt.error };
|
|
494
|
+
}
|
|
495
|
+
if (Date.now() >= deadline) {
|
|
496
|
+
return { ok: false, reason: 'timeout', existing: attempt.existing ?? null };
|
|
497
|
+
}
|
|
498
|
+
if (sync) {
|
|
499
|
+
sleepSync(effectivePoll);
|
|
500
|
+
} else {
|
|
501
|
+
await delay(effectivePoll);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const releaseHolder = acquired.body.holder;
|
|
506
|
+
try {
|
|
507
|
+
const value = await fn(acquired.body);
|
|
508
|
+
return { ok: true, value };
|
|
509
|
+
} finally {
|
|
510
|
+
releaseFileLock(lockPath, { holder: releaseHolder, ownerGuard });
|
|
511
|
+
}
|
|
512
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scripts/lib/frontend-detect/detect.mjs
|
|
3
|
+
*
|
|
4
|
+
* Deterministic frontend-slop detector — orchestration layer over rules.mjs.
|
|
5
|
+
*
|
|
6
|
+
* Provides:
|
|
7
|
+
* - detectContent(content, filePath, opts) → Finding[] (pure, no I/O)
|
|
8
|
+
* - detectFiles(paths, opts) → Finding[] (reads files)
|
|
9
|
+
* - SCANNABLE_EXTS → Set<string>
|
|
10
|
+
*
|
|
11
|
+
* No LLM, no network, no browser. Pure regex tier (see rules.mjs provenance).
|
|
12
|
+
*
|
|
13
|
+
* @typedef {Object} Rule
|
|
14
|
+
* @property {string} id
|
|
15
|
+
* @property {string} ruleRef
|
|
16
|
+
* @property {'high'|'medium'|'low'} severity
|
|
17
|
+
* @property {'ai-slop'|'quality'} category
|
|
18
|
+
* @property {string} title
|
|
19
|
+
* @property {string} recommendation
|
|
20
|
+
* @property {'low'|'medium'|'high'} fpRisk
|
|
21
|
+
* @property {(content: string) => Array<{line:number, snippet:string, value?:string}>} scan
|
|
22
|
+
*
|
|
23
|
+
* @typedef {Object} Finding
|
|
24
|
+
* @property {string} rule
|
|
25
|
+
* @property {string} ruleRef
|
|
26
|
+
* @property {'high'|'medium'|'low'} severity
|
|
27
|
+
* @property {'ai-slop'|'quality'} category
|
|
28
|
+
* @property {string} title
|
|
29
|
+
* @property {string} file
|
|
30
|
+
* @property {number} line
|
|
31
|
+
* @property {string} snippet
|
|
32
|
+
* @property {string} recommendation
|
|
33
|
+
* @property {'low'|'medium'|'high'} fpRisk
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { readFileSync } from 'node:fs';
|
|
37
|
+
import { RULES } from './rules.mjs';
|
|
38
|
+
|
|
39
|
+
/** File extensions worth scanning for frontend slop. */
|
|
40
|
+
export const SCANNABLE_EXTS = new Set([
|
|
41
|
+
'.css',
|
|
42
|
+
'.scss',
|
|
43
|
+
'.sass',
|
|
44
|
+
'.less',
|
|
45
|
+
'.html',
|
|
46
|
+
'.htm',
|
|
47
|
+
'.astro',
|
|
48
|
+
'.vue',
|
|
49
|
+
'.svelte',
|
|
50
|
+
'.jsx',
|
|
51
|
+
'.tsx',
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Run the rule set over a single in-memory string.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} content
|
|
58
|
+
* @param {string} [filePath] — for finding provenance (default '<inline>')
|
|
59
|
+
* @param {{ ignoreRules?: string[] }} [opts]
|
|
60
|
+
* @returns {Finding[]}
|
|
61
|
+
*/
|
|
62
|
+
export function detectContent(content, filePath = '<inline>', opts = {}) {
|
|
63
|
+
const ignore = new Set(opts.ignoreRules ?? []);
|
|
64
|
+
const findings = [];
|
|
65
|
+
for (const rule of RULES) {
|
|
66
|
+
if (ignore.has(rule.id)) continue;
|
|
67
|
+
let matches;
|
|
68
|
+
try {
|
|
69
|
+
matches = rule.scan(content) ?? [];
|
|
70
|
+
} catch {
|
|
71
|
+
// A rule must never crash the scan — fail-soft, skip this rule.
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
for (const mt of matches) {
|
|
75
|
+
findings.push({
|
|
76
|
+
rule: rule.id,
|
|
77
|
+
ruleRef: rule.ruleRef,
|
|
78
|
+
severity: rule.severity,
|
|
79
|
+
category: rule.category,
|
|
80
|
+
title: rule.title,
|
|
81
|
+
file: filePath,
|
|
82
|
+
line: mt.line,
|
|
83
|
+
snippet: mt.snippet,
|
|
84
|
+
recommendation: rule.recommendation,
|
|
85
|
+
fpRisk: rule.fpRisk,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return findings.sort((a, b) => a.line - b.line || a.rule.localeCompare(b.rule));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Run the rule set over a list of file paths. Unreadable / non-scannable files
|
|
94
|
+
* are skipped silently (fail-soft).
|
|
95
|
+
*
|
|
96
|
+
* @param {string[]} paths — absolute or cwd-relative file paths
|
|
97
|
+
* @param {{ ignoreRules?: string[] }} [opts]
|
|
98
|
+
* @returns {Finding[]}
|
|
99
|
+
*/
|
|
100
|
+
export function detectFiles(paths, opts = {}) {
|
|
101
|
+
const findings = [];
|
|
102
|
+
for (const p of paths) {
|
|
103
|
+
const dot = p.lastIndexOf('.');
|
|
104
|
+
const ext = dot === -1 ? '' : p.slice(dot).toLowerCase();
|
|
105
|
+
if (!SCANNABLE_EXTS.has(ext)) continue;
|
|
106
|
+
let content;
|
|
107
|
+
try {
|
|
108
|
+
content = readFileSync(p, 'utf8');
|
|
109
|
+
} catch {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
findings.push(...detectContent(content, p, opts));
|
|
113
|
+
}
|
|
114
|
+
return findings;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Aggregate findings into a summary roll-up by severity + category.
|
|
119
|
+
* @param {Finding[]} findings
|
|
120
|
+
*/
|
|
121
|
+
export function summarize(findings) {
|
|
122
|
+
const summary = {
|
|
123
|
+
total: findings.length,
|
|
124
|
+
high: 0,
|
|
125
|
+
medium: 0,
|
|
126
|
+
low: 0,
|
|
127
|
+
aiSlop: 0,
|
|
128
|
+
quality: 0,
|
|
129
|
+
byRule: {},
|
|
130
|
+
};
|
|
131
|
+
for (const f of findings) {
|
|
132
|
+
summary[f.severity]++;
|
|
133
|
+
if (f.category === 'ai-slop') summary.aiSlop++;
|
|
134
|
+
else summary.quality++;
|
|
135
|
+
summary.byRule[f.rule] = (summary.byRule[f.rule] ?? 0) + 1;
|
|
136
|
+
}
|
|
137
|
+
return summary;
|
|
138
|
+
}
|