session-orchestrator 5.3.0 → 5.5.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/claude-md-drift-check/SKILL.md +1 -1
- package/.agents/skills/test-audit/SKILL.md +21 -0
- package/.agents/skills/test-audit/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/claude-md-drift-check/SKILL.md +1 -1
- package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
- package/.codex-plugin/skills/test-audit/agents/openai.yaml +5 -0
- package/.cursor/commands/test-audit.md +14 -0
- package/.cursor/rules/060-evolve.mdc +28 -13
- package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
- package/.cursor/skills/test-audit/SKILL.md +12 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +1 -1
- package/AGENTS.md +9 -31
- package/CHANGELOG.md +162 -0
- package/README.md +14 -10
- package/docs/agent-authoring.md +2 -2
- package/docs/baseline.md +10 -1
- package/docs/ci-setup.md +154 -4
- package/docs/components.md +11 -11
- package/docs/events-schema.md +25 -19
- package/docs/install.md +14 -2
- package/docs/migration-v4.md +24 -17
- package/docs/persona-panel.md +5 -4
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/rule-authoring.md +27 -12
- package/docs/scope-collision-guard.md +40 -16
- package/docs/session-config-reference.md +61 -12
- package/docs/session-config-template.md +11 -2
- package/docs/vault-docs-architecture.md +8 -4
- package/hooks/_lib/hook-import-set.json +42 -29
- package/hooks/_lib/lock-bootstrap.mjs +118 -26
- package/hooks/config-protection.mjs +1 -1
- package/hooks/enforce-commands.mjs +28 -13
- package/hooks/enforce-scope.mjs +201 -23
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +5 -0
- package/hooks/hooks-pi.json +5 -0
- package/hooks/hooks.json +6 -1
- package/hooks/on-session-start.mjs +57 -2
- package/hooks/on-stop.mjs +70 -168
- package/hooks/post-bash-write-verify.mjs +98 -17
- package/hooks/post-edit-validate.mjs +15 -5
- package/hooks/post-tool-batch-wave-signal.mjs +32 -152
- package/hooks/pre-bash-memory-propose-audit.mjs +71 -10
- package/hooks/pre-bash-sessions-ledger-guard.mjs +28 -2
- package/hooks/pre-bash-staging-fence.mjs +29 -8
- package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
- package/hooks/pre-task-scope-disjoint.mjs +1143 -279
- package/hooks/subagent-telemetry.mjs +96 -19
- package/hooks/wave-scope-commit-guard.mjs +168 -72
- package/package.json +2 -6
- package/pi/prompts/test-audit.md +12 -0
- package/rules/README.md +6 -1
- package/rules/_index.md +3 -3
- package/rules/always-on/bash-harness-pitfalls.md +7 -1
- package/rules/opt-in-domain/prompt-caching.md +5 -1
- package/rules/opt-in-stack/backend-data.md +9 -1
- package/rules/opt-in-stack/backend.md +7 -1
- package/rules/opt-in-stack/frontend.md +6 -1
- package/rules/opt-in-stack/security-web.md +7 -1
- package/rules/opt-in-stack/swift.md +5 -1
- package/scripts/apply-session-learnings.mjs +513 -0
- package/scripts/archive-closed-prds.mjs +40 -12
- package/scripts/autopilot.mjs +57 -54
- package/scripts/backfill-abandoned-sessions.mjs +81 -6
- package/scripts/backfill-learnings-from-vault.mjs +49 -22
- package/scripts/baseline-archetypes.mjs +1 -1
- package/scripts/check-package-manager.mjs +62 -5
- package/scripts/codex-install.mjs +3 -0
- package/scripts/cursor-install.mjs +47 -6
- package/scripts/emit-session.mjs +298 -2
- package/scripts/export-hw-learnings.mjs +40 -10
- package/scripts/fleet-checkin.mjs +170 -0
- package/scripts/lib/agent-frontmatter.mjs +3 -3
- package/scripts/lib/auto-dream.mjs +7 -6
- package/scripts/lib/autopilot/flags.mjs +6 -1
- package/scripts/lib/autopilot/loop.mjs +32 -13
- package/scripts/lib/autopilot/stall-sampler.mjs +13 -9
- package/scripts/lib/baseline-archetypes.mjs +41 -22
- package/scripts/lib/blocked-commands-policy.mjs +40 -0
- package/scripts/lib/common.mjs +41 -0
- package/scripts/lib/config/host-paths.mjs +79 -0
- package/scripts/lib/config/persona-gate-wave.mjs +3 -3
- package/scripts/lib/config/reaper.mjs +7 -6
- package/scripts/lib/config/vault-integration.mjs +151 -16
- package/scripts/lib/config-schema.mjs +13 -0
- package/scripts/lib/config.mjs +75 -10
- package/scripts/lib/eval/engine.mjs +13 -0
- package/scripts/lib/events-retention-banner.mjs +393 -0
- package/scripts/lib/events-rotation.mjs +21 -6
- package/scripts/lib/events.mjs +72 -5
- package/scripts/lib/fleet-protocol.mjs +288 -0
- package/scripts/lib/gates/gate-baseline.mjs +13 -3
- package/scripts/lib/gates/gate-full.mjs +7 -2
- package/scripts/lib/gates/gate-helpers.mjs +93 -2
- package/scripts/lib/gates/gate-incremental.mjs +23 -22
- package/scripts/lib/gates/gate-per-file.mjs +16 -7
- package/scripts/lib/hardening.mjs +1 -0
- package/scripts/lib/harness-audit/categories/category4.mjs +8 -6
- package/scripts/lib/instruction-budget-guard.mjs +77 -11
- package/scripts/lib/learnings/expiry-sweep.mjs +420 -22
- package/scripts/lib/learnings/io.mjs +274 -37
- package/scripts/lib/learnings/schema.mjs +163 -15
- package/scripts/lib/learnings/sizing-subject.mjs +2 -2
- package/scripts/lib/lock-reaper.mjs +38 -14
- package/scripts/lib/maintenance-due-banner.mjs +23 -8
- package/scripts/lib/memory-proposals/schema.mjs +11 -5
- package/scripts/lib/named-baseline-resolver.mjs +53 -28
- package/scripts/lib/named-vault-resolver.mjs +147 -5
- package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
- package/scripts/lib/orphan-reaper/no-follow-append.mjs +87 -0
- package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
- package/scripts/lib/orphan-reaper/reaper-audit.mjs +323 -0
- package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
- package/scripts/lib/orphan-reaper/scan-throttle.mjs +144 -0
- package/scripts/lib/orphan-reaper/scan.mjs +510 -0
- package/scripts/lib/orphan-reaper/trigger.mjs +173 -0
- package/scripts/lib/orphan-reaper.mjs +47 -1315
- package/scripts/lib/owner-yaml.mjs +104 -6
- package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
- package/scripts/lib/platform.mjs +206 -2
- package/scripts/lib/plugin-update-banner.mjs +3 -1
- package/scripts/lib/process-group.mjs +134 -22
- package/scripts/lib/project-hygiene.mjs +21 -1
- package/scripts/lib/quality-gate.mjs +45 -5
- package/scripts/lib/resource-probe.mjs +1 -1
- package/scripts/lib/rule-loader.mjs +100 -62
- package/scripts/lib/rules-sync.mjs +27 -12
- package/scripts/lib/scope-baseline.mjs +119 -29
- package/scripts/lib/scope-echo.mjs +7 -0
- package/scripts/lib/scope-gate.mjs +194 -27
- package/scripts/lib/session-close-backfill.mjs +129 -4
- package/scripts/lib/session-end/tail-runner.mjs +10 -5
- package/scripts/lib/session-lock-shape.mjs +6 -3
- package/scripts/lib/session-lock.mjs +320 -67
- package/scripts/lib/session-record-repair.mjs +183 -26
- package/scripts/lib/session-registry.mjs +67 -17
- package/scripts/lib/session-schema/constants.mjs +30 -4
- package/scripts/lib/session-schema/validator.mjs +28 -1
- package/scripts/lib/session-start-probes.mjs +242 -68
- package/scripts/lib/session-token-rollup.mjs +252 -54
- package/scripts/lib/sessions-canonical.mjs +100 -9
- package/scripts/lib/sessions-staleness-banner.mjs +3 -1
- package/scripts/lib/sh-quote.mjs +29 -0
- package/scripts/lib/subagents-schema.mjs +30 -3
- package/scripts/lib/tail-window.mjs +27 -3
- package/scripts/lib/telemetry/pricing.mjs +121 -71
- package/scripts/lib/telemetry/sync.mjs +17 -0
- package/scripts/lib/tests-src-ratio.mjs +215 -23
- package/scripts/lib/tmux-layout/layouts.mjs +1 -11
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +12 -0
- package/scripts/lib/tmux-layout/tmux-shell.mjs +2 -11
- package/scripts/lib/ux-grill/reconcile.mjs +1 -1
- package/scripts/lib/validate/check-agents.mjs +1 -1
- package/scripts/lib/validate/check-hooks-symmetry.mjs +5 -0
- package/scripts/lib/validate/check-learnings-shell-writes.mjs +214 -0
- package/scripts/lib/validate/check-test-value-bans.mjs +388 -27
- package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
- package/scripts/lib/validate/check-unwired-features.mjs +1 -1
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +1 -1
- package/scripts/lib/validate-vendored-rules.mjs +206 -81
- package/scripts/lib/vault-mirror/auto-commit.mjs +5 -3
- package/scripts/lib/vault-mirror/namespace.mjs +47 -26
- package/scripts/lib/vault-mirror/process.mjs +350 -62
- package/scripts/lib/vault-mirror/pseudonym-map.mjs +14 -5
- package/scripts/lib/vault-mirror/render-learnings.mjs +66 -3
- package/scripts/lib/vault-mirror/render-sessions.mjs +28 -4
- package/scripts/lib/vault-mirror/telemetry.mjs +25 -11
- package/scripts/lib/vault-status/board-writer.mjs +53 -12
- package/scripts/lib/vault-status/narrative-mirror.mjs +40 -9
- package/scripts/lib/vcs-repo-spec.mjs +8 -0
- package/scripts/lib/wave-executor/dispatch-common.mjs +10 -18
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +1 -1
- package/scripts/lib/wave-executor/remote-dispatch.mjs +1 -1
- package/scripts/lib/wave-resource-gate.mjs +34 -8
- package/scripts/materialize-wave-scope.mjs +7 -4
- package/scripts/memory-propose.mjs +18 -5
- package/scripts/release-session-lock.mjs +2 -1
- package/scripts/release.mjs +11 -3
- package/scripts/repair-invalid-sessions.mjs +37 -4
- package/scripts/run-migrate-v2-cross-repo.mjs +171 -31
- package/scripts/run-quality-gate.mjs +331 -307
- package/scripts/self-update.mjs +229 -0
- package/scripts/site-numbers.mjs +163 -5
- package/scripts/sweep-expired-learnings.mjs +603 -20
- package/scripts/sweep-expired-rules.mjs +2 -1
- package/scripts/validate-plugin.mjs +7 -0
- package/scripts/validate-wave-scope.mjs +21 -2
- package/scripts/vault-backfill.mjs +50 -37
- package/scripts/vault-integration-watcher.mjs +2 -1
- package/scripts/vault-mirror.mjs +238 -75
- package/scripts/wave-scope-binding.mjs +2 -1
- package/skills/_shared/config-reading.md +3 -3
- package/skills/_shared/fleet-protocol.md +134 -0
- package/skills/_shared/state-ownership.md +41 -12
- package/skills/autopilot/SKILL.md +25 -11
- package/skills/bootstrap/SKILL.md +6 -4
- package/skills/bootstrap/_shared-template.md +97 -2
- package/skills/bootstrap/fast-template.md +4 -1
- package/skills/bootstrap/private-contract.md +2 -1
- package/skills/bootstrap/public-fallback.md +6 -3
- package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +5 -3
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
- package/skills/bootstrap/standard-template.md +2 -0
- package/skills/claude-md-drift-check/SKILL.md +10 -10
- package/skills/claude-md-drift-check/checker.mjs +26 -18
- package/skills/discovery/probes/ssot-code-diff.mjs +12 -6
- package/skills/discovery/probes/vault-narrative-staleness.mjs +6 -2
- package/skills/discovery/probes/vault-staleness.mjs +9 -3
- package/skills/docs-orchestrator/SKILL.md +12 -5
- package/skills/docs-orchestrator/audience-mapping.md +5 -3
- package/skills/evolve/SKILL.md +13 -4
- package/skills/evolve/references/evolve-analyze-mode.md +42 -18
- package/skills/memory-cleanup/SKILL.md +1 -1
- package/skills/persona-panel/SKILL.md +2 -2
- package/skills/persona-panel/persona-format.md +2 -2
- package/skills/persona-panel/presets/designer-lens.md +1 -1
- package/skills/persona-panel/presets/engineer-lens.md +1 -1
- package/skills/persona-panel/presets/pm-lens.md +1 -1
- package/skills/plan/mode-retro.md +19 -4
- package/skills/quality-gates/SKILL.md +6 -4
- package/skills/remote-offload/SKILL.md +4 -2
- package/skills/session-end/learning-patterns.md +23 -13
- package/skills/session-end/metrics-collection.md +6 -7
- package/skills/session-end/phase-3-2-docs-verification.md +7 -6
- package/skills/session-end/plan-verification.md +25 -9
- package/skills/session-end/references/phase-3-documentation-updates.md +13 -3
- package/skills/session-end/references/phase-4a-worktree-cleanup.md +2 -0
- package/skills/session-end/references/session-summary-template.md +1 -1
- package/skills/session-end/session-metrics-write.md +65 -68
- package/skills/session-plan/SKILL.md +0 -3
- package/skills/session-start/SKILL.md +8 -2
- package/skills/session-start/references/operations-contract.md +6 -0
- package/skills/session-start/references/phase-1-2-session-lock.md +32 -7
- package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
- package/skills/session-start/references/phase-4-ssot-environment-check.md +10 -3
- package/skills/test-audit/SKILL.md +152 -0
- package/skills/test-audit/references/campaign.md +111 -0
- package/skills/test-audit/references/mutate.sh +50 -0
- package/skills/vault-mirror/SKILL.md +21 -11
- package/skills/wave-executor/SKILL.md +2 -2
- package/skills/wave-executor/circuit-breaker.md +1 -1
- package/skills/wave-executor/references/wave-executor-state-init.md +3 -0
- package/skills/wave-executor/references/wave-loop-dispatch.md +11 -1
- package/skills/wave-executor/references/wave-loop-review.md +4 -6
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +13 -5
- package/templates/personas/README.md +5 -5
- package/templates/personas/accounting-compliance.v1.md +1 -1
- package/templates/personas/accounting-tax-advisor.v1.md +1 -1
- package/templates/personas/buyer-p1-cto.v1.md +1 -1
- package/templates/personas/buyer-p2-kanzlei.v1.md +1 -1
- package/templates/personas/buyer-p3-build.v1.md +1 -1
- package/templates/personas/buyer-p4-tech-veto.v1.md +1 -1
- package/templates/personas/buyer-p5-solo.v1.md +1 -1
- package/templates/personas/buyer-p6-ld.v1.md +1 -1
- package/templates/personas/klima-ai-expert.v1.md +1 -1
- package/templates/personas/klima-physicist.v1.md +1 -1
|
@@ -123,6 +123,14 @@ async function applyDecisionRules(measurements, opts) {
|
|
|
123
123
|
* declares a remote host that accepts this wave role, route the wave to that
|
|
124
124
|
* host at its full planned agent count instead of shrinking it.
|
|
125
125
|
*
|
|
126
|
+
* `offload-first: true` in Session Config (#1465) widens that trigger to a
|
|
127
|
+
* `proceed` verdict too: an offloadable wave role goes to a ready host even when
|
|
128
|
+
* the local host has headroom. Host selection, the witness contract and the
|
|
129
|
+
* HR-004 cap below are identical on both paths. With the switch on, a wave that
|
|
130
|
+
* stays local anyway (no host declared, none ready) carries a named
|
|
131
|
+
* `offload-first:` reason, so the switch never fails silently; a role absent
|
|
132
|
+
* from OFFLOADABLE_WAVE_ROLES stays local without one.
|
|
133
|
+
*
|
|
126
134
|
* The gate does NOT probe the network — a placement decision must stay a pure
|
|
127
135
|
* function of its inputs. The caller supplies a readiness WITNESS:
|
|
128
136
|
* - `opts.remoteReady` — `{ [alias]: boolean }`, e.g. built from the
|
|
@@ -140,13 +148,24 @@ async function applyDecisionRules(measurements, opts) {
|
|
|
140
148
|
* @returns {Promise<{decision: string, agents: number, reasons: string[], measurements: object, host?: string}>}
|
|
141
149
|
*/
|
|
142
150
|
async function applyOffloadDecision(result, opts) {
|
|
143
|
-
|
|
151
|
+
const offloadFirst = opts.config?.['offload-first'] === true;
|
|
152
|
+
if (!offloadFirst && result.decision !== 'reduce' && result.decision !== 'coordinator-direct') {
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
144
155
|
|
|
145
156
|
const { config, plannedAgents, waveRole, remoteReady, probeFn = null } = opts;
|
|
157
|
+
const role = String(waveRole ?? '').trim().toLowerCase();
|
|
146
158
|
const hosts = config?.['remote-hosts'];
|
|
147
|
-
if (!Array.isArray(hosts) || hosts.length === 0)
|
|
159
|
+
if (!Array.isArray(hosts) || hosts.length === 0) {
|
|
160
|
+
// Only an offloadable role could have been placed, so only it gets the note.
|
|
161
|
+
return offloadFirst && OFFLOADABLE_WAVE_ROLES[role] !== undefined
|
|
162
|
+
? {
|
|
163
|
+
...result,
|
|
164
|
+
reasons: [...result.reasons, 'offload-first: true but no remote-hosts declared — staying local'],
|
|
165
|
+
}
|
|
166
|
+
: result;
|
|
167
|
+
}
|
|
148
168
|
|
|
149
|
-
const role = String(waveRole ?? '').trim().toLowerCase();
|
|
150
169
|
// No isNeverForeignRole() check here (D8 #1204 LOW-1): the invariant is
|
|
151
170
|
// enforced STRUCTURALLY by this mapping table, not by an extra guard.
|
|
152
171
|
// NEVER_FOREIGN_ROLES {impl-core, security-review, migration, release,
|
|
@@ -186,9 +205,10 @@ async function applyOffloadDecision(result, opts) {
|
|
|
186
205
|
}
|
|
187
206
|
}
|
|
188
207
|
if (host === undefined) {
|
|
189
|
-
|
|
190
|
-
?
|
|
191
|
-
:
|
|
208
|
+
const notes = offloadFirst
|
|
209
|
+
? [...probeFailures, `offload-first: no ready host for '${role}' — staying local`]
|
|
210
|
+
: probeFailures;
|
|
211
|
+
return notes.length === 0 ? result : { ...result, reasons: [...result.reasons, ...notes] };
|
|
192
212
|
}
|
|
193
213
|
|
|
194
214
|
// The wave runs at its planned size again — but never above the HR-004 static
|
|
@@ -207,7 +227,9 @@ async function applyOffloadDecision(result, opts) {
|
|
|
207
227
|
host: host.alias,
|
|
208
228
|
reasons: [
|
|
209
229
|
...result.reasons,
|
|
210
|
-
|
|
230
|
+
result.decision === 'proceed'
|
|
231
|
+
? `offload-first: ${role} routed to host '${host.alias}' (verdict was proceed)`
|
|
232
|
+
: `offload: ${role} routed to host '${host.alias}' instead of reducing to ${result.agents}`,
|
|
211
233
|
],
|
|
212
234
|
measurements: result.measurements,
|
|
213
235
|
};
|
|
@@ -383,14 +405,18 @@ export async function evaluateWaveResourceGate(opts) {
|
|
|
383
405
|
}
|
|
384
406
|
|
|
385
407
|
// Rule 2: probe the system (or use override for tests).
|
|
408
|
+
// A failed probe yields `proceed`. Under `offload-first: true` (#1465) that
|
|
409
|
+
// result still goes through placement: routing to a witnessed-ready host needs
|
|
410
|
+
// no local measurement, so a broken probe must not silently disable the switch.
|
|
386
411
|
const measured = await extractMeasurements(opts);
|
|
387
412
|
if ('probeFailed' in measured) {
|
|
388
|
-
|
|
413
|
+
const failed = {
|
|
389
414
|
decision: 'proceed',
|
|
390
415
|
agents: plannedAgents,
|
|
391
416
|
reasons: ['probe failed (ignored)'],
|
|
392
417
|
measurements: {},
|
|
393
418
|
};
|
|
419
|
+
return config['offload-first'] === true ? applyOffloadDecision(failed, opts) : failed;
|
|
394
420
|
}
|
|
395
421
|
|
|
396
422
|
// Rules 3-8: apply decision rule sequence.
|
|
@@ -26,7 +26,7 @@ import { existsSync, readdirSync, readFileSync, unlinkSync } from 'node:fs';
|
|
|
26
26
|
import { dirname, resolve } from 'node:path';
|
|
27
27
|
import { writeJsonAtomicSync } from './lib/io.mjs';
|
|
28
28
|
import { isMainModule } from './lib/is-main-module.mjs';
|
|
29
|
-
import { isPeerRecordId } from './lib/scope-gate.mjs';
|
|
29
|
+
import { COORDINATOR_RECORD_ID, isCoordinatorRecordId, isPeerRecordId } from './lib/scope-gate.mjs';
|
|
30
30
|
import { SCOPE_MATERIALIZED_EVENT, scopeDigest } from './lib/scope-echo.mjs';
|
|
31
31
|
import {
|
|
32
32
|
MANIFEST_SESSION_KEYS,
|
|
@@ -214,8 +214,8 @@ export function validateScopeRecords(value) {
|
|
|
214
214
|
throw new InputError(`record #${recordIndex} has duplicate id (case-insensitive): ${record.id}`);
|
|
215
215
|
}
|
|
216
216
|
seenIds.add(idKey);
|
|
217
|
-
if (idKey ===
|
|
218
|
-
if (record.id
|
|
217
|
+
if (idKey === COORDINATOR_RECORD_ID) {
|
|
218
|
+
if (!isCoordinatorRecordId(record.id)) {
|
|
219
219
|
throw new InputError(`record #${recordIndex} id must be exactly coordinator (lowercase)`);
|
|
220
220
|
}
|
|
221
221
|
coordinatorCount++;
|
|
@@ -537,7 +537,10 @@ export function transportObservable(pluginRoot = resolve(import.meta.dirname, '.
|
|
|
537
537
|
async function emitScopeMaterialized({ stateDir, wave, records }) {
|
|
538
538
|
try {
|
|
539
539
|
const repoRoot = dirname(resolve(stateDir));
|
|
540
|
-
|
|
540
|
+
// Dispatched agents only: a peer's record gets no per-agent file, and the
|
|
541
|
+
// coordinator's is a declaration no dispatch carries — counted, its empty
|
|
542
|
+
// `files` (the common case) read as "two agents share one scope" (#1493.2).
|
|
543
|
+
const agentRecords = records.filter((r) => !isPeerRecordId(r.id) && !isCoordinatorRecordId(r.id));
|
|
541
544
|
const digests = new Set();
|
|
542
545
|
for (const record of agentRecords) {
|
|
543
546
|
if (Array.isArray(record.files) && record.files.length > 0) digests.add(scopeDigest(record.files));
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
* Related modules:
|
|
36
36
|
* scripts/lib/memory-proposals/schema.mjs — createProposalRecord, PROPOSAL_TYPES
|
|
37
37
|
* scripts/lib/memory-proposals/store.mjs — appendProposal
|
|
38
|
+
* scripts/lib/learnings/schema.mjs — isRepoRelativePathEntry (--file-paths)
|
|
38
39
|
* scripts/lib/state-md.mjs — resolveStateMdPath, parseStateMd
|
|
39
40
|
* scripts/parse-config.mjs — Session Config subprocess
|
|
40
41
|
*/
|
|
@@ -42,9 +43,10 @@
|
|
|
42
43
|
import { parseArgs } from 'node:util';
|
|
43
44
|
import { readFileSync, existsSync } from 'node:fs';
|
|
44
45
|
import { spawnSync } from 'node:child_process';
|
|
45
|
-
import { dirname, join
|
|
46
|
+
import { dirname, join } from 'node:path';
|
|
46
47
|
import { fileURLToPath } from 'node:url';
|
|
47
48
|
|
|
49
|
+
import { isRepoRelativePathEntry } from './lib/learnings/schema.mjs';
|
|
48
50
|
import { isWaveAgentContext, WAVE_AGENT_ENV_VAR, WAVE_AGENT_ENV_VALUE } from './lib/wave-context.mjs';
|
|
49
51
|
import { readProcessLocalSessionIds, manifestSessionBinding } from './lib/session-identity/own-session.mjs';
|
|
50
52
|
import { readLockDetailed } from './lib/session-lock.mjs';
|
|
@@ -197,6 +199,14 @@ if (confidenceRaw !== undefined) {
|
|
|
197
199
|
// (Step 6) — so a malformed --file-paths value produces the same exit-4
|
|
198
200
|
// argv-error contract as every other required/optional flag, never a
|
|
199
201
|
// downstream schema-validation surprise.
|
|
202
|
+
//
|
|
203
|
+
// The repo-relative rule is `isRepoRelativePathEntry` from learnings/schema.mjs
|
|
204
|
+
// — the predicate strict validateLearning() applies to `file_paths` at write
|
|
205
|
+
// time. A looser rule here (it was `path.isAbsolute`, blind to `~/x`, `C:\x`,
|
|
206
|
+
// UNC and URL entries) queued proposals that the operator approved and
|
|
207
|
+
// appendLearning() then refused, so the learning never landed. The checks below
|
|
208
|
+
// it (newline, glob metacharacters, count, length) are this CLI's own, stricter
|
|
209
|
+
// additions.
|
|
200
210
|
|
|
201
211
|
const FILE_PATHS_MAX_COUNT = 20;
|
|
202
212
|
const FILE_PATH_MAX_CHARS = 256;
|
|
@@ -219,10 +229,13 @@ if (filePaths !== undefined) {
|
|
|
219
229
|
);
|
|
220
230
|
}
|
|
221
231
|
for (const p of filePaths) {
|
|
222
|
-
if (
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
232
|
+
if (!isRepoRelativePathEntry(p)) {
|
|
233
|
+
// The `..` probe only picks the message; the predicate decides.
|
|
234
|
+
argErrors.push(
|
|
235
|
+
p.split(/[\\/]/).includes('..')
|
|
236
|
+
? `--file-paths must not contain ".." path segments: "${p}"`
|
|
237
|
+
: `--file-paths must be repo-relative — absolute path rejected (leading /, \\ or ~, Windows drive, UNC or URL): "${p}"`,
|
|
238
|
+
);
|
|
226
239
|
} else if (/[\r\n]/.test(p)) {
|
|
227
240
|
argErrors.push(`--file-paths entries must not contain newline characters: "${p}"`);
|
|
228
241
|
} else if (/[*?[\]{}]/.test(p)) {
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
*/
|
|
44
44
|
|
|
45
45
|
import { parseArgs } from 'node:util';
|
|
46
|
+
import path from 'node:path';
|
|
46
47
|
|
|
47
48
|
import { readLockDetailed, loadOwnerProof, release } from './lib/session-lock.mjs';
|
|
48
49
|
import { LOCK_RELEASED_EVENT } from './lib/session-transition.mjs';
|
|
@@ -174,7 +175,7 @@ export async function main({ argv = process.argv.slice(2) } = {}) {
|
|
|
174
175
|
return 1;
|
|
175
176
|
}
|
|
176
177
|
|
|
177
|
-
const repoRoot = values['repo-root'] ?? process.cwd();
|
|
178
|
+
const repoRoot = path.resolve(values['repo-root'] ?? process.cwd());
|
|
178
179
|
|
|
179
180
|
const emitResult = async (result) => {
|
|
180
181
|
if (values.json) process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
package/scripts/release.mjs
CHANGED
|
@@ -1303,14 +1303,21 @@ export function evaluatePublishReceipt(result, target) {
|
|
|
1303
1303
|
* failure remains visible, but none invalidates the already irreversible npm
|
|
1304
1304
|
* publish; callers must reconcile rather than retry `--publish`.
|
|
1305
1305
|
*
|
|
1306
|
+
* `npm view <pkg> version` resolves the `latest` dist-tag (and `npm view` fetches
|
|
1307
|
+
* with `preferOnline`, so no local cache masks the answer): the poll returns
|
|
1308
|
+
* `verified` on the first attempt whose `dist-tags.latest` equals `target`.
|
|
1309
|
+
* Default budget: 12 attempts, 15 s apart = 165 s between the first and last
|
|
1310
|
+
* poll. npm's registry needs ~2 min to serve a fresh `latest` (#1440 C2); the
|
|
1311
|
+
* previous 5 x 3 s = 12 s budget reported `timeout` on every real publish.
|
|
1312
|
+
*
|
|
1306
1313
|
* @param {string} repoRoot
|
|
1307
1314
|
* @param {string} target
|
|
1308
1315
|
* @param {{attempts?: number, delaySeconds?: number, runImpl?: Function, waitImpl?: Function}} [deps]
|
|
1309
1316
|
* @returns {{ok: boolean, kind: 'verified'|'timeout'|'query-failed'|'wait-failed', attempts: number, detail: string}}
|
|
1310
1317
|
*/
|
|
1311
1318
|
export function waitForRegistryPropagation(repoRoot, target, deps = {}) {
|
|
1312
|
-
const attempts = deps.attempts ??
|
|
1313
|
-
const delaySeconds = deps.delaySeconds ??
|
|
1319
|
+
const attempts = deps.attempts ?? 12;
|
|
1320
|
+
const delaySeconds = deps.delaySeconds ?? 15;
|
|
1314
1321
|
const runImpl = deps.runImpl ?? run;
|
|
1315
1322
|
const waitImpl = deps.waitImpl ?? (() => runImpl('sleep', [String(delaySeconds)], { cwd: repoRoot }));
|
|
1316
1323
|
|
|
@@ -1674,6 +1681,7 @@ export function printPublishOutcome(outcome, target, io = {}) {
|
|
|
1674
1681
|
log('\nPost-release checklist (manual):');
|
|
1675
1682
|
log(' 1. Rotate/delete the npm token: https://www.npmjs.com/settings/<user>/tokens');
|
|
1676
1683
|
log(' 2. pi.dev gallery indexes asynchronously — do not block on it.');
|
|
1684
|
+
log(' 3. Install the release on this host: npm run update:local -- --skip-pull, then restart the harness.');
|
|
1677
1685
|
return 0;
|
|
1678
1686
|
}
|
|
1679
1687
|
|
|
@@ -1894,7 +1902,7 @@ async function main() {
|
|
|
1894
1902
|
console.log(`Rewrote ${changed.length} surface file(s) to ${target}:`);
|
|
1895
1903
|
for (const f of changed) console.log(` ${f}`);
|
|
1896
1904
|
console.log(' package-lock.json (via npm install --package-lock-only)');
|
|
1897
|
-
console.log(' site/
|
|
1905
|
+
console.log(' site/ page cells + site/_census.json + site/sitemap.xml lastmod re-stamped (scripts/site-numbers.mjs --write) — commit ALL of them');
|
|
1898
1906
|
console.log('\nEditorial TODOs (enforced by --check):');
|
|
1899
1907
|
console.log(` 1. CHANGELOG.md — write the "## [${target}] - YYYY-MM-DD" entry, fold [Unreleased].`);
|
|
1900
1908
|
console.log(' 2. README.md — rewrite the "Recent highlights" section content.');
|
|
@@ -18,7 +18,11 @@
|
|
|
18
18
|
* over the target (atomic), and the result is then re-verified against BOTH
|
|
19
19
|
* `validateSession` and `checkSessionsIntegrity`. A failed verification restores
|
|
20
20
|
* the backup byte-identically and exits 3 — the ledger is never left in a state
|
|
21
|
-
* worse than the one this CLI found.
|
|
21
|
+
* worse than the one this CLI found. Records with no defensible repair (a
|
|
22
|
+
* timestamp that is not ISO-8601 at all, a waves entry that is not an object)
|
|
23
|
+
* are kept byte-identical and do NOT trigger that rollback (#1442): the repair
|
|
24
|
+
* of every other line is kept, and the run exits 4 so a caller can tell "clean"
|
|
25
|
+
* from "clean except N lines nobody can repair".
|
|
22
26
|
*
|
|
23
27
|
* TARGET CONSTRAINT (MED-3): `--file` MUST resolve inside
|
|
24
28
|
* `<repo-root>/.orchestrator/metrics/`. Any target outside it — including a
|
|
@@ -32,6 +36,10 @@
|
|
|
32
36
|
* 1 — user/input error (bad flag, unknown argument)
|
|
33
37
|
* 2 — system error (unreadable/unwritable ledger, I/O failure)
|
|
34
38
|
* 3 — post-verification failed; the backup was restored and nothing was kept
|
|
39
|
+
* 4 — completed, but N unrepairable lines remain (apply: every repairable
|
|
40
|
+
* line was fixed and verified, the N were kept byte-identical; dry-run:
|
|
41
|
+
* the N lines that WOULD remain are listed). Checked after 3: a
|
|
42
|
+
* rolled-back run is always 3, never 4.
|
|
35
43
|
*/
|
|
36
44
|
|
|
37
45
|
import fs from 'node:fs';
|
|
@@ -53,7 +61,9 @@ const USAGE =
|
|
|
53
61
|
');\n' +
|
|
54
62
|
' must resolve inside <repo-root>/.orchestrator/metrics/\n' +
|
|
55
63
|
' --repo-root project root (default: resolved project dir)\n' +
|
|
56
|
-
'Exit codes: 0 completed, 1 arg error, 2 system error, 3 post-verification failed\n'
|
|
64
|
+
'Exit codes: 0 completed, 1 arg error, 2 system error, 3 post-verification failed\n' +
|
|
65
|
+
' (backup restored), 4 unrepairable remain (N lines kept byte-identical;\n' +
|
|
66
|
+
' every repairable line fixed — dry-run: would remain)\n';
|
|
57
67
|
|
|
58
68
|
/**
|
|
59
69
|
* Resolve `--file` and REFUSE any target outside
|
|
@@ -127,7 +137,10 @@ export function renderHuman(s) {
|
|
|
127
137
|
}
|
|
128
138
|
if (s.backup_path) lines.push(` backup: ${s.backup_path}`);
|
|
129
139
|
if (s.errors.length > 0) {
|
|
130
|
-
|
|
140
|
+
// #1442: name every line that stays invalid — in dry-run BEFORE anything is
|
|
141
|
+
// written, so the operator knows the remainder --apply will leave behind.
|
|
142
|
+
const verb = s.mode === 'apply' ? 'remain' : 'would remain';
|
|
143
|
+
lines.push(` unrepairable: ${s.errors.length} (${verb} invalid; original line kept byte-identical)`);
|
|
131
144
|
for (const e of s.errors) lines.push(` line ${e.line} ${e.session_id ?? '<no session_id>'}: ${e.error}`);
|
|
132
145
|
}
|
|
133
146
|
if (s.post_verify && s.post_verify.integrity !== 'clean' && s.post_verify.integrity !== 'skipped-not-canonical-path') {
|
|
@@ -135,10 +148,30 @@ export function renderHuman(s) {
|
|
|
135
148
|
}
|
|
136
149
|
if (s.ok === false) {
|
|
137
150
|
lines.push(' POST-VERIFICATION FAILED — backup restored, ledger unchanged.');
|
|
151
|
+
} else if (s.errors.length > 0) {
|
|
152
|
+
lines.push(
|
|
153
|
+
s.mode === 'apply'
|
|
154
|
+
? ` Repairable lines fixed and verified; ${s.errors.length} unrepairable line(s) left as they were (exit 4).`
|
|
155
|
+
: ` --apply would fix every repairable line and leave ${s.errors.length} unrepairable line(s) as they are (exit 4).`
|
|
156
|
+
);
|
|
138
157
|
}
|
|
139
158
|
return lines.join('\n') + '\n';
|
|
140
159
|
}
|
|
141
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Map a `repairLedger` summary to the exit-code contract in the header.
|
|
163
|
+
* Rollback (3) wins over an unrepairable remainder (4): a restored ledger kept
|
|
164
|
+
* nothing, so "N remain" would describe a write that did not happen.
|
|
165
|
+
*
|
|
166
|
+
* @param {object} summary
|
|
167
|
+
* @returns {0|3|4}
|
|
168
|
+
*/
|
|
169
|
+
export function exitCodeFor(summary) {
|
|
170
|
+
if (summary.ok === false) return 3;
|
|
171
|
+
if (summary.unrepairable > 0) return 4;
|
|
172
|
+
return 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
142
175
|
async function main() {
|
|
143
176
|
let parsed;
|
|
144
177
|
try {
|
|
@@ -197,7 +230,7 @@ async function main() {
|
|
|
197
230
|
} else {
|
|
198
231
|
process.stdout.write(renderHuman(summary));
|
|
199
232
|
}
|
|
200
|
-
process.exit(summary
|
|
233
|
+
process.exit(exitCodeFor(summary));
|
|
201
234
|
}
|
|
202
235
|
|
|
203
236
|
const isDirectRun =isMainModule(import.meta.url);
|
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
* run-migrate-v2-cross-repo.mjs — cross-repo Migrate-CLI v2 runner.
|
|
4
4
|
*
|
|
5
5
|
* Walks a list of repos, applies the v2 migration to each repo's learnings.jsonl,
|
|
6
|
-
* and reports pre/post invalid-rate per repo
|
|
6
|
+
* and reports pre/post invalid-rate per repo — plus, per repo and in the
|
|
7
|
+
* aggregate, the count of invalid records per validation error class before and
|
|
8
|
+
* after migration (`errorClassesPre` / `errorClassesPost`, GitHub #69 / GitLab
|
|
9
|
+
* #1446). An error class is the validateLearning message up to `, got:`
|
|
10
|
+
* (e.g. `scope must be one of local|private|public`), so a dry run shows which
|
|
11
|
+
* failure kinds the migration fixes and which remain.
|
|
7
12
|
*
|
|
8
13
|
* Usage:
|
|
9
14
|
* node scripts/run-migrate-v2-cross-repo.mjs [--repos <comma-list>] [--apply] [--json] [--out <path>]
|
|
@@ -11,15 +16,20 @@
|
|
|
11
16
|
* Flags:
|
|
12
17
|
* --repos <comma-list> Comma-separated repo paths (absolute or ~-prefixed).
|
|
13
18
|
* When omitted, uses the hardcoded ROLLOUT_REPOS list.
|
|
14
|
-
* --apply Write migrated records back to each file (atomic)
|
|
15
|
-
*
|
|
19
|
+
* --apply Write migrated records back to each file (atomic),
|
|
20
|
+
* under that store's lock (`withLearningsLock`, the
|
|
21
|
+
* `<store>.lock` every learnings writer takes), so a
|
|
22
|
+
* live session appending in that repo is never lost.
|
|
23
|
+
* DEFAULT is dry-run (no writes, no lock).
|
|
16
24
|
* --json Output machine-readable JSON instead of Markdown table.
|
|
17
25
|
* --out <path> Write output to file instead of stdout.
|
|
18
26
|
*
|
|
19
27
|
* Exit codes:
|
|
20
28
|
* 0 Success (including repos with no learnings.jsonl — gracefully skipped)
|
|
21
29
|
* 1 Input/argument error
|
|
22
|
-
* 2 I/O error
|
|
30
|
+
* 2 I/O error — incl. a store lock not acquired under --apply: that repo
|
|
31
|
+
* is left unwritten and reported `status: 'error'`, the run continues
|
|
32
|
+
* with the next repo, and exits 2 at the end
|
|
23
33
|
*/
|
|
24
34
|
|
|
25
35
|
import { existsSync, readFileSync, writeFileSync, copyFileSync, renameSync } from 'node:fs';
|
|
@@ -30,6 +40,7 @@ import {
|
|
|
30
40
|
migrateLegacyLearning,
|
|
31
41
|
validateLearning,
|
|
32
42
|
} from './lib/learnings.mjs';
|
|
43
|
+
import { LearningsLockError, withLearningsLock } from './lib/learnings/io.mjs';
|
|
33
44
|
import { getCrossRepoProjects, getConfinementRoot } from './lib/config/cross-repo.mjs';
|
|
34
45
|
import { validatePathInsideProject } from './lib/path-utils.mjs';
|
|
35
46
|
|
|
@@ -135,47 +146,102 @@ if (reposArg) {
|
|
|
135
146
|
* @property {number} invalidPost - invalid records after migration
|
|
136
147
|
* @property {number} fixedByV2 - records that became valid after migration
|
|
137
148
|
* @property {number} malformed - JSON-parse failures (preserved, not counted in invalid)
|
|
149
|
+
* @property {Record<string, number>} errorClassesPre - invalid-pre count per error class
|
|
150
|
+
* @property {Record<string, number>} errorClassesPost - invalid-post count per error class
|
|
138
151
|
* @property {string|null} error - error message if status=error
|
|
139
152
|
*/
|
|
140
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Reduce a validateLearning error message to its class: the text before the
|
|
156
|
+
* record-specific `, got: <value>` tail. Messages without that tail (e.g.
|
|
157
|
+
* `learning missing required field: subject`) are their own class.
|
|
158
|
+
*
|
|
159
|
+
* @param {unknown} err
|
|
160
|
+
* @returns {string}
|
|
161
|
+
*/
|
|
162
|
+
function errorClassOf(err) {
|
|
163
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
164
|
+
const idx = message.indexOf(', got:');
|
|
165
|
+
return idx === -1 ? message : message.slice(0, idx);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @param {Record<string, number>} counts
|
|
170
|
+
* @param {string} cls
|
|
171
|
+
*/
|
|
172
|
+
function bump(counts, cls) {
|
|
173
|
+
counts[cls] = (counts[cls] ?? 0) + 1;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* A RepoResult that counted nothing.
|
|
178
|
+
*
|
|
179
|
+
* @param {string} repoPath
|
|
180
|
+
* @param {string} status
|
|
181
|
+
* @param {string|null} error
|
|
182
|
+
* @returns {RepoResult}
|
|
183
|
+
*/
|
|
184
|
+
function emptyResult(repoPath, status, error) {
|
|
185
|
+
return {
|
|
186
|
+
repo: repoPath,
|
|
187
|
+
status,
|
|
188
|
+
total: 0,
|
|
189
|
+
invalidPre: 0,
|
|
190
|
+
invalidPost: 0,
|
|
191
|
+
fixedByV2: 0,
|
|
192
|
+
malformed: 0,
|
|
193
|
+
errorClassesPre: {},
|
|
194
|
+
errorClassesPost: {},
|
|
195
|
+
error,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Repos whose store lock was not acquired under --apply (nothing written there). */
|
|
200
|
+
const lockFailedRepos = [];
|
|
201
|
+
|
|
141
202
|
/**
|
|
142
203
|
* Process a single repo. Returns a RepoResult.
|
|
143
204
|
*
|
|
205
|
+
* Under --apply the WHOLE read → backup → rename runs inside the store lock:
|
|
206
|
+
* these are other repos, where a live session may append between this read
|
|
207
|
+
* and the rename — outside the lock that record would be silently replaced.
|
|
208
|
+
* A dry run writes nothing and takes no lock.
|
|
209
|
+
*
|
|
144
210
|
* @param {string} repoPath - resolved absolute path to the repo
|
|
145
211
|
* @param {boolean} apply - whether to write changes back
|
|
146
|
-
* @returns {RepoResult}
|
|
212
|
+
* @returns {Promise<RepoResult>}
|
|
147
213
|
*/
|
|
148
|
-
function processRepo(repoPath, apply) {
|
|
214
|
+
async function processRepo(repoPath, apply) {
|
|
149
215
|
const learningsPath = join(repoPath, LEARNINGS_REL);
|
|
150
216
|
|
|
151
|
-
if (!existsSync(learningsPath))
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
error: null,
|
|
161
|
-
};
|
|
217
|
+
if (!existsSync(learningsPath)) return emptyResult(repoPath, 'skipped', null);
|
|
218
|
+
if (!apply) return migrateStore(repoPath, learningsPath, false);
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
return await withLearningsLock(learningsPath, () => migrateStore(repoPath, learningsPath, true));
|
|
222
|
+
} catch (err) {
|
|
223
|
+
if (!(err instanceof LearningsLockError)) throw err;
|
|
224
|
+
lockFailedRepos.push(repoPath);
|
|
225
|
+
return emptyResult(repoPath, 'error', err.message);
|
|
162
226
|
}
|
|
227
|
+
}
|
|
163
228
|
|
|
229
|
+
/**
|
|
230
|
+
* Read, migrate and (when `apply`) atomically rewrite one store. Under --apply
|
|
231
|
+
* the caller holds the store lock.
|
|
232
|
+
*
|
|
233
|
+
* @param {string} repoPath
|
|
234
|
+
* @param {string} learningsPath
|
|
235
|
+
* @param {boolean} apply
|
|
236
|
+
* @returns {RepoResult}
|
|
237
|
+
*/
|
|
238
|
+
function migrateStore(repoPath, learningsPath, apply) {
|
|
164
239
|
// Read
|
|
165
240
|
let raw;
|
|
166
241
|
try {
|
|
167
242
|
raw = readFileSync(learningsPath, 'utf8');
|
|
168
243
|
} catch (err) {
|
|
169
|
-
return {
|
|
170
|
-
repo: repoPath,
|
|
171
|
-
status: 'error',
|
|
172
|
-
total: 0,
|
|
173
|
-
invalidPre: 0,
|
|
174
|
-
invalidPost: 0,
|
|
175
|
-
fixedByV2: 0,
|
|
176
|
-
malformed: 0,
|
|
177
|
-
error: `read failed: ${err.message}`,
|
|
178
|
-
};
|
|
244
|
+
return emptyResult(repoPath, 'error', `read failed: ${err.message}`);
|
|
179
245
|
}
|
|
180
246
|
|
|
181
247
|
const lines = raw.split('\n').filter((l) => l.trim().length > 0);
|
|
@@ -184,6 +250,10 @@ function processRepo(repoPath, apply) {
|
|
|
184
250
|
let invalidPre = 0;
|
|
185
251
|
let invalidPost = 0;
|
|
186
252
|
let fixedByV2 = 0;
|
|
253
|
+
/** @type {Record<string, number>} */
|
|
254
|
+
const errorClassesPre = {};
|
|
255
|
+
/** @type {Record<string, number>} */
|
|
256
|
+
const errorClassesPost = {};
|
|
187
257
|
const outputLines = [];
|
|
188
258
|
|
|
189
259
|
for (const line of lines) {
|
|
@@ -201,9 +271,10 @@ function processRepo(repoPath, apply) {
|
|
|
201
271
|
let wasValidPre = true;
|
|
202
272
|
try {
|
|
203
273
|
validateLearning({ ...parsed, schema_version: parsed.schema_version ?? 1 });
|
|
204
|
-
} catch {
|
|
274
|
+
} catch (err) {
|
|
205
275
|
wasValidPre = false;
|
|
206
276
|
invalidPre++;
|
|
277
|
+
bump(errorClassesPre, errorClassOf(err));
|
|
207
278
|
}
|
|
208
279
|
|
|
209
280
|
// Migrate
|
|
@@ -217,9 +288,10 @@ function processRepo(repoPath, apply) {
|
|
|
217
288
|
...migrated,
|
|
218
289
|
schema_version: migrated.schema_version ?? 1,
|
|
219
290
|
});
|
|
220
|
-
} catch {
|
|
291
|
+
} catch (err) {
|
|
221
292
|
isValidPost = false;
|
|
222
293
|
invalidPost++;
|
|
294
|
+
bump(errorClassesPost, errorClassOf(err));
|
|
223
295
|
}
|
|
224
296
|
|
|
225
297
|
if (!wasValidPre && isValidPost) {
|
|
@@ -259,6 +331,8 @@ function processRepo(repoPath, apply) {
|
|
|
259
331
|
invalidPost,
|
|
260
332
|
fixedByV2,
|
|
261
333
|
malformed,
|
|
334
|
+
errorClassesPre,
|
|
335
|
+
errorClassesPost,
|
|
262
336
|
error: `write failed: ${err.message}`,
|
|
263
337
|
};
|
|
264
338
|
}
|
|
@@ -272,6 +346,8 @@ function processRepo(repoPath, apply) {
|
|
|
272
346
|
invalidPost,
|
|
273
347
|
fixedByV2,
|
|
274
348
|
malformed,
|
|
349
|
+
errorClassesPre,
|
|
350
|
+
errorClassesPost,
|
|
275
351
|
error: null,
|
|
276
352
|
};
|
|
277
353
|
}
|
|
@@ -280,7 +356,10 @@ function processRepo(repoPath, apply) {
|
|
|
280
356
|
// Run across all repos
|
|
281
357
|
// ---------------------------------------------------------------------------
|
|
282
358
|
|
|
283
|
-
|
|
359
|
+
// Sequential: one store lock at a time, and a lock failure in one repo never
|
|
360
|
+
// stops the next.
|
|
361
|
+
const results = [];
|
|
362
|
+
for (const r of repos) results.push(await processRepo(r, applyFlag));
|
|
284
363
|
|
|
285
364
|
// ---------------------------------------------------------------------------
|
|
286
365
|
// Aggregate
|
|
@@ -294,6 +373,12 @@ const aggregate = results.reduce(
|
|
|
294
373
|
acc.totalMalformed += r.malformed;
|
|
295
374
|
acc.totalReposProcessed += r.status !== 'skipped' ? 1 : 0;
|
|
296
375
|
acc.totalReposSkipped += r.status === 'skipped' ? 1 : 0;
|
|
376
|
+
for (const [cls, n] of Object.entries(r.errorClassesPre)) {
|
|
377
|
+
acc.errorClassesPre[cls] = (acc.errorClassesPre[cls] ?? 0) + n;
|
|
378
|
+
}
|
|
379
|
+
for (const [cls, n] of Object.entries(r.errorClassesPost)) {
|
|
380
|
+
acc.errorClassesPost[cls] = (acc.errorClassesPost[cls] ?? 0) + n;
|
|
381
|
+
}
|
|
297
382
|
return acc;
|
|
298
383
|
},
|
|
299
384
|
{
|
|
@@ -303,6 +388,10 @@ const aggregate = results.reduce(
|
|
|
303
388
|
totalMalformed: 0,
|
|
304
389
|
totalReposProcessed: 0,
|
|
305
390
|
totalReposSkipped: 0,
|
|
391
|
+
/** @type {Record<string, number>} */
|
|
392
|
+
errorClassesPre: {},
|
|
393
|
+
/** @type {Record<string, number>} */
|
|
394
|
+
errorClassesPost: {},
|
|
306
395
|
}
|
|
307
396
|
);
|
|
308
397
|
|
|
@@ -319,6 +408,26 @@ function fmtPct(count, total) {
|
|
|
319
408
|
return `${count} (${((count / total) * 100).toFixed(1)}%)`;
|
|
320
409
|
}
|
|
321
410
|
|
|
411
|
+
/**
|
|
412
|
+
* Markdown table of invalid-record counts per error class, pre vs post
|
|
413
|
+
* migration. Classes are the union of both sides, sorted by pre count desc.
|
|
414
|
+
*
|
|
415
|
+
* @param {Record<string, number>} pre
|
|
416
|
+
* @param {Record<string, number>} post
|
|
417
|
+
* @returns {string[]}
|
|
418
|
+
*/
|
|
419
|
+
function errorClassTable(pre, post) {
|
|
420
|
+
const classes = [...new Set([...Object.keys(pre), ...Object.keys(post)])].sort(
|
|
421
|
+
(a, b) => (pre[b] ?? 0) - (pre[a] ?? 0) || a.localeCompare(b)
|
|
422
|
+
);
|
|
423
|
+
if (classes.length === 0) return ['_No validation errors._'];
|
|
424
|
+
const rows = ['| Error class | Invalid pre | Invalid post |', '|-------------|-------------|--------------|'];
|
|
425
|
+
for (const cls of classes) {
|
|
426
|
+
rows.push(`| ${cls.replaceAll('|', '\\|')} | ${pre[cls] ?? 0} | ${post[cls] ?? 0} |`);
|
|
427
|
+
}
|
|
428
|
+
return rows;
|
|
429
|
+
}
|
|
430
|
+
|
|
322
431
|
function buildMarkdown(results, aggregate, mode) {
|
|
323
432
|
const lines = [];
|
|
324
433
|
lines.push(`# Cross-Repo Migrate-CLI v2 — ${mode} report`);
|
|
@@ -345,6 +454,17 @@ function buildMarkdown(results, aggregate, mode) {
|
|
|
345
454
|
lines.push(`- Repos processed: **${aggregate.totalReposProcessed}**`);
|
|
346
455
|
lines.push(`- Repos skipped (no learnings.jsonl): **${aggregate.totalReposSkipped}**`);
|
|
347
456
|
lines.push('');
|
|
457
|
+
lines.push('## Error classes (all repos)');
|
|
458
|
+
lines.push('');
|
|
459
|
+
lines.push(...errorClassTable(aggregate.errorClassesPre, aggregate.errorClassesPost));
|
|
460
|
+
lines.push('');
|
|
461
|
+
for (const r of results) {
|
|
462
|
+
if (r.status === 'skipped') continue;
|
|
463
|
+
lines.push(`### ${repoName(r.repo)}`);
|
|
464
|
+
lines.push('');
|
|
465
|
+
lines.push(...errorClassTable(r.errorClassesPre, r.errorClassesPost));
|
|
466
|
+
lines.push('');
|
|
467
|
+
}
|
|
348
468
|
return lines.join('\n');
|
|
349
469
|
}
|
|
350
470
|
|
|
@@ -382,4 +502,24 @@ process.stderr.write(
|
|
|
382
502
|
`${aggregate.totalStillInvalidPost} still-invalid\n`
|
|
383
503
|
);
|
|
384
504
|
|
|
385
|
-
|
|
505
|
+
let exitCode = 0;
|
|
506
|
+
if (lockFailedRepos.length > 0) {
|
|
507
|
+
process.stderr.write(
|
|
508
|
+
`run-migrate-v2: ERROR learnings store lock not acquired, nothing written in ${lockFailedRepos.length} repo(s): ` +
|
|
509
|
+
`${lockFailedRepos.join(', ')}\n`
|
|
510
|
+
);
|
|
511
|
+
exitCode = 2;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// #1487.15: every other `status: 'error'` (read failed / write failed) is an
|
|
515
|
+
// I/O error too — exit 2 as the header and --help document, never 0.
|
|
516
|
+
const ioFailed = results.filter((r) => r.status === 'error' && !lockFailedRepos.includes(r.repo));
|
|
517
|
+
if (ioFailed.length > 0) {
|
|
518
|
+
process.stderr.write(
|
|
519
|
+
`run-migrate-v2: ERROR I/O error in ${ioFailed.length} repo(s): ` +
|
|
520
|
+
`${ioFailed.map((r) => `${r.repo} (${r.error})`).join(', ')}\n`
|
|
521
|
+
);
|
|
522
|
+
exitCode = 2;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
process.exit(exitCode);
|