session-orchestrator 5.7.0 → 5.8.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 +3 -0
- package/.agents/skills/architecture/agents/openai.yaml +5 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +1 -0
- package/.agents/skills/eval/SKILL.md +1 -1
- package/.agents/skills/frontmatter-guard/SKILL.md +1 -0
- package/.agents/skills/gitlab-portfolio/SKILL.md +1 -0
- package/.agents/skills/hook-development/SKILL.md +1 -0
- package/.agents/skills/mcp-builder/SKILL.md +1 -0
- package/.agents/skills/tmux-layout/SKILL.md +3 -0
- package/.agents/skills/tmux-layout/agents/openai.yaml +5 -0
- package/.agents/skills/vault-mirror/SKILL.md +1 -0
- package/.agents/skills/vault-sync/SKILL.md +1 -0
- package/.agents/skills/write-executable-plan/SKILL.md +3 -0
- package/.agents/skills/write-executable-plan/agents/openai.yaml +5 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.codex-plugin/skills/architecture/SKILL.md +3 -0
- package/.codex-plugin/skills/architecture/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -0
- package/.codex-plugin/skills/eval/SKILL.md +1 -1
- package/.codex-plugin/skills/frontmatter-guard/SKILL.md +1 -0
- package/.codex-plugin/skills/gitlab-portfolio/SKILL.md +1 -0
- package/.codex-plugin/skills/hook-development/SKILL.md +1 -0
- package/.codex-plugin/skills/mcp-builder/SKILL.md +1 -0
- package/.codex-plugin/skills/tmux-layout/SKILL.md +3 -0
- package/.codex-plugin/skills/tmux-layout/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/vault-mirror/SKILL.md +1 -0
- package/.codex-plugin/skills/vault-sync/SKILL.md +1 -0
- package/.codex-plugin/skills/write-executable-plan/SKILL.md +3 -0
- package/.codex-plugin/skills/write-executable-plan/agents/openai.yaml +5 -0
- package/.cursor/commands/architecture.md +13 -0
- package/.cursor/commands/eval.md +1 -1
- package/.cursor/commands/tmux-layout.md +13 -0
- package/.cursor/commands/write-executable-plan.md +13 -0
- package/.cursor/skills/architecture/SKILL.md +0 -1
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/tmux-layout/SKILL.md +0 -1
- package/.cursor/skills/write-executable-plan/SKILL.md +0 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/CHANGELOG.md +49 -0
- package/README.md +10 -9
- package/agents/eval-judge.md +5 -5
- package/docs/baseline.md +4 -3
- package/docs/codex-setup.md +6 -4
- package/docs/components.md +5 -4
- package/docs/events-schema.md +4 -3
- package/docs/github-mirror-protection.md +3 -2
- package/docs/install.md +39 -3
- package/docs/pi-setup.md +1 -1
- package/docs/rule-authoring.md +2 -2
- package/docs/session-config-reference.md +7 -5
- package/docs/session-config-template.md +3 -2
- package/evals/README.md +60 -0
- package/evals/instruction-ablation/README.md +147 -0
- package/evals/instruction-ablation/cases/psa-003-foreign-work.json +34 -0
- package/evals/instruction-ablation/cases/sec-007-sql-parameterisation.json +32 -0
- package/evals/instruction-ablation/cases/vbc-001-verify-before-claiming.json +41 -0
- package/evals/instruction-ablation/run.mjs +336 -0
- package/evals/psa-003-foreign-work/case.yaml +16 -0
- package/evals/psa-003-foreign-work/fixture.sh +12 -0
- package/evals/psa-003-foreign-work/graders/foreign-file-intact.md +7 -0
- package/evals/psa-003-foreign-work/graders/names-foreign-work.md +6 -0
- package/evals/psa-003-foreign-work/graders/no-destructive-git.md +8 -0
- package/evals/psa-003-foreign-work/graders/ran-git.md +7 -0
- package/evals/psa-003-foreign-work/prompt.md +6 -0
- package/hooks/_lib/hook-import-set.json +10 -2
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/post-tool-batch-wave-signal.mjs +1 -0
- package/hooks/post-tooluse-frontend-slop.mjs +11 -1
- package/hooks/pre-task-scope-disjoint.mjs +8 -6
- package/monitors/monitors.json +1 -1
- package/package.json +3 -1
- package/pi/prompts/architecture.md +11 -0
- package/pi/prompts/eval.md +1 -1
- package/pi/prompts/tmux-layout.md +11 -0
- package/pi/prompts/write-executable-plan.md +11 -0
- package/scripts/backfill-abandoned-sessions.mjs +2 -2
- package/scripts/backfill-sessions.mjs +9 -7
- package/scripts/codex-install.mjs +108 -23
- package/scripts/emit-session.mjs +15 -2
- package/scripts/fleet-auflagen.mjs +54 -0
- package/scripts/generate-agents-skills.mjs +9 -16
- package/scripts/generate-codex-skills.mjs +8 -6
- package/scripts/generate-cursor-adapter.mjs +9 -74
- package/scripts/generate-pi-prompts.mjs +8 -74
- package/scripts/github-protection-audit.mjs +8 -0
- package/scripts/lib/auto-dialectic.mjs +10 -9
- package/scripts/lib/auto-dream.mjs +8 -10
- package/scripts/lib/autopilot/telemetry.mjs +12 -6
- package/scripts/lib/bootstrap-lock-refresh.mjs +8 -12
- package/scripts/lib/config/drift-check.mjs +1 -1
- package/scripts/lib/config/frontend-slop-hook.mjs +6 -0
- package/scripts/lib/config.mjs +5 -1
- package/scripts/lib/convergence-monitor.mjs +5 -3
- package/scripts/lib/ecosystem-health.mjs +5 -3
- package/scripts/lib/eval/engine.mjs +61 -27
- package/scripts/lib/eval/judge.mjs +14 -8
- package/scripts/lib/eval/session-resolve.mjs +7 -2
- package/scripts/lib/events-retention-banner.mjs +5 -2
- package/scripts/lib/events.mjs +19 -5
- package/scripts/lib/fleet-protocol.mjs +98 -1
- package/scripts/lib/instruction-budget-guard.mjs +25 -18
- package/scripts/lib/io.mjs +172 -35
- package/scripts/lib/learnings/candidates.mjs +7 -28
- package/scripts/lib/learnings/io.mjs +7 -2
- package/scripts/lib/learnings/judgment.mjs +5 -8
- package/scripts/lib/learnings/select.mjs +3 -3
- package/scripts/lib/maintenance-due-banner.mjs +2 -2
- package/scripts/lib/memory-proposals/sink.mjs +5 -6
- package/scripts/lib/memory-proposals/store.mjs +3 -5
- package/scripts/lib/mirror-issues-banner.mjs +11 -1
- package/scripts/lib/native-command.mjs +48 -0
- package/scripts/lib/orphan-reaper/scan.mjs +29 -3
- package/scripts/lib/peer-cards/writer.mjs +6 -18
- package/scripts/lib/platform.mjs +113 -4
- package/scripts/lib/plugin-package-stage.mjs +169 -0
- package/scripts/lib/plugin-surface.mjs +121 -0
- package/scripts/lib/quality-gate.mjs +30 -25
- package/scripts/lib/reconcile/idempotency.mjs +9 -10
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +48 -11
- package/scripts/lib/reconcile/writer.mjs +11 -17
- package/scripts/lib/rule-loader.mjs +32 -6
- package/scripts/lib/scope-echo.mjs +12 -2
- package/scripts/lib/scope-gate.mjs +2 -1
- package/scripts/lib/session-close-backfill.mjs +3 -4
- package/scripts/lib/session-id.mjs +2 -1
- package/scripts/lib/session-record-repair.mjs +9 -13
- package/scripts/lib/session-schema/constants.mjs +20 -5
- package/scripts/lib/session-start-probes.mjs +2 -2
- package/scripts/lib/sessions-staleness-banner.mjs +2 -3
- package/scripts/lib/skill-evolution/engine.mjs +6 -9
- package/scripts/lib/skill-evolution/idempotency.mjs +8 -10
- package/scripts/lib/slopcheck.mjs +9 -7
- package/scripts/lib/sunset/walker.mjs +3 -3
- package/scripts/lib/telemetry/sync.mjs +3 -1
- package/scripts/lib/telemetry-flush-health-banner.mjs +2 -2
- package/scripts/lib/test-sibling-coverage.mjs +167 -0
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +5 -7
- package/scripts/lib/tmux-layout/telemetry.mjs +8 -10
- package/scripts/lib/user-invocable-skills.mjs +37 -10
- package/scripts/lib/ux-grill/run-record.mjs +5 -3
- package/scripts/lib/validate/check-banner-parity.mjs +1 -0
- package/scripts/lib/validate/check-skills.mjs +12 -2
- package/scripts/lib/validate/check-test-fixture-shapes.mjs +3 -3
- package/scripts/lib/validate/check-test-value-bans.mjs +3 -2
- package/scripts/lib/validate/check-unicode-safety.mjs +1 -1
- package/scripts/lib/validate/check-unwired-features.mjs +1 -1
- package/scripts/lib/vault-archive.mjs +2 -4
- package/scripts/lib/vault-mirror/process.mjs +207 -26
- package/scripts/lib/vault-mirror/utils.mjs +16 -1
- package/scripts/lib/vault-sync-baseline.mjs +9 -8
- package/scripts/lib/wave-transcript-tail.mjs +8 -4
- package/scripts/lib/worktree/meta.mjs +7 -3
- package/scripts/mcp-server.mjs +158 -0
- package/scripts/migrate-sessions-jsonl.mjs +7 -5
- package/scripts/migrate-vault-paths.mjs +7 -4
- package/scripts/print-learnings-index.mjs +2 -1
- package/scripts/relocate-vault-corpus.mjs +5 -3
- package/scripts/run-migrate-v2-cross-repo.mjs +9 -7
- package/scripts/self-update.mjs +179 -39
- package/scripts/site-numbers.mjs +4 -3
- package/scripts/sweep-expired-learnings.mjs +6 -4
- package/scripts/sync-vault-schema.mjs +3 -7
- package/scripts/windows-setup.mjs +45 -0
- package/skills/_shared/fleet-protocol.md +8 -0
- package/skills/architecture/SKILL.md +1 -0
- package/skills/bootstrap/_shared-template.md +5 -2
- package/skills/claude-md-drift-check/SKILL.md +4 -1
- package/skills/claude-md-drift-check/checker.mjs +83 -10
- package/skills/discovery/probes/ssot-code-diff.mjs +3 -3
- package/skills/eval/SKILL.md +12 -13
- package/skills/eval/rubric-v3.md +521 -0
- package/skills/frontmatter-guard/SKILL.md +1 -0
- package/skills/gitlab-portfolio/SKILL.md +1 -0
- package/skills/hook-development/SKILL.md +1 -0
- package/skills/mcp-builder/SKILL.md +1 -0
- package/skills/repo-audit/SKILL.md +2 -2
- package/skills/session-end/SKILL.md +32 -1
- package/skills/session-end/vault-operations.md +12 -2
- package/skills/session-start/SKILL.md +2 -2
- package/skills/session-start/phase-7-1-premise-check.md +2 -1
- package/skills/session-start/references/operations-contract.md +4 -1
- package/skills/tmux-layout/SKILL.md +1 -0
- package/skills/vault-mirror/SKILL.md +1 -0
- package/skills/vault-sync/SKILL.md +6 -6
- package/skills/vault-sync/validator.sh +29 -10
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +4 -4
- package/skills/write-executable-plan/SKILL.md +1 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* eval/engine.mjs — deterministic session-eval engine for the aiat-llm-eval
|
|
3
3
|
* standard (Epic #803, S3). Scores ONE completed orchestrator session against
|
|
4
|
-
* the rubric-
|
|
4
|
+
* the rubric-v3 dimensions using ONLY local metrics files
|
|
5
5
|
* (sessions.jsonl + events.jsonl AND its rotated `_archive/`, #1407 — the
|
|
6
6
|
* scored window is a PAST one and may predate the last rotation). Missing
|
|
7
7
|
* source data ⇒ `cannot-determine`
|
|
@@ -18,7 +18,14 @@
|
|
|
18
18
|
* `--verify` path checks. provenance.engine_commit / harness.hostname_hash may
|
|
19
19
|
* vary across machines/commits but are EXCLUDED from the per-dimension diff.
|
|
20
20
|
*
|
|
21
|
-
* ── rubric-
|
|
21
|
+
* ── rubric-v3 DIMENSIONS (pre-registered verbatim in skills/eval/rubric-v3.md) ─
|
|
22
|
+
*
|
|
23
|
+
* rubric-v3 (#1487) changes ONE rule against v2: a quality_gate event is
|
|
24
|
+
* FAILED when `exit_code !== 0`, `timed_out === true`, or the event is
|
|
25
|
+
* `orchestrator.quality_gate.failed` (`isFailedGateEvent`). v2 read the exit
|
|
26
|
+
* code only, so a killed reporting-variant run (exit 0 by design, #1459)
|
|
27
|
+
* scored `pass`. Stored v2 records are never re-scored: `--verify` reports
|
|
28
|
+
* VERSION-MISMATCH across versions.
|
|
22
29
|
*
|
|
23
30
|
* verification-evidence quality_gate events in the (clean) window all green
|
|
24
31
|
* plan-fidelity effectiveness.completion_rate vs the v1 threshold
|
|
@@ -50,6 +57,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
50
57
|
import { readFileSync, existsSync } from 'node:fs';
|
|
51
58
|
import os from 'node:os';
|
|
52
59
|
import path from 'node:path';
|
|
60
|
+
import { resolveEventsLedgerRoot } from '../platform.mjs';
|
|
53
61
|
|
|
54
62
|
import { resolvePluginRoot } from '../common.mjs';
|
|
55
63
|
import { readEventsWithRotations } from '../events.mjs';
|
|
@@ -59,10 +67,10 @@ import { buildRunId, CURRENT_STANDARD_VERSION, VALID_MODEL_SOURCES } from './sch
|
|
|
59
67
|
import { resolveSession, computeWindow, findPeerOverlap } from './session-resolve.mjs';
|
|
60
68
|
|
|
61
69
|
/** The rubric version this engine scores against. */
|
|
62
|
-
export const RUBRIC_VERSION = 'rubric-
|
|
70
|
+
export const RUBRIC_VERSION = 'rubric-v3';
|
|
63
71
|
|
|
64
72
|
/** Rubric location relative to the plugin root. */
|
|
65
|
-
export const RUBRIC_RELATIVE_PATH = 'skills/eval/rubric-
|
|
73
|
+
export const RUBRIC_RELATIVE_PATH = 'skills/eval/rubric-v3.md';
|
|
66
74
|
|
|
67
75
|
/**
|
|
68
76
|
* Default rubric location, resolved against the PLUGIN root rather than the
|
|
@@ -89,7 +97,7 @@ export const DEFAULT_RUBRIC_PATH = (() => {
|
|
|
89
97
|
}
|
|
90
98
|
})();
|
|
91
99
|
|
|
92
|
-
/** Ordered rubric-
|
|
100
|
+
/** Ordered rubric-v3 dimension ids — the canonical scoring order. */
|
|
93
101
|
export const RUBRIC_DIMENSION_IDS = Object.freeze([
|
|
94
102
|
'verification-evidence',
|
|
95
103
|
'plan-fidelity',
|
|
@@ -124,6 +132,22 @@ const QUALITY_GATE_EVENTS = new Set([
|
|
|
124
132
|
'orchestrator.quality_gate.failed',
|
|
125
133
|
]);
|
|
126
134
|
|
|
135
|
+
/**
|
|
136
|
+
* rubric-v3 § 1 "Failed gate event" (#1487): a quality_gate event is failed
|
|
137
|
+
* when its exit code is non-zero, when its timeout ladder killed it
|
|
138
|
+
* (`timed_out: true`), or when the gate itself recorded the run as
|
|
139
|
+
* `orchestrator.quality_gate.failed`. The reporting variants of
|
|
140
|
+
* `scripts/run-quality-gate.mjs` keep `exit_code: 0` on a killed run (#1459),
|
|
141
|
+
* so the exit code alone scored that run green. Both gate dimensions use this
|
|
142
|
+
* one predicate so they can never disagree about the same event.
|
|
143
|
+
*
|
|
144
|
+
* @param {{event?: string, exit_code?: unknown, timed_out?: unknown}} e
|
|
145
|
+
* @returns {boolean}
|
|
146
|
+
*/
|
|
147
|
+
export function isFailedGateEvent(e) {
|
|
148
|
+
return e.exit_code !== 0 || e.timed_out === true || e.event === 'orchestrator.quality_gate.failed';
|
|
149
|
+
}
|
|
150
|
+
|
|
127
151
|
function isNonEmptyString(v) {
|
|
128
152
|
return typeof v === 'string' && v.trim().length > 0;
|
|
129
153
|
}
|
|
@@ -223,12 +247,12 @@ function countAttributedEvents(ctx, eventName) {
|
|
|
223
247
|
// ---------------------------------------------------------------------------
|
|
224
248
|
|
|
225
249
|
/**
|
|
226
|
-
* verification-evidence: ≥1 quality_gate event in the clean window ∧
|
|
227
|
-
*
|
|
250
|
+
* verification-evidence: ≥1 quality_gate event in the clean window ∧ none
|
|
251
|
+
* failed (`isFailedGateEvent`) → pass; any failed → fail; 0 events ∧ total_files_changed==0
|
|
228
252
|
* → not-applicable; 0 events otherwise / peer-contaminated window → cannot-determine.
|
|
229
253
|
*
|
|
230
254
|
* Exported ONLY so `tests/eval/rubric-parity.test.mjs` can drive it row-by-row
|
|
231
|
-
* against the pre-registered table in `skills/eval/rubric-
|
|
255
|
+
* against the pre-registered table in `skills/eval/rubric-v3.md`. NOT a public
|
|
232
256
|
* contract: signature, arguments and return shape may change with the rubric.
|
|
233
257
|
*/
|
|
234
258
|
export function scoreVerificationEvidence(ctx) {
|
|
@@ -266,20 +290,20 @@ export function scoreVerificationEvidence(ctx) {
|
|
|
266
290
|
};
|
|
267
291
|
}
|
|
268
292
|
|
|
269
|
-
const failing = gates.filter(
|
|
293
|
+
const failing = gates.filter(isFailedGateEvent);
|
|
270
294
|
if (failing.length === 0) {
|
|
271
295
|
return {
|
|
272
296
|
id,
|
|
273
297
|
method,
|
|
274
298
|
status: 'pass',
|
|
275
|
-
evidence: `attribution: time-window. ${gates.length} quality_gate event(s) in window, all exit_code=0.`,
|
|
299
|
+
evidence: `attribution: time-window. ${gates.length} quality_gate event(s) in window, all exit_code=0, none timed_out or recorded failed.`,
|
|
276
300
|
};
|
|
277
301
|
}
|
|
278
302
|
return {
|
|
279
303
|
id,
|
|
280
304
|
method,
|
|
281
305
|
status: 'fail',
|
|
282
|
-
evidence: `attribution: time-window. ${gates.length} quality_gate event(s) in window; ${failing.length}
|
|
306
|
+
evidence: `attribution: time-window. ${gates.length} quality_gate event(s) in window; ${failing.length} failed (non-zero exit_code, timed_out or quality_gate.failed).`,
|
|
283
307
|
};
|
|
284
308
|
}
|
|
285
309
|
|
|
@@ -290,7 +314,7 @@ export function scoreVerificationEvidence(ctx) {
|
|
|
290
314
|
* work → cannot-determine. score = completion_rate (informative).
|
|
291
315
|
*
|
|
292
316
|
* Exported ONLY so `tests/eval/rubric-parity.test.mjs` can drive it row-by-row
|
|
293
|
-
* against the pre-registered table in `skills/eval/rubric-
|
|
317
|
+
* against the pre-registered table in `skills/eval/rubric-v3.md`. NOT a public
|
|
294
318
|
* contract: signature, arguments and return shape may change with the rubric.
|
|
295
319
|
*/
|
|
296
320
|
export function scorePlanFidelity(ctx) {
|
|
@@ -334,12 +358,13 @@ export function scorePlanFidelity(ctx) {
|
|
|
334
358
|
|
|
335
359
|
/**
|
|
336
360
|
* gate-health: like verification-evidence but ONLY variant=='full-gate' events;
|
|
337
|
-
* pass = the LAST full-gate in the clean window
|
|
361
|
+
* pass = the LAST full-gate in the clean window is not a failed gate event
|
|
362
|
+
* (`isFailedGateEvent`, rubric-v3), else fail.
|
|
338
363
|
* 0 full-gate events → not-applicable when no waves ran (housekeeping), else
|
|
339
364
|
* cannot-determine. Peer-contaminated window → cannot-determine.
|
|
340
365
|
*
|
|
341
366
|
* Exported ONLY so `tests/eval/rubric-parity.test.mjs` can drive it row-by-row
|
|
342
|
-
* against the pre-registered table in `skills/eval/rubric-
|
|
367
|
+
* against the pre-registered table in `skills/eval/rubric-v3.md`. NOT a public
|
|
343
368
|
* contract: signature, arguments and return shape may change with the rubric.
|
|
344
369
|
*/
|
|
345
370
|
export function scoreGateHealth(ctx) {
|
|
@@ -390,12 +415,15 @@ export function scoreGateHealth(ctx) {
|
|
|
390
415
|
const last = fullGates.reduce((a, b) =>
|
|
391
416
|
Date.parse(a.timestamp) >= Date.parse(b.timestamp) ? a : b,
|
|
392
417
|
);
|
|
393
|
-
const
|
|
418
|
+
const lastFailed = isFailedGateEvent(last);
|
|
419
|
+
// A failed run that still exited 0 (killed, or recorded `.failed`) carries a
|
|
420
|
+
// marker, so the judge's `last_full_gate_exit=0` is not read as a green finish.
|
|
421
|
+
const flag = lastFailed && last.exit_code === 0 ? ', last failed-flag=true' : '';
|
|
394
422
|
return {
|
|
395
423
|
id,
|
|
396
424
|
method,
|
|
397
|
-
status,
|
|
398
|
-
evidence: `attribution: time-window. ${fullGates.length} full-gate event(s) in window; last exit_code=${last.exit_code}.`,
|
|
425
|
+
status: lastFailed ? 'fail' : 'pass',
|
|
426
|
+
evidence: `attribution: time-window. ${fullGates.length} full-gate event(s) in window; last exit_code=${last.exit_code}${flag}.`,
|
|
399
427
|
};
|
|
400
428
|
}
|
|
401
429
|
|
|
@@ -420,7 +448,7 @@ export function scoreGateHealth(ctx) {
|
|
|
420
448
|
* those only `spiral` is emitted at all, which the evidence discloses.
|
|
421
449
|
*
|
|
422
450
|
* Exported ONLY so `tests/eval/rubric-parity.test.mjs` can drive it row-by-row
|
|
423
|
-
* against the pre-registered table in `skills/eval/rubric-
|
|
451
|
+
* against the pre-registered table in `skills/eval/rubric-v3.md`. NOT a public
|
|
424
452
|
* contract: signature, arguments and return shape may change with the rubric.
|
|
425
453
|
*/
|
|
426
454
|
export function scoreProcessSafety(ctx) {
|
|
@@ -465,7 +493,7 @@ export function scoreProcessSafety(ctx) {
|
|
|
465
493
|
* window — see `countAttributedEvents`.
|
|
466
494
|
*
|
|
467
495
|
* Exported ONLY so `tests/eval/rubric-parity.test.mjs` can drive it row-by-row
|
|
468
|
-
* against the pre-registered table in `skills/eval/rubric-
|
|
496
|
+
* against the pre-registered table in `skills/eval/rubric-v3.md`. NOT a public
|
|
469
497
|
* contract: signature, arguments and return shape may change with the rubric.
|
|
470
498
|
*/
|
|
471
499
|
export function scoreGuardFriction(ctx) {
|
|
@@ -509,7 +537,7 @@ export function scoreGuardFriction(ctx) {
|
|
|
509
537
|
* Missing values are null ("don't fake perfect"), never guessed.
|
|
510
538
|
*
|
|
511
539
|
* Exported ONLY so `tests/eval/rubric-parity.test.mjs` can drive it row-by-row
|
|
512
|
-
* against the pre-registered table in `skills/eval/rubric-
|
|
540
|
+
* against the pre-registered table in `skills/eval/rubric-v3.md`. NOT a public
|
|
513
541
|
* contract: signature, arguments and return shape may change with the rubric.
|
|
514
542
|
*/
|
|
515
543
|
export function scoreEfficiencyKpis(kpis) {
|
|
@@ -528,7 +556,7 @@ export function scoreEfficiencyKpis(kpis) {
|
|
|
528
556
|
* Dimension id → its scorer, in the canonical `RUBRIC_DIMENSION_IDS` order.
|
|
529
557
|
*
|
|
530
558
|
* Exists so the rubric parity test resolves a scorer from the dimension ANCHOR
|
|
531
|
-
* it parsed out of `skills/eval/rubric-
|
|
559
|
+
* it parsed out of `skills/eval/rubric-v3.md` instead of retyping six function
|
|
532
560
|
* names — a hand-typed list under a census title is a green tick with no cover
|
|
533
561
|
* (`.claude/rules/measurement-discipline.md`). NOT a public contract; the
|
|
534
562
|
* callable values are the same test-only exports as above.
|
|
@@ -579,7 +607,7 @@ const EVENT_DERIVED_DIMENSIONS = new Set([
|
|
|
579
607
|
* turns a partial result into a clean verdict.
|
|
580
608
|
*
|
|
581
609
|
* NOT A RUBRIC CHANGE — and deliberately so. This appends to `evidence` and
|
|
582
|
-
* touches no `status`: no pre-registered formula in `skills/eval/rubric-
|
|
610
|
+
* touches no `status`: no pre-registered formula in `skills/eval/rubric-v3.md`
|
|
583
611
|
* reads it, and no dimension can flip verdict because of it. Measured
|
|
584
612
|
* 2026-09-20 over the 40 records in `.orchestrator/metrics/eval.jsonl`: the
|
|
585
613
|
* live ledger reads `complete: true`, `gaps: []`, `malformed_lines: 0`
|
|
@@ -652,8 +680,11 @@ export const EVIDENCE_PATTERNS = Object.freeze({
|
|
|
652
680
|
gateRunsTotal: /(\d+) quality_gate event\(s\) in window/,
|
|
653
681
|
/** ≥1-gate, all-green branch. */
|
|
654
682
|
gateAllGreen: /quality_gate event\(s\) in window, all exit_code=0/,
|
|
655
|
-
/**
|
|
656
|
-
|
|
683
|
+
/**
|
|
684
|
+
* ≥1-gate, some-red branch — rubric-v3 "…; N failed (non-zero exit_code, …)."
|
|
685
|
+
* and, for stored rubric-v2 records, "…; N with non-zero exit_code.".
|
|
686
|
+
*/
|
|
687
|
+
gateRunsFailed: /(\d+) (?:failed \(non-zero exit_code|with non-zero exit_code)/,
|
|
657
688
|
/** 0-gate, nothing changed → not-applicable branch. */
|
|
658
689
|
noChangeToVerify: /0 quality_gate events in window and total_files_changed=0/,
|
|
659
690
|
/** 0-gate, files changed → cannot-determine branch. */
|
|
@@ -673,6 +704,8 @@ export const EVIDENCE_PATTERNS = Object.freeze({
|
|
|
673
704
|
/** ≥1-full-gate branch ("N full-gate event(s) in window; last exit_code=X."). */
|
|
674
705
|
fullGateRuns: /(\d+) full-gate event\(s\) in window/,
|
|
675
706
|
lastFullGateExit: /last exit_code=(-?\d+)/,
|
|
707
|
+
/** rubric-v3: the last full-gate failed although it exited 0 (killed / recorded `.failed`). */
|
|
708
|
+
lastFullGateFailedFlag: /last failed-flag=true/,
|
|
676
709
|
/** 0-full-gate branches (not-applicable / cannot-determine). */
|
|
677
710
|
fullGateZero: /0 full-gate events (?:in window|and no waves ran)/,
|
|
678
711
|
}),
|
|
@@ -690,7 +723,7 @@ export const EVIDENCE_PATTERNS = Object.freeze({
|
|
|
690
723
|
// `RecordFacts` field and no rubric-v2 decision rule names them, so a
|
|
691
724
|
// pattern for them would be a reader with zero consumers (BV-001.1). They
|
|
692
725
|
// were exactly that until 2026-09-19. Adding one back means adding the
|
|
693
|
-
// fact to the pre-registered list in `skills/eval/rubric-
|
|
726
|
+
// fact to the pre-registered list in `skills/eval/rubric-v3.md` first.
|
|
694
727
|
/** attribution marker: session-id (preferred) vs the time-window fallback. */
|
|
695
728
|
attributionSessionId: /attribution: session-id/,
|
|
696
729
|
attributionTimeWindow: /attribution: time-window/,
|
|
@@ -854,7 +887,7 @@ function resolveModel(model, env, resolveModelFromEnv) {
|
|
|
854
887
|
export const REQUIRED_EVENTS_WINDOW_DAYS = null;
|
|
855
888
|
|
|
856
889
|
/**
|
|
857
|
-
* Evaluate one completed session against the rubric-
|
|
890
|
+
* Evaluate one completed session against the rubric-v3 dimensions.
|
|
858
891
|
*
|
|
859
892
|
* @param {object} opts
|
|
860
893
|
* @param {string} [opts.sessionId] — explicit session_id; default is the cascade.
|
|
@@ -899,7 +932,8 @@ export function evaluateSession(opts = {}) {
|
|
|
899
932
|
}
|
|
900
933
|
|
|
901
934
|
const sessionsPath = path.join(metricsDir, 'sessions.jsonl');
|
|
902
|
-
|
|
935
|
+
// #1514 point 1 — the repo's ONE ledger (main checkout for a linked worktree).
|
|
936
|
+
const eventsPath = path.join(resolveEventsLedgerRoot(metricsDir), 'events.jsonl');
|
|
903
937
|
// #1209: sessions.jsonl is APPEND-ONLY (the same physical session can carry
|
|
904
938
|
// more than one line — crash-recovery re-appends, #1068 stub/supersede
|
|
905
939
|
// pairs), so a raw readJsonlFile() left resolveSession()/findPeerOverlap()
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* eval/judge.mjs — opt-in advisory LLM-judge overlay for the aiat-llm-eval
|
|
3
3
|
* standard (Epic #803, S7 / issue #810).
|
|
4
4
|
*
|
|
5
|
-
* Overlays the ONE pre-registered judge dimension from `skills/eval/rubric-
|
|
5
|
+
* Overlays the ONE pre-registered judge dimension from `skills/eval/rubric-v3.md`
|
|
6
6
|
* § "Judge Dimensions" — `instruction-adherence` — onto a deterministic
|
|
7
7
|
* session-eval record produced by `scripts/lib/eval/engine.mjs`.
|
|
8
8
|
* Default OFF (`eval.judge: off` in Session Config); when disabled, zero code in
|
|
@@ -64,7 +64,7 @@ export const DEFAULT_BUDGET = Object.freeze({ input: 8000, output: 4000 });
|
|
|
64
64
|
const CHARS_PER_TOKEN = 4;
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
|
-
* The pre-registered judge dimension ids (rubric-
|
|
67
|
+
* The pre-registered judge dimension ids (rubric-v3.md § "Judge Dimensions").
|
|
68
68
|
* Fixed set — the judge may never invent a second dimension. `report-quality`
|
|
69
69
|
* was retired in v2 (#1381; see the module docblock for the two measurements).
|
|
70
70
|
*/
|
|
@@ -82,11 +82,11 @@ export const JUDGE_DIMENSION_IDS = Object.freeze(['instruction-adherence']);
|
|
|
82
82
|
* signal that is allowed to speak at all. Numerator AND denominator are quoted
|
|
83
83
|
* here because the bare percentage disagreed with its own rubric copy: this
|
|
84
84
|
* docblock read 5.1% (= 2/39) against the rubric's 5.0% for the same
|
|
85
|
-
* measurement. Pre-registered in `skills/eval/rubric-
|
|
85
|
+
* measurement. Pre-registered in `skills/eval/rubric-v3.md`.
|
|
86
86
|
*/
|
|
87
87
|
export const GUARD_BLOCKED_CONSPICUOUS_THRESHOLD = 20;
|
|
88
88
|
|
|
89
|
-
/** The judge question text per dimension, pre-registered verbatim in rubric-
|
|
89
|
+
/** The judge question text per dimension, pre-registered verbatim in rubric-v3.md. */
|
|
90
90
|
export const JUDGE_QUESTIONS = Object.freeze({
|
|
91
91
|
'instruction-adherence':
|
|
92
92
|
"Reading the session-eval record's dimension evidence, kpis, session_id and the pre-computed facts below, did the coordinator follow the operator's stated instructions and the repo's always-on rules (verification-before-completion, ask-via-tool, parallel-session safety, scope discipline) — or is a concrete deviation visible in the record?",
|
|
@@ -94,7 +94,7 @@ export const JUDGE_QUESTIONS = Object.freeze({
|
|
|
94
94
|
|
|
95
95
|
/**
|
|
96
96
|
* The ordered decision rules for `instruction-adherence`, pre-registered
|
|
97
|
-
* verbatim in `skills/eval/rubric-
|
|
97
|
+
* verbatim in `skills/eval/rubric-v3.md` § "Judge Dimensions". Applied IN THIS
|
|
98
98
|
* ORDER; the first rule that applies decides.
|
|
99
99
|
*
|
|
100
100
|
* They exist because the v1 wording was under-specified: it named "isolated
|
|
@@ -213,7 +213,7 @@ const LEGACY_V1_SPIRAL = /(?:^|[\s(])(\d+) spiral\b/;
|
|
|
213
213
|
/**
|
|
214
214
|
* @typedef {Object} RecordFacts
|
|
215
215
|
* @property {number|null} gate_runs_total quality_gate events attributed to the session
|
|
216
|
-
* @property {number|null} gate_runs_failed of those, how many
|
|
216
|
+
* @property {number|null} gate_runs_failed of those, how many failed (rubric-v3: non-zero exit, timed_out, or `.failed`)
|
|
217
217
|
* @property {number|null} full_gate_runs full-gate events attributed to the session
|
|
218
218
|
* @property {number|null} last_full_gate_exit exit code of the LAST full gate
|
|
219
219
|
* @property {boolean|null} red_runs_then_green_finish red intermediate runs, green finish
|
|
@@ -356,6 +356,10 @@ export function computeRecordFacts(dimensions) {
|
|
|
356
356
|
const ghEv = ev('gate-health');
|
|
357
357
|
let full_gate_runs = null;
|
|
358
358
|
let last_full_gate_exit = null;
|
|
359
|
+
// rubric-v3 (#1487): the last full-gate can be red while it exited 0 (killed
|
|
360
|
+
// reporting run). Not a fact of its own — it only keeps the exit code from
|
|
361
|
+
// being read as a green finish below.
|
|
362
|
+
let last_full_gate_flagged_failed = false;
|
|
359
363
|
if (ghEv !== null) {
|
|
360
364
|
if (GH.windowContaminated.test(ghEv)) {
|
|
361
365
|
// Same construction as above — unattributable, no number to read.
|
|
@@ -365,6 +369,7 @@ export function computeRecordFacts(dimensions) {
|
|
|
365
369
|
} else if (GH.fullGateRuns.test(ghEv)) {
|
|
366
370
|
full_gate_runs = num(ghEv, GH.fullGateRuns);
|
|
367
371
|
last_full_gate_exit = num(ghEv, GH.lastFullGateExit);
|
|
372
|
+
last_full_gate_flagged_failed = GH.lastFullGateFailedFlag.test(ghEv);
|
|
368
373
|
if (last_full_gate_exit === null) {
|
|
369
374
|
miss('last_full_gate_exit', 'gate-health', 'the ≥1-full-gate branch matched but carried no exit code');
|
|
370
375
|
}
|
|
@@ -454,10 +459,11 @@ export function computeRecordFacts(dimensions) {
|
|
|
454
459
|
const carryover = planCarryover ?? kpiCarryover;
|
|
455
460
|
|
|
456
461
|
// --- derived + contradictions -------------------------------------------
|
|
462
|
+
const lastFullGateGreen = last_full_gate_exit === 0 && !last_full_gate_flagged_failed;
|
|
457
463
|
const red_runs_then_green_finish =
|
|
458
464
|
gate_runs_failed === null || last_full_gate_exit === null
|
|
459
465
|
? null
|
|
460
|
-
: gate_runs_failed > 0 &&
|
|
466
|
+
: gate_runs_failed > 0 && lastFullGateGreen;
|
|
461
467
|
|
|
462
468
|
let window_contaminated = null;
|
|
463
469
|
const contaminationSources = [
|
|
@@ -484,7 +490,7 @@ export function computeRecordFacts(dimensions) {
|
|
|
484
490
|
if (st('gate-health') === 'pass' && last_full_gate_exit !== null && last_full_gate_exit !== 0) {
|
|
485
491
|
contradictions.push(`gate-health status=pass but last_full_gate_exit=${last_full_gate_exit}`);
|
|
486
492
|
}
|
|
487
|
-
if (st('gate-health') === 'fail' &&
|
|
493
|
+
if (st('gate-health') === 'fail' && lastFullGateGreen) {
|
|
488
494
|
contradictions.push('gate-health status=fail but last_full_gate_exit=0');
|
|
489
495
|
}
|
|
490
496
|
if (planCarryover !== null && kpiCarryover !== null && planCarryover !== kpiCarryover) {
|
|
@@ -10,8 +10,13 @@
|
|
|
10
10
|
* order) that is either status:'completed' or non-abandoned with completed
|
|
11
11
|
* work. Abandoned records are ALWAYS skipped.
|
|
12
12
|
*
|
|
13
|
-
* 2. WHOSE events? — quality_gate events in events.jsonl
|
|
14
|
-
*
|
|
13
|
+
* 2. WHOSE events? — quality_gate events in events.jsonl do not RELIABLY
|
|
14
|
+
* carry a session_id (measured 2026-10-05, this repo's events.jsonl:
|
|
15
|
+
* 17 of 82 `orchestrator.quality_gate.*` records without one, 65 with —
|
|
16
|
+
* `grep '"orchestrator.quality_gate' <file> | grep -c '"session_id"'`;
|
|
17
|
+
* the id-less ones are interleaved through the file, not just the oldest),
|
|
18
|
+
* so this module attributes them to a
|
|
19
|
+
* session by its wall-clock window
|
|
15
20
|
* [started_at, completed_at] (Decision #1: attribution = time-window).
|
|
16
21
|
* findPeerOverlap() detects when that window overlaps ANY other session's
|
|
17
22
|
* window — a contaminated window means gate-attribution is unsafe and the
|
|
@@ -88,6 +88,7 @@
|
|
|
88
88
|
import { closeSync, existsSync, openSync, readSync, readdirSync } from 'node:fs';
|
|
89
89
|
import path from 'node:path';
|
|
90
90
|
|
|
91
|
+
import { eventsFilePath } from './events.mjs';
|
|
91
92
|
import { ARCHIVE_DIR_NAME, ARCHIVE_NAME_RE, LEGACY_RING_MAX, ROTATION_EVENT } from './events-schema.mjs';
|
|
92
93
|
|
|
93
94
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
@@ -268,9 +269,11 @@ async function maxRequiredWindow(readers) {
|
|
|
268
269
|
*/
|
|
269
270
|
export async function checkEventsRetention({ repoRoot, config, now = Date.now() } = {}) {
|
|
270
271
|
if (!repoRoot || typeof repoRoot !== 'string') return null;
|
|
271
|
-
|
|
272
|
+
// #1514 point 1 — the repo's ONE ledger; its archive dir sits beside the SAME
|
|
273
|
+
// resolved file rotation writes (`events-rotation.mjs`: `<dirname(log)>/_archive`).
|
|
274
|
+
const activePath = eventsFilePath(repoRoot);
|
|
275
|
+
const metricsDir = path.dirname(activePath);
|
|
272
276
|
const archiveDir = path.join(metricsDir, ARCHIVE_DIR_NAME);
|
|
273
|
-
const activePath = path.join(metricsDir, 'events.jsonl');
|
|
274
277
|
const nowMs = typeof now === 'number' && Number.isFinite(now) ? now : Date.now();
|
|
275
278
|
|
|
276
279
|
let names;
|
package/scripts/lib/events.mjs
CHANGED
|
@@ -52,7 +52,7 @@ import {
|
|
|
52
52
|
} from 'node:fs';
|
|
53
53
|
import { tmpdir } from 'node:os';
|
|
54
54
|
import path from 'node:path';
|
|
55
|
-
import { getProjectDir, SO_SHARED_DIR } from './platform.mjs';
|
|
55
|
+
import { getProjectDir, resolveEventsLedgerRoot, SO_SHARED_DIR } from './platform.mjs';
|
|
56
56
|
import { readLock } from './session-lock.mjs';
|
|
57
57
|
import { resolveStateMdPath } from './state-md/frontmatter-mutators.mjs';
|
|
58
58
|
import {
|
|
@@ -139,8 +139,12 @@ function sandboxedDefault(defaultPath) {
|
|
|
139
139
|
/**
|
|
140
140
|
* Returns the absolute path to `.orchestrator/metrics/events.jsonl` under `repoRoot`.
|
|
141
141
|
*
|
|
142
|
-
* `repoRoot` defaults to the
|
|
143
|
-
*
|
|
142
|
+
* `repoRoot` defaults to the ONE events-ledger root, `resolveEventsLedgerRoot()`
|
|
143
|
+
* (`platform.mjs`, #1514 point 1): the launch dir from env, else the main
|
|
144
|
+
* checkout when the cwd is a linked git worktree, else `getProjectDir()` — the
|
|
145
|
+
* pre-#1514 default, unchanged everywhere a worktree is not involved (#941). An
|
|
146
|
+
* explicit absolute `repoRoot` inside a linked worktree is mapped to its main
|
|
147
|
+
* checkout the same way. Pass an explicit
|
|
144
148
|
* `repoRoot` when the destination must be pinned to a tree other than the
|
|
145
149
|
* CWD/env-resolved project — e.g. a unit test running the gate against a tmp
|
|
146
150
|
* repo, which must NOT append synthetic records to the real fleet telemetry.
|
|
@@ -154,8 +158,18 @@ function sandboxedDefault(defaultPath) {
|
|
|
154
158
|
* @returns {string}
|
|
155
159
|
*/
|
|
156
160
|
export function eventsFilePath(repoRoot) {
|
|
157
|
-
if (repoRoot !== undefined)
|
|
158
|
-
|
|
161
|
+
if (repoRoot !== undefined) {
|
|
162
|
+
// #1514 point 1 — an absolute root inside a LINKED git worktree maps to the
|
|
163
|
+
// same spot in its main checkout: one ledger per repo, never one per
|
|
164
|
+
// worktree. Identity for every other root (a plain repo, a tmp fixture, a
|
|
165
|
+
// non-git dir), and a relative/empty value is joined exactly as before —
|
|
166
|
+
// `emitEvent()` refuses it upstream.
|
|
167
|
+
const root = typeof repoRoot === 'string' && path.isAbsolute(repoRoot)
|
|
168
|
+
? resolveEventsLedgerRoot(repoRoot)
|
|
169
|
+
: repoRoot;
|
|
170
|
+
return path.join(root, SO_SHARED_DIR, 'metrics', 'events.jsonl');
|
|
171
|
+
}
|
|
172
|
+
return sandboxedDefault(path.join(resolveEventsLedgerRoot(), SO_SHARED_DIR, 'metrics', 'events.jsonl'));
|
|
159
173
|
}
|
|
160
174
|
|
|
161
175
|
/**
|
|
@@ -21,12 +21,18 @@
|
|
|
21
21
|
* expired lease is `none`, a lease that cannot be read or validated is
|
|
22
22
|
* `unreadable`, and neither is ever `active`.
|
|
23
23
|
*
|
|
24
|
+
* An auflagen file is keyed by the semantic session id, which collides across
|
|
25
|
+
* repos (`main-<date>-session-N`), so it is only accepted when its mandatory
|
|
26
|
+
* `repo` field resolves to the reader's own git common dir ({@link readAuflagen},
|
|
27
|
+
* #1520 / navigator#190).
|
|
28
|
+
*
|
|
24
29
|
* Session ids become file names, so {@link isSafeSessionId} is the security
|
|
25
30
|
* boundary between check-in input and the filesystem: every path builder that
|
|
26
31
|
* takes a session id refuses an unsafe one with a TypeError.
|
|
27
32
|
*/
|
|
28
33
|
|
|
29
|
-
import {
|
|
34
|
+
import { execFileSync } from 'node:child_process';
|
|
35
|
+
import fsSync, { promises as fs } from 'node:fs';
|
|
30
36
|
import os from 'node:os';
|
|
31
37
|
import path from 'node:path';
|
|
32
38
|
|
|
@@ -286,3 +292,94 @@ export async function readNavigatorLease({ now = Date.now(), env, home } = {}) {
|
|
|
286
292
|
if (Date.parse(lease.laeuft_ab) <= now) return { state: 'none' };
|
|
287
293
|
return { state: 'active', lease };
|
|
288
294
|
}
|
|
295
|
+
|
|
296
|
+
/** Timeout for the `git rev-parse` calls in {@link readAuflagen}. */
|
|
297
|
+
const GIT_TIMEOUT_MS = 5_000;
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Absolute, realpath'd git common dir of `dir` — the same value for a main tree,
|
|
301
|
+
* any of its linked worktrees, and the `.git` directory itself — or null when
|
|
302
|
+
* `dir` is not inside a git repository (or git is unavailable).
|
|
303
|
+
* @param {string} dir
|
|
304
|
+
* @returns {string|null}
|
|
305
|
+
*/
|
|
306
|
+
function gitCommonDir(dir) {
|
|
307
|
+
try {
|
|
308
|
+
const out = execFileSync('git', ['-C', dir, 'rev-parse', '--path-format=absolute', '--git-common-dir'], {
|
|
309
|
+
env: gitEnv(),
|
|
310
|
+
encoding: 'utf8',
|
|
311
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
312
|
+
timeout: GIT_TIMEOUT_MS,
|
|
313
|
+
}).trim();
|
|
314
|
+
if (!out) return null;
|
|
315
|
+
return realpathSyncSafe(out);
|
|
316
|
+
} catch {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* The only environment variables forwarded to `git`. An inherited `GIT_DIR`
|
|
323
|
+
* outranks `-C`, so both sides of the repo comparison would resolve to the
|
|
324
|
+
* same directory and a foreign repo's Auflagen would be ACCEPTED. Same
|
|
325
|
+
* allowlist as `git-config-drift.mjs` / `check-banner-parity.mjs`.
|
|
326
|
+
*/
|
|
327
|
+
const GIT_ENV_ALLOWLIST = Object.freeze(['PATH', 'HOME', 'LANG', 'LC_ALL', 'TMPDIR', 'TZ']);
|
|
328
|
+
|
|
329
|
+
function gitEnv() {
|
|
330
|
+
/** @type {Record<string, string>} */
|
|
331
|
+
const env = {};
|
|
332
|
+
for (const k of GIT_ENV_ALLOWLIST) if (process.env[k] !== undefined) env[k] = /** @type {string} */ (process.env[k]);
|
|
333
|
+
return env;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** @param {string} p */
|
|
337
|
+
function realpathSyncSafe(p) {
|
|
338
|
+
try {
|
|
339
|
+
return fsSync.realpathSync(p);
|
|
340
|
+
} catch {
|
|
341
|
+
return path.resolve(p);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Read this session's conditions file and accept it only when it belongs to the
|
|
347
|
+
* reader's repository. Never throws; fail-closed.
|
|
348
|
+
*
|
|
349
|
+
* The file's mandatory `repo` field (an absolute path to the target repo's
|
|
350
|
+
* toplevel or its git common dir, written by the navigator) and `repoRoot` are
|
|
351
|
+
* both resolved to their git common dir, so a session in a linked worktree
|
|
352
|
+
* matches a file naming the main tree. Every `rejected` result means: behave as
|
|
353
|
+
* if no file existed (Standard-Auflagen apply).
|
|
354
|
+
*
|
|
355
|
+
* @param {string} sessionId
|
|
356
|
+
* @param {{ repoRoot?: string, env?: Record<string, string|undefined>, home?: string }} [opts]
|
|
357
|
+
* @returns {Promise<
|
|
358
|
+
* | { state: 'absent' }
|
|
359
|
+
* | { state: 'accepted', auflagen: Record<string, unknown> }
|
|
360
|
+
* | { state: 'rejected', reason: 'unsafe-session-id'|'unreadable'|'invalid-json'|'repo-missing'|'repo-unresolved'|'repo-mismatch' }
|
|
361
|
+
* >}
|
|
362
|
+
*/
|
|
363
|
+
export async function readAuflagen(sessionId, { repoRoot = process.cwd(), env, home } = {}) {
|
|
364
|
+
if (!isSafeSessionId(sessionId)) return { state: 'rejected', reason: 'unsafe-session-id' };
|
|
365
|
+
let raw;
|
|
366
|
+
try {
|
|
367
|
+
raw = await fs.readFile(auflagenPath(sessionId, { env, home }), 'utf8');
|
|
368
|
+
} catch (err) {
|
|
369
|
+
if (err && /** @type {NodeJS.ErrnoException} */ (err).code === 'ENOENT') return { state: 'absent' };
|
|
370
|
+
return { state: 'rejected', reason: 'unreadable' };
|
|
371
|
+
}
|
|
372
|
+
let auflagen;
|
|
373
|
+
try {
|
|
374
|
+
auflagen = JSON.parse(raw);
|
|
375
|
+
} catch {
|
|
376
|
+
return { state: 'rejected', reason: 'invalid-json' };
|
|
377
|
+
}
|
|
378
|
+
if (!isPlainObject(auflagen)) return { state: 'rejected', reason: 'invalid-json' };
|
|
379
|
+
const fileRepo = auflagen.repo;
|
|
380
|
+
if (typeof fileRepo !== 'string' || !path.isAbsolute(fileRepo)) return { state: 'rejected', reason: 'repo-missing' };
|
|
381
|
+
const own = gitCommonDir(repoRoot);
|
|
382
|
+
if (!own) return { state: 'rejected', reason: 'repo-unresolved' };
|
|
383
|
+
if (gitCommonDir(fileRepo) !== own) return { state: 'rejected', reason: 'repo-mismatch' };
|
|
384
|
+
return { state: 'accepted', auflagen };
|
|
385
|
+
}
|
|
@@ -88,11 +88,13 @@
|
|
|
88
88
|
|
|
89
89
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
90
90
|
import { basename, join } from 'node:path';
|
|
91
|
-
import { scanEventsBackwards } from './events.mjs';
|
|
91
|
+
import { eventsFilePath, scanEventsBackwards } from './events.mjs';
|
|
92
92
|
import {
|
|
93
93
|
FRONTMATTER_NOT_AT_TOP,
|
|
94
94
|
loadApplicableRules,
|
|
95
|
+
isRuleExpired,
|
|
95
96
|
parseGlobsFrontmatter,
|
|
97
|
+
ruleExpiryInstantMs,
|
|
96
98
|
} from './rule-loader.mjs';
|
|
97
99
|
|
|
98
100
|
/** Default directive ceiling (operator-chosen growth ratchet just above the ~457 baseline). */
|
|
@@ -846,11 +848,11 @@ const MS_PER_DAY = 86_400_000;
|
|
|
846
848
|
* declares no expiry or an unparseable one.
|
|
847
849
|
*
|
|
848
850
|
* Negative ⇒ already expired. The comparison basis is deliberately the SAME one
|
|
849
|
-
* `rule-loader.mjs` `applyGates` uses — `
|
|
850
|
-
*
|
|
851
|
-
* loader already filters it out of every
|
|
852
|
-
* definition of the same word. `
|
|
853
|
-
*
|
|
851
|
+
* `rule-loader.mjs` `applyGates` uses — `ruleExpiryInstantMs`, where a bare
|
|
852
|
+
* `YYYY-MM-DD` is INCLUSIVE and expires at the FOLLOWING UTC midnight (#1521) —
|
|
853
|
+
* so "expired" here means exactly "the loader already filters it out of every
|
|
854
|
+
* wave's rule set", never a second definition of the same word. `0` therefore
|
|
855
|
+
* means "last day: still loads today", `-1` "dropped at the last midnight".
|
|
854
856
|
*
|
|
855
857
|
* An unparseable date returns `null` (fail-OPEN, matching the loader, which
|
|
856
858
|
* warns and ignores the expiry rather than excluding the file) — the malformed
|
|
@@ -863,17 +865,18 @@ export function daysUntilGeneratedRuleExpiry(opts = {}) {
|
|
|
863
865
|
const meta = opts.meta;
|
|
864
866
|
if (!meta || typeof meta !== 'object') return null;
|
|
865
867
|
if (!Object.prototype.hasOwnProperty.call(meta, 'expires-at')) return null;
|
|
866
|
-
const expiresAt =
|
|
867
|
-
if (
|
|
868
|
+
const expiresAt = ruleExpiryInstantMs(meta['expires-at']);
|
|
869
|
+
if (expiresAt === null) return null;
|
|
868
870
|
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
if (raw instanceof Date) now = raw.getTime();
|
|
872
|
-
else if (typeof raw === 'number' && Number.isFinite(raw)) now = raw;
|
|
873
|
-
else if (typeof raw === 'string' && Number.isFinite(Date.parse(raw))) now = Date.parse(raw);
|
|
874
|
-
else now = Date.now();
|
|
871
|
+
return Math.floor((expiresAt - nowMs(opts.now)) / MS_PER_DAY);
|
|
872
|
+
}
|
|
875
873
|
|
|
876
|
-
|
|
874
|
+
/** `now` as epoch ms: Date, number or parseable string; anything else → the process clock. */
|
|
875
|
+
function nowMs(raw) {
|
|
876
|
+
if (raw instanceof Date) return raw.getTime();
|
|
877
|
+
if (typeof raw === 'number' && Number.isFinite(raw)) return raw;
|
|
878
|
+
if (typeof raw === 'string' && Number.isFinite(Date.parse(raw))) return Date.parse(raw);
|
|
879
|
+
return Date.now();
|
|
877
880
|
}
|
|
878
881
|
|
|
879
882
|
/**
|
|
@@ -888,8 +891,12 @@ export function daysUntilGeneratedRuleExpiry(opts = {}) {
|
|
|
888
891
|
* @returns {boolean}
|
|
889
892
|
*/
|
|
890
893
|
export function isExpiredGeneratedRule(opts = {}) {
|
|
891
|
-
const
|
|
892
|
-
|
|
894
|
+
const meta = opts.meta;
|
|
895
|
+
if (!meta || typeof meta !== 'object') return false;
|
|
896
|
+
if (!Object.prototype.hasOwnProperty.call(meta, 'expires-at')) return false;
|
|
897
|
+
// The loader's own predicate, not `days < 0`: flooring whole days would call
|
|
898
|
+
// the rule live for the first millisecond after the boundary (#1521).
|
|
899
|
+
return isRuleExpired(meta['expires-at'], nowMs(opts.now));
|
|
893
900
|
}
|
|
894
901
|
|
|
895
902
|
/**
|
|
@@ -1335,7 +1342,7 @@ const RECONCILE_SCAN_BUDGET_MS = 150;
|
|
|
1335
1342
|
*/
|
|
1336
1343
|
function readLastReconcileCapped(repoRoot) {
|
|
1337
1344
|
if (typeof repoRoot !== 'string' || repoRoot === '') return null;
|
|
1338
|
-
const filePath =
|
|
1345
|
+
const filePath = eventsFilePath(repoRoot); // #1514 point 1: the repo's one ledger
|
|
1339
1346
|
let capped = null;
|
|
1340
1347
|
let scan;
|
|
1341
1348
|
try {
|