session-orchestrator 5.11.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 +12 -1
- 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 +180 -3
- package/README.md +126 -117
- 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 +4 -2
- 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 +102 -25
- 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 +55 -21
- package/docs/session-config-template.md +32 -24
- 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 +158 -59
- 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 -3
- 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 +73 -3
- 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/remote-hosts.mjs +2 -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 +3 -3
- 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 +21 -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-end/vault-orphans.mjs +205 -0
- package/scripts/lib/session-id.mjs +9 -1
- package/scripts/lib/session-identity/own-session.mjs +63 -0
- package/scripts/lib/session-invocation.mjs +14 -3
- package/scripts/lib/session-lock.mjs +114 -0
- package/scripts/lib/session-record-repair.mjs +245 -35
- package/scripts/lib/session-registry.mjs +85 -12
- 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 +174 -0
- package/scripts/lib/so-hud/runtime.mjs +87 -0
- 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 +248 -0
- package/scripts/lib/state-md/mission-status.mjs +5 -1
- package/scripts/lib/state-md/recommendations.mjs +26 -4
- 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 +22 -9
- package/scripts/lib/vault-mirror/process.mjs +197 -65
- package/scripts/lib/vault-mirror/prune.mjs +5 -2
- package/scripts/lib/vault-mirror/render-learnings.mjs +8 -7
- 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 +106 -13
- package/scripts/lib/vault-staleness-banner.mjs +4 -7
- package/scripts/lib/vault-status/board-writer.mjs +34 -5
- 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 +57 -8
- 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 +68 -0
- package/scripts/state-md-idle-reset.mjs +82 -0
- 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/scripts/vault-orphans.mjs +171 -0
- package/skills/_shared/fleet-protocol.md +166 -3
- 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 +5 -4
- package/skills/architecture/references/domain-model.md +1 -1
- package/skills/autopilot/SKILL.md +3 -2
- 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 +97 -13
- 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-7e-vault-orphans.md +63 -0
- package/skills/session-end/references/phase-3-documentation-updates.md +25 -13
- package/skills/session-end/references/phase-5-issue-cleanup.md +1 -1
- package/skills/session-end/references/session-summary-template.md +8 -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 +10 -4
- package/skills/session-start/phase-4-5-resource-health.md +11 -5
- package/skills/session-start/phase-7-5-mode-selector.md +3 -1
- 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 +13 -5
- 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/AGENTS.md +0 -207
- package/assets/og-card.png +0 -0
- package/assets/og-card.svg +0 -47
- package/assets/wave-lifecycle.svg +0 -98
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
* Then reads the navigator lease and emits `orchestrator.fleet.checkin` with the
|
|
16
16
|
* payload `{ session, repo, modus, kandidaten, navigator_state }` — no paths, no
|
|
17
17
|
* quota, no `auftrag_ref`.
|
|
18
|
+
* Without fleet integration, valid input exits 0 with `skipped: "no-fleet"` and no directory, file or event.
|
|
19
|
+
* That holds for `plattform: "kopflos"` too: the record cannot prove a coordinator started the run,
|
|
20
|
+
* and a write would create the navigator directory, which then flips every later solo session to
|
|
21
|
+
* fleet. A headless run without fleet integration applies the Standard-Auflagen instead (#1559 review).
|
|
18
22
|
*
|
|
19
23
|
* stdout: one JSON line
|
|
20
24
|
* `{ ok, path, navigator_state, navigator_adresse, fallback_due_at, event }`.
|
|
@@ -29,7 +33,7 @@
|
|
|
29
33
|
* Exit codes:
|
|
30
34
|
* 0 — check-in written (an event failure only WARNs; `event: false` on stdout)
|
|
31
35
|
* 1 — write error
|
|
32
|
-
* 2 — invalid input
|
|
36
|
+
* 2 — invalid input or semantic session ID occupied by another repo (no write/event)
|
|
33
37
|
*/
|
|
34
38
|
|
|
35
39
|
import { randomBytes } from 'node:crypto';
|
|
@@ -39,6 +43,7 @@ import path from 'node:path';
|
|
|
39
43
|
import { emitEvent } from './lib/events.mjs';
|
|
40
44
|
import {
|
|
41
45
|
checkinPath,
|
|
46
|
+
fleetIntegrationEnabled,
|
|
42
47
|
fallbackDueAt,
|
|
43
48
|
navigatorDir,
|
|
44
49
|
readNavigatorLease,
|
|
@@ -46,6 +51,7 @@ import {
|
|
|
46
51
|
validateCheckin,
|
|
47
52
|
} from './lib/fleet-protocol.mjs';
|
|
48
53
|
import { isMainModule } from './lib/is-main-module.mjs';
|
|
54
|
+
import { openNoFollow } from './lib/no-follow-open.mjs';
|
|
49
55
|
|
|
50
56
|
const PREFIX = 'fleet-checkin:';
|
|
51
57
|
|
|
@@ -63,8 +69,14 @@ async function readStdin() {
|
|
|
63
69
|
* planted symlink cannot redirect the chmod or the write.
|
|
64
70
|
* @param {string} target
|
|
65
71
|
* @param {object} record
|
|
72
|
+
* @param {object} [options]
|
|
73
|
+
* @param {typeof openNoFollow} [options.open] opener for the ownership read.
|
|
74
|
+
* Injectable only so tests can reach the fail-closed `ERR_FILE_CHANGED`
|
|
75
|
+
* branch, which `openNoFollow` never takes on POSIX; production passes nothing.
|
|
76
|
+
* @returns {Promise<boolean>} false when another repo already owns the target,
|
|
77
|
+
* or the target changed during the ownership check
|
|
66
78
|
*/
|
|
67
|
-
async function writeCheckinFile(target, record) {
|
|
79
|
+
export async function writeCheckinFile(target, record, { open = openNoFollow } = {}) {
|
|
68
80
|
const checkinDir = path.dirname(target);
|
|
69
81
|
await fs.mkdir(navigatorDir(), { recursive: true, mode: 0o700 });
|
|
70
82
|
await fs.mkdir(checkinDir, { recursive: true, mode: 0o700 });
|
|
@@ -79,7 +91,52 @@ async function writeCheckinFile(target, record) {
|
|
|
79
91
|
await fs.writeFile(tmp, JSON.stringify(record, null, 2) + '\n', { mode: 0o600, flag: 'wx' });
|
|
80
92
|
// The creation mode is filtered through the umask; pin it explicitly.
|
|
81
93
|
await fs.chmod(tmp, 0o600);
|
|
94
|
+
let existing;
|
|
95
|
+
try {
|
|
96
|
+
// Never follow a symlink at the target and never block on a FIFO; the
|
|
97
|
+
// helper also covers platforms without O_NOFOLLOW (win32, #1744).
|
|
98
|
+
const handle = await open(target);
|
|
99
|
+
try {
|
|
100
|
+
const stat = await handle.stat();
|
|
101
|
+
const limit = 64 * 1024;
|
|
102
|
+
// Check-ins are small: non-files and larger targets are unreadable for
|
|
103
|
+
// this guard and fail-open. Bound reads even if the file grows after stat.
|
|
104
|
+
if (stat.isFile() && stat.size <= limit) {
|
|
105
|
+
const buffer = Buffer.alloc(limit);
|
|
106
|
+
let length = 0;
|
|
107
|
+
while (length < limit) {
|
|
108
|
+
const { bytesRead } = await handle.read(buffer, length, limit - length, length);
|
|
109
|
+
if (bytesRead === 0) break;
|
|
110
|
+
length += bytesRead;
|
|
111
|
+
}
|
|
112
|
+
if ((await handle.stat()).size <= limit) {
|
|
113
|
+
existing = JSON.parse(buffer.subarray(0, length).toString('utf8'));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} finally {
|
|
117
|
+
await handle.close();
|
|
118
|
+
}
|
|
119
|
+
} catch (err) {
|
|
120
|
+
// A target swapped during the check may hide a foreign check-in: fail closed.
|
|
121
|
+
if (err?.code === 'ERR_FILE_CHANGED') {
|
|
122
|
+
await fs.rm(tmp, { force: true });
|
|
123
|
+
process.stderr.write(`${PREFIX} check-in target ${path.basename(target)} changed during the safety check; refusing to overwrite\n`);
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
// Fail-open otherwise: a missing (first write), symlinked, non-regular,
|
|
127
|
+
// unreadable or invalid-JSON target gives the navigator no readable
|
|
128
|
+
// check-in to preserve. `rename` replaces a symlink itself, never follows it.
|
|
129
|
+
}
|
|
130
|
+
if (typeof existing?.repo === 'string' && existing.repo !== record.repo) {
|
|
131
|
+
await fs.rm(tmp, { force: true });
|
|
132
|
+
process.stderr.write(`${PREFIX} semantic session ID ${record.session} is already occupied by another repo: ${JSON.stringify(existing.repo)}\n`);
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
// BV-004 ceiling: protects an already-visible foreign check-in, not concurrent
|
|
136
|
+
// writers between read and rename; revisit with atomic coordination when a
|
|
137
|
+
// simultaneous cross-repo collision is observed.
|
|
82
138
|
await fs.rename(tmp, target);
|
|
139
|
+
return true;
|
|
83
140
|
} catch (err) {
|
|
84
141
|
await fs.rm(tmp, { force: true }).catch(() => {});
|
|
85
142
|
throw err;
|
|
@@ -107,9 +164,22 @@ async function main() {
|
|
|
107
164
|
return 2;
|
|
108
165
|
}
|
|
109
166
|
|
|
167
|
+
if (!fleetIntegrationEnabled()) {
|
|
168
|
+
process.stdout.write(JSON.stringify({
|
|
169
|
+
ok: true,
|
|
170
|
+
skipped: 'no-fleet',
|
|
171
|
+
path: null,
|
|
172
|
+
navigator_state: 'none',
|
|
173
|
+
navigator_adresse: null,
|
|
174
|
+
fallback_due_at: null,
|
|
175
|
+
event: false,
|
|
176
|
+
}) + '\n');
|
|
177
|
+
return 0;
|
|
178
|
+
}
|
|
179
|
+
|
|
110
180
|
const target = checkinPath(record.session);
|
|
111
181
|
try {
|
|
112
|
-
await writeCheckinFile(target, record);
|
|
182
|
+
if (!await writeCheckinFile(target, record)) return 2;
|
|
113
183
|
} catch (err) {
|
|
114
184
|
process.stderr.write(`${PREFIX} write failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
115
185
|
return 1;
|
|
@@ -27,6 +27,7 @@ import path from 'node:path';
|
|
|
27
27
|
|
|
28
28
|
import { validateWorkspacePath } from './lib/worktree/lifecycle.mjs';
|
|
29
29
|
import { isMainModule } from './lib/is-main-module.mjs';
|
|
30
|
+
import { resolveEventsLedgerRoot } from './lib/platform.mjs';
|
|
30
31
|
|
|
31
32
|
// ---------------------------------------------------------------------------
|
|
32
33
|
// Constants
|
|
@@ -79,7 +80,11 @@ export function isPidAlive(pid) {
|
|
|
79
80
|
*/
|
|
80
81
|
function loadRecentSessionRefs(repoRoot) {
|
|
81
82
|
const refs = new Set();
|
|
82
|
-
|
|
83
|
+
// #1592: the repo's ONE ledger (main checkout from a linked worktree, which
|
|
84
|
+
// has none since #1584) — reading the worktree's would drop every "kept"
|
|
85
|
+
// reference and let GC remove a worktree a recent session still names.
|
|
86
|
+
// The worktree-meta dir stays per checkout: it is tmp state, not the ledger.
|
|
87
|
+
const sessionsPath = path.join(resolveEventsLedgerRoot(repoRoot), SESSIONS_JSONL);
|
|
83
88
|
|
|
84
89
|
let raw;
|
|
85
90
|
try {
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* github-export.mjs — build the curated public GitHub snapshot (`mirror: curated`, #1631).
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node scripts/github-export.mjs [--source <rev>] [--dry-run]
|
|
7
|
+
* node scripts/github-export.mjs --verify <export sha> --source <source sha>
|
|
8
|
+
* node scripts/github-export.mjs --plain-guard --mirror-key <github|unset>
|
|
9
|
+
*
|
|
10
|
+
* The parent is ALWAYS the public GitHub main: `refs/remotes/github/main`, required to
|
|
11
|
+
* equal `git ls-remote github refs/heads/main` (one read-only network call). There is
|
|
12
|
+
* deliberately no parent override, and refs/heads/github-export is only the output slot.
|
|
13
|
+
*
|
|
14
|
+
* Writes ONE snapshot commit to refs/heads/github-export (see scripts/lib/github-export.mjs);
|
|
15
|
+
* session-end then pushes it to github main (skills/session-end/SKILL.md § 4.4).
|
|
16
|
+
*
|
|
17
|
+
* Exit codes:
|
|
18
|
+
* 0 snapshot created (stdout: new sha) — or, with --dry-run, stats printed
|
|
19
|
+
* 3 nothing to publish: the curated tree equals github main's (stdout: that sha;
|
|
20
|
+
* refs/heads/github-export is not touched)
|
|
21
|
+
* 1 refused, one-line reason on stderr (replace refs or grafts present, github main
|
|
22
|
+
* not verifiable or tracking ref stale, source not descended from the published
|
|
23
|
+
* snapshot's source, excluded path survived, leakage scan failed, a public file
|
|
24
|
+
* still cites an excluded path, push URL differs from fetch URL, compare-and-swap
|
|
25
|
+
* lost) — or a usage error
|
|
26
|
+
*
|
|
27
|
+
* --verify re-checks an already built snapshot BY SHA right before session-end pushes
|
|
28
|
+
* that sha: graph integrity, push URL == fetch URL, github main verified via ls-remote,
|
|
29
|
+
* exactly one commit on it with `Source-Commit: <source>`, no excluded path in its tree.
|
|
30
|
+
* Exit 0 ok, 1 refused. stdout stays empty in every mode except the sha lines above.
|
|
31
|
+
*
|
|
32
|
+
* --plain-guard answers whether a PLAIN push of source history (`git push github HEAD`)
|
|
33
|
+
* is allowed: refused (exit 1, reason on stderr) when HEAD or origin's main carries
|
|
34
|
+
* .orchestrator/policy/public-export.json, i.e. the repo has adopted the curated export
|
|
35
|
+
* even if this checkout's Session Config predates `mirror: curated` (#1645), and — without
|
|
36
|
+
* an `origin` remote to consult — unless `--mirror-key github` (explicit `mirror: github`).
|
|
37
|
+
* Exit 0 allowed.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import { spawnSync } from 'node:child_process';
|
|
41
|
+
|
|
42
|
+
import { buildGithubExport, createGitRunner, plainPushGuard, verifyExportCommit } from './lib/github-export.mjs';
|
|
43
|
+
|
|
44
|
+
function usage(msg) {
|
|
45
|
+
if (msg) console.error(`github-export: ${msg}`);
|
|
46
|
+
console.error('Usage: node scripts/github-export.mjs [--source <rev>] [--dry-run] | --verify <sha> --source <sha> | --plain-guard --mirror-key <github|unset>');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const opts = { source: 'HEAD', dryRun: false };
|
|
51
|
+
let verifySha = null;
|
|
52
|
+
let plainGuard = false;
|
|
53
|
+
let mirrorKey = null;
|
|
54
|
+
const args = process.argv.slice(2);
|
|
55
|
+
for (let i = 0; i < args.length; i++) {
|
|
56
|
+
const a = args[i];
|
|
57
|
+
if (a === '--dry-run') opts.dryRun = true;
|
|
58
|
+
else if (a === '--plain-guard') plainGuard = true;
|
|
59
|
+
else if (a === '--mirror-key') {
|
|
60
|
+
mirrorKey = args[++i];
|
|
61
|
+
if (mirrorKey !== 'github' && mirrorKey !== 'unset') usage('--mirror-key must be github or unset');
|
|
62
|
+
}
|
|
63
|
+
else if (a === '--source' || a === '--verify') {
|
|
64
|
+
const v = args[++i];
|
|
65
|
+
if (!v || v.startsWith('--')) usage(`${a} requires a value`);
|
|
66
|
+
if (a === '--source') opts.source = v;
|
|
67
|
+
else verifySha = v;
|
|
68
|
+
} else if (a === '--help' || a === '-h') usage();
|
|
69
|
+
else usage(`unknown argument '${a}'`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const top = spawnSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' });
|
|
73
|
+
if (top.status !== 0) {
|
|
74
|
+
console.error('github-export: refused — not inside a git work tree');
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const git = createGitRunner(top.stdout.trim());
|
|
79
|
+
|
|
80
|
+
if (plainGuard) {
|
|
81
|
+
if (args.length !== 3 || mirrorKey === null) usage('--plain-guard takes exactly --mirror-key <github|unset>');
|
|
82
|
+
const verdict = plainPushGuard({ git, explicitGithub: mirrorKey === 'github' });
|
|
83
|
+
if (!verdict.ok) {
|
|
84
|
+
console.error(`github-export: plain push refused — ${verdict.reason}`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (verifySha !== null) {
|
|
91
|
+
if (opts.dryRun) usage('--verify does not combine with --dry-run');
|
|
92
|
+
const verdict = verifyExportCommit({ git, sha: verifySha, source: opts.source });
|
|
93
|
+
if (!verdict.ok) {
|
|
94
|
+
console.error(`github-export: verify refused — ${verdict.reason}`);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
process.exit(0);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const result = buildGithubExport({ git, ...opts });
|
|
101
|
+
|
|
102
|
+
if (result.stats) {
|
|
103
|
+
const s = result.stats;
|
|
104
|
+
console.error(
|
|
105
|
+
`github-export: source files ${s.sourceFiles}, excluded ${s.excludedFiles}, snapshot files ${s.snapshotFiles}, changed vs parent ${s.changedVsParent}`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
for (const line of result.findings ?? []) console.error(line);
|
|
109
|
+
for (const line of result.warnings ?? []) console.error(`github-export: WARN — ${line} (a real export would refuse)`);
|
|
110
|
+
|
|
111
|
+
switch (result.status) {
|
|
112
|
+
case 'created':
|
|
113
|
+
console.log(result.sha);
|
|
114
|
+
process.exit(0);
|
|
115
|
+
break;
|
|
116
|
+
case 'unchanged':
|
|
117
|
+
console.log(result.sha);
|
|
118
|
+
console.error('github-export: nothing to publish — snapshot tree equals github main, no new commit');
|
|
119
|
+
process.exit(3);
|
|
120
|
+
break;
|
|
121
|
+
case 'dry-run':
|
|
122
|
+
console.error(`github-export: dry-run — ${result.stats?.unchanged ? 'tree unchanged' : 'snapshot built and scanned'}, refs/heads/github-export not updated`);
|
|
123
|
+
process.exit(0);
|
|
124
|
+
break;
|
|
125
|
+
default:
|
|
126
|
+
console.error(`github-export: refused — ${result.reason}`);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
* the DEPLOYING path is the SAME as the REVIEWING path. This repo's GitHub
|
|
10
10
|
* mirror breaks that: `origin` (GitLab) is reviewed via MR, `github` is
|
|
11
11
|
* pushed directly by `skills/session-end/SKILL.md`'s mirror-push block
|
|
12
|
-
* (`git push github HEAD`,
|
|
12
|
+
* (`git push github HEAD`, or with `mirror: curated` (#1631) one verified
|
|
13
|
+
* snapshot `git push github <sha>:refs/heads/main`, published only through
|
|
14
|
+
* /close; no MR, no review either way) — and since a Vercel Git
|
|
13
15
|
* integration deploys on every push to that mirror's `main`, a compromised
|
|
14
16
|
* mirror token or an admin bypass IS a deploy with zero review. See
|
|
15
17
|
* `docs/github-mirror-protection.md` for the full runbook.
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* handover-queue-check.mjs — every open point of a fleet handover carries a
|
|
4
|
+
* queue mark before the session reports "Bereit zum Schließen." (#1639).
|
|
5
|
+
*
|
|
6
|
+
* Callers: `skills/_shared/fleet-protocol.md` § Übergabe und Zug-Manifest
|
|
7
|
+
* Schritt 1a (the waiting turn that writes the file) and session-end § 4.5
|
|
8
|
+
* (re-check at `/close`). Parsing lives in `lib/handover-queue-marks.mjs`.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* node scripts/handover-queue-check.mjs (--file <path> | --fleet-dir <dir> --since <iso>)
|
|
12
|
+
* [--name <name>] [--apply --repo <repo> [--navigator-cli <path>]] [--json]
|
|
13
|
+
*
|
|
14
|
+
* --file the handover or train manifest to check
|
|
15
|
+
* --fleet-dir/--since newest of uebergabe-<name>.md, uebergabe-<name>-*.md,
|
|
16
|
+
* <name>-zug-*.md in <dir> modified at or after <iso> or
|
|
17
|
+
* epoch ms (the session's start, e.g. `startedAt` of its
|
|
18
|
+
* ~/.claude/sessions entry): session names repeat across days, so an
|
|
19
|
+
* older file of the same name is a predecessor's (#1639 F1)
|
|
20
|
+
* --name the session name; queue titles read `<name>: <point>`
|
|
21
|
+
* (required with --fleet-dir and with --apply)
|
|
22
|
+
* --apply file each unmarked point via `owner-queue add --json`
|
|
23
|
+
* and write `queue:#<id>` into the file; points under an
|
|
24
|
+
* owner/decision heading are reported, never filed. Only
|
|
25
|
+
* for an own file (the --fleet-dir name pattern for
|
|
26
|
+
* --name); a foreign file is refused untouched (#1647).
|
|
27
|
+
* Check mode without --apply reads any file.
|
|
28
|
+
* --repo owner-queue repo name (required with --apply)
|
|
29
|
+
* --navigator-cli coordinator CLI: a `.js` path runs under node, anything
|
|
30
|
+
* else as a binary; default `navigator` from PATH
|
|
31
|
+
*
|
|
32
|
+
* Exit: 0 every open point marked (or no fleet integration: nothing to do) ·
|
|
33
|
+
* 3 unmarked points remain (listed on stdout) · 2 usage error ·
|
|
34
|
+
* 4 --apply on a file that is not the session's own — foreign name, a
|
|
35
|
+
* symlink, or older than --since (nothing filed or written).
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { spawnSync } from 'node:child_process';
|
|
39
|
+
import { lstatSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
40
|
+
import { basename, join } from 'node:path';
|
|
41
|
+
import { fleetIntegrationEnabled } from './lib/fleet-protocol.mjs';
|
|
42
|
+
import { findOpenPoints, insertMarks, kindFor, refFor, unmarkedPoints } from './lib/handover-queue-marks.mjs';
|
|
43
|
+
|
|
44
|
+
const EXIT_OK = 0;
|
|
45
|
+
const EXIT_USAGE = 2;
|
|
46
|
+
const EXIT_UNMARKED = 3;
|
|
47
|
+
const EXIT_FOREIGN = 4;
|
|
48
|
+
|
|
49
|
+
function parseArgs(argv) {
|
|
50
|
+
const out = { apply: false, json: false };
|
|
51
|
+
for (let i = 0; i < argv.length; i++) {
|
|
52
|
+
const a = argv[i];
|
|
53
|
+
if (a === '--apply') out.apply = true;
|
|
54
|
+
else if (a === '--json') out.json = true;
|
|
55
|
+
else if (['--file', '--name', '--fleet-dir', '--since', '--repo', '--navigator-cli'].includes(a)) {
|
|
56
|
+
const v = argv[++i];
|
|
57
|
+
if (v === undefined || v.startsWith('--')) throw new Error(`${a} needs a value`);
|
|
58
|
+
out[a.slice(2).replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = v;
|
|
59
|
+
} else throw new Error(`unknown argument: ${a}`);
|
|
60
|
+
}
|
|
61
|
+
if (!out.file && !(out.name && out.fleetDir && out.since)) {
|
|
62
|
+
throw new Error('need --file, or --fleet-dir with --name and --since');
|
|
63
|
+
}
|
|
64
|
+
if (out.since !== undefined) {
|
|
65
|
+
// ISO date, or epoch ms as the session registry's `startedAt` carries it.
|
|
66
|
+
out.sinceMs = /^\d+$/.test(out.since) ? Number(out.since) : Date.parse(out.since);
|
|
67
|
+
if (Number.isNaN(out.sinceMs)) throw new Error(`--since is not a date: ${out.since}`);
|
|
68
|
+
}
|
|
69
|
+
if (out.apply && !out.repo) throw new Error('--apply needs --repo');
|
|
70
|
+
if (out.apply && !out.name) throw new Error('--apply needs --name');
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* True when basename `f` is a handover or train file of session `name`:
|
|
76
|
+
* `uebergabe-<name>.md`, `uebergabe-<name>-*.md` or `<name>-zug-*.md`. The
|
|
77
|
+
* hyphen after `<name>` keeps `uebergabe-foo2-*.md` out of `foo`'s files.
|
|
78
|
+
*/
|
|
79
|
+
function isOwnHandover(f, name) {
|
|
80
|
+
return (
|
|
81
|
+
f.endsWith('.md') &&
|
|
82
|
+
(f === `uebergabe-${name}.md` || f.startsWith(`uebergabe-${name}-`) || f.startsWith(`${name}-zug-`))
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Newest handover/train file of `name` in `dir` modified at/after `sinceMs`, or null. */
|
|
87
|
+
function newestHandover(dir, name, sinceMs) {
|
|
88
|
+
let names;
|
|
89
|
+
try {
|
|
90
|
+
names = readdirSync(dir);
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const own = names.filter((f) => isOwnHandover(f, name));
|
|
95
|
+
const withTime = own.map((f) => ({ f, t: statSync(join(dir, f)).mtimeMs })).filter((x) => x.t >= sinceMs);
|
|
96
|
+
withTime.sort((a, b) => b.t - a.t);
|
|
97
|
+
return withTime.length ? join(dir, withTime[0].f) : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** `owner-queue add --json` → queue id (exit 0: new, exit 4: existing), or an error string. */
|
|
101
|
+
function fileQueueItem(cli, { repo, kind, title, ref }) {
|
|
102
|
+
const args = ['--json', 'owner-queue', 'add', '--repo', repo, '--kind', kind, '--waits-on', 'lead', '--title', title];
|
|
103
|
+
if (ref) args.push('--ref', ref);
|
|
104
|
+
const [cmd, argv] = cli.endsWith('.js') ? [process.execPath, [cli, ...args]] : [cli, args];
|
|
105
|
+
const r = spawnSync(cmd, argv, { encoding: 'utf8', timeout: 30_000 });
|
|
106
|
+
if (r.error) return { error: r.error.code ?? r.error.message };
|
|
107
|
+
let parsed;
|
|
108
|
+
try {
|
|
109
|
+
parsed = JSON.parse((r.stdout || '').trim().split('\n').pop() || 'null');
|
|
110
|
+
} catch {
|
|
111
|
+
parsed = null;
|
|
112
|
+
}
|
|
113
|
+
if (r.status === 0 && Number.isInteger(parsed?.id)) return { id: parsed.id };
|
|
114
|
+
if (r.status === 4 && Number.isInteger(parsed?.ids?.[0])) return { id: parsed.ids[0] };
|
|
115
|
+
return { error: `owner-queue add exit ${r.status}` };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function main() {
|
|
119
|
+
let opts;
|
|
120
|
+
try {
|
|
121
|
+
opts = parseArgs(process.argv.slice(2));
|
|
122
|
+
} catch (err) {
|
|
123
|
+
process.stderr.write(`handover-queue-check: ${err.message}\n`);
|
|
124
|
+
return EXIT_USAGE;
|
|
125
|
+
}
|
|
126
|
+
const say = (obj, text) => process.stdout.write(opts.json ? `${JSON.stringify(obj)}\n` : `${text}\n`);
|
|
127
|
+
|
|
128
|
+
if (!fleetIntegrationEnabled()) {
|
|
129
|
+
say({ fleet: false, unmarked: 0 }, 'handover-queue-check: keine Flottenintegration — nichts zu prüfen');
|
|
130
|
+
return EXIT_OK;
|
|
131
|
+
}
|
|
132
|
+
const file = opts.file ?? newestHandover(opts.fleetDir, opts.name, opts.sinceMs);
|
|
133
|
+
if (!file) {
|
|
134
|
+
say({ fleet: true, file: null, unmarked: 0 }, 'handover-queue-check: keine Übergabe-Datei gefunden');
|
|
135
|
+
return EXIT_OK;
|
|
136
|
+
}
|
|
137
|
+
// --apply writes into the file and files queue items titled with --name:
|
|
138
|
+
// only the session's own file qualifies (#1647: a fleet-wide loop over
|
|
139
|
+
// --file marked 17 foreign handovers).
|
|
140
|
+
// The name alone is not enough: a symlink named like an own file writes
|
|
141
|
+
// through to its target, and session names repeat across days, so with
|
|
142
|
+
// --since an older file of the same name is a predecessor's (#1639 F1).
|
|
143
|
+
if (opts.apply) {
|
|
144
|
+
let st = null;
|
|
145
|
+
try {
|
|
146
|
+
st = lstatSync(file);
|
|
147
|
+
} catch {
|
|
148
|
+
// unreadable: the read below reports it
|
|
149
|
+
}
|
|
150
|
+
const reason = !isOwnHandover(basename(file), opts.name)
|
|
151
|
+
? 'fremde Datei'
|
|
152
|
+
: st?.isSymbolicLink()
|
|
153
|
+
? 'Symlink'
|
|
154
|
+
: st && opts.sinceMs !== undefined && st.mtimeMs < opts.sinceMs
|
|
155
|
+
? 'Datei älter als --since (Vorgänger gleichen Namens)'
|
|
156
|
+
: null;
|
|
157
|
+
if (reason) {
|
|
158
|
+
process.stderr.write(`handover-queue-check: ${reason}: ${basename(file)} — --apply nur für die eigene Übergabe von ${opts.name}\n`);
|
|
159
|
+
return EXIT_FOREIGN;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
let content;
|
|
163
|
+
try {
|
|
164
|
+
content = readFileSync(file, 'utf8');
|
|
165
|
+
} catch (err) {
|
|
166
|
+
process.stderr.write(`handover-queue-check: ${file} unlesbar (${err.code ?? err.message})\n`);
|
|
167
|
+
return EXIT_USAGE;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let open = unmarkedPoints(findOpenPoints(content));
|
|
171
|
+
const filed = [];
|
|
172
|
+
const failed = [];
|
|
173
|
+
if (opts.apply && open.length > 0) {
|
|
174
|
+
const cli = opts.navigatorCli ?? 'navigator';
|
|
175
|
+
const ids = new Map();
|
|
176
|
+
for (const p of open) {
|
|
177
|
+
const kind = kindFor(p);
|
|
178
|
+
if (kind === null) {
|
|
179
|
+
failed.push({ line: p.line + 1, error: 'Owner-Entscheid: mit --options/--class selbst anlegen' });
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
// The CLI shortens titles over 200 code units itself (cleanly, with a
|
|
183
|
+
// WARN); slicing here could split a surrogate pair.
|
|
184
|
+
const title = `${opts.name}: ${p.text}`;
|
|
185
|
+
const r = fileQueueItem(cli, { repo: opts.repo, kind, title, ref: refFor(p, opts.repo) });
|
|
186
|
+
if (r.id !== undefined) {
|
|
187
|
+
ids.set(p.line, r.id);
|
|
188
|
+
filed.push({ line: p.line + 1, id: r.id });
|
|
189
|
+
} else {
|
|
190
|
+
failed.push({ line: p.line + 1, error: r.error });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (ids.size > 0) {
|
|
194
|
+
content = insertMarks(content, ids);
|
|
195
|
+
writeFileSync(file, content);
|
|
196
|
+
}
|
|
197
|
+
open = open.filter((p) => !ids.has(p.line));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const unmarked = open.map((p) => ({ line: p.line + 1, section: p.section, text: p.text }));
|
|
201
|
+
const lines = [`handover-queue-check: ${basename(file)} — ${unmarked.length} Punkt(e) ohne Queue-ID`];
|
|
202
|
+
for (const f of filed) lines.push(` angelegt: Zeile ${f.line} → queue:#${f.id}`);
|
|
203
|
+
for (const u of unmarked) lines.push(` offen: Zeile ${u.line} [${u.section}] ${u.text}`);
|
|
204
|
+
for (const f of failed) lines.push(` Anlegen gescheitert: Zeile ${f.line} (${f.error})`);
|
|
205
|
+
say({ fleet: true, file, unmarked: unmarked.length, filed, failed, points: unmarked }, lines.join('\n'));
|
|
206
|
+
return unmarked.length > 0 ? EXIT_UNMARKED : EXIT_OK;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
process.exitCode = main();
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { existsSync, readFileSync, mkdirSync, appendFileSync } from 'node:fs';
|
|
18
|
-
import { join } from 'node:path';
|
|
18
|
+
import { dirname, join } from 'node:path';
|
|
19
19
|
import { execFileSync } from 'node:child_process';
|
|
20
20
|
|
|
21
21
|
import {
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
runCategory8,
|
|
30
30
|
runCategory9,
|
|
31
31
|
} from './lib/harness-audit/categories.mjs';
|
|
32
|
+
import { resolveLedgerPath } from './lib/ledger-path.mjs';
|
|
32
33
|
|
|
33
34
|
const RUBRIC_VERSION = '2026-06';
|
|
34
35
|
|
|
@@ -193,11 +194,12 @@ function writeSummary(categories, summary) {
|
|
|
193
194
|
// ---------------------------------------------------------------------------
|
|
194
195
|
|
|
195
196
|
function appendAuditRecord(root, record) {
|
|
196
|
-
|
|
197
|
+
// #1722: main checkout's copy when root is a linked worktree that gitignores it.
|
|
198
|
+
const ledger = resolveLedgerPath('.orchestrator/metrics/audit.jsonl', root);
|
|
197
199
|
try {
|
|
198
|
-
mkdirSync(
|
|
200
|
+
mkdirSync(dirname(ledger), { recursive: true });
|
|
199
201
|
const line = JSON.stringify(record) + '\n';
|
|
200
|
-
appendFileSync(
|
|
202
|
+
appendFileSync(ledger, line, 'utf8');
|
|
201
203
|
} catch (err) {
|
|
202
204
|
process.stderr.write(`harness-audit: Failed to append audit.jsonl: ${err.message}\n`);
|
|
203
205
|
}
|
|
@@ -31,6 +31,35 @@ const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
|
31
31
|
export const ALLOWED_MODEL_ALIASES = new Set(['inherit', 'sonnet', 'opus', 'haiku', 'fable']);
|
|
32
32
|
export const MODEL_ID_RE = /^claude-(opus|sonnet|haiku|fable)-\d+(-\d+)?(-\d{8})?$/;
|
|
33
33
|
|
|
34
|
+
// Reasoning effort per https://code.claude.com/docs/en/sub-agents (#1614).
|
|
35
|
+
// REQUIRED whenever `model` is not `inherit`: the per-model default differs
|
|
36
|
+
// (Sonnet 5.5 defaults to `high`, Opus 5.5 to `medium`), so a pinned model
|
|
37
|
+
// without an explicit effort silently thinks longer than the role needs.
|
|
38
|
+
// With `model: inherit` the agent follows the session, effort included.
|
|
39
|
+
export const ALLOWED_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The effort rule shared by this validator and check-agents.mjs (#1614).
|
|
43
|
+
*
|
|
44
|
+
* @param {unknown} model - the frontmatter `model` value
|
|
45
|
+
* @param {unknown} effort - the frontmatter `effort` value (undefined when absent)
|
|
46
|
+
* @returns {{ rule: 'required'|'enum', message: string } | null} null when valid
|
|
47
|
+
*/
|
|
48
|
+
export function checkAgentEffort(model, effort) {
|
|
49
|
+
const effortList = [...ALLOWED_EFFORTS].join('|');
|
|
50
|
+
if (effort === undefined || effort === null || effort === '') {
|
|
51
|
+
if (model === undefined || model === '' || model === 'inherit') return null;
|
|
52
|
+
return {
|
|
53
|
+
rule: 'required',
|
|
54
|
+
message: `effort is required when model is not inherit (got model ${JSON.stringify(model)}); set effort: ${effortList}`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (!ALLOWED_EFFORTS.has(/** @type {string} */ (effort))) {
|
|
58
|
+
return { rule: 'enum', message: `effort must be one of ${effortList} (got ${JSON.stringify(effort)})` };
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
34
63
|
// Canonical Anthropic palette (red|blue|green|yellow|purple|orange|pink|cyan)
|
|
35
64
|
// plus magenta from plugin-dev SKILL.md for backward-compat.
|
|
36
65
|
const ALLOWED_COLORS = new Set(['blue', 'cyan', 'green', 'yellow', 'magenta', 'red', 'purple', 'orange', 'pink']);
|
|
@@ -129,8 +158,14 @@ export function parseAgentFrontmatter(contents) {
|
|
|
129
158
|
/**
|
|
130
159
|
* Validates a parsed frontmatter object against CLAUDE.md Agent Authoring Rules.
|
|
131
160
|
*
|
|
161
|
+
* `warnings` is present only when non-empty and never flips `ok`. A missing
|
|
162
|
+
* `effort` on a pinned model is a warning here, because this validator also
|
|
163
|
+
* reads consumer `.claude/agents` (discovery probe); the plugin's own agents
|
|
164
|
+
* fail on it in check-agents.mjs. A set but invalid `effort` stays an error.
|
|
165
|
+
*
|
|
132
166
|
* @param {Record<string,string>|null|undefined} frontmatter
|
|
133
|
-
* @returns {{ ok: true
|
|
167
|
+
* @returns {{ ok: true, warnings?: Array<{path: string, rule: string, message: string}> }
|
|
168
|
+
* |{ ok: false, errors: Array<{path: string, rule: string, message: string}>, warnings?: Array<{path: string, rule: string, message: string}> }}
|
|
134
169
|
*/
|
|
135
170
|
export function validateAgentFrontmatter(frontmatter) {
|
|
136
171
|
if (frontmatter === null || frontmatter === undefined || typeof frontmatter !== 'object') {
|
|
@@ -141,6 +176,7 @@ export function validateAgentFrontmatter(frontmatter) {
|
|
|
141
176
|
}
|
|
142
177
|
|
|
143
178
|
const errors = /** @type {Array<{path: string, rule: string, message: string}>} */ ([]);
|
|
179
|
+
const warnings = /** @type {Array<{path: string, rule: string, message: string}>} */ ([]);
|
|
144
180
|
|
|
145
181
|
// --- name ---
|
|
146
182
|
const name = frontmatter['name'];
|
|
@@ -171,7 +207,7 @@ export function validateAgentFrontmatter(frontmatter) {
|
|
|
171
207
|
path: 'description',
|
|
172
208
|
rule: 'no-block-scalar',
|
|
173
209
|
message:
|
|
174
|
-
'description must be an inline single-line string, not a YAML block scalar (> or |).
|
|
210
|
+
'description must be an inline single-line string, not a YAML block scalar (> or |). Keep it to the routing sentence; put <example> blocks in a body `## Examples` section.',
|
|
175
211
|
});
|
|
176
212
|
}
|
|
177
213
|
|
|
@@ -191,6 +227,20 @@ export function validateAgentFrontmatter(frontmatter) {
|
|
|
191
227
|
});
|
|
192
228
|
}
|
|
193
229
|
|
|
230
|
+
// --- effort (#1614): missing on a pinned model = warning, invalid value = error ---
|
|
231
|
+
const effortError = checkAgentEffort(model, frontmatter['effort']);
|
|
232
|
+
if (effortError) (effortError.rule === 'required' ? warnings : errors).push({ path: 'effort', ...effortError });
|
|
233
|
+
|
|
234
|
+
// --- omitClaudeMd (optional boolean, #1616) ---
|
|
235
|
+
const omit = frontmatter['omitClaudeMd'];
|
|
236
|
+
if (omit !== undefined && omit !== 'true' && omit !== 'false' && omit !== true && omit !== false) {
|
|
237
|
+
errors.push({
|
|
238
|
+
path: 'omitClaudeMd',
|
|
239
|
+
rule: 'boolean',
|
|
240
|
+
message: `omitClaudeMd must be true or false (got ${JSON.stringify(omit)})`,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
194
244
|
// --- color ---
|
|
195
245
|
const color = frontmatter['color'];
|
|
196
246
|
if (color === undefined || color === '') {
|
|
@@ -244,7 +294,8 @@ export function validateAgentFrontmatter(frontmatter) {
|
|
|
244
294
|
}
|
|
245
295
|
}
|
|
246
296
|
|
|
247
|
-
|
|
297
|
+
const warn = warnings.length > 0 ? { warnings } : {};
|
|
298
|
+
return errors.length > 0 ? { ok: false, errors, ...warn } : { ok: true, ...warn };
|
|
248
299
|
}
|
|
249
300
|
|
|
250
301
|
/**
|
|
@@ -253,8 +304,8 @@ export function validateAgentFrontmatter(frontmatter) {
|
|
|
253
304
|
* Never throws. Returns a result object with the file path attached.
|
|
254
305
|
*
|
|
255
306
|
* @param {string} filePath - absolute or relative path to the agent .md file
|
|
256
|
-
* @returns {{ ok: true, file: string }
|
|
257
|
-
* |{ ok: false, file: string, errors: Array<{path: string, rule: string, message: string}> }}
|
|
307
|
+
* @returns {{ ok: true, file: string, warnings?: Array<{path: string, rule: string, message: string}> }
|
|
308
|
+
* |{ ok: false, file: string, errors: Array<{path: string, rule: string, message: string}>, warnings?: Array<{path: string, rule: string, message: string}> }}
|
|
258
309
|
*/
|
|
259
310
|
export function validateAgentFile(filePath) {
|
|
260
311
|
let contents;
|
|
@@ -280,9 +331,5 @@ export function validateAgentFile(filePath) {
|
|
|
280
331
|
}
|
|
281
332
|
|
|
282
333
|
const validation = validateAgentFrontmatter(parsed.frontmatter);
|
|
283
|
-
|
|
284
|
-
return { ok: false, file: filePath, errors: validation.errors };
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
return { ok: true, file: filePath };
|
|
334
|
+
return { ...validation, file: filePath };
|
|
288
335
|
}
|