session-orchestrator 5.2.0 → 5.3.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 -1
- package/.agents/skills/autopilot/SKILL.md +5 -1
- package/.agents/skills/autopilot/agents/openai.yaml +5 -0
- package/.agents/skills/bootstrap/SKILL.md +5 -1
- package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
- package/.agents/skills/brainstorm/SKILL.md +5 -1
- package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +3 -1
- package/.agents/skills/close/SKILL.md +5 -1
- package/.agents/skills/close/agents/openai.yaml +5 -0
- package/.agents/skills/convergence-monitoring/SKILL.md +4 -2
- package/.agents/skills/debug/SKILL.md +5 -1
- package/.agents/skills/debug/agents/openai.yaml +5 -0
- package/.agents/skills/discovery/SKILL.md +5 -1
- package/.agents/skills/discovery/agents/openai.yaml +5 -0
- package/.agents/skills/dispatcher/SKILL.md +5 -1
- package/.agents/skills/dispatcher/agents/openai.yaml +5 -0
- package/.agents/skills/docs-orchestrator/SKILL.md +3 -1
- package/.agents/skills/ecosystem-health/SKILL.md +3 -1
- package/.agents/skills/eli5/SKILL.md +5 -1
- package/.agents/skills/eli5/agents/openai.yaml +5 -0
- package/.agents/skills/eval/SKILL.md +6 -2
- package/.agents/skills/eval/agents/openai.yaml +5 -0
- package/.agents/skills/evolve/SKILL.md +6 -2
- package/.agents/skills/evolve/agents/openai.yaml +5 -0
- package/.agents/skills/frontmatter-guard/SKILL.md +3 -1
- package/.agents/skills/gitlab-ops/SKILL.md +3 -1
- package/.agents/skills/gitlab-portfolio/SKILL.md +3 -1
- package/.agents/skills/go/SKILL.md +5 -1
- package/.agents/skills/go/agents/openai.yaml +5 -0
- package/.agents/skills/grill/SKILL.md +5 -1
- package/.agents/skills/grill/agents/openai.yaml +5 -0
- package/.agents/skills/harness-audit/SKILL.md +5 -1
- package/.agents/skills/harness-audit/agents/openai.yaml +5 -0
- package/.agents/skills/hook-development/SKILL.md +3 -1
- package/.agents/skills/mcp-builder/SKILL.md +3 -1
- package/.agents/skills/memory-cleanup/SKILL.md +5 -1
- package/.agents/skills/memory-cleanup/agents/openai.yaml +5 -0
- package/.agents/skills/mode-selector/SKILL.md +3 -1
- package/.agents/skills/npm-publish/SKILL.md +4 -2
- package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
- package/.agents/skills/persona-panel/SKILL.md +5 -1
- package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
- package/.agents/skills/plan/SKILL.md +5 -1
- package/.agents/skills/plan/agents/openai.yaml +5 -0
- package/.agents/skills/playwright-driver/SKILL.md +3 -1
- package/.agents/skills/portfolio/SKILL.md +5 -1
- package/.agents/skills/portfolio/agents/openai.yaml +5 -0
- package/.agents/skills/quality-gates/SKILL.md +3 -1
- package/.agents/skills/reconcile/SKILL.md +5 -1
- package/.agents/skills/reconcile/agents/openai.yaml +5 -0
- package/.agents/skills/release/SKILL.md +5 -1
- package/.agents/skills/release/agents/openai.yaml +5 -0
- package/.agents/skills/remote-offload/SKILL.md +3 -1
- package/.agents/skills/repo-audit/SKILL.md +5 -1
- package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
- package/.agents/skills/session/SKILL.md +21 -0
- package/.agents/skills/session/agents/openai.yaml +5 -0
- package/.agents/skills/session-end/SKILL.md +3 -1
- package/.agents/skills/session-plan/SKILL.md +3 -1
- package/.agents/skills/session-start/SKILL.md +3 -1
- package/.agents/skills/spinout/SKILL.md +5 -1
- package/.agents/skills/spinout/agents/openai.yaml +5 -0
- package/.agents/skills/sunset-review/SKILL.md +5 -1
- package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
- package/.agents/skills/templates-ack/SKILL.md +21 -0
- package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
- package/.agents/skills/test/SKILL.md +5 -1
- package/.agents/skills/test/agents/openai.yaml +5 -0
- package/.agents/skills/test-runner/SKILL.md +3 -1
- package/.agents/skills/tmux-layout/SKILL.md +3 -1
- package/.agents/skills/using-orchestrator/SKILL.md +3 -1
- package/.agents/skills/ux-grill/SKILL.md +5 -1
- package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
- package/.agents/skills/vault-mirror/SKILL.md +3 -1
- package/.agents/skills/vault-sync/SKILL.md +3 -1
- package/.agents/skills/wave-executor/SKILL.md +3 -1
- package/.agents/skills/write-executable-plan/SKILL.md +3 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +4 -4
- package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -3
- package/.codex-plugin/skills/eval/SKILL.md +1 -1
- package/.codex-plugin/skills/evolve/SKILL.md +1 -1
- package/.codex-plugin/skills/npm-publish/SKILL.md +1 -3
- package/.codex-plugin/skills/session/SKILL.md +1 -1
- package/.cursor/commands/eval.md +1 -1
- package/.cursor/commands/session.md +1 -1
- package/.cursor/rules/000-session-orchestrator.mdc +0 -2
- package/.cursor/rules/050-plan.mdc +1 -1
- package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/npm-publish/SKILL.md +1 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +12 -3
- package/AGENTS.md +3 -2
- package/CHANGELOG.md +136 -0
- package/README.md +9 -9
- package/SECURITY.md +12 -0
- package/agents/dialectic-deriver.md +13 -10
- package/agents/eval-judge.md +67 -45
- package/agents/skill-applied-judge.md +34 -19
- package/commands/session.md +7 -3
- package/docs/baseline.md +12 -6
- package/docs/codex-setup.md +14 -2
- package/docs/components.md +7 -5
- package/docs/events-schema.md +56 -9
- package/docs/rule-authoring.md +58 -6
- package/docs/session-config-reference.md +100 -7
- package/docs/session-config-template.md +31 -2
- package/docs/telemetry.md +2 -0
- package/hooks/_lib/hook-import-set.json +85 -8
- package/hooks/_lib/subagent-transcript.mjs +582 -31
- package/hooks/config-protection.mjs +11 -3
- package/hooks/cwd-change-restore.mjs +11 -3
- package/hooks/enforce-commands.mjs +70 -23
- package/hooks/enforce-scope.mjs +143 -33
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/loop-guard.mjs +11 -3
- package/hooks/on-session-end.mjs +58 -23
- package/hooks/on-session-start.mjs +48 -11
- package/hooks/on-stop.mjs +168 -22
- package/hooks/operator-steer.mjs +11 -3
- package/hooks/post-bash-issue-budget-refund.mjs +18 -8
- package/hooks/post-bash-write-verify.mjs +3 -2
- package/hooks/post-edit-import-probe.mjs +17 -9
- package/hooks/post-edit-validate.mjs +13 -5
- package/hooks/post-subagent-discovery-validator.mjs +98 -13
- package/hooks/post-tool-batch-wave-signal.mjs +200 -38
- package/hooks/post-tool-failure-corrective-context.mjs +11 -5
- package/hooks/post-tooluse-frontend-slop.mjs +10 -4
- package/hooks/pre-auq-clarity.mjs +15 -2
- package/hooks/pre-bash-destructive-guard.mjs +80 -9
- package/hooks/pre-bash-issue-budget.mjs +16 -11
- package/hooks/pre-bash-memory-propose-audit.mjs +86 -54
- package/hooks/pre-bash-sessions-ledger-guard.mjs +391 -20
- package/hooks/pre-bash-staging-fence.mjs +335 -31
- package/hooks/pre-bash-templates-first.mjs +19 -14
- package/hooks/pre-task-scope-disjoint.mjs +233 -2
- package/hooks/subagent-telemetry.mjs +15 -19
- package/hooks/wave-scope-commit-guard.mjs +197 -100
- package/monitors/monitors.json +1 -1
- package/output-styles/wave-summary.md +1 -1
- package/package.json +1 -1
- package/pi/prompts/eval.md +1 -1
- package/pi/prompts/session.md +1 -1
- package/rules/README.md +1 -1
- package/rules/opt-in-domain/prompt-caching.md +1 -1
- package/rules/opt-in-stack/backend-data.md +1 -1
- package/rules/opt-in-stack/backend.md +3 -3
- package/rules/opt-in-stack/frontend.md +1 -1
- package/rules/opt-in-stack/security-web.md +3 -3
- package/rules/opt-in-stack/swift.md +1 -1
- package/scripts/autopilot.mjs +23 -2
- package/scripts/backfill-abandoned-sessions.mjs +117 -15
- package/scripts/check-sessions-integrity.mjs +300 -0
- package/scripts/dialectic-deriver.mjs +50 -13
- package/scripts/emit-session.mjs +75 -29
- package/scripts/eval-session.mjs +65 -3
- package/scripts/generate-agents-skills.mjs +102 -29
- package/scripts/generate-cursor-adapter.mjs +61 -16
- package/scripts/lib/agent-status.mjs +2 -31
- package/scripts/lib/auq/clarity.mjs +10 -2
- package/scripts/lib/auq/parse.mjs +12 -31
- package/scripts/lib/auq/schema.mjs +56 -41
- package/scripts/lib/auto-dialectic.mjs +304 -15
- package/scripts/lib/autopilot/flags.mjs +12 -1
- package/scripts/lib/autopilot/kill-switches.mjs +6 -3
- package/scripts/lib/autopilot/loop.mjs +14 -1
- package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
- package/scripts/lib/ci-status-banner.mjs +376 -16
- package/scripts/lib/command-blocker.mjs +275 -28
- package/scripts/lib/config/dialectic.mjs +12 -3
- package/scripts/lib/config/gate.mjs +74 -0
- package/scripts/lib/config/reaper.mjs +162 -0
- package/scripts/lib/config.mjs +14 -0
- package/scripts/lib/convergence-monitor.mjs +74 -11
- package/scripts/lib/ecosystem-health.mjs +11 -0
- package/scripts/lib/eval/engine.mjs +421 -53
- package/scripts/lib/eval/judge.mjs +463 -40
- package/scripts/lib/eval/schema.mjs +10 -1
- package/scripts/lib/events-rotation.mjs +221 -25
- package/scripts/lib/events-schema.mjs +114 -0
- package/scripts/lib/events.mjs +524 -5
- package/scripts/lib/frontmatter-guard.mjs +21 -10
- package/scripts/lib/gates/gate-baseline.mjs +27 -2
- package/scripts/lib/gates/gate-full.mjs +28 -3
- package/scripts/lib/gates/gate-helpers.mjs +243 -21
- package/scripts/lib/gates/gate-incremental.mjs +28 -3
- package/scripts/lib/gates/gate-per-file.mjs +27 -2
- package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
- package/scripts/lib/instruction-budget-guard.mjs +146 -4
- package/scripts/lib/io.mjs +42 -8
- package/scripts/lib/issue-close-strip-labels.mjs +207 -49
- package/scripts/lib/js-mask.mjs +197 -0
- package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
- package/scripts/lib/maintenance-due-banner.mjs +53 -88
- package/scripts/lib/orphan-reaper.mjs +1588 -0
- package/scripts/lib/peer-cards/merger.mjs +48 -10
- package/scripts/lib/peer-cards/reader.mjs +78 -2
- package/scripts/lib/process-group.mjs +899 -0
- package/scripts/lib/quality-gate.mjs +107 -28
- package/scripts/lib/reconcile/backlog.mjs +368 -0
- package/scripts/lib/reconcile/engine.mjs +55 -188
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +302 -60
- package/scripts/lib/reconcile/sanitize.mjs +69 -3
- package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
- package/scripts/lib/resource-probe/parsers.mjs +31 -0
- package/scripts/lib/rule-loader.mjs +41 -12
- package/scripts/lib/scope-echo.mjs +39 -2
- package/scripts/lib/scope-gate.mjs +605 -1
- package/scripts/lib/session-close-backfill.mjs +33 -6
- package/scripts/lib/session-id.mjs +9 -20
- package/scripts/lib/session-invocation.mjs +20 -0
- package/scripts/lib/session-schema/constants.mjs +30 -2
- package/scripts/lib/session-schema/normalizer.mjs +56 -4
- package/scripts/lib/session-schema.mjs +8 -3
- package/scripts/lib/session-start-probes.mjs +95 -10
- package/scripts/lib/sessions-canonical.mjs +23 -0
- package/scripts/lib/sessions-integrity-banner.mjs +7 -1
- package/scripts/lib/sessions-staleness-banner.mjs +193 -51
- package/scripts/lib/skill-evidence-window.mjs +891 -0
- package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
- package/scripts/lib/skill-evolution/engine.mjs +18 -9
- package/scripts/lib/skill-judge.mjs +45 -3
- package/scripts/lib/tail-window.mjs +56 -0
- package/scripts/lib/telemetry/schema.mjs +30 -0
- package/scripts/lib/telemetry/sync.mjs +61 -6
- package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
- package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +72 -13
- package/scripts/lib/user-invocable-skills.mjs +23 -3
- package/scripts/lib/ux-grill/reconcile.mjs +48 -22
- package/scripts/lib/validate/check-agents-skills.mjs +26 -15
- package/scripts/lib/validate/check-cursor-adapter.mjs +1 -0
- package/scripts/lib/validate/check-entry-guard.mjs +13 -50
- package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
- package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
- package/scripts/lib/validate/check-rules.mjs +7 -5
- package/scripts/lib/validate/check-skill-links.mjs +9 -1
- package/scripts/lib/validate/check-skill-script-paths.mjs +239 -27
- package/scripts/lib/validate/check-test-git-config-target.mjs +24 -34
- package/scripts/lib/validate/check-untracked-test-deps.mjs +7 -102
- package/scripts/lib/validate/check-unwired-features.mjs +130 -27
- package/scripts/lib/validate/check-validator-registration.mjs +34 -10
- package/scripts/lib/validate/confidential-names.mjs +10 -0
- package/scripts/lib/validate-vendored-rules.mjs +4 -3
- package/scripts/lib/vault-mirror/namespace.mjs +46 -8
- package/scripts/lib/vault-mirror/process.mjs +10 -3
- package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
- package/scripts/lib/vault-status/narrative-mirror.mjs +31 -7
- package/scripts/lib/vault-yaml.mjs +118 -0
- package/scripts/lib/worktree/lifecycle.mjs +153 -1
- package/scripts/release-session-lock.mjs +305 -0
- package/scripts/release.mjs +30 -5
- package/scripts/resolve-session-invocation.mjs +59 -0
- package/scripts/run-quality-gate.mjs +156 -17
- package/scripts/sweep-expired-rules.mjs +14 -3
- package/scripts/validate-plugin.mjs +12 -0
- package/scripts/validate-wave-scope.mjs +32 -105
- package/scripts/vault-mirror.mjs +9 -1
- package/skills/_shared/platform-tools.md +23 -11
- package/skills/autopilot/SKILL.md +22 -7
- package/skills/claude-md-drift-check/SKILL.md +1 -1
- package/skills/convergence-monitoring/README.md +8 -1
- package/skills/convergence-monitoring/SIGNALS.md +50 -6
- package/skills/convergence-monitoring/SKILL.md +15 -6
- package/skills/eval/SKILL.md +39 -24
- package/skills/eval/rubric-v1.md +1 -0
- package/skills/eval/rubric-v2.md +457 -0
- package/skills/evolve/SKILL.md +1 -1
- package/skills/evolve/references/evolve-dialectic-mode.md +42 -25
- package/skills/gitlab-ops/SKILL.md +3 -2
- package/skills/npm-publish/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +11 -0
- package/skills/session-end/SKILL.md +13 -16
- package/skills/session-end/discovery-scan.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +55 -9
- package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
- package/skills/session-end/session-metrics-write.md +10 -0
- package/skills/session-plan/SKILL.md +17 -5
- package/skills/session-plan/references/session-plan-task-classification.md +2 -2
- package/skills/session-start/references/phase-4-ssot-environment-check.md +2 -1
- package/skills/ux-grill/SKILL.md +1 -1
- package/skills/wave-executor/SKILL.md +8 -4
- package/skills/wave-executor/circuit-breaker.md +2 -0
- package/skills/wave-executor/references/wave-executor-state-init.md +5 -3
- package/skills/wave-executor/references/wave-loop-dispatch.md +2 -1
- package/.codex-plugin/skills/convergence-monitoring/agents/openai.yaml +0 -5
- package/.codex-plugin/skills/npm-publish/agents/openai.yaml +0 -5
- package/.cursor/commands/convergence-monitoring.md +0 -13
- package/.cursor/commands/npm-publish.md +0 -13
- package/pi/prompts/convergence-monitoring.md +0 -11
- package/pi/prompts/npm-publish.md +0 -11
|
@@ -45,6 +45,14 @@
|
|
|
45
45
|
* - `surfaceTopN` (`./learnings/surface.mjs`) — the canonical active-learning
|
|
46
46
|
* filter (confidence > floor, not expired); called with a large cap to get
|
|
47
47
|
* the full active set rather than a top-N slice.
|
|
48
|
+
* - `countReconcileBacklog` (`./reconcile/backlog.mjs`) — the SAME eligibility +
|
|
49
|
+
* materialization partition `/reconcile` applies, imported from the LEAF module
|
|
50
|
+
* rather than from `./reconcile/engine.mjs`. The engine is WRITE-capable (it
|
|
51
|
+
* merges the candidate store) and drags the emitter, renderer and unicode
|
|
52
|
+
* validator behind it: importing the count from there took this probe's static
|
|
53
|
+
* closure from 11 modules / 151,946 B to 19 / 322,897 B (+112 %, measured
|
|
54
|
+
* 2026-09-18), inherited unasked by `maintenance-due-banner.mjs`. A banner must
|
|
55
|
+
* not be able to reach a writer.
|
|
48
56
|
* - `filterEligible` (`./reconcile/eligibility.mjs`) — the SAME type/file_paths
|
|
49
57
|
* allow-list gate the reconcile engine itself uses (deliberately NOT
|
|
50
58
|
* confidence-gated — mirrors `runReconcile`'s own posture, see
|
|
@@ -68,7 +76,7 @@ import { join } from 'node:path';
|
|
|
68
76
|
|
|
69
77
|
import { readLearnings } from './learnings/io.mjs';
|
|
70
78
|
import { surfaceTopN } from './learnings/surface.mjs';
|
|
71
|
-
import {
|
|
79
|
+
import { countReconcileBacklog } from './reconcile/backlog.mjs';
|
|
72
80
|
import { loadCandidates } from './reconcile/idempotency.mjs';
|
|
73
81
|
import { readConfigFile } from './config/io.mjs';
|
|
74
82
|
import { _parseReconcile } from './config/reconcile.mjs';
|
|
@@ -82,9 +90,67 @@ export const NUDGE_MIN_LEARNINGS = 20;
|
|
|
82
90
|
/** Nudge threshold (b): new learnings accrued since the last determinable reconcile run. */
|
|
83
91
|
export const NUDGE_MIN_DELTA = 15;
|
|
84
92
|
|
|
85
|
-
/**
|
|
93
|
+
/**
|
|
94
|
+
* Nudge threshold (c): reconcile BACKLOG — rule-eligible learnings (type + file_paths
|
|
95
|
+
* allow-list, not expired, regardless of confidence) MINUS those already materialized
|
|
96
|
+
* in the idempotency sidecar or a `.claude/rules/` provenance marker. Counting the
|
|
97
|
+
* materialized ones too made the probe un-greenable (#1380: 98 eligible, 35 already
|
|
98
|
+
* materialized, and no `/reconcile` run could ever clear it).
|
|
99
|
+
*/
|
|
86
100
|
export const NUDGE_MIN_ELIGIBLE = 3;
|
|
87
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Resolve the `reconcile` Session Config settings this module needs, in ONE read.
|
|
104
|
+
*
|
|
105
|
+
* Both consumers go through here, so the nudge can never judge on a different
|
|
106
|
+
* population than `/reconcile` does: the skill reads `reconcile.min-insight-chars`
|
|
107
|
+
* (default 24) and forwards it to the engine, and so must this probe. Before #1380
|
|
108
|
+
* follow-up the value was only read AFTER the backlog had already been counted, which
|
|
109
|
+
* left the placeholder-insight gate practically OFF for the banner.
|
|
110
|
+
*
|
|
111
|
+
* Precedence: an injected already-parsed Session Config wins per KEY (DI seam, avoids a
|
|
112
|
+
* second CLAUDE.md read); any key it does not carry falls back to parsing
|
|
113
|
+
* CLAUDE.md/AGENTS.md; an unreadable config falls back to `_parseReconcile('')`, i.e.
|
|
114
|
+
* the parser's OWN documented defaults — never a literal repeated here.
|
|
115
|
+
*
|
|
116
|
+
* Never throws.
|
|
117
|
+
*
|
|
118
|
+
* @param {string} repoRoot
|
|
119
|
+
* @param {unknown} [config] already-parsed Session Config
|
|
120
|
+
* @returns {Promise<{enabled: boolean, minInsightChars: number}>}
|
|
121
|
+
*/
|
|
122
|
+
async function _resolveReconcileSettings(repoRoot, config) {
|
|
123
|
+
const injected =
|
|
124
|
+
config && typeof config === 'object' && /** @type {any} */ (config).reconcile &&
|
|
125
|
+
typeof (/** @type {any} */ (config).reconcile) === 'object'
|
|
126
|
+
? /** @type {Record<string, unknown>} */ (/** @type {any} */ (config).reconcile)
|
|
127
|
+
: null;
|
|
128
|
+
|
|
129
|
+
const hasEnabled = injected !== null && typeof injected.enabled === 'boolean';
|
|
130
|
+
const hasChars = injected !== null && Number.isFinite(injected['min-insight-chars']);
|
|
131
|
+
if (hasEnabled && hasChars) {
|
|
132
|
+
return {
|
|
133
|
+
enabled: /** @type {boolean} */ (injected.enabled),
|
|
134
|
+
minInsightChars: Number(injected['min-insight-chars']),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let parsed;
|
|
139
|
+
try {
|
|
140
|
+
parsed = _parseReconcile(await readConfigFile(repoRoot));
|
|
141
|
+
} catch {
|
|
142
|
+
// Config unreadable — take the parser's own defaults (single source of truth).
|
|
143
|
+
parsed = _parseReconcile('');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
enabled: hasEnabled ? /** @type {boolean} */ (injected.enabled) : parsed.enabled,
|
|
148
|
+
minInsightChars: hasChars
|
|
149
|
+
? Number(injected['min-insight-chars'])
|
|
150
|
+
: parsed['min-insight-chars'],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
88
154
|
/**
|
|
89
155
|
* Resolve the max `created_at` across reconcile-candidate sidecar records —
|
|
90
156
|
* the most recent reconcile run's timestamp, or `null` when no run is on
|
|
@@ -110,11 +176,19 @@ function _lastRunAt(candidates) {
|
|
|
110
176
|
*
|
|
111
177
|
* @param {object} [opts]
|
|
112
178
|
* @param {string} [opts.repoRoot] — project root (defaults to process.cwd()).
|
|
113
|
-
* @param {Date|number} [opts.now] — injectable clock, forwarded to the active-learning
|
|
179
|
+
* @param {Date|number} [opts.now] — injectable clock, forwarded to the active-learning
|
|
180
|
+
* filter AND the eligibility expiry gate.
|
|
181
|
+
* @param {number} [opts.minInsightChars] — forwarded to the eligibility filter. When
|
|
182
|
+
* OMITTED the value is read from Session Config (`reconcile.min-insight-chars`,
|
|
183
|
+
* default 24) rather than left inert, so the nudge and `/reconcile` count the SAME
|
|
184
|
+
* population — see `_resolveReconcileSettings`.
|
|
185
|
+
* @param {object} [opts.config] — optional already-parsed Session Config (DI seam).
|
|
114
186
|
* @returns {Promise<{
|
|
115
187
|
* totalLearnings: number,
|
|
116
188
|
* activeLearnings: number,
|
|
117
189
|
* eligibleCount: number,
|
|
190
|
+
* alreadyMaterialized: number,
|
|
191
|
+
* backlogCount: number,
|
|
118
192
|
* lastRunAt: string|null,
|
|
119
193
|
* lastRunCandidateCount: number,
|
|
120
194
|
* delta: number,
|
|
@@ -131,6 +205,8 @@ export async function computeReconcileNudge(opts = {}) {
|
|
|
131
205
|
totalLearnings: 0,
|
|
132
206
|
activeLearnings: 0,
|
|
133
207
|
eligibleCount: 0,
|
|
208
|
+
alreadyMaterialized: 0,
|
|
209
|
+
backlogCount: 0,
|
|
134
210
|
lastRunAt: null,
|
|
135
211
|
lastRunCandidateCount: 0,
|
|
136
212
|
delta: 0,
|
|
@@ -166,20 +242,29 @@ export async function computeReconcileNudge(opts = {}) {
|
|
|
166
242
|
|
|
167
243
|
if (active.length === 0) return empty;
|
|
168
244
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
245
|
+
// Same partition a real /reconcile run applies (engine.mjs step 3/3a) — one
|
|
246
|
+
// truth, so the nudge can only fire on work /reconcile is able to clear.
|
|
247
|
+
// That includes the placeholder-insight gate: when the caller did not supply
|
|
248
|
+
// `minInsightChars`, read it from Session Config BEFORE counting, or the
|
|
249
|
+
// banner counts learnings `/reconcile` would reject.
|
|
250
|
+
const minInsightChars = Number.isFinite(opts.minInsightChars)
|
|
251
|
+
? Number(opts.minInsightChars)
|
|
252
|
+
: (await _resolveReconcileSettings(repoRoot, opts.config)).minInsightChars;
|
|
175
253
|
|
|
254
|
+
// The candidate store is read ONCE, BEFORE the backlog count — the count needs
|
|
255
|
+
// the same records for its sidecar-dedupe half, and reading them here lets it
|
|
256
|
+
// be handed down instead of read a second time (`candidatesRead` says whether
|
|
257
|
+
// the hand-down is trustworthy; on a failed read we hand nothing down and let
|
|
258
|
+
// `countReconcileBacklog` decide for itself rather than pass a fake empty set).
|
|
176
259
|
/** @type {Array<Record<string, unknown>>} */
|
|
177
260
|
let candidates;
|
|
178
261
|
/** @type {number|undefined} */
|
|
179
262
|
let skippedCandidates;
|
|
263
|
+
let candidatesRead = false;
|
|
180
264
|
try {
|
|
181
265
|
const diag = loadCandidates({ repoRoot });
|
|
182
266
|
candidates = Array.isArray(diag?.records) ? diag.records : [];
|
|
267
|
+
candidatesRead = Array.isArray(diag?.records);
|
|
183
268
|
// Absence-preserving, mirroring `engine.mjs` `summary.skipped`: only a
|
|
184
269
|
// finite count means "the store was inspected". Absent ⇒ never checked,
|
|
185
270
|
// 0 ⇒ checked and clean.
|
|
@@ -190,6 +275,25 @@ export async function computeReconcileNudge(opts = {}) {
|
|
|
190
275
|
// fabricating a clean 0.
|
|
191
276
|
}
|
|
192
277
|
|
|
278
|
+
// ONE corpus, ONE population. `entries` (read above via `readLearnings`) is
|
|
279
|
+
// handed down instead of letting the count re-read and re-normalize the same
|
|
280
|
+
// file: the banner line names `activeLearnings` and `backlogCount` side by
|
|
281
|
+
// side, and two independent reads of one file are two populations wearing one
|
|
282
|
+
// sentence (HR-106). `repoRoot` stays required — the sidecar and the
|
|
283
|
+
// `.claude/rules/` provenance scan are resolved from it.
|
|
284
|
+
// Never throws (all-zero on failure).
|
|
285
|
+
const {
|
|
286
|
+
eligible: eligibleCount,
|
|
287
|
+
alreadyMaterialized,
|
|
288
|
+
backlog: backlogCount,
|
|
289
|
+
} = countReconcileBacklog({
|
|
290
|
+
repoRoot,
|
|
291
|
+
learnings: entries,
|
|
292
|
+
...(candidatesRead ? { existingCandidates: candidates } : {}),
|
|
293
|
+
now: opts.now,
|
|
294
|
+
minInsightChars,
|
|
295
|
+
});
|
|
296
|
+
|
|
193
297
|
const lastRunAt = _lastRunAt(candidates);
|
|
194
298
|
const lastRunCandidateCount = Array.isArray(candidates) ? candidates.length : 0;
|
|
195
299
|
const delta = entries.length - lastRunCandidateCount;
|
|
@@ -213,15 +317,18 @@ export async function computeReconcileNudge(opts = {}) {
|
|
|
213
317
|
if (lastRunAt !== null && delta > NUDGE_MIN_DELTA) {
|
|
214
318
|
reasons.push(`${delta} new learnings since the last reconcile run`);
|
|
215
319
|
}
|
|
216
|
-
// (c) — enough rule-eligible learnings to be worth a
|
|
217
|
-
|
|
218
|
-
|
|
320
|
+
// (c) — enough NOT-YET-MATERIALIZED rule-eligible learnings to be worth a
|
|
321
|
+
// batch, independent of confidence. HR-106: the reason names the number judged.
|
|
322
|
+
if (backlogCount >= NUDGE_MIN_ELIGIBLE) {
|
|
323
|
+
reasons.push(`${backlogCount} rule-eligible learnings awaiting a rule`);
|
|
219
324
|
}
|
|
220
325
|
|
|
221
326
|
const computed = {
|
|
222
327
|
totalLearnings: entries.length,
|
|
223
328
|
activeLearnings: active.length,
|
|
224
329
|
eligibleCount,
|
|
330
|
+
alreadyMaterialized,
|
|
331
|
+
backlogCount,
|
|
225
332
|
lastRunAt,
|
|
226
333
|
lastRunCandidateCount,
|
|
227
334
|
delta,
|
|
@@ -236,25 +343,6 @@ export async function computeReconcileNudge(opts = {}) {
|
|
|
236
343
|
return computed;
|
|
237
344
|
}
|
|
238
345
|
|
|
239
|
-
/**
|
|
240
|
-
* Best-effort extraction of `reconcile.enabled` from an already-parsed Session
|
|
241
|
-
* Config object (DI/test seam — avoids re-reading CLAUDE.md when the caller
|
|
242
|
-
* already has it). Returns `null` when unresolvable from `config` alone, in
|
|
243
|
-
* which case `checkReconcileNudge` falls back to reading CLAUDE.md/AGENTS.md.
|
|
244
|
-
*
|
|
245
|
-
* @param {unknown} config
|
|
246
|
-
* @returns {boolean|null}
|
|
247
|
-
*/
|
|
248
|
-
function _reconcileEnabledFromConfig(config) {
|
|
249
|
-
if (config && typeof config === 'object') {
|
|
250
|
-
const rc = /** @type {Record<string, unknown>} */ (config).reconcile;
|
|
251
|
-
if (rc && typeof rc === 'object' && typeof (/** @type {any} */ (rc).enabled) === 'boolean') {
|
|
252
|
-
return /** @type {any} */ (rc).enabled;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
return null;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
346
|
/**
|
|
259
347
|
* Check reconcile-nudge readiness and produce a session-start banner.
|
|
260
348
|
*
|
|
@@ -274,26 +362,30 @@ export async function checkReconcileNudge(opts = {}) {
|
|
|
274
362
|
const { repoRoot, config, now } = opts;
|
|
275
363
|
if (!repoRoot || typeof repoRoot !== 'string') return null;
|
|
276
364
|
|
|
365
|
+
// ONE config read for both settings, BEFORE the backlog is counted — the
|
|
366
|
+
// `min-insight-chars` gate must reach `countReconcileBacklog`, and reading the
|
|
367
|
+
// config afterwards (as this did before) left it inert.
|
|
368
|
+
let reconcileEnabled = false;
|
|
369
|
+
let minInsightChars;
|
|
370
|
+
try {
|
|
371
|
+
({ enabled: reconcileEnabled, minInsightChars } = await _resolveReconcileSettings(
|
|
372
|
+
repoRoot,
|
|
373
|
+
config,
|
|
374
|
+
));
|
|
375
|
+
} catch {
|
|
376
|
+
// Unreachable in practice (_resolveReconcileSettings never throws) — keep the
|
|
377
|
+
// conservative default and let computeReconcileNudge resolve the gate itself.
|
|
378
|
+
reconcileEnabled = false;
|
|
379
|
+
}
|
|
380
|
+
|
|
277
381
|
let computed;
|
|
278
382
|
try {
|
|
279
|
-
computed = await computeReconcileNudge({ repoRoot, now });
|
|
383
|
+
computed = await computeReconcileNudge({ repoRoot, now, minInsightChars });
|
|
280
384
|
} catch {
|
|
281
385
|
return null;
|
|
282
386
|
}
|
|
283
387
|
if (!computed || computed.nudge !== true) return null;
|
|
284
388
|
|
|
285
|
-
let reconcileEnabled = _reconcileEnabledFromConfig(config);
|
|
286
|
-
if (reconcileEnabled === null) {
|
|
287
|
-
try {
|
|
288
|
-
const content = await readConfigFile(repoRoot);
|
|
289
|
-
reconcileEnabled = _parseReconcile(content).enabled;
|
|
290
|
-
} catch {
|
|
291
|
-
// Config unreadable — fall back to the documented config default (false)
|
|
292
|
-
// so the informational parenthetical still renders conservatively.
|
|
293
|
-
reconcileEnabled = false;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
389
|
// Three-state last-run label. `never` is a claim about history and must only
|
|
298
390
|
// be made when the store was inspected and held nothing: a store whose lines
|
|
299
391
|
// were quarantined by the shape guard is EVIDENCE OF A RUN that can no longer
|
|
@@ -323,7 +415,8 @@ export async function checkReconcileNudge(opts = {}) {
|
|
|
323
415
|
}
|
|
324
416
|
|
|
325
417
|
const lines = [
|
|
326
|
-
`⚠ reconcile-nudge: ${computed.activeLearnings} active learnings, ${computed.
|
|
418
|
+
`⚠ reconcile-nudge: ${computed.activeLearnings} active learnings, ${computed.backlogCount} rule-eligible awaiting a rule ` +
|
|
419
|
+
`(${computed.alreadyMaterialized} already materialized), ` +
|
|
327
420
|
`last reconcile run: ${lastRunLabel} — run /reconcile to convert learnings into rules.`,
|
|
328
421
|
];
|
|
329
422
|
if (reconcileEnabled === false) {
|
|
@@ -30,6 +30,37 @@ export function parseEtimeToMinutes(etime) {
|
|
|
30
30
|
return days * 24 * 60 + hours * 60 + mins;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Parse a `ps` etime field (`[[DD-]HH:]MM:SS`) into whole SECONDS.
|
|
35
|
+
*
|
|
36
|
+
* Twin of {@link parseEtimeToMinutes}, which drops the seconds field because
|
|
37
|
+
* minute resolution is all the zombie counter needs. The orphan reaper
|
|
38
|
+
* (`scripts/lib/orphan-reaper.mjs`) cannot use that: its minimum age is 300
|
|
39
|
+
* seconds and its identity check reconstructs a start time as
|
|
40
|
+
* `now - etimeSeconds * 1000`, where a discarded seconds field would be a
|
|
41
|
+
* systematic error of up to 59 s against a 2 s tolerance.
|
|
42
|
+
*
|
|
43
|
+
* Deliberately a sibling rather than a refactor of the twin: `parseEtimeToMinutes`
|
|
44
|
+
* has live callers (`countZombieProcesses`) whose behaviour must not shift.
|
|
45
|
+
*
|
|
46
|
+
* Returns null when the format is unrecognised. Pure function.
|
|
47
|
+
* @param {string} etime
|
|
48
|
+
* @returns {number|null}
|
|
49
|
+
*/
|
|
50
|
+
export function parseEtimeToSeconds(etime) {
|
|
51
|
+
if (typeof etime !== 'string') return null;
|
|
52
|
+
const s = etime.trim();
|
|
53
|
+
// Same shape as parseEtimeToMinutes: optional "DD-", optional "HH:", then "MM:SS".
|
|
54
|
+
const m = /^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/.exec(s);
|
|
55
|
+
if (!m) return null;
|
|
56
|
+
const days = parseInt(m[1] ?? '0', 10);
|
|
57
|
+
const hours = parseInt(m[2] ?? '0', 10);
|
|
58
|
+
const mins = parseInt(m[3], 10);
|
|
59
|
+
const secs = parseInt(m[4], 10);
|
|
60
|
+
if ([days, hours, mins, secs].some(Number.isNaN)) return null;
|
|
61
|
+
return days * 86400 + hours * 3600 + mins * 60 + secs;
|
|
62
|
+
}
|
|
63
|
+
|
|
33
64
|
/**
|
|
34
65
|
* Parse the detailed ps output and count Claude/Node zombie candidates.
|
|
35
66
|
* A zombie candidate is a process matching "claude" or "node" that:
|
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
* when at least one `scopePath` matches at least one glob pattern.
|
|
12
12
|
*
|
|
13
13
|
* `paths:` is a same-shape alias for `globs:` (issue #795) — some repos use a
|
|
14
|
-
* `paths:` frontmatter convention instead of `globs:` (
|
|
15
|
-
*
|
|
14
|
+
* `paths:` frontmatter convention instead of `globs:` (projects-baseline did
|
|
15
|
+
* when #795 landed; it now carries both keys — measured count:
|
|
16
|
+
* docs/baseline.md). Before #795 these repos'
|
|
16
17
|
* path-scoped rules were silently misclassified as always-on, inflating the
|
|
17
18
|
* `instruction-budget-guard.mjs` (#687) always-on count with a false positive.
|
|
18
19
|
* `paths:` supports the identical inline-array and block-list forms as
|
|
@@ -365,6 +366,35 @@ function metaToEntryFields(meta) {
|
|
|
365
366
|
return out;
|
|
366
367
|
}
|
|
367
368
|
|
|
369
|
+
/**
|
|
370
|
+
* The expiry predicate, shared by the injection gate below and the `/reconcile`
|
|
371
|
+
* provenance reader (`scripts/lib/reconcile/backlog.mjs`) — one `Date.parse`
|
|
372
|
+
* for both, so "expired" can never mean two different things to the two
|
|
373
|
+
* consumers (#1387).
|
|
374
|
+
*
|
|
375
|
+
* FAIL-OPEN by design: an absent, empty or unparseable `expires-at` is NOT
|
|
376
|
+
* expired. Fail-closed here would un-inject (and, in the reconcile reader,
|
|
377
|
+
* un-materialize) the whole corpus on a single typo.
|
|
378
|
+
*
|
|
379
|
+
* Pure apart from the optional `onUnparseable` callback, which exists so the
|
|
380
|
+
* injection path can keep its stderr WARN while the reconcile reader stays
|
|
381
|
+
* silent.
|
|
382
|
+
*
|
|
383
|
+
* @param {unknown} rawExpiry - the raw `expires-at` frontmatter value (or undefined)
|
|
384
|
+
* @param {number} [now] - epoch ms; defaults to `Date.now()`
|
|
385
|
+
* @param {(raw: unknown) => void} [onUnparseable] - called when `expires-at` is present but unparseable
|
|
386
|
+
* @returns {boolean} true only when a PARSEABLE `expires-at` lies before `now`
|
|
387
|
+
*/
|
|
388
|
+
export function isRuleExpired(rawExpiry, now = Date.now(), onUnparseable) {
|
|
389
|
+
if (rawExpiry === undefined || rawExpiry === null) return false;
|
|
390
|
+
const ts = Date.parse(String(rawExpiry));
|
|
391
|
+
if (Number.isNaN(ts)) {
|
|
392
|
+
if (typeof onUnparseable === 'function') onUnparseable(rawExpiry);
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
return ts < now;
|
|
396
|
+
}
|
|
397
|
+
|
|
368
398
|
/**
|
|
369
399
|
* Deterministic gating check (issue #694 + #692). Returns `{ excluded: boolean,
|
|
370
400
|
* reason?: string }`. Applied to BOTH always-on and glob-matched rules.
|
|
@@ -378,18 +408,17 @@ function metaToEntryFields(meta) {
|
|
|
378
408
|
* @returns {{ excluded: boolean }}
|
|
379
409
|
*/
|
|
380
410
|
function applyGates(meta, filePath, mode, hostClass, now, context = null) {
|
|
381
|
-
// Expiry gate — fail-open on a malformed `expires-at
|
|
411
|
+
// Expiry gate — fail-open on a malformed `expires-at` ({@link isRuleExpired}).
|
|
382
412
|
if (Object.prototype.hasOwnProperty.call(meta, 'expires-at')) {
|
|
383
413
|
const rawExpiry = meta['expires-at'];
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
);
|
|
414
|
+
if (
|
|
415
|
+
isRuleExpired(rawExpiry, now, (raw) =>
|
|
416
|
+
process.stderr.write(
|
|
417
|
+
`[rule-loader] Rule ${filePath} has unparseable expires-at ${JSON.stringify(raw)} — ignoring expiry\n`,
|
|
418
|
+
),
|
|
419
|
+
)
|
|
420
|
+
) {
|
|
421
|
+
process.stderr.write(`[rule-loader] Rule ${filePath} expired at ${rawExpiry} — excluded\n`);
|
|
393
422
|
return { excluded: true };
|
|
394
423
|
}
|
|
395
424
|
}
|
|
@@ -21,6 +21,13 @@
|
|
|
21
21
|
* when the platform exposes a stable prompt-assembly boundary; at that point the
|
|
22
22
|
* digest can be computed against the real assembled prompt instead of echoed.
|
|
23
23
|
*
|
|
24
|
+
* CALLERS (#1298): the CLI modes run only from prose — `--instruction` in
|
|
25
|
+
* `skills/wave-executor/references/wave-loop-dispatch.md`, `--emit` and `--verify`
|
|
26
|
+
* in `wave-loop-review.md`. Ceiling: `check-unwired-features` S4 cannot notice both
|
|
27
|
+
* dropping that citation — it judges modules, not CLI modes, and reads this one wired
|
|
28
|
+
* as a CLI entrypoint AND via its hook importer (`scopeDigest`). Revisit on a third
|
|
29
|
+
* prose caller, or when the census learns to track prose-invoked CLI entrypoints.
|
|
30
|
+
*
|
|
24
31
|
* Pure + stdlib only. Nothing here throws on malformed input — a broken echo
|
|
25
32
|
* check must never change a wave's outcome.
|
|
26
33
|
*
|
|
@@ -34,6 +41,8 @@
|
|
|
34
41
|
* node scripts/lib/scope-echo.mjs --scope-file <path> --instruction
|
|
35
42
|
* node scripts/lib/scope-echo.mjs --scope-file <path> --report-file <path> \
|
|
36
43
|
* [--wave N --agent-id ID --emit]
|
|
44
|
+
* node scripts/lib/scope-echo.mjs --verify --wave <N> --state-dir <dir> \
|
|
45
|
+
* [--session <id> --events <path> --json --emit]
|
|
37
46
|
* node scripts/lib/scope-echo.mjs --help
|
|
38
47
|
*/
|
|
39
48
|
|
|
@@ -312,13 +321,22 @@ function parseJsonl(raw) {
|
|
|
312
321
|
* Digest every per-agent scope file of one wave — shape (a) of the two scope
|
|
313
322
|
* shapes (CLAUDE.md / AGENTS.md § allowedPaths).
|
|
314
323
|
*
|
|
324
|
+
* A file that cannot be read or parsed is the FILE-side counterpart of a
|
|
325
|
+
* truncated ledger line: skipping it silently makes a wave whose scope files are
|
|
326
|
+
* corrupt indistinguishable from one that was never injected (`digest-unknown` /
|
|
327
|
+
* `injection-missing`). The count therefore rides back on the returned Map as
|
|
328
|
+
* `malformedFiles` — an additive own property, so every existing consumer that
|
|
329
|
+
* only iterates the entries is unaffected. (A file that parses to a non-array or
|
|
330
|
+
* to an empty scope stays a silent skip: an empty scope is legitimate.)
|
|
331
|
+
*
|
|
315
332
|
* @param {string} stateDir
|
|
316
333
|
* @param {number} wave
|
|
317
334
|
* @param {{readDir?: typeof readdirSync, readFile?: typeof readFileSync}} [io]
|
|
318
|
-
* @returns {Map<string, string[]>} digest → agent ids (file basenames)
|
|
335
|
+
* @returns {Map<string, string[]> & {malformedFiles: number}} digest → agent ids (file basenames)
|
|
319
336
|
*/
|
|
320
337
|
export function digestScopeFiles(stateDir, wave, { readDir = readdirSync, readFile = readFileSync } = {}) {
|
|
321
338
|
const out = new Map();
|
|
339
|
+
out.malformedFiles = 0;
|
|
322
340
|
const dir = resolve(String(stateDir), 'filescopes', `wave-${wave}`);
|
|
323
341
|
let names;
|
|
324
342
|
try {
|
|
@@ -332,7 +350,10 @@ export function digestScopeFiles(stateDir, wave, { readDir = readdirSync, readFi
|
|
|
332
350
|
let parsed;
|
|
333
351
|
try {
|
|
334
352
|
parsed = JSON.parse(readFile(join(dir, file), 'utf8'));
|
|
335
|
-
} catch {
|
|
353
|
+
} catch {
|
|
354
|
+
out.malformedFiles += 1; // unreadable or unparseable — not nothing
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
336
357
|
if (!Array.isArray(parsed) || normalizeScopePaths(parsed).length === 0) continue;
|
|
337
358
|
const digest = scopeDigest(parsed);
|
|
338
359
|
const ids = out.get(digest) ?? [];
|
|
@@ -372,6 +393,12 @@ export function digestScopeFiles(stateDir, wave, { readDir = readdirSync, readFi
|
|
|
372
393
|
* evidence — the counts and verdicts below are a floor, not a census — and the
|
|
373
394
|
* human table says so beside them.
|
|
374
395
|
*
|
|
396
|
+
* `malformed_scope_files` is the same measurement on the FILE side (#1379 P2):
|
|
397
|
+
* a scope file that cannot be read or parsed was silently skipped, which made a
|
|
398
|
+
* corrupt wave yield `digest-unknown` / `injection-missing` verdicts
|
|
399
|
+
* indistinguishable from a genuinely missing injection. Always present,
|
|
400
|
+
* including as `0`.
|
|
401
|
+
*
|
|
375
402
|
* ## Transport degradation
|
|
376
403
|
*
|
|
377
404
|
* On a platform with no `PreToolUse` `Agent` matcher (Codex, Cursor, Pi) the
|
|
@@ -491,6 +518,7 @@ export function verifyWaveScope({
|
|
|
491
518
|
injected,
|
|
492
519
|
echoed,
|
|
493
520
|
malformed_lines: malformedLines,
|
|
521
|
+
malformed_scope_files: fileIds.malformedFiles ?? 0,
|
|
494
522
|
by_verdict: byVerdict,
|
|
495
523
|
agents,
|
|
496
524
|
};
|
|
@@ -506,6 +534,8 @@ export function verifyWaveScope({
|
|
|
506
534
|
* including as `0` — it is the denominator's honesty check: without it a join
|
|
507
535
|
* that silently dropped half the ledger is indistinguishable in the record from
|
|
508
536
|
* a wave where nothing went wrong (`.claude/rules/host-resources.md` § HR-105).
|
|
537
|
+
* `malformed_scope_files` carries the same guarantee for the scope files the
|
|
538
|
+
* join reads (#1379 P2).
|
|
509
539
|
*
|
|
510
540
|
* @param {ReturnType<typeof verifyWaveScope>} report
|
|
511
541
|
* @returns {Record<string, unknown>}
|
|
@@ -518,6 +548,7 @@ export function scopeVerifiedPayload(report) {
|
|
|
518
548
|
injected: report.injected,
|
|
519
549
|
echoed: report.echoed,
|
|
520
550
|
malformed_lines: report.malformed_lines ?? 0,
|
|
551
|
+
malformed_scope_files: report.malformed_scope_files ?? 0,
|
|
521
552
|
by_verdict: report.by_verdict,
|
|
522
553
|
digests: report.agents.map((a) => a.digest),
|
|
523
554
|
};
|
|
@@ -636,6 +667,12 @@ async function mainVerify(args) {
|
|
|
636
667
|
? [` WARNING: ${report.malformed_lines} malformed ledger line(s) skipped — `
|
|
637
668
|
+ 'counts and verdicts below are a floor, not a census']
|
|
638
669
|
: []),
|
|
670
|
+
// Same honesty check on the FILE side: an unparseable scope file makes a
|
|
671
|
+
// corrupt wave look exactly like one that was never injected.
|
|
672
|
+
...(report.malformed_scope_files > 0
|
|
673
|
+
? [` WARNING: ${report.malformed_scope_files} malformed scope file(s) skipped — `
|
|
674
|
+
+ 'digest-unknown / injection-missing below may be corruption, not a missing injection']
|
|
675
|
+
: []),
|
|
639
676
|
...report.agents.map((a) => ` ${a.digest} ${a.verdict.padEnd(20)} ${a.agent_id}`),
|
|
640
677
|
];
|
|
641
678
|
process.stdout.write(`${lines.join('\n')}\n`);
|