session-orchestrator 3.24.0 → 4.0.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/.agents/skills/architecture/SKILL.md +18 -0
- package/.agents/skills/autopilot/SKILL.md +17 -0
- package/.agents/skills/bootstrap/SKILL.md +20 -0
- package/.agents/skills/brainstorm/SKILL.md +22 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +15 -0
- package/.agents/skills/convergence-monitoring/SKILL.md +22 -0
- package/.agents/skills/debug/SKILL.md +22 -0
- package/.agents/skills/discovery/SKILL.md +20 -0
- package/.agents/skills/dispatcher/SKILL.md +15 -0
- package/.agents/skills/docs-orchestrator/SKILL.md +18 -0
- package/.agents/skills/ecosystem-health/SKILL.md +20 -0
- package/.agents/skills/eli5/SKILL.md +20 -0
- package/.agents/skills/eval/SKILL.md +21 -0
- package/.agents/skills/evolve/SKILL.md +21 -0
- package/.agents/skills/frontmatter-guard/SKILL.md +15 -0
- package/.agents/skills/gitlab-ops/SKILL.md +20 -0
- package/.agents/skills/gitlab-portfolio/SKILL.md +15 -0
- package/.agents/skills/grill/SKILL.md +22 -0
- package/.agents/skills/hook-development/SKILL.md +15 -0
- package/.agents/skills/mcp-builder/SKILL.md +15 -0
- package/.agents/skills/memory-cleanup/SKILL.md +21 -0
- package/.agents/skills/mode-selector/SKILL.md +17 -0
- package/.agents/skills/npm-publish/SKILL.md +16 -0
- package/.agents/skills/peekaboo-driver/SKILL.md +18 -0
- package/.agents/skills/persona-panel/SKILL.md +17 -0
- package/.agents/skills/plan/SKILL.md +20 -0
- package/.agents/skills/playwright-driver/SKILL.md +20 -0
- package/.agents/skills/quality-gates/SKILL.md +20 -0
- package/.agents/skills/reconcile/SKILL.md +21 -0
- package/.agents/skills/remote-offload/SKILL.md +20 -0
- package/.agents/skills/repo-audit/SKILL.md +16 -0
- package/.agents/skills/session-end/SKILL.md +20 -0
- package/.agents/skills/session-plan/SKILL.md +20 -0
- package/.agents/skills/session-start/SKILL.md +20 -0
- package/.agents/skills/spinout/SKILL.md +16 -0
- package/.agents/skills/sunset-review/SKILL.md +16 -0
- package/.agents/skills/test-runner/SKILL.md +20 -0
- package/.agents/skills/tmux-layout/SKILL.md +21 -0
- package/.agents/skills/using-orchestrator/SKILL.md +17 -0
- package/.agents/skills/vault-mirror/SKILL.md +15 -0
- package/.agents/skills/vault-sync/SKILL.md +15 -0
- package/.agents/skills/wave-executor/SKILL.md +20 -0
- package/.agents/skills/write-executable-plan/SKILL.md +22 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/commands/autopilot.md +2 -2
- package/.cursor/commands/bootstrap.md +1 -1
- package/.cursor/commands/brainstorm.md +1 -1
- package/.cursor/commands/debug.md +1 -1
- package/.cursor/commands/discovery.md +1 -1
- package/.cursor/commands/dispatcher.md +2 -2
- package/.cursor/commands/eli5.md +2 -2
- package/.cursor/commands/eval.md +2 -2
- package/.cursor/commands/evolve.md +1 -1
- package/.cursor/commands/go.md +1 -1
- package/.cursor/commands/grill.md +2 -2
- package/.cursor/commands/memory-cleanup.md +2 -2
- package/.cursor/commands/persona-panel.md +1 -1
- package/.cursor/commands/plan.md +1 -1
- package/.cursor/commands/portfolio.md +1 -1
- package/.cursor/commands/reconcile.md +2 -2
- package/.cursor/commands/release.md +2 -2
- package/.cursor/commands/session.md +2 -2
- package/.cursor/commands/spinout.md +2 -2
- package/.cursor/commands/sunset-review.md +2 -2
- package/.cursor/commands/templates-ack.md +2 -2
- package/.cursor/commands/test.md +2 -2
- package/.cursor/skills/brainstorm/SKILL.md +1 -1
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/quality-gates/SKILL.md +1 -1
- package/.cursor/skills/remote-offload/SKILL.md +1 -1
- package/.orchestrator/policy/blocked-commands.json +121 -0
- package/.orchestrator/policy/ecosystem.schema.json +66 -0
- package/.orchestrator/policy/quality-gates.example.json +16 -0
- package/.orchestrator/policy/quality-gates.schema.json +38 -0
- package/.orchestrator/policy/templates-policy.json +27 -0
- package/.orchestrator/policy/test-profiles.json +47 -0
- package/AGENTS.md +225 -0
- package/CHANGELOG.md +1125 -2
- package/NOTICE +11 -6
- package/README.md +127 -94
- package/agents/eval-judge.md +1 -1
- package/agents/skill-applied-judge.md +1 -1
- package/assets/wave-lifecycle.svg +98 -0
- package/commands/release.md +6 -3
- package/commands/session.md +18 -3
- package/docs/README.md +4 -0
- package/{agents/AGENTS.md → docs/agent-authoring.md} +19 -26
- package/docs/baseline.md +67 -0
- package/docs/ci-setup.md +108 -62
- package/docs/codex-setup.md +65 -21
- package/docs/components.md +36 -15
- package/docs/cursor-setup.md +6 -2
- package/docs/events-schema.md +9 -6
- package/docs/instruction-delivery.md +62 -0
- package/{agents/memory-proposal-collector.md → docs/memory-proposal-flow.md} +1 -8
- package/docs/migration-v4.md +341 -0
- package/docs/pi-setup.md +6 -1
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/rule-authoring.md +85 -19
- package/docs/scope-collision-guard.md +5 -5
- package/docs/session-config-reference.md +57 -56
- package/docs/session-config-template.md +6 -29
- package/docs/telemetry.md +157 -3
- package/docs/vault-docs-architecture.md +50 -11
- package/hooks/_lib/hook-import-set.json +1487 -0
- package/hooks/_lib/subagent-transcript.mjs +562 -0
- package/hooks/config-protection.mjs +2 -2
- package/hooks/cwd-change-restore.mjs +2 -2
- package/hooks/enforce-commands.mjs +69 -0
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +10 -0
- package/hooks/hooks-pi.json +5 -0
- package/hooks/hooks.json +6 -1
- package/hooks/loop-guard.mjs +3 -3
- package/hooks/on-session-end.mjs +2 -2
- package/hooks/on-session-start.mjs +103 -2
- package/hooks/on-stop.mjs +36 -11
- package/hooks/operator-steer.mjs +2 -2
- package/hooks/post-bash-write-verify.mjs +85 -0
- package/hooks/post-edit-import-probe.mjs +344 -0
- package/hooks/post-subagent-discovery-validator.mjs +187 -431
- package/hooks/post-tool-batch-wave-signal.mjs +118 -4
- package/hooks/post-tool-failure-corrective-context.mjs +2 -2
- package/hooks/post-tooluse-frontend-slop.mjs +3 -3
- package/hooks/pre-bash-destructive-guard.mjs +39 -13
- package/hooks/skill-invocation-telemetry.mjs +17 -5
- package/hooks/subagent-telemetry.mjs +13 -4
- package/monitors/monitors.json +3 -3
- package/package.json +9 -1
- package/pi/prompts/session.md +2 -2
- package/plugin.json +27 -0
- package/scripts/backfill-abandoned-sessions.mjs +50 -4
- package/scripts/backfill-learnings-from-vault.mjs +9 -3
- package/scripts/dialectic-deriver.mjs +73 -8
- package/scripts/export-hw-learnings.mjs +113 -1
- package/scripts/generate-agents-skills.mjs +378 -0
- package/scripts/generate-cursor-adapter.mjs +45 -8
- package/scripts/generate-hook-import-set.mjs +249 -0
- package/scripts/lib/agent-status.mjs +13 -2
- package/scripts/lib/auto-dream.mjs +38 -36
- package/scripts/lib/autonomy/suitability.mjs +6 -0
- package/scripts/lib/autopilot/loop.mjs +2 -2
- package/scripts/lib/ci-status-banner.mjs +220 -75
- package/scripts/lib/codex/plugin-contract.mjs +82 -6
- package/scripts/lib/config/auto-dream.mjs +2 -1
- package/scripts/lib/config/block-header.mjs +8 -0
- package/scripts/lib/config/block-preprocess.mjs +177 -0
- package/scripts/lib/config/broken-window.mjs +2 -1
- package/scripts/lib/config/cold-start.mjs +2 -1
- package/scripts/lib/config/config-protection.mjs +22 -2
- package/scripts/lib/config/context-coverage.mjs +2 -1
- package/scripts/lib/config/cross-repo.mjs +2 -1
- package/scripts/lib/config/custom-phases.mjs +2 -1
- package/scripts/lib/config/dialectic.mjs +2 -1
- package/scripts/lib/config/discovery-validator.mjs +2 -1
- package/scripts/lib/config/dispatcher-autonomy-capture.mjs +24 -1
- package/scripts/lib/config/dispatcher-autonomy.mjs +2 -1
- package/scripts/lib/config/docs-orchestrator.mjs +2 -1
- package/scripts/lib/config/docs-staleness.mjs +2 -1
- package/scripts/lib/config/drift-check.mjs +2 -1
- package/scripts/lib/config/eval.mjs +2 -1
- package/scripts/lib/config/events-rotation.mjs +2 -1
- package/scripts/lib/config/evolve.mjs +8 -2
- package/scripts/lib/config/frontend-slop-hook.mjs +7 -3
- package/scripts/lib/config/gitlab-portfolio.mjs +2 -1
- package/scripts/lib/config/handover-gate.mjs +2 -1
- package/scripts/lib/config/health-endpoints.mjs +7 -2
- package/scripts/lib/config/issue-budget.mjs +2 -1
- package/scripts/lib/config/loop-guard.mjs +2 -1
- package/scripts/lib/config/memory.mjs +2 -1
- package/scripts/lib/config/moc-staleness.mjs +2 -1
- package/scripts/lib/config/persona-gate-wave.mjs +2 -1
- package/scripts/lib/config/private-config-dir.mjs +67 -0
- package/scripts/lib/config/reconcile.mjs +2 -1
- package/scripts/lib/config/remote-hosts.mjs +2 -1
- package/scripts/lib/config/section-extractor.mjs +7 -1
- package/scripts/lib/config/skill-evolution.mjs +2 -1
- package/scripts/lib/config/slopcheck.mjs +2 -1
- package/scripts/lib/config/state-md-lock.mjs +2 -1
- package/scripts/lib/config/templates-first.mjs +2 -1
- package/scripts/lib/config/test.mjs +2 -1
- package/scripts/lib/config/vault-integration.mjs +7 -1
- package/scripts/lib/config/vault-mirror-quality.mjs +2 -1
- package/scripts/lib/config/vault-staleness.mjs +2 -1
- package/scripts/lib/config/vault-sync.mjs +2 -1
- package/scripts/lib/config/verification-auto-fix.mjs +2 -1
- package/scripts/lib/config/wave-reviewers.mjs +2 -1
- package/scripts/lib/config/worktree-orphans.mjs +2 -1
- package/scripts/lib/convergence-monitor.mjs +82 -16
- package/scripts/lib/dispatcher/rank.mjs +124 -48
- package/scripts/lib/ecosystem-health.mjs +16 -2
- package/scripts/lib/eval/engine.mjs +9 -1
- package/scripts/lib/eval/session-resolve.mjs +23 -4
- package/scripts/lib/events.mjs +22 -6
- package/scripts/lib/frontmatter-guard.mjs +131 -13
- package/scripts/lib/gates/gate-full.mjs +26 -0
- package/scripts/lib/gates/gate-helpers.mjs +76 -0
- package/scripts/lib/hardware-pattern-detector.mjs +18 -1
- package/scripts/lib/harness-audit/categories/category4.mjs +31 -11
- package/scripts/lib/host-identity.mjs +50 -11
- package/scripts/lib/instruction-budget-guard.mjs +171 -5
- package/scripts/lib/learnings/evolve-telemetry.mjs +178 -0
- package/scripts/lib/learnings/io.mjs +60 -6
- package/scripts/lib/memory-proposals/store.mjs +30 -22
- package/scripts/lib/owner-config-banner.mjs +43 -6
- package/scripts/lib/owner-config-loader.mjs +21 -10
- package/scripts/lib/owner-interview.mjs +3 -3
- package/scripts/lib/owner-yaml.mjs +207 -14
- package/scripts/lib/platform.mjs +108 -15
- package/scripts/lib/plugin-update-banner.mjs +406 -0
- package/scripts/lib/project-hygiene.mjs +38 -2
- package/scripts/lib/qg-command-drift-banner.mjs +50 -12
- package/scripts/lib/quality-gate.mjs +133 -44
- package/scripts/lib/reconcile/emitter.mjs +68 -6
- package/scripts/lib/reconcile/engine.mjs +13 -4
- package/scripts/lib/reconcile/idempotency.mjs +37 -4
- package/scripts/lib/reconcile/writer.mjs +40 -18
- package/scripts/lib/session-close-backfill.mjs +67 -9
- package/scripts/lib/session-id.mjs +12 -23
- package/scripts/lib/session-identity/own-session.mjs +125 -10
- package/scripts/lib/session-lock-shape.mjs +43 -0
- package/scripts/lib/session-lock.mjs +5 -10
- package/scripts/lib/session-registry.mjs +25 -9
- package/scripts/lib/session-schema/constants.mjs +36 -2
- package/scripts/lib/session-schema/validator.mjs +38 -4
- package/scripts/lib/session-start-probes.mjs +18 -1
- package/scripts/lib/sessions-staleness-banner.mjs +18 -11
- package/scripts/lib/skill-health/join.mjs +17 -4
- package/scripts/lib/state-md.mjs +78 -0
- package/scripts/lib/sunset/walker.mjs +6 -0
- package/scripts/lib/telemetry/schema.mjs +181 -9
- package/scripts/lib/telemetry/sync.mjs +368 -12
- package/scripts/lib/validate/check-agents-skills.mjs +327 -0
- package/scripts/lib/validate/check-agents.mjs +3 -3
- package/scripts/lib/validate/check-cursor-adapter.mjs +234 -72
- package/scripts/lib/validate/check-hooks-symmetry.mjs +45 -16
- package/scripts/lib/validate/check-owner-leakage.mjs +281 -20
- package/scripts/lib/validate/check-skill-links.mjs +163 -0
- package/scripts/lib/validate/check-skill-script-paths.mjs +47 -28
- package/scripts/lib/validate/check-unwired-features.mjs +0 -2
- package/scripts/lib/validate/check-validator-registration.mjs +10 -4
- package/scripts/lib/validate/enumerate-repo-files.mjs +317 -0
- package/scripts/lib/vault-backfill/template.mjs +63 -6
- package/scripts/lib/vault-mirror/process.mjs +165 -42
- package/scripts/lib/vault-mirror/telemetry.mjs +2 -2
- package/scripts/lib/vault-status/narrative-mirror.mjs +127 -18
- package/scripts/lib/wave-executor/dispatch-common.mjs +164 -0
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +7 -142
- package/scripts/lib/wave-executor/remote-dispatch.mjs +5 -7
- package/scripts/lib/wave-resource-gate.mjs +8 -2
- package/scripts/lib/wave-sizing.mjs +4 -1
- package/scripts/lib/wave-transcript-tail.mjs +118 -4
- package/scripts/materialize-wave-scope.mjs +12 -5
- package/scripts/memory-propose.mjs +19 -5
- package/scripts/migrate-cold-start-seed.mjs +4 -1
- package/scripts/parse-config.mjs +60 -3
- package/scripts/release.mjs +337 -29
- package/scripts/repair-invalid-sessions.mjs +3 -3
- package/scripts/run-quality-gate.mjs +128 -11
- package/scripts/sweep-expired-learnings.mjs +90 -0
- package/scripts/sync-vault-schema.mjs +3 -1
- package/scripts/telemetry.mjs +2 -2
- package/scripts/validate-plugin.mjs +161 -0
- package/scripts/validate-wave-scope.mjs +28 -8
- package/scripts/wave-scope-binding.mjs +215 -0
- package/skills/_shared/instruction-file-resolution.md +10 -0
- package/skills/_shared/parallel-aware-preamble.md +1 -0
- package/skills/_shared/platform-tools.md +1 -1
- package/skills/_shared/state-ownership.md +1 -1
- package/skills/architecture/SKILL.md +7 -5
- package/skills/{domain-model/SKILL.md → architecture/references/domain-model.md} +9 -9
- package/skills/autopilot/SKILL.md +4 -18
- package/skills/claude-md-drift-check/SKILL.md +5 -1
- package/skills/claude-md-drift-check/checker.mjs +62 -2
- package/skills/convergence-monitoring/SIGNALS.md +55 -0
- package/skills/discovery/probes/vault-staleness.mjs +37 -13
- package/skills/discovery/probes-arch.md +20 -18
- package/skills/dispatcher/SKILL.md +3 -2
- package/skills/evolve/SKILL.md +65 -26
- package/skills/frontmatter-guard/SKILL.md +11 -5
- package/skills/npm-publish/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +33 -0
- package/skills/remote-offload/SKILL.md +1 -1
- package/skills/session-end/SKILL.md +18 -905
- package/skills/session-end/phase-3-6-tail.md +10 -3
- package/skills/session-end/plan-verification.md +221 -155
- package/skills/session-end/references/phase-2-quality-gate.md +93 -0
- package/skills/session-end/references/phase-3-documentation-updates.md +229 -0
- package/skills/session-end/references/phase-4a-worktree-cleanup.md +120 -0
- package/skills/session-end/references/phase-4b-worktree-orphan-sweep.md +58 -0
- package/skills/session-end/references/phase-5-issue-cleanup.md +104 -0
- package/skills/session-end/references/session-summary-template.md +62 -0
- package/skills/session-plan/SKILL.md +49 -0
- package/skills/session-start/SKILL.md +22 -904
- package/skills/session-start/phase-8-5-express-path.md +1 -1
- package/skills/session-start/references/phase-1-1-dispatcher-autonomy-capture.md +55 -0
- package/skills/session-start/references/phase-1-2-session-lock.md +140 -0
- package/skills/session-start/references/phase-1-5-session-continuity.md +254 -0
- package/skills/session-start/references/phase-1-7-vault-status-board.md +53 -0
- package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +75 -0
- package/skills/session-start/references/phase-4-ssot-environment-check.md +155 -0
- package/skills/session-start/references/phase-6-5-forced-reads.md +75 -0
- package/skills/session-start/references/phase-6-6-project-intelligence.md +81 -0
- package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +103 -0
- package/skills/vault-sync/validator.mjs +21 -27
- package/skills/wave-executor/SKILL.md +15 -1
- package/skills/wave-executor/references/wave-loop-dispatch.md +612 -0
- package/skills/wave-executor/references/wave-loop-review.md +570 -0
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +162 -0
- package/skills/wave-executor/wave-loop.md +14 -1309
- package/templates/_shared/journey-manifest.md +10 -6
- package/.cursor/commands/autopilot-multi.md +0 -14
- package/.cursor/commands/contract-version-bump.md +0 -14
- package/.cursor/commands/journey-audit.md +0 -14
- package/.cursor/skills/contract-version-bump/SKILL.md +0 -12
- package/.cursor/skills/daily/SKILL.md +0 -12
- package/.cursor/skills/domain-model/SKILL.md +0 -13
- package/.cursor/skills/journey-audit/SKILL.md +0 -13
- package/.cursor/skills/skill-creator/SKILL.md +0 -13
- package/.cursor/skills/ubiquitous-language/SKILL.md +0 -13
- package/commands/autopilot-multi.md +0 -74
- package/commands/contract-version-bump.md +0 -28
- package/commands/journey-audit.md +0 -43
- package/pi/prompts/autopilot-multi.md +0 -12
- package/pi/prompts/contract-version-bump.md +0 -12
- package/pi/prompts/journey-audit.md +0 -12
- package/scripts/autopilot-multi.mjs +0 -885
- package/scripts/backfill-learnings-expires.mjs +0 -196
- package/scripts/backfill-learnings.mjs +0 -203
- package/scripts/fleet-instruction-scan.mjs +0 -141
- package/scripts/lib/autopilot/dep-graph.mjs +0 -417
- package/scripts/lib/autopilot/multi-killswitch.mjs +0 -184
- package/scripts/lib/webhook-url.mjs +0 -105
- package/scripts/lifecycle-sim-v6.mjs +0 -347
- package/scripts/migrate-learnings-jsonl.mjs +0 -189
- package/scripts/migrate-subagents-jsonl.mjs +0 -196
- package/scripts/upload-social-preview.mjs +0 -316
- package/skills/_shared/model-selection.md +0 -64
- package/skills/contract-version-bump/SKILL.md +0 -219
- package/skills/daily/SKILL.md +0 -222
- package/skills/daily/generate.sh +0 -92
- package/skills/daily/templates/daily.md.tpl +0 -36
- package/skills/journey-audit/SKILL.md +0 -270
- package/skills/skill-creator/SKILL.md +0 -168
- package/skills/ubiquitous-language/SKILL.md +0 -97
- package/skills/vault-sync/package-lock.json +0 -40
- /package/skills/{domain-model → architecture/references}/ADR-FORMAT.md +0 -0
- /package/skills/{domain-model → architecture/references}/CONTEXT-FORMAT.md +0 -0
|
@@ -67,7 +67,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
67
67
|
import { emitEvent, sessionAttribution } from './events.mjs';
|
|
68
68
|
import { admitSuiteCounts, extractTestCounts } from './gates/gate-helpers.mjs';
|
|
69
69
|
import { redactDiagnosticsBundle } from './quality-gate/diagnostics.mjs';
|
|
70
|
-
import {
|
|
70
|
+
import { readProcessLocalSessionIds } from './session-identity/own-session.mjs';
|
|
71
71
|
|
|
72
72
|
export { redactDiagnosticsBundle } from './quality-gate/diagnostics.mjs';
|
|
73
73
|
|
|
@@ -114,30 +114,99 @@ function resolveRepoRoot(explicit) {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
/**
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
117
|
+
* Closed set of `degraded` reasons for {@link loadCommandsFromSessionConfigDetailed}.
|
|
118
|
+
*
|
|
119
|
+
* Its own enum, deliberately not shared with `ci-status-banner.mjs`'s
|
|
120
|
+
* `DEGRADED_REASONS`: the members below are exactly the three ways THIS
|
|
121
|
+
* config-read can fail, and an enum whose members are not exhaustively
|
|
122
|
+
* reachable cannot be switched on exhaustively.
|
|
123
|
+
*
|
|
124
|
+
* - `script-missing` — `scripts/parse-config.mjs` is not on disk.
|
|
125
|
+
* - `spawn-failed` — the subprocess exited non-zero, timed out, or wrote
|
|
126
|
+
* nothing to stdout.
|
|
127
|
+
* - `parse-error` — stdout was not parseable JSON (or the read threw).
|
|
128
|
+
*/
|
|
129
|
+
export const CONFIG_READ_DEGRADED_REASONS = Object.freeze([
|
|
130
|
+
'script-missing',
|
|
131
|
+
'spawn-failed',
|
|
132
|
+
'parse-error',
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Does `repoRoot` carry a Session Config file at all?
|
|
137
|
+
*
|
|
138
|
+
* Mirrors the file-name precedence `scripts/parse-config.mjs::resolveConfigFile`
|
|
139
|
+
* uses (`SO_CONFIG_FILE` → `CLAUDE.md` → `AGENTS.md`) but NOT its upward
|
|
140
|
+
* project-root walk — see the ceiling note at the call site.
|
|
141
|
+
*
|
|
142
|
+
* @param {string} repoRoot
|
|
143
|
+
* @returns {boolean}
|
|
144
|
+
*/
|
|
145
|
+
function sessionConfigFileExists(repoRoot) {
|
|
146
|
+
try {
|
|
147
|
+
const soConfigFile = process.env.SO_CONFIG_FILE;
|
|
148
|
+
if (soConfigFile && existsSync(join(repoRoot, soConfigFile))) return true;
|
|
149
|
+
return existsSync(join(repoRoot, 'CLAUDE.md')) || existsSync(join(repoRoot, 'AGENTS.md'));
|
|
150
|
+
} catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Load default commands from Session Config via `scripts/parse-config.mjs`,
|
|
157
|
+
* distinguishing "the config declares no `*-command` keys" from "the config
|
|
158
|
+
* could not be read at all".
|
|
159
|
+
*
|
|
160
|
+
* Both cases yield `commands: {}` — that half is unchanged, and every command
|
|
161
|
+
* resolution keeps falling through to DEFAULT_COMMANDS exactly as before. What
|
|
162
|
+
* is new is the second channel: on failure the result also carries a
|
|
163
|
+
* `degraded` reason from {@link CONFIG_READ_DEGRADED_REASONS}. Without it,
|
|
164
|
+
* `checkQgCommandDrift` reported "no drift" for a config it never managed to
|
|
165
|
+
* read — a silent all-clear derived from an absent measurement (#1031's
|
|
166
|
+
* failure class, one consumer over).
|
|
167
|
+
*
|
|
168
|
+
* `degraded` is OMITTED, not set to null, on the success path, so a strict
|
|
169
|
+
* `toEqual({commands: {…}})` pin holds for every readable config.
|
|
120
170
|
*
|
|
121
171
|
* Never throws.
|
|
122
172
|
*
|
|
123
173
|
* @param {string} repoRoot
|
|
124
|
-
* @returns {{lint?: string, typecheck?: string, test?: string}
|
|
174
|
+
* @returns {{commands: {lint?: string, typecheck?: string, test?: string},
|
|
175
|
+
* degraded?: 'script-missing'|'spawn-failed'|'parse-error'}}
|
|
125
176
|
*/
|
|
126
|
-
export function
|
|
177
|
+
export function loadCommandsFromSessionConfigDetailed(repoRoot) {
|
|
127
178
|
try {
|
|
128
179
|
const scriptPath = join(
|
|
129
180
|
dirname(fileURLToPath(import.meta.url)),
|
|
130
181
|
'..',
|
|
131
182
|
'parse-config.mjs',
|
|
132
183
|
);
|
|
133
|
-
if (!existsSync(scriptPath)) return {};
|
|
184
|
+
if (!existsSync(scriptPath)) return { commands: {}, degraded: 'script-missing' };
|
|
134
185
|
const result = spawnSync('node', [scriptPath], {
|
|
135
186
|
cwd: repoRoot,
|
|
136
187
|
encoding: 'utf8',
|
|
137
188
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
138
189
|
timeout: 10_000,
|
|
139
190
|
});
|
|
140
|
-
if (result.status !== 0 || !result.stdout)
|
|
191
|
+
if (result.status !== 0 || !result.stdout) {
|
|
192
|
+
// `parse-config.mjs` exits 1 for EVERY failure, including the benign
|
|
193
|
+
// "this repo has no CLAUDE.md / AGENTS.md at all" — measured 2026-09-05
|
|
194
|
+
// in an empty tmp dir: stderr `CLAUDE.md or AGENTS.md required`, exit 1.
|
|
195
|
+
// A repo with no Session Config has no `*-command` value that could
|
|
196
|
+
// drift, so reporting that as a failed READ would fire a warning on every
|
|
197
|
+
// such repo (`.claude/rules/host-resources.md` § HR-101 — a warning class
|
|
198
|
+
// that is usually present is a broken instrument).
|
|
199
|
+
//
|
|
200
|
+
// Ceiling (BV-004): `parse-config.mjs` resolves its config via
|
|
201
|
+
// `findProjectRoot(cwd)` walking UPWARD, this check looks only at
|
|
202
|
+
// `repoRoot` itself. For a `repoRoot` nested below the project root the
|
|
203
|
+
// two disagree — and the disagreement resolves toward SILENCE (no
|
|
204
|
+
// `degraded`), i.e. the pre-#1031 behaviour, never toward a false alarm.
|
|
205
|
+
// Revisit if a caller starts passing sub-directory roots.
|
|
206
|
+
return sessionConfigFileExists(repoRoot)
|
|
207
|
+
? { commands: {}, degraded: 'spawn-failed' }
|
|
208
|
+
: { commands: {} };
|
|
209
|
+
}
|
|
141
210
|
const cfg = JSON.parse(result.stdout);
|
|
142
211
|
const out = {};
|
|
143
212
|
if (typeof cfg['lint-command'] === 'string' && cfg['lint-command'].trim()) {
|
|
@@ -149,12 +218,30 @@ export function loadCommandsFromSessionConfig(repoRoot) {
|
|
|
149
218
|
if (typeof cfg['test-command'] === 'string' && cfg['test-command'].trim()) {
|
|
150
219
|
out.test = cfg['test-command'];
|
|
151
220
|
}
|
|
152
|
-
return out;
|
|
221
|
+
return { commands: out };
|
|
153
222
|
} catch {
|
|
154
|
-
return {};
|
|
223
|
+
return { commands: {}, degraded: 'parse-error' };
|
|
155
224
|
}
|
|
156
225
|
}
|
|
157
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Load default commands from Session Config via `scripts/parse-config.mjs`.
|
|
229
|
+
* Returns a partial object — keys that fail to resolve are simply absent
|
|
230
|
+
* (the caller falls through to DEFAULT_COMMANDS for those).
|
|
231
|
+
*
|
|
232
|
+
* Thin wrapper over {@link loadCommandsFromSessionConfigDetailed}; byte-identical
|
|
233
|
+
* return value for every input, including every failure path. Callers that need
|
|
234
|
+
* to tell a failed read from an empty config use the detailed variant.
|
|
235
|
+
*
|
|
236
|
+
* Never throws.
|
|
237
|
+
*
|
|
238
|
+
* @param {string} repoRoot
|
|
239
|
+
* @returns {{lint?: string, typecheck?: string, test?: string}}
|
|
240
|
+
*/
|
|
241
|
+
export function loadCommandsFromSessionConfig(repoRoot) {
|
|
242
|
+
return loadCommandsFromSessionConfigDetailed(repoRoot).commands;
|
|
243
|
+
}
|
|
244
|
+
|
|
158
245
|
/**
|
|
159
246
|
* Resolve the three gate commands. Precedence: override > session config > defaults.
|
|
160
247
|
*
|
|
@@ -420,44 +507,32 @@ function listChangedFiles(repoRoot, ref) {
|
|
|
420
507
|
* The id-space THIS process belongs to, for comparison against a repo-global
|
|
421
508
|
* file that any session in the working copy may have written.
|
|
422
509
|
*
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
*
|
|
510
|
+
* Process-local witnesses ONLY (`CLAUDE_CODE_SESSION_ID`, via
|
|
511
|
+
* {@link readProcessLocalSessionIds}) — the `session.lock` fallback this
|
|
512
|
+
* function used to carry is deliberately GONE, not merely deprioritised.
|
|
513
|
+
* `session.lock` is a repo-GLOBAL artefact any session in the working copy
|
|
514
|
+
* can hold; unioning or falling back to it made a shared resource stand in
|
|
515
|
+
* for a process-local identity, which is exactly the #1194 hazard class
|
|
516
|
+
* (`.claude/rules/host-resources.md` § HR-102 — "a better signal REPLACES a
|
|
517
|
+
* worse one, it does not merely suppress it"). Concretely for this module
|
|
518
|
+
* (#1205): without the env var, a process that is NOT the lock holder used to
|
|
519
|
+
* silently adopt the lock holder's id as its own, so a `current-session.json`
|
|
520
|
+
* written by that same holder passed the ownership check by construction.
|
|
521
|
+
* `readProcessLocalSessionIds()` (`./session-identity/own-session.mjs`) is
|
|
522
|
+
* the shared, already-hardened implementation of this exact question — see
|
|
523
|
+
* its JSDoc for the full lock/STATE.md exclusion rationale.
|
|
524
|
+
*
|
|
525
|
+
* An empty result is not a mismatch: {@link classifyCurrentSessionOwnership}
|
|
526
|
+
* treats an empty `ownIds` as `'unknown'`, never `'foreign'`, so
|
|
527
|
+
* `corrective_context` is kept — the existing fail-open contract from #1058,
|
|
528
|
+
* unchanged by this fix.
|
|
439
529
|
*
|
|
440
|
-
* @param {string} repoRoot
|
|
441
530
|
* @returns {Set<string>} possibly empty — an empty set means "identity
|
|
442
531
|
* unresolvable", which the classifier below treats as `unknown`, never as a
|
|
443
532
|
* mismatch.
|
|
444
533
|
*/
|
|
445
|
-
function readOwnSessionIds(
|
|
446
|
-
|
|
447
|
-
// `.trim()` first: a whitespace-only env var is truthy and would otherwise
|
|
448
|
-
// enter the set as a phantom id (`development.md` § env-var whitespace trap).
|
|
449
|
-
const fromEnv = (process.env.CLAUDE_CODE_SESSION_ID || '').trim();
|
|
450
|
-
if (fromEnv) ids.add(fromEnv);
|
|
451
|
-
if (ids.size === 0) {
|
|
452
|
-
try {
|
|
453
|
-
const lock = readLock({ repoRoot });
|
|
454
|
-
for (const key of ['session_id', 'semantic_session_id']) {
|
|
455
|
-
const value = typeof lock?.[key] === 'string' ? lock[key].trim() : '';
|
|
456
|
-
if (value) ids.add(value);
|
|
457
|
-
}
|
|
458
|
-
} catch { /* readLock never throws, but the contract is not ours to trust */ }
|
|
459
|
-
}
|
|
460
|
-
return ids;
|
|
534
|
+
function readOwnSessionIds() {
|
|
535
|
+
return new Set(readProcessLocalSessionIds({ env: process.env, hookInput: null }));
|
|
461
536
|
}
|
|
462
537
|
|
|
463
538
|
/**
|
|
@@ -534,7 +609,8 @@ function readCorrectiveContext(repoRoot) {
|
|
|
534
609
|
if (!existsSync(p)) return [];
|
|
535
610
|
const raw = readFileSync(p, 'utf8');
|
|
536
611
|
const parsed = JSON.parse(raw);
|
|
537
|
-
const
|
|
612
|
+
const ownIds = readOwnSessionIds();
|
|
613
|
+
const { verdict, fileIds } = classifyCurrentSessionOwnership(parsed, ownIds);
|
|
538
614
|
if (verdict === 'foreign') {
|
|
539
615
|
process.stderr.write(
|
|
540
616
|
`⚠️ quality-gate: .orchestrator/current-session.json belongs to another session ` +
|
|
@@ -543,6 +619,19 @@ function readCorrectiveContext(repoRoot) {
|
|
|
543
619
|
);
|
|
544
620
|
return [];
|
|
545
621
|
}
|
|
622
|
+
// The fail-open half of `verdict === 'unknown'`: the file DOES name an id,
|
|
623
|
+
// but this process has no process-local witness of its own (ownIds is
|
|
624
|
+
// empty — no `CLAUDE_CODE_SESSION_ID`, e.g. Codex/Cursor). Ownership is
|
|
625
|
+
// unprovable, so the content is kept per the #1058 contract — but silently
|
|
626
|
+
// is the wrong word for that: make the fail-open visible on stderr rather
|
|
627
|
+
// than indistinguishable from a verified 'own' match.
|
|
628
|
+
if (verdict === 'unknown' && fileIds.length > 0 && ownIds.size === 0) {
|
|
629
|
+
process.stderr.write(
|
|
630
|
+
'⚠ quality-gate: cannot verify ownership of .orchestrator/current-session.json ' +
|
|
631
|
+
'(no process-local session id — CLAUDE_CODE_SESSION_ID unset); keeping corrective_context ' +
|
|
632
|
+
`from session ${fileIds.join(', ')} UNVERIFIED\n`,
|
|
633
|
+
);
|
|
634
|
+
}
|
|
546
635
|
const arr = Array.isArray(parsed?.corrective_context) ? parsed.corrective_context : [];
|
|
547
636
|
return arr.slice(-CORRECTIVE_CONTEXT_TAIL);
|
|
548
637
|
} catch {
|
|
@@ -172,18 +172,37 @@ const GLOB_METACHAR_RE = /[*?[\]{}()]|[!@+](?=\()/;
|
|
|
172
172
|
* {@link UNSAFE_PATH_QUOTE_RE}). Results are deduped, order-preserving on
|
|
173
173
|
* first occurrence.
|
|
174
174
|
*
|
|
175
|
+
* #1153 P13: skipping used to be entirely silent, so a learning whose ONLY
|
|
176
|
+
* `file_paths` entry was unusable produced the generic "no activation axis"
|
|
177
|
+
* rejection with nothing naming WHY the axis was empty. Every skipped entry is
|
|
178
|
+
* therefore recorded into the optional `dropped` sink (value + one-word
|
|
179
|
+
* reason), which {@link formatDroppedFilePaths} renders into that rejection
|
|
180
|
+
* reason. The sink is an out-parameter rather than a changed return type so
|
|
181
|
+
* every existing caller stays byte-identical.
|
|
182
|
+
*
|
|
175
183
|
* @param {string[]} filePaths
|
|
184
|
+
* @param {{value: string, reason: string}[]} [dropped] - out-param sink
|
|
176
185
|
* @returns {string[]}
|
|
177
186
|
*/
|
|
178
|
-
function globsFromFilePaths(filePaths) {
|
|
187
|
+
function globsFromFilePaths(filePaths, dropped) {
|
|
179
188
|
const out = [];
|
|
180
189
|
const seen = new Set();
|
|
181
190
|
for (const raw of filePaths) {
|
|
182
191
|
if (typeof raw !== 'string' || raw === '') continue;
|
|
183
|
-
if (GLOB_METACHAR_RE.test(raw))
|
|
192
|
+
if (GLOB_METACHAR_RE.test(raw)) {
|
|
193
|
+
dropped?.push({ value: raw, reason: 'glob-metachar' });
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
184
196
|
// #1015: frontmatter-structure guard. Skipping (never escaping) keeps this
|
|
185
197
|
// idempotent and non-overlapping with the renderer's sanitiser.
|
|
186
|
-
if (CONTROL_CHARS_TEST_RE.test(raw)
|
|
198
|
+
if (CONTROL_CHARS_TEST_RE.test(raw)) {
|
|
199
|
+
dropped?.push({ value: raw, reason: 'control-char' });
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (UNSAFE_PATH_QUOTE_RE.test(raw)) {
|
|
203
|
+
dropped?.push({ value: raw, reason: 'quote' });
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
187
206
|
const normalized = raw.replace(/\\/g, '/');
|
|
188
207
|
const dir = dirname(normalized);
|
|
189
208
|
const pattern = dir === '.' ? normalized : `${dir}/**`;
|
|
@@ -195,6 +214,42 @@ function globsFromFilePaths(filePaths) {
|
|
|
195
214
|
return out;
|
|
196
215
|
}
|
|
197
216
|
|
|
217
|
+
/** Max chars of a single dropped `file_paths` value in a rejection reason. */
|
|
218
|
+
const DROPPED_VALUE_MAX = 60;
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Render the `dropped` sink of {@link globsFromFilePaths} into a suffix for the
|
|
222
|
+
* never-always-on rejection reason (#1153 P13).
|
|
223
|
+
*
|
|
224
|
+
* The reason string is OPERATOR-VISIBLE (engine.mjs forwards it verbatim into
|
|
225
|
+
* `reason: emit/render error: …` and from there into the candidates ledger), so
|
|
226
|
+
* a hostile `file_paths` entry must not be able to inject newlines or ANSI into
|
|
227
|
+
* it. Each value is therefore `JSON.stringify`-escaped — which turns every
|
|
228
|
+
* control char into a `\uXXXX` literal — and then hard-truncated to
|
|
229
|
+
* {@link DROPPED_VALUE_MAX} chars, with any trailing partial backslash escape
|
|
230
|
+
* removed so the cut cannot end mid-sequence.
|
|
231
|
+
*
|
|
232
|
+
* @param {{value: string, reason: string}[]} dropped
|
|
233
|
+
* @returns {string} `''` when nothing was dropped, else a ` — dropped …` suffix
|
|
234
|
+
*/
|
|
235
|
+
function formatDroppedFilePaths(dropped) {
|
|
236
|
+
if (!Array.isArray(dropped) || dropped.length === 0) return '';
|
|
237
|
+
const rendered = dropped.map(({ value, reason }) => {
|
|
238
|
+
const escaped = JSON.stringify(String(value));
|
|
239
|
+
const clipped =
|
|
240
|
+
escaped.length <= DROPPED_VALUE_MAX
|
|
241
|
+
? escaped
|
|
242
|
+
// A `\uXXXX` escape is 6 chars, so the cut can land at FIVE distinct
|
|
243
|
+
// depths inside one — and `/\\+$/` only ever caught the shallowest
|
|
244
|
+
// (`\`). Measured at DROPPED_VALUE_MAX=60 with a NUL at offsets 53..56:
|
|
245
|
+
// the old form left `\u000`, `\u00`, `\u0` and `\u` standing. Strip the
|
|
246
|
+
// backslash together with up to three hex digits of a partial `u`-escape.
|
|
247
|
+
: `${escaped.slice(0, DROPPED_VALUE_MAX - 1).replace(/\\(u[0-9a-fA-F]{0,3})?$/, '')}…`;
|
|
248
|
+
return `${clipped} (${reason})`;
|
|
249
|
+
});
|
|
250
|
+
return ` — dropped file_paths: ${dropped.length} (${rendered.join(', ')})`;
|
|
251
|
+
}
|
|
252
|
+
|
|
198
253
|
/**
|
|
199
254
|
* Build a single-line, frontmatter-safe description from a learning.
|
|
200
255
|
*
|
|
@@ -320,7 +375,8 @@ export function toActivationMetadata(learning, { ruleExpiryDays, now, minRuleDay
|
|
|
320
375
|
}
|
|
321
376
|
|
|
322
377
|
const filePaths = Array.isArray(learning.file_paths) ? learning.file_paths : [];
|
|
323
|
-
const
|
|
378
|
+
const droppedFilePaths = [];
|
|
379
|
+
const globs = globsFromFilePaths(filePaths, droppedFilePaths);
|
|
324
380
|
|
|
325
381
|
// #1015: `host_class` is copied straight into an UNQUOTED `host-class:`
|
|
326
382
|
// frontmatter line by the renderer, so its SHAPE is load-bearing. Reject a
|
|
@@ -358,16 +414,22 @@ export function toActivationMetadata(learning, { ruleExpiryDays, now, minRuleDay
|
|
|
358
414
|
// The brandmauer: an auto-generated rule must carry ≥1 activation axis. If we
|
|
359
415
|
// could derive neither a glob nor a host-class, refuse — never emit always-on.
|
|
360
416
|
if (globs.length === 0 && hostClass === undefined) {
|
|
417
|
+
// #1153 P13: name the entries that were silently skipped on the way here —
|
|
418
|
+
// otherwise "no activation axis" is indistinguishable from "no file_paths
|
|
419
|
+
// at all", and the operator has no way to see that a malformed record (not
|
|
420
|
+
// an empty one) caused the rejection.
|
|
421
|
+
const droppedSuffix = formatDroppedFilePaths(droppedFilePaths);
|
|
361
422
|
if (hostClassDroppedByType) {
|
|
362
423
|
throw new Error(
|
|
363
424
|
`emitter: host_class ${JSON.stringify(learning.host_class)} is present and well-formed, but type ` +
|
|
364
425
|
`${JSON.stringify(learning.type)} is not host-specific (#1090 — host-class is only copied through for ` +
|
|
365
426
|
`${[...HOST_SPECIFIC_TYPES].join(', ')}) — refusing to emit always-on auto-generated rule ` +
|
|
366
|
-
|
|
427
|
+
`(never-always-on invariant); no other activation axis (globs) is available either.${droppedSuffix}`,
|
|
367
428
|
);
|
|
368
429
|
}
|
|
369
430
|
throw new Error(
|
|
370
|
-
'emitter: no activation axis (globs/host-class) — refusing to emit always-on auto-generated rule
|
|
431
|
+
'emitter: no activation axis (globs/host-class) — refusing to emit always-on auto-generated rule ' +
|
|
432
|
+
`(never-always-on invariant)${droppedSuffix}`,
|
|
371
433
|
);
|
|
372
434
|
}
|
|
373
435
|
|
|
@@ -160,8 +160,10 @@ function zeroedResult(error) {
|
|
|
160
160
|
/**
|
|
161
161
|
* Default learnings loader — read + parse `<repoRoot>/.orchestrator/metrics/learnings.jsonl`
|
|
162
162
|
* line-by-line, migrate/normalize records through the learnings schema SSOT,
|
|
163
|
-
* and skip blank/malformed lines. A missing file yields `[]
|
|
164
|
-
*
|
|
163
|
+
* and skip blank/malformed lines. A missing file (ENOENT) yields `[]`
|
|
164
|
+
* silently; an unreadable one (EACCES/EISDIR/…) yields `[]` with a stderr
|
|
165
|
+
* WARN (#1210 — ENOENT and other read failures are different facts, same
|
|
166
|
+
* split as `sessions-canonical.mjs` `readCanonicalSessions`).
|
|
165
167
|
*
|
|
166
168
|
* @param {string|undefined} repoRoot
|
|
167
169
|
* @returns {Array<Record<string, unknown>>}
|
|
@@ -175,8 +177,15 @@ function defaultLoadLearnings(repoRoot) {
|
|
|
175
177
|
let raw;
|
|
176
178
|
try {
|
|
177
179
|
raw = readFileSync(absPath, 'utf8');
|
|
178
|
-
} catch {
|
|
179
|
-
|
|
180
|
+
} catch (err) {
|
|
181
|
+
if (!err || err.code !== 'ENOENT') {
|
|
182
|
+
process.stderr.write(
|
|
183
|
+
`⚠ defaultLoadLearnings: cannot read ${absPath} ` +
|
|
184
|
+
`(${err?.code ?? '?'}: ${err?.message ?? String(err)}) — ` +
|
|
185
|
+
'treating as EMPTY, counts below are floors\n',
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
return [];
|
|
180
189
|
}
|
|
181
190
|
|
|
182
191
|
/** @type {Array<Record<string, unknown>>} */
|
|
@@ -391,7 +391,14 @@ export function mergeCandidates({ candidates, repoRoot, storePath } = {}) {
|
|
|
391
391
|
* @param {number} [params.fallbackConfidence] - used only when no existing record is found.
|
|
392
392
|
* @param {string} [params.repoRoot]
|
|
393
393
|
* @param {string} [params.storePath]
|
|
394
|
-
* @returns {{ written: boolean, stamped: ReconcileCandidate|null }}
|
|
394
|
+
* @returns {{ written: boolean, stamped: ReconcileCandidate|null, alreadyProcessed?: boolean }}
|
|
395
|
+
* `stamped` is the record as PERSISTED (read back out of `mergeCandidates`'s
|
|
396
|
+
* merged array), never the one this call merely proposed — when a prior
|
|
397
|
+
* terminal verdict wins the dedupe, the two differ and only the former is
|
|
398
|
+
* true. `alreadyProcessed: true` marks exactly that case; `written` stays
|
|
399
|
+
* `true` there, because the store holds the intended terminal state and
|
|
400
|
+
* `written: false` means a WRITE FAILURE to the one production caller
|
|
401
|
+
* (`writer.mjs`, which turns it into an operator-visible error).
|
|
395
402
|
*/
|
|
396
403
|
export function markCandidateProcessed({
|
|
397
404
|
learningKey,
|
|
@@ -419,6 +426,7 @@ export function markCandidateProcessed({
|
|
|
419
426
|
stamped = { ...found, processed_at: stampAt, outcome: typeof outcome === 'string' ? outcome : (found.outcome ?? null) };
|
|
420
427
|
} else {
|
|
421
428
|
const slug = typeof fallbackSlug === 'string' ? fallbackSlug : '';
|
|
429
|
+
const finalOutcome = typeof outcome === 'string' ? outcome : null;
|
|
422
430
|
stamped = buildCandidate({
|
|
423
431
|
id:
|
|
424
432
|
typeof fallbackCandidateId === 'string' && fallbackCandidateId.length > 0
|
|
@@ -426,15 +434,40 @@ export function markCandidateProcessed({
|
|
|
426
434
|
: makeCandidateId(learningKey, slug),
|
|
427
435
|
learningKey,
|
|
428
436
|
slug,
|
|
429
|
-
|
|
437
|
+
// A minted record is TERMINAL from birth (`processed_at` is set two lines
|
|
438
|
+
// below), so `status` must agree with `outcome` instead of always saying
|
|
439
|
+
// `'proposed'` — a record reading `status:'proposed'` while carrying
|
|
440
|
+
// `outcome:'rejected'` is self-contradictory and misreads as a live
|
|
441
|
+
// proposal to anything that renders `status`. The vocabulary is exactly
|
|
442
|
+
// the two members of the ReconcileCandidate typedef; nothing new is
|
|
443
|
+
// invented here: `'rejected'` for the operator-rejection outcome
|
|
444
|
+
// (writer.mjs, issue #1042), `'proposed'` for the accepted outcomes
|
|
445
|
+
// (`'written'` / `'already-on-disk'`), where the proposal stood.
|
|
446
|
+
status: finalOutcome === 'rejected' ? 'rejected' : 'proposed',
|
|
430
447
|
reason: 'stamped without a prior sidecar record',
|
|
431
448
|
confidence: typeof fallbackConfidence === 'number' ? fallbackConfidence : 0,
|
|
432
449
|
createdAt: stampAt,
|
|
433
450
|
});
|
|
434
451
|
stamped.processed_at = stampAt;
|
|
435
|
-
stamped.outcome =
|
|
452
|
+
stamped.outcome = finalOutcome;
|
|
436
453
|
}
|
|
437
454
|
|
|
438
455
|
const result = mergeCandidates({ candidates: [stamped], repoRoot, storePath });
|
|
439
|
-
|
|
456
|
+
|
|
457
|
+
// Report what is PERSISTED, not what we asked for. `mergeCandidates`'s dedupe
|
|
458
|
+
// rule keeps an already-terminal existing record and DROPS the incoming one,
|
|
459
|
+
// so when `found` was already processed the store still holds the OLD
|
|
460
|
+
// `processed_at`/`outcome` — returning our freshly-built `stamped` would tell
|
|
461
|
+
// the caller a verdict that is nowhere on disk. `merged` IS the array that was
|
|
462
|
+
// just written, so reading the record back out of it is the honest answer.
|
|
463
|
+
const persisted = result.merged.find((rec) => rec && rec.learning_key === learningKey);
|
|
464
|
+
return {
|
|
465
|
+
written: result.written === true,
|
|
466
|
+
stamped: persisted ?? stamped,
|
|
467
|
+
// `true` iff a prior terminal verdict won and this call changed nothing on
|
|
468
|
+
// disk. `written` deliberately stays `true` in that case: the store IS in
|
|
469
|
+
// the intended terminal state, and writer.mjs treats `written:false` as a
|
|
470
|
+
// stamp FAILURE worth an operator-visible error.
|
|
471
|
+
alreadyProcessed: isTerminal(found),
|
|
472
|
+
};
|
|
440
473
|
}
|
|
@@ -457,26 +457,37 @@ function frontmatterRefusalReason(content) {
|
|
|
457
457
|
* not yet mature enough — silently, and with no way back short of editing the
|
|
458
458
|
* sidecar by hand.
|
|
459
459
|
*
|
|
460
|
-
*
|
|
461
|
-
*
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
* `
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
*
|
|
473
|
-
*
|
|
460
|
+
* PRIMARY discriminator (#1153 P15): the explicit `operatorRejected: true`
|
|
461
|
+
* flag that `skills/session-end/phase-3-6-tail.md` step 6 stamps on every
|
|
462
|
+
* proposal the operator left unselected before concatenating it into the one
|
|
463
|
+
* `rejected` array. `engine.mjs` pushes its own rejections as
|
|
464
|
+
* `{learningKey, type, reason, status:'rejected'}` and never sets the flag, so
|
|
465
|
+
* the two shapes are now distinguishable by a marker rather than by inference.
|
|
466
|
+
*
|
|
467
|
+
* FALLBACK (@deprecated — remove once no supported skill body predates the P15
|
|
468
|
+
* stamp): when the flag is absent, fall back to the old implicit signal — the
|
|
469
|
+
* rendered `content`. Engine rejections never reach the renderer, so they never
|
|
470
|
+
* carry `content`/`slug`/`path`, while an operator-declined proposal is a full
|
|
471
|
+
* `ReconcileProposal` whose `content` is the rule text the AUQ showed him. The
|
|
472
|
+
* fallback exists because the skill prose and this module ship together but
|
|
473
|
+
* consumer repos may pin an OLDER `phase-3-6-tail.md` that emits no flag;
|
|
474
|
+
* dropping it immediately would silently stop stamping their operator
|
|
475
|
+
* rejections terminal (the exact #1042 bug). REMOVAL TRIGGER: the next major
|
|
476
|
+
* release in which no supported consumer ships a pre-P15 skill body.
|
|
477
|
+
*
|
|
478
|
+
* Either way the decision stays conservative in the same direction: a false
|
|
479
|
+
* negative costs nothing (the learning is re-proposed next run); a false
|
|
480
|
+
* positive would permanently suppress a learning that was only capped.
|
|
481
|
+
*
|
|
482
|
+
* @param {WriterRejectedItem & {content?: unknown, learningKey?: unknown, operatorRejected?: unknown}} item
|
|
474
483
|
* @returns {boolean}
|
|
475
484
|
*/
|
|
476
485
|
function isOperatorRejection(item) {
|
|
477
486
|
if (!item || typeof item !== 'object') return false;
|
|
478
|
-
if (typeof item.
|
|
479
|
-
|
|
487
|
+
if (typeof item.learningKey !== 'string' || item.learningKey.length === 0) return false;
|
|
488
|
+
if (item.operatorRejected === true) return true;
|
|
489
|
+
// @deprecated fallback — pre-P15 skill bodies emit no flag; see above.
|
|
490
|
+
return typeof item.content === 'string' && item.content.length > 0;
|
|
480
491
|
}
|
|
481
492
|
|
|
482
493
|
// ---------------------------------------------------------------------------
|
|
@@ -663,8 +674,12 @@ export async function writeApprovedRules({
|
|
|
663
674
|
repoRoot,
|
|
664
675
|
});
|
|
665
676
|
if (!stampResult.written) {
|
|
677
|
+
// Same discrimination as the rejected path below (identical shape,
|
|
678
|
+
// identical reader): an already-terminal record is not a lost stamp.
|
|
666
679
|
errors.push(
|
|
667
|
-
|
|
680
|
+
stampResult.alreadyProcessed
|
|
681
|
+
? `sidecar-stamp for "${item.path ?? item.slug}" (learningKey=${item.learningKey}) wrote nothing — a prior terminal verdict is already on disk, so the rule file landed and the candidate stays terminal`
|
|
682
|
+
: `sidecar-stamp failed for "${item.path ?? item.slug}" (learningKey=${item.learningKey}) — rule file was written but the idempotency sidecar was not updated`,
|
|
668
683
|
);
|
|
669
684
|
}
|
|
670
685
|
}
|
|
@@ -724,8 +739,15 @@ export async function writeApprovedRules({
|
|
|
724
739
|
repoRoot,
|
|
725
740
|
});
|
|
726
741
|
if (!stampResult.written) {
|
|
742
|
+
// `alreadyProcessed` distinguishes the two failures that look
|
|
743
|
+
// identical from `written:false` alone. When a PRIOR terminal
|
|
744
|
+
// verdict is already on disk, the re-proposal warning is
|
|
745
|
+
// simply false — the sidecar holds the state we wanted — and
|
|
746
|
+
// telling the operator otherwise sends him after a non-bug.
|
|
727
747
|
errors.push(
|
|
728
|
-
|
|
748
|
+
stampResult.alreadyProcessed
|
|
749
|
+
? `sidecar-stamp for rejected "${item.learningKey}" wrote nothing — a prior terminal verdict is already on disk, so the rejection is recorded and it will NOT be re-proposed`
|
|
750
|
+
: `sidecar-stamp failed for rejected "${item.learningKey}" — the rejection was archived but the idempotency sidecar was not updated, so a later run may re-propose it`,
|
|
729
751
|
);
|
|
730
752
|
}
|
|
731
753
|
}
|