session-orchestrator 5.12.0 → 5.13.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 +2 -2
- package/.claude-plugin/plugin.json +4 -1
- package/.codex-plugin/agents/explorer.toml +1 -1
- package/.codex-plugin/agents/session-reviewer.toml +2 -2
- package/.codex-plugin/agents/wave-worker.toml +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +8 -0
- package/.orchestrator/policy/public-export.json +30 -0
- package/CHANGELOG.md +162 -4
- package/README.md +126 -127
- package/agents/analyst.md +7 -2
- package/agents/architect-reviewer.md +7 -2
- package/agents/code-implementer.md +4 -2
- package/agents/db-specialist.md +9 -2
- package/agents/dialectic-deriver.md +9 -3
- package/agents/docs-writer.md +7 -2
- package/agents/eval-judge.md +7 -1
- package/agents/qa-strategist.md +7 -2
- package/agents/security-reviewer.md +9 -2
- package/agents/session-reviewer.md +18 -2
- package/agents/skill-applied-judge.md +7 -1
- package/agents/test-writer.md +9 -2
- package/agents/ui-developer.md +9 -2
- package/agents/ux-evaluator.md +6 -1
- package/commands/session.md +1 -1
- package/commands/templates-ack.md +89 -18
- package/docs/agent-authoring.md +30 -11
- package/docs/ci-setup.md +38 -2
- package/docs/codex-setup.md +33 -9
- package/docs/components.md +13 -2
- package/docs/cursor-setup.md +3 -3
- package/docs/events-schema.md +5 -4
- package/docs/evidence-candidates.md +63 -0
- package/docs/fleet-checkin-audit.md +7 -4
- package/docs/github-mirror-protection.md +150 -3
- package/docs/install.md +90 -17
- package/docs/instruction-delivery.md +1 -1
- package/docs/migration-v4.md +2 -2
- package/docs/mods.md +118 -0
- package/docs/owner-config-schema.md +1 -0
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/pm-skills-marketplace.md +4 -3
- package/docs/rule-authoring.md +24 -13
- package/docs/scope-collision-guard.md +2 -1
- package/docs/session-config-reference.md +49 -15
- package/docs/session-config-template.md +30 -22
- package/docs/sessions-ledger-repair.md +137 -0
- package/docs/vault-docs-architecture.md +1 -1
- package/hooks/_lib/hook-import-set.json +406 -73
- package/hooks/_lib/hook-root.mjs +301 -0
- package/hooks/_lib/lock-bootstrap.mjs +434 -4
- package/hooks/_lib/orchestrator-ignore.mjs +3 -0
- package/hooks/_lib/subagent-transcript.mjs +2 -2
- package/hooks/_lib/vcs-create-matcher.mjs +13 -17
- package/hooks/cwd-change-restore.mjs +5 -1
- package/hooks/enforce-commands.mjs +4 -3
- package/hooks/enforce-scope.mjs +11 -1
- package/hooks/hooks-codex.json +48 -2
- package/hooks/hooks.json +31 -31
- package/hooks/on-session-end.mjs +38 -15
- package/hooks/on-session-start.mjs +139 -49
- package/hooks/on-stop.mjs +11 -6
- package/hooks/post-bash-issue-budget-refund.mjs +6 -1
- package/hooks/post-bash-write-verify.mjs +8 -4
- package/hooks/post-edit-validate.mjs +45 -52
- package/hooks/post-subagent-discovery-validator.mjs +2 -2
- package/hooks/post-tool-batch-wave-signal.mjs +8 -4
- package/hooks/post-tool-failure-corrective-context.mjs +5 -1
- package/hooks/pre-bash-destructive-guard.mjs +220 -8
- package/hooks/pre-bash-issue-budget.mjs +6 -1
- package/hooks/pre-bash-sessions-ledger-guard.mjs +691 -123
- package/hooks/pre-bash-staging-fence.mjs +258 -14
- package/hooks/pre-bash-templates-first.mjs +28 -27
- package/hooks/run-node.sh +1 -1
- package/hooks/skill-invocation-telemetry.mjs +15 -8
- package/hooks/subagent-telemetry.mjs +77 -14
- package/hooks/wave-scope-commit-guard.mjs +143 -14
- package/mods/so-hud/.claude-plugin/plugin.json +11 -0
- package/mods/so-hud/README.md +57 -0
- package/mods/so-hud/hooks/beacon-io.mjs +101 -0
- package/mods/so-hud/hooks/heredoc-scan.ts +394 -0
- package/mods/so-hud/hooks/hooks.json +1 -0
- package/mods/so-hud/hooks/logic.ts +182 -0
- package/mods/so-hud/hooks/register.tsx +670 -0
- package/mods/so-hud/hooks/rules.ts +2998 -0
- package/mods/so-hud/tsconfig.json +8 -0
- package/mods/so-hud/types/index.d.ts +36 -0
- package/package.json +4 -2
- package/rules/_index.md +1 -1
- package/rules/always-on/ask-via-tool.md +1 -1
- package/rules/always-on/loop-and-monitor.md +17 -194
- package/rules/always-on/parallel-sessions.md +3 -1
- package/rules/opt-in-stack/backend-data.md +1 -1
- package/rules/opt-in-stack/backend.md +1 -1
- package/rules/opt-in-stack/security-web.md +5 -1
- package/scripts/agent-usage-census.mjs +79 -0
- package/scripts/apply-session-learnings.mjs +34 -6
- package/scripts/archive-closed-prds.mjs +21 -5
- package/scripts/autopilot.mjs +11 -5
- package/scripts/backfill-abandoned-sessions.mjs +13 -3
- package/scripts/backfill-evidence-digest.mjs +2 -1
- package/scripts/backfill-learnings-from-vault.mjs +61 -6
- package/scripts/backfill-sessions.mjs +16 -2
- package/scripts/check-package-manager.mjs +26 -8
- package/scripts/check-sessions-integrity.mjs +4 -1
- package/scripts/ci/assert-vitest-green.mjs +3 -0
- package/scripts/ci/windows-smoke.mjs +201 -0
- package/scripts/close-precheck.mjs +55 -0
- package/scripts/dialectic-deriver.mjs +20 -12
- package/scripts/emit-event.mjs +2 -2
- package/scripts/emit-session.mjs +239 -20
- package/scripts/eval-session.mjs +17 -3
- package/scripts/export-hw-learnings.mjs +29 -2
- package/scripts/fleet-checkin-audit.mjs +8 -4
- package/scripts/fleet-checkin.mjs +39 -27
- package/scripts/gc-stale-worktrees.mjs +6 -1
- package/scripts/github-export.mjs +128 -0
- package/scripts/github-protection-audit.mjs +3 -1
- package/scripts/handover-queue-check.mjs +209 -0
- package/scripts/harness-audit.mjs +6 -4
- package/scripts/lib/agent-frontmatter.mjs +57 -10
- package/scripts/lib/agent-status.mjs +125 -7
- package/scripts/lib/agent-usage-census.mjs +394 -0
- package/scripts/lib/auto-dialectic.mjs +17 -10
- package/scripts/lib/auto-dream.mjs +22 -14
- package/scripts/lib/autopilot/loop.mjs +3 -1
- package/scripts/lib/autopilot/recent-runs.mjs +4 -2
- package/scripts/lib/autopilot/stall-sampler.mjs +2 -1
- package/scripts/lib/autopilot/telemetry.mjs +5 -1
- package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
- package/scripts/lib/baseline-archetypes.mjs +10 -2
- package/scripts/lib/blocked-commands-policy.mjs +4 -3
- package/scripts/lib/build-live-signals.mjs +13 -9
- package/scripts/lib/ci-status-banner.mjs +329 -24
- package/scripts/lib/codex/plugin-contract.mjs +8 -0
- package/scripts/lib/cold-start-detector.mjs +5 -2
- package/scripts/lib/command-blocker.mjs +603 -43
- package/scripts/lib/common.mjs +3 -2
- package/scripts/lib/config/cross-repo.mjs +74 -7
- package/scripts/lib/config/host-paths.mjs +6 -2
- package/scripts/lib/config/section-extractor.mjs +65 -11
- package/scripts/lib/config/template-keys.mjs +153 -0
- package/scripts/lib/config/vault-integration.mjs +1 -0
- package/scripts/lib/config.mjs +2 -2
- package/scripts/lib/coordinator-snapshot.mjs +1 -1
- package/scripts/lib/discovery/triage-state.mjs +2 -1
- package/scripts/lib/dispatcher/rank.mjs +5 -7
- package/scripts/lib/ecosystem-health.mjs +2 -2
- package/scripts/lib/eval/engine.mjs +3 -2
- package/scripts/lib/eval/session-resolve.mjs +6 -2
- package/scripts/lib/eval/sink.mjs +4 -3
- package/scripts/lib/events-retention-banner.mjs +3 -2
- package/scripts/lib/events-schema.mjs +10 -0
- package/scripts/lib/events.mjs +23 -12
- package/scripts/lib/evolve/autonomy-verdict.mjs +4 -9
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +8 -18
- package/scripts/lib/fleet-protocol.mjs +4 -0
- package/scripts/lib/gates/gate-full.mjs +172 -11
- package/scripts/lib/gates/gate-helpers.mjs +236 -23
- package/scripts/lib/git-config-drift.mjs +141 -7
- package/scripts/lib/github-export.mjs +447 -0
- package/scripts/lib/gitlab-portfolio/cli.mjs +16 -5
- package/scripts/lib/handover-queue-marks.mjs +159 -0
- package/scripts/lib/hardware-pattern-detector.mjs +103 -13
- package/scripts/lib/harness-audit/categories/category1.mjs +16 -4
- package/scripts/lib/harness-audit/categories/category4.mjs +11 -8
- package/scripts/lib/harness-audit/categories/category6.mjs +25 -5
- package/scripts/lib/harness-audit/categories/category9.mjs +10 -4
- package/scripts/lib/host-identity.mjs +9 -5
- package/scripts/lib/http-request.mjs +64 -0
- package/scripts/lib/instruction-budget-guard.mjs +30 -22
- package/scripts/lib/io.mjs +6 -3
- package/scripts/lib/issue-budget-reconcile.mjs +3 -2
- package/scripts/lib/issue-budget.mjs +36 -7
- package/scripts/lib/learnings/evidence-candidates.mjs +154 -0
- package/scripts/lib/learnings/expiry-sweep.mjs +99 -7
- package/scripts/lib/learnings/io.mjs +289 -15
- package/scripts/lib/learnings/select.mjs +53 -4
- package/scripts/lib/learnings/store-role.mjs +209 -0
- package/scripts/lib/learnings/surface.mjs +46 -7
- package/scripts/lib/ledger-path.mjs +113 -0
- package/scripts/lib/maintenance-due-banner.mjs +63 -12
- package/scripts/lib/memory-banner.mjs +5 -6
- package/scripts/lib/memory-proposals/collector.mjs +5 -2
- package/scripts/lib/memory-proposals/sink.mjs +7 -4
- package/scripts/lib/memory-proposals/store.mjs +22 -5
- package/scripts/lib/mode-selector-accuracy.mjs +2 -1
- package/scripts/lib/no-follow-open.mjs +57 -0
- package/scripts/lib/orphan-reaper/reaper-audit.mjs +4 -2
- package/scripts/lib/own-lock-root.mjs +95 -0
- package/scripts/lib/owner-interview.mjs +2 -1
- package/scripts/lib/owner-yaml.mjs +70 -7
- package/scripts/lib/platform.mjs +161 -9
- package/scripts/lib/plugin-update-banner.mjs +15 -11
- package/scripts/lib/pre-push-local-partial.mjs +117 -0
- package/scripts/lib/pre-push-remote-gate.mjs +151 -25
- package/scripts/lib/process-group.mjs +270 -34
- package/scripts/lib/project-hygiene.mjs +28 -0
- package/scripts/lib/quality-gate.mjs +33 -9
- package/scripts/lib/reconcile/backlog.mjs +29 -15
- package/scripts/lib/reconcile/engine.mjs +26 -2
- package/scripts/lib/reconcile/idempotency.mjs +65 -3
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +53 -12
- package/scripts/lib/reconcile/verdict-ledger.mjs +224 -0
- package/scripts/lib/reconcile-nudge-banner.mjs +40 -6
- package/scripts/lib/resource-probe/evaluate.mjs +32 -9
- package/scripts/lib/resource-probe/probe-platform.mjs +3 -1
- package/scripts/lib/rule-loader.mjs +20 -10
- package/scripts/lib/scope-baseline.mjs +123 -8
- package/scripts/lib/scope-echo.mjs +5 -2
- package/scripts/lib/scope-gate.mjs +55 -20
- package/scripts/lib/session-close-backfill.mjs +75 -7
- package/scripts/lib/session-end/close-precheck.mjs +151 -0
- package/scripts/lib/session-end/phase-skip.mjs +8 -7
- package/scripts/lib/session-end/tail-runner.mjs +7 -2
- package/scripts/lib/session-id.mjs +9 -1
- package/scripts/lib/session-identity/own-session.mjs +63 -0
- package/scripts/lib/session-lock.mjs +114 -0
- package/scripts/lib/session-record-repair.mjs +245 -35
- package/scripts/lib/session-registry.mjs +72 -9
- package/scripts/lib/session-schema/constants.mjs +9 -0
- package/scripts/lib/session-schema/filters.mjs +56 -1
- package/scripts/lib/session-schema/validator.mjs +15 -5
- package/scripts/lib/session-start-probes.mjs +149 -22
- package/scripts/lib/session-token-rollup.mjs +2 -1
- package/scripts/lib/session-work-trees.mjs +166 -0
- package/scripts/lib/sessions-canonical.mjs +61 -3
- package/scripts/lib/sessions-integrity-banner.mjs +17 -39
- package/scripts/lib/sessions-staleness-banner.mjs +8 -5
- package/scripts/lib/sh-quote.mjs +1 -0
- package/scripts/lib/skill-evidence-window.mjs +5 -1
- package/scripts/lib/skill-health/join.mjs +10 -9
- package/scripts/lib/skill-invocations-schema.mjs +13 -0
- package/scripts/lib/skill-judgments-schema.mjs +6 -2
- package/scripts/lib/snapshot-probe.mjs +414 -0
- package/scripts/lib/so-hud/collect.mjs +49 -5
- package/scripts/lib/so-hud/runtime.mjs +15 -4
- package/scripts/lib/spiral-carryover.mjs +8 -1
- package/scripts/lib/state-md/frontmatter-mutators.mjs +266 -8
- package/scripts/lib/state-md/idle-reset.mjs +14 -5
- package/scripts/lib/state-md/mission-status.mjs +5 -1
- package/scripts/lib/state-md-peer-guard.mjs +2 -0
- package/scripts/lib/state-md.mjs +31 -8
- package/scripts/lib/sunset/walker.mjs +4 -3
- package/scripts/lib/telemetry/consent.mjs +4 -2
- package/scripts/lib/telemetry/queue.mjs +3 -3
- package/scripts/lib/telemetry/sync.mjs +45 -9
- package/scripts/lib/test-runner/artifact-paths.mjs +10 -2
- package/scripts/lib/tmux-layout/layouts.mjs +20 -3
- package/scripts/lib/ux-grill/paths.mjs +5 -1
- package/scripts/lib/validate/check-agents.mjs +37 -6
- package/scripts/lib/validate/check-component-paths.mjs +25 -1
- package/scripts/lib/validate/check-guard-requires-parity.mjs +3 -3
- package/scripts/lib/validate/check-hooks-symmetry.mjs +53 -32
- package/scripts/lib/validate/check-json-files.mjs +3 -1
- package/scripts/lib/validate/check-learning-provenance.mjs +3 -2
- package/scripts/lib/validate/check-learnings-shell-writes.mjs +10 -4
- package/scripts/lib/validate/check-metrics-ledger-root.mjs +234 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +159 -11
- package/scripts/lib/validate/check-test-posix-shell.mjs +246 -0
- package/scripts/lib/validate/check-unwired-features.mjs +4 -2
- package/scripts/lib/validate/check-vcs-repo-flag-positional.mjs +58 -0
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +259 -73
- package/scripts/lib/validate/check-workflow-continue-on-error.mjs +63 -0
- package/scripts/lib/validate/confidential-names.mjs +3 -1
- package/scripts/lib/vault-mirror/migrate-ids.mjs +321 -0
- package/scripts/lib/vault-mirror/namespace.mjs +19 -7
- package/scripts/lib/vault-mirror/process.mjs +56 -27
- package/scripts/lib/vault-mirror/prune.mjs +5 -2
- package/scripts/lib/vault-mirror/render-learnings.mjs +3 -3
- package/scripts/lib/vault-mirror/render-sessions.mjs +36 -53
- package/scripts/lib/vault-mirror/retention.mjs +2 -2
- package/scripts/lib/vault-mirror/utils.mjs +79 -0
- package/scripts/lib/vault-staleness-banner.mjs +4 -7
- package/scripts/lib/vault-status/board-writer.mjs +2 -1
- package/scripts/lib/vault-status/narrative-mirror.mjs +66 -7
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +20 -0
- package/scripts/lib/win-process-tree.mjs +137 -0
- package/scripts/lib/workspace.mjs +1 -1
- package/scripts/lib/worktree/constants.mjs +1 -1
- package/scripts/lib/worktree/lifecycle.mjs +1 -1
- package/scripts/lib/worktree/listing.mjs +1 -1
- package/scripts/lib/zx.mjs +50 -0
- package/scripts/mcp-server.mjs +16 -7
- package/scripts/mcp-server.sh +13 -83
- package/scripts/measure-policy-cache-effectiveness.mjs +7 -5
- package/scripts/memory-propose.mjs +10 -3
- package/scripts/migrate-cold-start-seed.mjs +7 -0
- package/scripts/migrate-legacy-learnings.sh +22 -0
- package/scripts/migrate-sessions-jsonl.mjs +16 -2
- package/scripts/migrate-vault-paths.mjs +29 -13
- package/scripts/parse-config.mjs +71 -14
- package/scripts/print-learnings-index.mjs +25 -2
- package/scripts/release-session-lock.mjs +94 -7
- package/scripts/release.mjs +451 -24
- package/scripts/relocate-vault-corpus.mjs +11 -7
- package/scripts/repair-invalid-sessions.mjs +68 -3
- package/scripts/resolve-session-invocation.mjs +10 -17
- package/scripts/run-migrate-v2-cross-repo.mjs +56 -1
- package/scripts/run-quality-gate.mjs +33 -7
- package/scripts/self-update.mjs +56 -7
- package/scripts/session-commit-files.mjs +133 -0
- package/scripts/site-numbers.mjs +52 -9
- package/scripts/snapshot-probe.mjs +156 -0
- package/scripts/so-hud.mjs +29 -6
- package/scripts/state-md-idle-reset.mjs +2 -1
- package/scripts/sweep-expired-learnings.mjs +54 -6
- package/scripts/sweep-expired-rules.mjs +4 -1
- package/scripts/validate-plugin.mjs +22 -0
- package/scripts/vault-integration-watcher.mjs +54 -6
- package/scripts/vault-mirror.mjs +9 -0
- package/skills/_shared/fleet-protocol.md +142 -1
- package/skills/_shared/instruction-file-resolution.md +1 -1
- package/skills/_shared/monitor-patterns.md +44 -25
- package/skills/_shared/platform-tools.md +7 -0
- package/skills/_shared/state-ownership.md +3 -2
- package/skills/architecture/references/domain-model.md +1 -1
- package/skills/bootstrap/SKILL.md +3 -1
- package/skills/bootstrap/_shared-template.md +4 -4
- package/skills/bootstrap/fast-template.md +1 -1
- package/skills/bootstrap/private-contract.md +1 -1
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +17 -9
- package/skills/bootstrap/templates/agents/project-code-review.md +5 -1
- package/skills/bootstrap/templates/agents/project-discovery.md +5 -1
- package/skills/bootstrap/templates/agents/project-quality-gate.md +5 -1
- package/skills/claude-md-drift-check/SKILL.md +3 -3
- package/skills/claude-md-drift-check/checker.mjs +61 -101
- package/skills/close/SKILL.md +20 -7
- package/skills/discovery/probes/docs-staleness.mjs +7 -5
- package/skills/discovery/probes/frontend-slop.mjs +7 -3
- package/skills/discovery/probes/ssot-code-diff.mjs +5 -3
- package/skills/discovery/probes/supply-chain-slopcheck.mjs +10 -8
- package/skills/discovery/probes/vault-narrative-staleness.mjs +30 -10
- package/skills/discovery/probes/vault-staleness.mjs +5 -3
- package/skills/discovery/probes-session.md +3 -0
- package/skills/discovery/probes-vault.md +2 -0
- package/skills/dispatcher/SKILL.md +1 -1
- package/skills/eval/SKILL.md +3 -1
- package/skills/evolve/SKILL.md +2 -0
- package/skills/evolve/references/evolve-analyze-mode.md +3 -3
- package/skills/evolve/references/evolve-dialectic-mode.md +1 -1
- package/skills/gitlab-ops/SKILL.md +7 -6
- package/skills/gitlab-portfolio/SKILL.md +2 -2
- package/skills/go/SKILL.md +13 -1
- package/skills/npm-publish/SKILL.md +5 -2
- package/skills/plan/SKILL.md +23 -10
- package/skills/plan/mode-feature.md +13 -8
- package/skills/plan/mode-new.md +6 -14
- package/skills/plan/prd-feature-template.md +19 -0
- package/skills/plan/prd-full-template.md +12 -5
- package/skills/plan/prd-premortem-panel.md +48 -0
- package/skills/plan/prd-reviewer-prompt.md +11 -3
- package/skills/plan/soul.md +1 -1
- package/skills/portfolio/SKILL.md +5 -6
- package/skills/quality-gates/SKILL.md +2 -4
- package/skills/reconcile/SKILL.md +27 -4
- package/skills/release/SKILL.md +16 -8
- package/skills/session-end/SKILL.md +94 -11
- package/skills/session-end/phase-3-2-docs-verification.md +1 -0
- package/skills/session-end/phase-3-6-tail.md +11 -7
- package/skills/session-end/phase-3-7a-recommendations.md +9 -2
- package/skills/session-end/plan-verification.md +16 -1
- package/skills/session-end/references/phase-3-documentation-updates.md +14 -12
- package/skills/session-end/references/phase-5-issue-cleanup.md +1 -1
- package/skills/session-end/references/session-summary-template.md +4 -0
- package/skills/session-end/session-metrics-write.md +7 -4
- package/skills/session-plan/SKILL.md +5 -3
- package/skills/session-start/SKILL.md +9 -3
- package/skills/session-start/phase-4-5-resource-health.md +11 -5
- package/skills/session-start/phase-8-5-express-path.md +4 -3
- package/skills/session-start/references/phase-1-2-session-lock.md +7 -5
- package/skills/session-start/references/phase-1-5-session-continuity.md +2 -2
- package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
- package/skills/session-start/references/phase-4-ssot-environment-check.md +3 -2
- package/skills/tmux-layout/SKILL.md +2 -2
- package/skills/vault-sync/validator.mjs +3 -2
- package/skills/wave-executor/SKILL.md +5 -4
- package/skills/wave-executor/references/wave-executor-state-init.md +3 -3
- package/skills/wave-executor/references/wave-loop-dispatch.md +11 -5
- package/skills/wave-executor/references/wave-loop-review.md +13 -5
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +1 -1
- package/assets/og-card.png +0 -0
- package/assets/og-card.svg +0 -47
- package/assets/wave-lifecycle.svg +0 -98
package/scripts/autopilot.mjs
CHANGED
|
@@ -53,6 +53,8 @@ import { probe, evaluate, DEFAULT_RESOURCE_THRESHOLDS } from './lib/resource-pro
|
|
|
53
53
|
import { detectPeers } from './lib/session-registry.mjs';
|
|
54
54
|
import { normalizeSession } from './lib/session-schema.mjs';
|
|
55
55
|
import { isBackfillStub, readCanonicalSessions } from './lib/sessions-canonical.mjs';
|
|
56
|
+
import { resolveEventsLedgerRoot } from './lib/platform.mjs';
|
|
57
|
+
import { resolveLedgerPath } from './lib/ledger-path.mjs';
|
|
56
58
|
|
|
57
59
|
// ---------------------------------------------------------------------------
|
|
58
60
|
// CLI-level flag extraction
|
|
@@ -152,7 +154,7 @@ const thresholds = loadResourceThresholds();
|
|
|
152
154
|
// decay-floor-factor) actually take effect here (#670). Errors swallowed → [].
|
|
153
155
|
|
|
154
156
|
const surfacedLearnings = await surfaceTopN(
|
|
155
|
-
|
|
157
|
+
resolveLedgerPath('.orchestrator/metrics/learnings.jsonl', process.cwd()), // #1722
|
|
156
158
|
15,
|
|
157
159
|
{ decay: decayOptsFromConfig(config && config['evolve.decay']) }
|
|
158
160
|
);
|
|
@@ -182,7 +184,10 @@ const branch = detectBranch();
|
|
|
182
184
|
// sessions.jsonl helpers for sessionRunner
|
|
183
185
|
// ---------------------------------------------------------------------------
|
|
184
186
|
|
|
185
|
-
|
|
187
|
+
// #1592: the child runs with cwd = process.cwd() and its close writes the
|
|
188
|
+
// ledger at resolveEventsLedgerRoot(process.cwd()) (emit-session.mjs) — read
|
|
189
|
+
// the same file, or a run from a linked worktree finds no record.
|
|
190
|
+
const SESSIONS_JSONL_PATH = resolve(resolveEventsLedgerRoot(process.cwd()), '.orchestrator/metrics/sessions.jsonl');
|
|
186
191
|
|
|
187
192
|
/**
|
|
188
193
|
* Read the child's canonical record by its raw harness UUID, independent of
|
|
@@ -253,9 +258,10 @@ async function modeSelector() {
|
|
|
253
258
|
// in backlog-scan.mjs (`DEFAULT_BACKLOG_LIMIT`). The hand-written 50 that
|
|
254
259
|
// stood here under-read this repo's own backlog and hid its critical issues.
|
|
255
260
|
// `repoRoot` is passed EXPLICITLY (#1071) rather than left to the helper's
|
|
256
|
-
// cwd default: this driver already reads learnings.jsonl
|
|
257
|
-
//
|
|
258
|
-
//
|
|
261
|
+
// cwd default: this driver already reads learnings.jsonl relative to the
|
|
262
|
+
// invocation directory (and sessions.jsonl from that directory's ledger root,
|
|
263
|
+
// `SESSIONS_JSONL_PATH`, #1592), and an implicit default is the seam where
|
|
264
|
+
// those roots silently drift apart.
|
|
259
265
|
const signals = await buildLiveSignals({
|
|
260
266
|
repoRoot: process.cwd(),
|
|
261
267
|
learnings: surfacedLearnings,
|
|
@@ -44,7 +44,7 @@ import { parseArgs } from 'node:util';
|
|
|
44
44
|
import { backfillAbandonedSession, isUuid } from './lib/session-close-backfill.mjs';
|
|
45
45
|
import { SCAN_CHUNK_BYTES, emitEvent, eventsFilePath, listEventSourcesNewestFirst, tombstoneGap } from './lib/events.mjs';
|
|
46
46
|
import { ARCHIVE_DIR_NAME } from './lib/events-schema.mjs';
|
|
47
|
-
import { getProjectDir } from './lib/platform.mjs';
|
|
47
|
+
import { getProjectDir, resolveEventsLedgerRoot } from './lib/platform.mjs';
|
|
48
48
|
import { isMainModule } from './lib/is-main-module.mjs';
|
|
49
49
|
import { isLockLive, readLock } from './lib/session-lock.mjs';
|
|
50
50
|
import { readProcessLocalSessionIds } from './lib/session-identity/own-session.mjs';
|
|
@@ -277,7 +277,13 @@ function forEachJsonlRecord(filePath, onRecord) {
|
|
|
277
277
|
return;
|
|
278
278
|
}
|
|
279
279
|
try {
|
|
280
|
-
const
|
|
280
|
+
const stats = fs.fstatSync(fd);
|
|
281
|
+
// Windows opens a directory "as a file" with size 0; surface it through the
|
|
282
|
+
// same warning POSIX reaches via EISDIR on the first read.
|
|
283
|
+
if (!stats.isFile()) {
|
|
284
|
+
throw Object.assign(new Error('EISDIR: illegal operation on a directory, read'), { code: 'EISDIR' });
|
|
285
|
+
}
|
|
286
|
+
const size = stats.size;
|
|
281
287
|
const buf = Buffer.alloc(SCAN_CHUNK_BYTES);
|
|
282
288
|
let pos = 0;
|
|
283
289
|
let carry = Buffer.alloc(0);
|
|
@@ -365,7 +371,10 @@ function readRunningSessionIds(repoRoot) {
|
|
|
365
371
|
* @returns {Set<string>}
|
|
366
372
|
*/
|
|
367
373
|
function readRecordedIds(repoRoot) {
|
|
368
|
-
|
|
374
|
+
// #1592: the SAME ledger the core dedupes and writes against (main checkout
|
|
375
|
+
// of a linked worktree) — a worktree-local read let already-recorded
|
|
376
|
+
// candidates through and spent the SessionStart latency cap on them.
|
|
377
|
+
const records = readJsonl(path.join(resolveEventsLedgerRoot(repoRoot), '.orchestrator', 'metrics', 'sessions.jsonl'));
|
|
369
378
|
const ids = new Set();
|
|
370
379
|
for (const r of records) {
|
|
371
380
|
if (!r) continue;
|
|
@@ -655,6 +664,7 @@ async function flushTelemetryOnSessionStart(repoRoot) {
|
|
|
655
664
|
try {
|
|
656
665
|
const { flush } = await import('./lib/telemetry/sync.mjs');
|
|
657
666
|
await flush({
|
|
667
|
+
// ledger-root: metricsDir for the telemetry flush; buildBatch resolves each ledger in it
|
|
658
668
|
metricsDir: path.join(repoRoot, '.orchestrator', 'metrics'),
|
|
659
669
|
timeoutMs: STARTUP_FLUSH_TIMEOUT_MS,
|
|
660
670
|
});
|
|
@@ -51,6 +51,7 @@ import { join, relative, resolve } from 'node:path';
|
|
|
51
51
|
|
|
52
52
|
import { computeEvidenceDigest } from './lib/reconcile/renderer.mjs';
|
|
53
53
|
import { isMainModule } from './lib/is-main-module.mjs';
|
|
54
|
+
import { resolveLedgerPath } from './lib/ledger-path.mjs';
|
|
54
55
|
|
|
55
56
|
/** ISO-8601 instant — mirrors the renderer's `EVIDENCE_RECORDED_AT_RE`. */
|
|
56
57
|
const EVIDENCE_RECORDED_AT_RE = /^\d{4}-\d{2}-\d{2}T[0-9:.]+(?:Z|[+-]\d{2}:\d{2})$/;
|
|
@@ -198,7 +199,7 @@ const kebab = (s) =>
|
|
|
198
199
|
*/
|
|
199
200
|
function loadCreatedAtByKey(repoRoot) {
|
|
200
201
|
const map = new Map();
|
|
201
|
-
const p =
|
|
202
|
+
const p = resolveLedgerPath('.orchestrator/metrics/learnings.jsonl', repoRoot); // #1722
|
|
202
203
|
if (!existsSync(p)) return map;
|
|
203
204
|
for (const line of readFileSync(p, 'utf8').split('\n')) {
|
|
204
205
|
const trimmed = line.trim();
|
|
@@ -72,7 +72,15 @@ import { subjectToSlug, parseFrontmatter } from './lib/vault-mirror/utils.mjs';
|
|
|
72
72
|
import { kebab } from './lib/learnings/kebab.mjs';
|
|
73
73
|
import { validateLearning } from './lib/learnings/schema.mjs';
|
|
74
74
|
import { appendLearning, isBackupOf, withLearningsLock } from './lib/learnings/io.mjs';
|
|
75
|
+
import {
|
|
76
|
+
isCanonicalStore,
|
|
77
|
+
inboxPathFor,
|
|
78
|
+
receiptsPathFor,
|
|
79
|
+
archivePathFor,
|
|
80
|
+
resolveStoreRole,
|
|
81
|
+
} from './lib/learnings/store-role.mjs';
|
|
75
82
|
import { isMainModule } from './lib/is-main-module.mjs';
|
|
83
|
+
import { resolveLedgerPath } from './lib/ledger-path.mjs';
|
|
76
84
|
|
|
77
85
|
const DEFAULT_RULES_DIR = '.claude/rules';
|
|
78
86
|
const DEFAULT_STORE = '.orchestrator/metrics/learnings.jsonl';
|
|
@@ -621,6 +629,39 @@ function idsInStore(filePath) {
|
|
|
621
629
|
return ids;
|
|
622
630
|
}
|
|
623
631
|
|
|
632
|
+
/**
|
|
633
|
+
* Inbox ids that count as already present for the canonical store (#1516,
|
|
634
|
+
* #1685). The rule depends on this host's store role:
|
|
635
|
+
*
|
|
636
|
+
* - `inbox`: EVERY inbox id is present. `appendLearning()` lands a restore in
|
|
637
|
+
* the inbox here, and re-appending a receipted or archived id is pointless —
|
|
638
|
+
* the receipt or archive hides it again, so every --apply would re-append.
|
|
639
|
+
* - any other role (`writer`, `invalid` — invalid refuses the write anyway):
|
|
640
|
+
* only VISIBLE inbox ids, i.e. not receipted and not archived, mirroring
|
|
641
|
+
* `readInboxView()` in io.mjs. A receipted id that later left the live store
|
|
642
|
+
* is seen by no reader, so it must stay an orphan the backfill restores.
|
|
643
|
+
* Live ids need no exclusion here: callers check the live store themselves.
|
|
644
|
+
*
|
|
645
|
+
* `hidden` is the subset of `present` that no reader sees (receipted or
|
|
646
|
+
* archived) — non-empty only on an inbox host, where such ids still count as
|
|
647
|
+
* present. The report gives them their own status (`present-in-inbox-hidden`,
|
|
648
|
+
* #1689) instead of claiming they sit in the store's read view.
|
|
649
|
+
*
|
|
650
|
+
* Both empty for a non-canonical store path, which has no inbox semantics.
|
|
651
|
+
*
|
|
652
|
+
* @param {string} storeAbs
|
|
653
|
+
* @returns {{ present: Set<string>, hidden: Set<string> }}
|
|
654
|
+
*/
|
|
655
|
+
function presentInboxIds(storeAbs) {
|
|
656
|
+
if (!isCanonicalStore(storeAbs)) return { present: new Set(), hidden: new Set() };
|
|
657
|
+
const inboxIds = idsInStore(inboxPathFor(storeAbs));
|
|
658
|
+
// Receipt lines are `{id, ingested_at}` — idsInStore reads their ids.
|
|
659
|
+
const hiddenAny = new Set([...idsInStore(receiptsPathFor(storeAbs)), ...idsInStore(archivePathFor(storeAbs))]);
|
|
660
|
+
const hidden = new Set([...inboxIds].filter((id) => hiddenAny.has(id)));
|
|
661
|
+
if (resolveStoreRole().role === 'inbox') return { present: inboxIds, hidden };
|
|
662
|
+
return { present: new Set([...inboxIds].filter((id) => !hidden.has(id))), hidden: new Set() };
|
|
663
|
+
}
|
|
664
|
+
|
|
624
665
|
/**
|
|
625
666
|
* List `<store>.bak[-.]*` siblings (a verbatim original beats any
|
|
626
667
|
* reconstruction). Both delimiter forms count — the legacy dot form
|
|
@@ -681,7 +722,7 @@ function printHuman(report) {
|
|
|
681
722
|
out.push(` vault : ${report.vault_learnings_dir} (${s.vault_notes_indexed} learning notes indexed)`);
|
|
682
723
|
out.push('');
|
|
683
724
|
out.push(
|
|
684
|
-
`Rules with provenance: ${s.rules_with_provenance} · resolved in store: ${s.present_in_store} · in archive: ${s.present_in_archive} · recoverable verbatim from a .bak: ${s.recoverable_from_backup} · ORPHANED: ${s.orphans}`
|
|
725
|
+
`Rules with provenance: ${s.rules_with_provenance} · resolved in store: ${s.present_in_store} · in archive: ${s.present_in_archive} · in inbox, hidden: ${s.present_in_inbox_hidden} · recoverable verbatim from a .bak: ${s.recoverable_from_backup} · ORPHANED: ${s.orphans}`
|
|
685
726
|
);
|
|
686
727
|
out.push('');
|
|
687
728
|
|
|
@@ -757,6 +798,8 @@ export async function main(argv = [], deps = {}) {
|
|
|
757
798
|
let rulesDir = DEFAULT_RULES_DIR;
|
|
758
799
|
let storeArg = DEFAULT_STORE;
|
|
759
800
|
let archiveArg = DEFAULT_ARCHIVE;
|
|
801
|
+
let storeExplicit = false;
|
|
802
|
+
let archiveExplicit = false;
|
|
760
803
|
|
|
761
804
|
for (let i = 0; i < argv.length; i++) {
|
|
762
805
|
const a = argv[i];
|
|
@@ -767,8 +810,8 @@ export async function main(argv = [], deps = {}) {
|
|
|
767
810
|
else if (a === '--vault-dir') vaultDirArg = argv[++i];
|
|
768
811
|
else if (a === '--vault-subdir') vaultSubdir = argv[++i];
|
|
769
812
|
else if (a === '--rules-dir') rulesDir = argv[++i];
|
|
770
|
-
else if (a === '--store') storeArg = argv[++i];
|
|
771
|
-
else if (a === '--archive') archiveArg = argv[++i];
|
|
813
|
+
else if (a === '--store') { storeArg = argv[++i]; storeExplicit = true; }
|
|
814
|
+
else if (a === '--archive') { archiveArg = argv[++i]; archiveExplicit = true; }
|
|
772
815
|
else {
|
|
773
816
|
process.stderr.write(`backfill-learnings-from-vault: unknown argument: ${a}\n`);
|
|
774
817
|
return { code: 1 };
|
|
@@ -819,8 +862,10 @@ export async function main(argv = [], deps = {}) {
|
|
|
819
862
|
// `resolve` (not `join`) so an ABSOLUTE --store/--archive/--rules-dir wins over
|
|
820
863
|
// the repo root instead of being silently nested under it.
|
|
821
864
|
const rulesAbs = resolve(root, rulesDir);
|
|
822
|
-
|
|
823
|
-
|
|
865
|
+
// #1722: the DEFAULT store/archive route through the ledger resolver (main
|
|
866
|
+
// checkout when root is a linked worktree that gitignores them).
|
|
867
|
+
const storeAbs = storeExplicit ? resolve(root, storeArg) : resolveLedgerPath(DEFAULT_STORE, root);
|
|
868
|
+
const archiveAbs = archiveExplicit ? resolve(root, archiveArg) : resolveLedgerPath(DEFAULT_ARCHIVE, root);
|
|
824
869
|
const learningsAbs = resolve(vaultDir, vaultSubdir);
|
|
825
870
|
|
|
826
871
|
if (!existsSync(rulesAbs)) {
|
|
@@ -834,6 +879,7 @@ export async function main(argv = [], deps = {}) {
|
|
|
834
879
|
|
|
835
880
|
// ── Load inputs ──────────────────────────────────────────────────────────
|
|
836
881
|
const storeIds = idsInStore(storeAbs);
|
|
882
|
+
const { present: inboxIds, hidden: hiddenInboxIds } = presentInboxIds(storeAbs);
|
|
837
883
|
const archiveIds = idsInStore(archiveAbs);
|
|
838
884
|
const backups = backupPaths(storeAbs).map((p) => ({ path: p, ids: idsInStore(p) }));
|
|
839
885
|
const { notes, unreadable } = indexVaultNotes(learningsAbs);
|
|
@@ -868,12 +914,20 @@ export async function main(argv = [], deps = {}) {
|
|
|
868
914
|
const id = prov.learningId;
|
|
869
915
|
const foundIn = [];
|
|
870
916
|
if (storeIds.has(id)) foundIn.push('store');
|
|
917
|
+
if (inboxIds.has(id)) foundIn.push('inbox');
|
|
871
918
|
if (archiveIds.has(id)) foundIn.push('archive');
|
|
872
919
|
for (const b of backups) if (b.ids.has(id)) foundIn.push(basename(b.path));
|
|
873
920
|
|
|
874
921
|
let status;
|
|
922
|
+
// A visible inbox id is part of the store's read view (readers union it
|
|
923
|
+
// in), so it counts as present-in-store — after the archive, which wins
|
|
924
|
+
// for an id in both; `found_in` names the file. A present but HIDDEN one
|
|
925
|
+
// (inbox host, receipted/archived) is seen by no reader, so it is not
|
|
926
|
+
// claimed as part of the store (#1689).
|
|
875
927
|
if (storeIds.has(id)) status = 'present-in-store';
|
|
876
928
|
else if (archiveIds.has(id)) status = 'present-in-archive';
|
|
929
|
+
else if (hiddenInboxIds.has(id)) status = 'present-in-inbox-hidden';
|
|
930
|
+
else if (inboxIds.has(id)) status = 'present-in-store';
|
|
877
931
|
else if (foundIn.length > 0) status = 'recoverable-verbatim-from-backup';
|
|
878
932
|
else status = 'orphan';
|
|
879
933
|
|
|
@@ -942,7 +996,7 @@ export async function main(argv = [], deps = {}) {
|
|
|
942
996
|
// appendLearning() re-enters the held lock (reentrant per async call
|
|
943
997
|
// chain, io.mjs).
|
|
944
998
|
await withLearningsLock(storeAbs, async () => {
|
|
945
|
-
const present = new Set([...idsInStore(storeAbs), ...idsInStore(archiveAbs)]);
|
|
999
|
+
const present = new Set([...idsInStore(storeAbs), ...presentInboxIds(storeAbs).present, ...idsInStore(archiveAbs)]);
|
|
946
1000
|
for (const r of candidates) {
|
|
947
1001
|
// This is what makes a second --apply a no-op rather than a
|
|
948
1002
|
// duplicate append; `present.add` below covers two rules naming one id.
|
|
@@ -983,6 +1037,7 @@ export async function main(argv = [], deps = {}) {
|
|
|
983
1037
|
vault_notes_indexed: notes.length,
|
|
984
1038
|
present_in_store: records.filter((r) => r.status === 'present-in-store').length,
|
|
985
1039
|
present_in_archive: records.filter((r) => r.status === 'present-in-archive').length,
|
|
1040
|
+
present_in_inbox_hidden: records.filter((r) => r.status === 'present-in-inbox-hidden').length,
|
|
986
1041
|
recoverable_from_backup: records.filter((r) => r.status === 'recoverable-verbatim-from-backup').length,
|
|
987
1042
|
orphans: orphans.length,
|
|
988
1043
|
located_in_vault: orphans.filter((r) => r.vault_note).length,
|
|
@@ -29,9 +29,10 @@
|
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
import { readFileSync, copyFileSync, existsSync } from 'node:fs';
|
|
32
|
-
import { basename } from 'node:path';
|
|
32
|
+
import { basename, join, resolve } from 'node:path';
|
|
33
33
|
import { atomicWriteWithBackup, envelopeToError } from './lib/io.mjs';
|
|
34
34
|
import { validateSession, normalizeSession, ValidationError } from './lib/session-schema.mjs';
|
|
35
|
+
import { resolveEventsLedgerRoot } from './lib/platform.mjs';
|
|
35
36
|
|
|
36
37
|
const DEFAULT_FILE = '.orchestrator/metrics/sessions.jsonl';
|
|
37
38
|
|
|
@@ -115,8 +116,21 @@ function detectUnmappableReason(entry) {
|
|
|
115
116
|
return null;
|
|
116
117
|
}
|
|
117
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Default target: sessions.jsonl of the repo's one ledger root (#1514/#1584,
|
|
121
|
+
* #1722) — the main checkout when cwd is a linked worktree. Outside one the
|
|
122
|
+
* relative DEFAULT_FILE is kept verbatim, so paths in output do not change.
|
|
123
|
+
*
|
|
124
|
+
* @returns {string}
|
|
125
|
+
*/
|
|
126
|
+
function defaultFile() {
|
|
127
|
+
const root = resolveEventsLedgerRoot(process.cwd());
|
|
128
|
+
// ledger-root: `root` is resolveEventsLedgerRoot() of the line above
|
|
129
|
+
return root === resolve(process.cwd()) ? DEFAULT_FILE : join(root, DEFAULT_FILE);
|
|
130
|
+
}
|
|
131
|
+
|
|
118
132
|
function parseArgs(argv) {
|
|
119
|
-
const args = { file:
|
|
133
|
+
const args = { file: defaultFile(), mode: 'dry-run' };
|
|
120
134
|
for (let i = 0; i < argv.length; i++) {
|
|
121
135
|
const a = argv[i];
|
|
122
136
|
if (a === '--file') args.file = argv[++i];
|
|
@@ -30,6 +30,10 @@
|
|
|
30
30
|
// `prepare: husky` script never ran because ignore-scripts=true,
|
|
31
31
|
// so commit-msg/pre-commit/pre-push are inactive. Warns locally
|
|
32
32
|
// and recommends `npx husky`; silent under CI.
|
|
33
|
+
// h2 core.hooksPath IS `.husky/_` but that directory is missing in
|
|
34
|
+
// this worktree (#1662) — a linked worktree inherits the value from
|
|
35
|
+
// the shared `.git/config`, yet `.husky/_` is never generated there
|
|
36
|
+
// because `prepare: husky` does not run. Same advisory treatment.
|
|
33
37
|
//
|
|
34
38
|
// SCOPE GUARD:
|
|
35
39
|
// This script only guards the REPOSITORY ROOT package.json. If invoked
|
|
@@ -275,21 +279,34 @@ export function parseCiEnv(value) {
|
|
|
275
279
|
export const HUSKY_INACTIVE_WARNING =
|
|
276
280
|
'⚠ check-package-manager: git core.hooksPath is not set to .husky/_ — Husky hooks (commit-msg, pre-commit, pre-push) are INACTIVE in this clone because .npmrc ignore-scripts=true skips the prepare script (#1458). Run: npx husky';
|
|
277
281
|
|
|
282
|
+
/** One-line stderr WARN for a set-but-missing hooks directory (#1662, linked worktrees). */
|
|
283
|
+
export const HUSKY_MISSING_DIR_WARNING =
|
|
284
|
+
'⚠ check-package-manager: git core.hooksPath is .husky/_ but that directory is MISSING in this worktree — Husky hooks (commit-msg, pre-commit, pre-push) are INACTIVE here; a linked worktree inherits core.hooksPath from the shared .git/config, but ignore-scripts=true skips the prepare script that generates .husky/_ (#1662). Run: npx husky';
|
|
285
|
+
|
|
278
286
|
/**
|
|
279
287
|
* Advisory check (#1458): `.npmrc` `ignore-scripts=true` (SEC-020) means the
|
|
280
288
|
* `prepare: husky` script never runs on `npm ci`, so a fresh clone has no
|
|
281
289
|
* `core.hooksPath` and every Husky hook is silently inactive. This only
|
|
282
290
|
* REPORTS the state — it never runs `npx husky`, never writes git config and
|
|
283
291
|
* never affects the guard's `ok`/exit code. Silent under CI.
|
|
292
|
+
*
|
|
293
|
+
* A matching value alone is not enough (#1662): a linked worktree inherits
|
|
294
|
+
* `core.hooksPath=.husky/_` from the shared `.git/config` while `.husky/_`
|
|
295
|
+
* itself was never generated there. The directory is therefore resolved —
|
|
296
|
+
* a relative hooksPath against `repoRoot` (the current worktree top-level),
|
|
297
|
+
* an absolute one as is — and a missing directory reports `active: false`
|
|
298
|
+
* with `missingDir: true`.
|
|
284
299
|
* @param {string} repoRoot
|
|
285
300
|
* @param {{ env?: NodeJS.ProcessEnv, ci?: boolean }} [opts] — `env` is also
|
|
286
301
|
* passed to the git subprocess (lets tests isolate from global git config).
|
|
287
|
-
* @returns {{ hooksPath: string, active: boolean, skipped: 'ci'|'no-husky-dir'|null }}
|
|
302
|
+
* @returns {{ hooksPath: string, active: boolean, missingDir: boolean, skipped: 'ci'|'no-husky-dir'|null }}
|
|
288
303
|
*/
|
|
289
304
|
export function checkHuskyHooksPath(repoRoot, { env = process.env, ci } = {}) {
|
|
290
|
-
if (ci ?? parseCiEnv(env.CI))
|
|
305
|
+
if (ci ?? parseCiEnv(env.CI)) {
|
|
306
|
+
return { hooksPath: '', active: false, missingDir: false, skipped: 'ci' };
|
|
307
|
+
}
|
|
291
308
|
if (!existsSync(join(repoRoot, '.husky'))) {
|
|
292
|
-
return { hooksPath: '', active: false, skipped: 'no-husky-dir' };
|
|
309
|
+
return { hooksPath: '', active: false, missingDir: false, skipped: 'no-husky-dir' };
|
|
293
310
|
}
|
|
294
311
|
|
|
295
312
|
let hooksPath;
|
|
@@ -305,11 +322,12 @@ export function checkHuskyHooksPath(repoRoot, { env = process.env, ci } = {}) {
|
|
|
305
322
|
}
|
|
306
323
|
|
|
307
324
|
const norm = hooksPath.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
308
|
-
const
|
|
325
|
+
const matches =
|
|
309
326
|
norm !== '' &&
|
|
310
327
|
(isAbsolute(hooksPath) ? norm.endsWith('/.husky/_') : posix.normalize(norm) === '.husky/_');
|
|
328
|
+
const missingDir = matches && !existsSync(resolve(repoRoot, hooksPath));
|
|
311
329
|
|
|
312
|
-
return { hooksPath, active, skipped: null };
|
|
330
|
+
return { hooksPath, active: matches && !missingDir, missingDir, skipped: null };
|
|
313
331
|
}
|
|
314
332
|
|
|
315
333
|
/**
|
|
@@ -317,7 +335,7 @@ export function checkHuskyHooksPath(repoRoot, { env = process.env, ci } = {}) {
|
|
|
317
335
|
* only side effects are read-only fs/git calls. Never throws or exits.
|
|
318
336
|
* `hooks` carries the advisory Husky check (#1458); it never affects `ok`.
|
|
319
337
|
* @param {{ cwd?: string, ci?: boolean, allowMissingNodeModules?: boolean, repoRoot?: string, env?: NodeJS.ProcessEnv }} [opts]
|
|
320
|
-
* @returns {{ ok: boolean, ci: boolean, skipped: boolean, repoRoot: string, findings: Array<{code: string, severity: string, effectiveSeverity: string, message: string}>, hooks?: { hooksPath: string, active: boolean, skipped: 'ci'|'no-husky-dir'|null } }}
|
|
338
|
+
* @returns {{ ok: boolean, ci: boolean, skipped: boolean, repoRoot: string, findings: Array<{code: string, severity: string, effectiveSeverity: string, message: string}>, hooks?: { hooksPath: string, active: boolean, missingDir: boolean, skipped: 'ci'|'no-husky-dir'|null } }}
|
|
321
339
|
*/
|
|
322
340
|
export function runPackageManagerGuard(opts = {}) {
|
|
323
341
|
const cwd = opts.cwd ?? process.cwd();
|
|
@@ -464,9 +482,9 @@ if (isMain) {
|
|
|
464
482
|
|
|
465
483
|
const result = runPackageManagerGuard({ allowMissingNodeModules: opts.allowMissingNodeModules });
|
|
466
484
|
|
|
467
|
-
// Advisory only (#1458) — never changes the exit code.
|
|
485
|
+
// Advisory only (#1458, #1662) — never changes the exit code.
|
|
468
486
|
if (result.hooks && !result.hooks.active && result.hooks.skipped === null) {
|
|
469
|
-
console.error(HUSKY_INACTIVE_WARNING);
|
|
487
|
+
console.error(result.hooks.missingDir ? HUSKY_MISSING_DIR_WARNING : HUSKY_INACTIVE_WARNING);
|
|
470
488
|
}
|
|
471
489
|
|
|
472
490
|
if (opts.json) {
|
|
@@ -50,6 +50,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
50
50
|
|
|
51
51
|
import { checkSessionsIntegrity, SESSIONS_PATH } from './lib/sessions-integrity-banner.mjs';
|
|
52
52
|
import { isMainModule } from './lib/is-main-module.mjs';
|
|
53
|
+
import { resolveEventsLedgerRoot } from './lib/platform.mjs';
|
|
53
54
|
|
|
54
55
|
const SELF_DIR = dirname(fileURLToPath(import.meta.url));
|
|
55
56
|
|
|
@@ -146,7 +147,9 @@ export function parseArgs(argv) {
|
|
|
146
147
|
* @returns {number}
|
|
147
148
|
*/
|
|
148
149
|
function countLedgerRecords(repoRoot, sessionId) {
|
|
149
|
-
|
|
150
|
+
// #1592: the same file checkSessionsIntegrity reads — the main checkout's
|
|
151
|
+
// ledger from a linked worktree, or a record written there reads as missing.
|
|
152
|
+
const filePath = join(resolveEventsLedgerRoot(repoRoot), SESSIONS_PATH);
|
|
150
153
|
if (!existsSync(filePath)) return 0;
|
|
151
154
|
let matched = 0;
|
|
152
155
|
for (const line of readFileSync(filePath, 'utf8').split('\n')) {
|
|
@@ -96,6 +96,9 @@ export function verifyVitestResult(result, opts = {}) {
|
|
|
96
96
|
if (success !== true) {
|
|
97
97
|
return { ok: false, reason: `success is ${JSON.stringify(success)} (expected true)`, summary };
|
|
98
98
|
}
|
|
99
|
+
if (numPassedTests <= 0 || result.numPendingTests >= numTotalTests) {
|
|
100
|
+
return { ok: false, reason: 'no executed passing tests (all tests pending/skipped)', summary };
|
|
101
|
+
}
|
|
99
102
|
// Floor guard: a suspiciously small run signals a truncated/partial/empty
|
|
100
103
|
// execution masquerading as green. The real suite is ~12.1k tests (2026-07-17).
|
|
101
104
|
if (numTotalTests < minTests) {
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Native Windows regression probes (#1612). Fresh SessionStart needs npm-registry
|
|
3
|
+
// access; unavailable network is explicitly unmeasurable, never a measured PASS.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
9
|
+
|
|
10
|
+
if (process.platform !== 'win32') {
|
|
11
|
+
console.log('windows-smoke: nur win32');
|
|
12
|
+
console.log('SUMMARY 0/0');
|
|
13
|
+
} else {
|
|
14
|
+
const repo = fileURLToPath(new URL('../../', import.meta.url));
|
|
15
|
+
const { runCheck } = await import('../lib/gates/gate-helpers.mjs');
|
|
16
|
+
const { spawnInGroup } = await import('../lib/process-group.mjs');
|
|
17
|
+
const { REGISTRY_URL, CACHE_FILENAME, readCachedLatest } = await import('../lib/plugin-update-banner.mjs');
|
|
18
|
+
const { httpRequest } = await import('../lib/http-request.mjs');
|
|
19
|
+
// Hook stdout is untrusted: a crashed hook prints nothing or a stack trace.
|
|
20
|
+
const parseJson = (text) => { try { return JSON.parse(text); } catch { return null; } };
|
|
21
|
+
const results = [];
|
|
22
|
+
const record = (name, ok, detail) => {
|
|
23
|
+
results.push(ok);
|
|
24
|
+
console.log(`${ok ? 'PASS' : 'FAIL'} ${name} :: ${JSON.stringify(detail)}`);
|
|
25
|
+
};
|
|
26
|
+
const probe = async (name, fn) => {
|
|
27
|
+
try { await fn(); } catch (error) { record(name, false, error.message); }
|
|
28
|
+
};
|
|
29
|
+
let work;
|
|
30
|
+
try {
|
|
31
|
+
work = fs.mkdtempSync(path.join(os.tmpdir(), 'so windows smoke '));
|
|
32
|
+
const config = path.join(work, 'private config');
|
|
33
|
+
const env = {
|
|
34
|
+
...process.env,
|
|
35
|
+
SO_CONFIG_HOME: config,
|
|
36
|
+
SO_SESSION_REGISTRY_DIR: path.join(config, 'sessions'),
|
|
37
|
+
SO_HOST_ALIASES_FILE: path.join(config, 'host-aliases.json'),
|
|
38
|
+
SO_LOOP_GUARD_DIR: path.join(work, 'loop guard'),
|
|
39
|
+
SO_VAULT_DIR: path.join(work, 'vault'),
|
|
40
|
+
NAVIGATOR_CONFIG_DIR: path.join(work, 'navigator'),
|
|
41
|
+
SO_EVENTS_LEDGER_SANDBOX: path.join(work, 'events.jsonl'),
|
|
42
|
+
CLANK_EVENT_URL: '', CLANK_EVENT_SECRET: '',
|
|
43
|
+
CLAUDE_CONFIG_DIR: path.join(work, 'claude config'),
|
|
44
|
+
XDG_CONFIG_HOME: path.join(work, 'xdg'),
|
|
45
|
+
TMPDIR: work, TMP: work, TEMP: work,
|
|
46
|
+
SO_TELEMETRY_DISABLED: '1',
|
|
47
|
+
SO_DISABLE_UPDATE_CHECK: '1',
|
|
48
|
+
SO_DISABLE_STARTUP_PROBES: '1',
|
|
49
|
+
SO_HOOK_PROFILE: 'full', SO_DISABLED_HOOKS: '',
|
|
50
|
+
};
|
|
51
|
+
for (const dir of [config, env.SO_SESSION_REGISTRY_DIR, env.SO_LOOP_GUARD_DIR,
|
|
52
|
+
env.SO_VAULT_DIR, env.CLAUDE_CONFIG_DIR, env.XDG_CONFIG_HOME]) {
|
|
53
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
54
|
+
}
|
|
55
|
+
const node = `"${process.execPath}"`;
|
|
56
|
+
await probe('gate capture', async () => {
|
|
57
|
+
const r = await runCheck(`${node} -e "console.log('out-äöü-€'); console.error('err-line')"`, { repoRoot: work, cwd: work, env });
|
|
58
|
+
record('gate capture', r.status === 'pass' && r.fullOutput.includes('out-äöü-€') && r.fullOutput.includes('err-line'), r.fullOutput);
|
|
59
|
+
});
|
|
60
|
+
for (const code of [0, 3]) await probe(`gate exit ${code}`, async () => {
|
|
61
|
+
const r = await runCheck(`${node} -e "process.exit(${code})"`, { repoRoot: work, cwd: work, env });
|
|
62
|
+
record(`gate exit ${code}`, r.exitCode === code && r.status === (code === 0 ? 'pass' : 'fail'), { exit: r.exitCode, status: r.status });
|
|
63
|
+
});
|
|
64
|
+
await probe('gate missing command', async () => {
|
|
65
|
+
const r = await runCheck('so-windows-smoke-definitely-not-a-command', { repoRoot: work, cwd: work, env });
|
|
66
|
+
// `cmd /d /s /c` exits 1 for an unknown command (9009 only inside batch
|
|
67
|
+
// files), so the hint text is absent on Windows; what must hold is that a
|
|
68
|
+
// missing command is a failure, never a skip (#1612, GH#76 point 5).
|
|
69
|
+
record('gate missing command', r.status === 'fail' && r.exitCode !== 0, { status: r.status, exitCode: r.exitCode });
|
|
70
|
+
});
|
|
71
|
+
await probe('gate timeout tree', async () => {
|
|
72
|
+
const pidsDir = path.join(work, 'pids');
|
|
73
|
+
fs.mkdirSync(pidsDir);
|
|
74
|
+
const grandchild = path.join(work, 'grandchild.mjs');
|
|
75
|
+
const child = path.join(work, 'child.mjs');
|
|
76
|
+
fs.writeFileSync(grandchild, `import fs from 'node:fs'; fs.writeFileSync(${JSON.stringify(pidsDir)} + '/gc-' + process.pid, ''); setInterval(() => {}, 1000);`);
|
|
77
|
+
fs.writeFileSync(child, `import fs from 'node:fs'; import { spawn } from 'node:child_process'; fs.writeFileSync(${JSON.stringify(pidsDir)} + '/ch-' + process.pid, ''); for (let i=0;i<2;i++) spawn(process.execPath, [${JSON.stringify(grandchild)}], { stdio: 'ignore' }); setInterval(() => {}, 1000);`);
|
|
78
|
+
const r = await runCheck(`${node} "${child}"`, { repoRoot: work, cwd: work, env, timeoutMs: 4000 });
|
|
79
|
+
const pids = fs.readdirSync(pidsDir).map(f => Number(f.split('-')[1]));
|
|
80
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
81
|
+
const still = pids.filter(pid => {
|
|
82
|
+
const check = spawnSync('tasklist.exe', ['/FI', `PID eq ${pid}`, '/NH', '/FO', 'CSV'], { encoding: 'utf8', timeout: 5000 });
|
|
83
|
+
if (check.error || check.status !== 0) throw new Error(`tasklist failed: ${check.error?.message ?? check.status}`);
|
|
84
|
+
return check.stdout.includes(`"${pid}"`);
|
|
85
|
+
});
|
|
86
|
+
record('gate timeout tree', r.status === 'fail' && r.timedOut && pids.length === 3 && still.length === 0 && r.survivors?.length === 0,
|
|
87
|
+
{ pids, still, timedOut: r.timedOut, survivors: r.survivors, output: r.output });
|
|
88
|
+
for (const pid of still) {
|
|
89
|
+
const killed = spawnSync('taskkill.exe', ['/PID', String(pid), '/T', '/F'], { encoding: 'utf8', timeout: 5000 });
|
|
90
|
+
if (killed.error || killed.status !== 0) throw new Error(`survivor cleanup failed: ${pid}`);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
await probe('argv literal', async () => {
|
|
94
|
+
const dir = path.join(work, "dir with space's");
|
|
95
|
+
fs.mkdirSync(dir);
|
|
96
|
+
const script = path.join(dir, 'argv.mjs');
|
|
97
|
+
fs.writeFileSync(script, 'console.log(JSON.stringify(process.argv.slice(1)));');
|
|
98
|
+
const args = [script, 'a b', "it's", '%PATH%', '&echo x'];
|
|
99
|
+
const r = await spawnInGroup([process.execPath, ...args], { repoRoot: work, cwd: work, env, timeoutMs: 20000 });
|
|
100
|
+
const parsed = JSON.parse(r.fullOutput.trim());
|
|
101
|
+
record('argv literal', r.exitCode === 0 && JSON.stringify(parsed) === JSON.stringify(args), parsed);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const bash = path.join(process.env.ProgramFiles || 'C:\\Program Files', 'Git', 'bin', 'bash.exe');
|
|
105
|
+
const hooks = JSON.parse(fs.readFileSync(path.join(repo, 'hooks/hooks.json'), 'utf8')).hooks;
|
|
106
|
+
const project = path.join(work, 'project with spaces');
|
|
107
|
+
fs.mkdirSync(project);
|
|
108
|
+
fs.writeFileSync(path.join(project, 'transcript.jsonl'), '');
|
|
109
|
+
const runtimes = [];
|
|
110
|
+
const runHook = async (event, matcher, command, cwd = project, extra = {}, overrides = {}) => {
|
|
111
|
+
const tool = (matcher === '*' ? 'Bash' : matcher || 'Bash').split('|')[0];
|
|
112
|
+
const input = tool === 'Bash' ? { command: 'ls -la' }
|
|
113
|
+
: tool === 'Skill' ? { skill: 'session-orchestrator:session' }
|
|
114
|
+
: tool === 'Agent' ? { description: 'smoke', prompt: 'smoke' }
|
|
115
|
+
: tool === 'AskUserQuestion' ? { questions: [] }
|
|
116
|
+
: { file_path: path.join(cwd, 'src/a.mjs'), content: 'export const a = 1;\n', old_string: 'x', new_string: 'y' };
|
|
117
|
+
const payload = { session_id: `windows-smoke-${path.basename(cwd)}`, cwd,
|
|
118
|
+
transcript_path: path.join(cwd, 'transcript.jsonl'), permission_mode: 'default', hook_event_name: event,
|
|
119
|
+
...(event.includes('Tool') ? { tool_name: tool, tool_input: input, tool_response: {} } : {}),
|
|
120
|
+
...(event === 'SessionStart' ? { source: 'startup' } : {}),
|
|
121
|
+
...(event === 'SessionEnd' ? { reason: 'other' } : {}), ...extra };
|
|
122
|
+
// Reuse the production timeout/tree cleanup; only adapt stdin for hook payloads.
|
|
123
|
+
const stdoutChunks = [];
|
|
124
|
+
let stdinError = null;
|
|
125
|
+
const r = await spawnInGroup([bash, '-c', command], { cwd, repoRoot: cwd, timeoutMs: 5000,
|
|
126
|
+
env: { ...env, CLAUDE_PLUGIN_ROOT: repo, CLAUDE_PROJECT_DIR: cwd, ...overrides },
|
|
127
|
+
spawnFn: (executable, args, options) => {
|
|
128
|
+
const child = spawn(executable, args, { ...options, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
129
|
+
child.stdout.on('data', chunk => { stdoutChunks.push(chunk); });
|
|
130
|
+
child.stdin.on('error', error => {
|
|
131
|
+
stdinError = error.message;
|
|
132
|
+
});
|
|
133
|
+
child.stdin.end(JSON.stringify(payload));
|
|
134
|
+
return child;
|
|
135
|
+
} });
|
|
136
|
+
runtimes.push({ event, command, ms: r.durationMs });
|
|
137
|
+
return { ...r, stdout: Buffer.concat(stdoutChunks).toString('utf8'), stdinError };
|
|
138
|
+
};
|
|
139
|
+
for (const [event, groups] of Object.entries(hooks)) for (const group of groups) for (const hook of group.hooks) {
|
|
140
|
+
const name = `${event}/${path.basename(hook.command.split('"').at(-2) || hook.command)}`;
|
|
141
|
+
await probe(name, async () => {
|
|
142
|
+
const r = await runHook(event, group.matcher, hook.command);
|
|
143
|
+
record(name, r.exitCode === 0 && !r.timedOut && !r.stdinError,
|
|
144
|
+
{ exit: r.exitCode, ms: r.durationMs, stdinError: r.stdinError, output: r.output });
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
const guard = hooks.PreToolUse.flatMap(group => group.hooks).find(hook => hook.command.includes('pre-bash-destructive-guard'));
|
|
148
|
+
await probe('destructive guard', async () => {
|
|
149
|
+
// Payload only: never execute this command. Split for the outer shell guard.
|
|
150
|
+
const command = ['git', 'reset', '--hard', 'HEAD~1'].join(' ');
|
|
151
|
+
const r = await runHook('PreToolUse', 'Bash', guard.command, project, { tool_input: { command } });
|
|
152
|
+
const output = parseJson(r.stdout.trim());
|
|
153
|
+
record('destructive guard', r.exitCode === 0 && !r.timedOut && !r.stdinError && output?.hookSpecificOutput?.permissionDecision === 'deny', output ?? r.output);
|
|
154
|
+
});
|
|
155
|
+
const start = hooks.SessionStart.flatMap(group => group.hooks).find(hook => hook.command.includes('on-session-start'));
|
|
156
|
+
// Do not inherit any update kill-switch into the first-start regression path.
|
|
157
|
+
const freshEnv = { SO_DISABLE_UPDATE_CHECK: undefined, DO_NOT_TRACK: undefined,
|
|
158
|
+
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: undefined, SO_DISABLE_STARTUP_PROBES: undefined };
|
|
159
|
+
for (let i = 0; i < 5; i++) await probe(`fresh SessionStart ${i + 1}`, async () => {
|
|
160
|
+
const fresh = fs.mkdtempSync(path.join(work, 'fresh project '));
|
|
161
|
+
// Minimal empty git repository fixture; no git-writing commands are needed.
|
|
162
|
+
for (const dir of ['.git/objects', '.git/refs/heads']) fs.mkdirSync(path.join(fresh, dir), { recursive: true });
|
|
163
|
+
fs.writeFileSync(path.join(fresh, '.git/HEAD'), 'ref: refs/heads/main\n');
|
|
164
|
+
fs.writeFileSync(path.join(fresh, '.git/config'), '[core]\nrepositoryformatversion = 0\nbare = false\n');
|
|
165
|
+
fs.writeFileSync(path.join(fresh, 'transcript.jsonl'), '');
|
|
166
|
+
const r = await runHook('SessionStart', 'startup', start.command, fresh, {}, freshEnv);
|
|
167
|
+
const output = parseJson(r.stdout.trim());
|
|
168
|
+
const ok = r.exitCode === 0 && !r.timedOut && !r.stdinError && typeof output === 'object' && output !== null && !/Assertion failed/.test(r.fullOutput);
|
|
169
|
+
record(`fresh SessionStart ${i + 1}`, ok, { exit: r.exitCode, ms: r.durationMs, output: r.output });
|
|
170
|
+
if (!ok) return;
|
|
171
|
+
const cacheDir = path.join(fresh, '.orchestrator/runtime');
|
|
172
|
+
const cache = readCachedLatest({ cacheDir });
|
|
173
|
+
if (cache) record(`fresh update ${i + 1}`, true, { version: cache.version });
|
|
174
|
+
else {
|
|
175
|
+
// A missing cache alone cannot distinguish offline from a broken hook.
|
|
176
|
+
let reachable = false;
|
|
177
|
+
try {
|
|
178
|
+
// Never fetch here: fetch + exit is the Windows crash this smoke guards (#1612).
|
|
179
|
+
const response = await httpRequest(REGISTRY_URL, { timeoutMs: 2000 });
|
|
180
|
+
reachable = response.ok && typeof (await response.json())?.version === 'string';
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.log(`INFO fresh update ${i + 1} :: ${JSON.stringify(error.message)}`);
|
|
183
|
+
}
|
|
184
|
+
if (reachable || fs.existsSync(path.join(cacheDir, CACHE_FILENAME))) {
|
|
185
|
+
record(`fresh update ${i + 1}`, false, 'registry reachable or cache invalid, but hook wrote no usable cache');
|
|
186
|
+
} else console.log(`SKIP fresh update ${i + 1} :: nicht messbar: npm registry unavailable (network required)`);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
record('Hook-Laufzeit', runtimes.length > 0 && runtimes.every(r => r.ms < 4000),
|
|
190
|
+
{ maxMs: Math.max(...runtimes.map(r => r.ms)), slow: runtimes.filter(r => r.ms >= 4000) });
|
|
191
|
+
} catch (error) {
|
|
192
|
+
record('smoke setup', false, error.message);
|
|
193
|
+
} finally {
|
|
194
|
+
if (work) {
|
|
195
|
+
try { fs.rmSync(work, { recursive: true, force: true, maxRetries: 3, retryDelay: 200 }); }
|
|
196
|
+
catch (error) { record('cleanup', false, error.message); }
|
|
197
|
+
}
|
|
198
|
+
console.log(`SUMMARY ${results.filter(Boolean).length}/${results.length}`);
|
|
199
|
+
process.exitCode = results.every(Boolean) ? 0 : 1;
|
|
200
|
+
}
|
|
201
|
+
}
|