@codyswann/lisa 4.84.11 → 4.86.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/dist/cli/doctor-session-manager-pending.d.ts +17 -0
- package/dist/cli/doctor-session-manager-pending.d.ts.map +1 -0
- package/dist/cli/doctor-session-manager-pending.js +130 -0
- package/dist/cli/doctor-session-manager-pending.js.map +1 -0
- package/dist/cli/doctor-session-manager-token.d.ts +69 -0
- package/dist/cli/doctor-session-manager-token.d.ts.map +1 -0
- package/dist/cli/doctor-session-manager-token.js +264 -0
- package/dist/cli/doctor-session-manager-token.js.map +1 -0
- package/dist/cli/doctor-session-manager.d.ts +3 -1
- package/dist/cli/doctor-session-manager.d.ts.map +1 -1
- package/dist/cli/doctor-session-manager.js +168 -146
- package/dist/cli/doctor-session-manager.js.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +34 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/nightly-e2e-guard-behavior-certificate.js.map +1 -1
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +172 -23
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/package.json +4 -4
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/SKILL.md +5 -3
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/providers.mjs +121 -3
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/validate-config.mjs +47 -26
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/SKILL.md +197 -54
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/references/runbook-template.md +23 -10
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-cycle.mjs +21 -3
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-human.mjs +199 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-manager-token.mjs +99 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-manager.mjs +52 -52
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-queue.mjs +51 -139
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-runtime.mjs +5 -3
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-setup.mjs +94 -14
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-status.mjs +73 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-usage.mjs +105 -15
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/config.mjs +39 -11
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/human-answer.mjs +162 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/human-needed.mjs +198 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/json-input.mjs +34 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/launch-shell.mjs +115 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/launch.mjs +69 -60
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/ledger-questions.mjs +252 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/ledger.mjs +22 -56
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/queue-github.mjs +117 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/session-manager.mjs +17 -5
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/token-migrate.mjs +180 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/token-provider.mjs +150 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/token-read.mjs +166 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/token-store.mjs +248 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/token.mjs +39 -120
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-session-manager/SKILL.md +82 -19
- package/plugins/lisa/commands/session-manager.md +1 -1
- package/plugins/lisa/commands/setup/session-manager.md +1 -1
- package/plugins/lisa/rules/reference/config-resolution.md +39 -7
- package/plugins/lisa/skills/lisa-secrets-access/SKILL.md +5 -3
- package/plugins/lisa/skills/lisa-secrets-access/scripts/providers.mjs +121 -3
- package/plugins/lisa/skills/lisa-secrets-access/scripts/validate-config.mjs +47 -26
- package/plugins/lisa/skills/lisa-session-manager/SKILL.md +197 -54
- package/plugins/lisa/skills/lisa-session-manager/references/runbook-template.md +23 -10
- package/plugins/lisa/skills/lisa-session-manager/scripts/cli-cycle.mjs +21 -3
- package/plugins/lisa/skills/lisa-session-manager/scripts/cli-human.mjs +199 -0
- package/plugins/lisa/skills/lisa-session-manager/scripts/cli-manager-token.mjs +99 -0
- package/plugins/lisa/skills/lisa-session-manager/scripts/cli-manager.mjs +52 -52
- package/plugins/lisa/skills/lisa-session-manager/scripts/cli-queue.mjs +51 -139
- package/plugins/lisa/skills/lisa-session-manager/scripts/cli-runtime.mjs +5 -3
- package/plugins/lisa/skills/lisa-session-manager/scripts/cli-setup.mjs +94 -14
- package/plugins/lisa/skills/lisa-session-manager/scripts/cli-status.mjs +73 -0
- package/plugins/lisa/skills/lisa-session-manager/scripts/cli-usage.mjs +105 -15
- package/plugins/lisa/skills/lisa-session-manager/scripts/config.mjs +39 -11
- package/plugins/lisa/skills/lisa-session-manager/scripts/human-answer.mjs +162 -0
- package/plugins/lisa/skills/lisa-session-manager/scripts/human-needed.mjs +198 -0
- package/plugins/lisa/skills/lisa-session-manager/scripts/json-input.mjs +34 -0
- package/plugins/lisa/skills/lisa-session-manager/scripts/launch-shell.mjs +115 -0
- package/plugins/lisa/skills/lisa-session-manager/scripts/launch.mjs +69 -60
- package/plugins/lisa/skills/lisa-session-manager/scripts/ledger-questions.mjs +252 -0
- package/plugins/lisa/skills/lisa-session-manager/scripts/ledger.mjs +22 -56
- package/plugins/lisa/skills/lisa-session-manager/scripts/queue-github.mjs +117 -0
- package/plugins/lisa/skills/lisa-session-manager/scripts/session-manager.mjs +17 -5
- package/plugins/lisa/skills/lisa-session-manager/scripts/token-migrate.mjs +180 -0
- package/plugins/lisa/skills/lisa-session-manager/scripts/token-provider.mjs +150 -0
- package/plugins/lisa/skills/lisa-session-manager/scripts/token-read.mjs +166 -0
- package/plugins/lisa/skills/lisa-session-manager/scripts/token-store.mjs +248 -0
- package/plugins/lisa/skills/lisa-session-manager/scripts/token.mjs +39 -120
- package/plugins/lisa/skills/lisa-setup-session-manager/SKILL.md +82 -19
- package/plugins/lisa-agy/commands/lisa/session-manager.md +1 -1
- package/plugins/lisa-agy/commands/lisa/setup/session-manager.md +1 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-secrets-access/SKILL.md +5 -3
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/providers.mjs +121 -3
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/validate-config.mjs +47 -26
- package/plugins/lisa-agy/skills/lisa-session-manager/SKILL.md +197 -54
- package/plugins/lisa-agy/skills/lisa-session-manager/references/runbook-template.md +23 -10
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-cycle.mjs +21 -3
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-human.mjs +199 -0
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-manager-token.mjs +99 -0
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-manager.mjs +52 -52
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-queue.mjs +51 -139
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-runtime.mjs +5 -3
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-setup.mjs +94 -14
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-status.mjs +73 -0
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-usage.mjs +105 -15
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/config.mjs +39 -11
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/human-answer.mjs +162 -0
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/human-needed.mjs +198 -0
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/json-input.mjs +34 -0
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/launch-shell.mjs +115 -0
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/launch.mjs +69 -60
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/ledger-questions.mjs +252 -0
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/ledger.mjs +22 -56
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/queue-github.mjs +117 -0
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/session-manager.mjs +17 -5
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/token-migrate.mjs +180 -0
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/token-provider.mjs +150 -0
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/token-read.mjs +166 -0
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/token-store.mjs +248 -0
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/token.mjs +39 -120
- package/plugins/lisa-agy/skills/lisa-setup-session-manager/SKILL.md +82 -19
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/commands/lisa/session-manager.md +1 -1
- package/plugins/lisa-copilot/commands/lisa/setup/session-manager.md +1 -1
- package/plugins/lisa-copilot/rules/reference/config-resolution.md +39 -7
- package/plugins/lisa-copilot/skills/lisa-secrets-access/SKILL.md +5 -3
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/providers.mjs +121 -3
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/validate-config.mjs +47 -26
- package/plugins/lisa-copilot/skills/lisa-session-manager/SKILL.md +197 -54
- package/plugins/lisa-copilot/skills/lisa-session-manager/references/runbook-template.md +23 -10
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-cycle.mjs +21 -3
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-human.mjs +199 -0
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-manager-token.mjs +99 -0
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-manager.mjs +52 -52
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-queue.mjs +51 -139
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-runtime.mjs +5 -3
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-setup.mjs +94 -14
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-status.mjs +73 -0
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-usage.mjs +105 -15
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/config.mjs +39 -11
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/human-answer.mjs +162 -0
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/human-needed.mjs +198 -0
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/json-input.mjs +34 -0
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/launch-shell.mjs +115 -0
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/launch.mjs +69 -60
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/ledger-questions.mjs +252 -0
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/ledger.mjs +22 -56
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/queue-github.mjs +117 -0
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/session-manager.mjs +17 -5
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/token-migrate.mjs +180 -0
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/token-provider.mjs +150 -0
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/token-read.mjs +166 -0
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/token-store.mjs +248 -0
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/token.mjs +39 -120
- package/plugins/lisa-copilot/skills/lisa-setup-session-manager/SKILL.md +82 -19
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/commands/lisa/session-manager.md +1 -1
- package/plugins/lisa-cursor/commands/lisa/setup/session-manager.md +1 -1
- package/plugins/lisa-cursor/rules/config-resolution-reference.mdc +39 -7
- package/plugins/lisa-cursor/skills/lisa-secrets-access/SKILL.md +5 -3
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/providers.mjs +121 -3
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/validate-config.mjs +47 -26
- package/plugins/lisa-cursor/skills/lisa-session-manager/SKILL.md +197 -54
- package/plugins/lisa-cursor/skills/lisa-session-manager/references/runbook-template.md +23 -10
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-cycle.mjs +21 -3
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-human.mjs +199 -0
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-manager-token.mjs +99 -0
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-manager.mjs +52 -52
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-queue.mjs +51 -139
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-runtime.mjs +5 -3
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-setup.mjs +94 -14
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-status.mjs +73 -0
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-usage.mjs +105 -15
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/config.mjs +39 -11
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/human-answer.mjs +162 -0
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/human-needed.mjs +198 -0
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/json-input.mjs +34 -0
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/launch-shell.mjs +115 -0
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/launch.mjs +69 -60
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/ledger-questions.mjs +252 -0
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/ledger.mjs +22 -56
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/queue-github.mjs +117 -0
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/session-manager.mjs +17 -5
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/token-migrate.mjs +180 -0
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/token-provider.mjs +150 -0
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/token-read.mjs +166 -0
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/token-store.mjs +248 -0
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/token.mjs +39 -120
- package/plugins/lisa-cursor/skills/lisa-setup-session-manager/SKILL.md +82 -19
- package/plugins/lisa-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-devin/rules/reference/config-resolution.md +39 -7
- package/plugins/lisa-devin/skills/lisa-secrets-access/SKILL.md +5 -3
- package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/providers.mjs +121 -3
- package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/validate-config.mjs +47 -26
- package/plugins/lisa-devin/skills/lisa-session-manager/SKILL.md +197 -54
- package/plugins/lisa-devin/skills/lisa-session-manager/references/runbook-template.md +23 -10
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-cycle.mjs +21 -3
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-human.mjs +199 -0
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-manager-token.mjs +99 -0
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-manager.mjs +52 -52
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-queue.mjs +51 -139
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-runtime.mjs +5 -3
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-setup.mjs +94 -14
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-status.mjs +73 -0
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-usage.mjs +105 -15
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/config.mjs +39 -11
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/human-answer.mjs +162 -0
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/human-needed.mjs +198 -0
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/json-input.mjs +34 -0
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/launch-shell.mjs +115 -0
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/launch.mjs +69 -60
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/ledger-questions.mjs +252 -0
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/ledger.mjs +22 -56
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/queue-github.mjs +117 -0
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/session-manager.mjs +17 -5
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/token-migrate.mjs +180 -0
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/token-provider.mjs +150 -0
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/token-read.mjs +166 -0
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/token-store.mjs +248 -0
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/token.mjs +39 -120
- package/plugins/lisa-devin/skills/lisa-setup-session-manager/SKILL.md +82 -19
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/src/base/commands/session-manager.md +1 -1
- package/plugins/src/base/commands/setup/session-manager.md +1 -1
- package/plugins/src/base/rules/reference/config-resolution.md +39 -7
- package/plugins/src/base/skills/lisa-secrets-access/SKILL.md +5 -3
- package/plugins/src/base/skills/lisa-secrets-access/scripts/providers.mjs +121 -3
- package/plugins/src/base/skills/lisa-secrets-access/scripts/validate-config.mjs +47 -26
- package/plugins/src/base/skills/lisa-session-manager/SKILL.md +197 -54
- package/plugins/src/base/skills/lisa-session-manager/references/runbook-template.md +23 -10
- package/plugins/src/base/skills/lisa-session-manager/scripts/cli-cycle.mjs +21 -3
- package/plugins/src/base/skills/lisa-session-manager/scripts/cli-human.mjs +199 -0
- package/plugins/src/base/skills/lisa-session-manager/scripts/cli-manager-token.mjs +99 -0
- package/plugins/src/base/skills/lisa-session-manager/scripts/cli-manager.mjs +52 -52
- package/plugins/src/base/skills/lisa-session-manager/scripts/cli-queue.mjs +51 -139
- package/plugins/src/base/skills/lisa-session-manager/scripts/cli-runtime.mjs +5 -3
- package/plugins/src/base/skills/lisa-session-manager/scripts/cli-setup.mjs +94 -14
- package/plugins/src/base/skills/lisa-session-manager/scripts/cli-status.mjs +73 -0
- package/plugins/src/base/skills/lisa-session-manager/scripts/cli-usage.mjs +105 -15
- package/plugins/src/base/skills/lisa-session-manager/scripts/config.mjs +39 -11
- package/plugins/src/base/skills/lisa-session-manager/scripts/human-answer.mjs +162 -0
- package/plugins/src/base/skills/lisa-session-manager/scripts/human-needed.mjs +198 -0
- package/plugins/src/base/skills/lisa-session-manager/scripts/json-input.mjs +34 -0
- package/plugins/src/base/skills/lisa-session-manager/scripts/launch-shell.mjs +115 -0
- package/plugins/src/base/skills/lisa-session-manager/scripts/launch.mjs +69 -60
- package/plugins/src/base/skills/lisa-session-manager/scripts/ledger-questions.mjs +252 -0
- package/plugins/src/base/skills/lisa-session-manager/scripts/ledger.mjs +22 -56
- package/plugins/src/base/skills/lisa-session-manager/scripts/queue-github.mjs +117 -0
- package/plugins/src/base/skills/lisa-session-manager/scripts/session-manager.mjs +17 -5
- package/plugins/src/base/skills/lisa-session-manager/scripts/token-migrate.mjs +180 -0
- package/plugins/src/base/skills/lisa-session-manager/scripts/token-provider.mjs +150 -0
- package/plugins/src/base/skills/lisa-session-manager/scripts/token-read.mjs +166 -0
- package/plugins/src/base/skills/lisa-session-manager/scripts/token-store.mjs +248 -0
- package/plugins/src/base/skills/lisa-session-manager/scripts/token.mjs +39 -120
- package/plugins/src/base/skills/lisa-setup-session-manager/SKILL.md +82 -19
- package/rails/copy-overwrite/scripts/lisa-rspec-shards.mjs +747 -0
- package/rails/copy-overwrite/scripts/lisa_rspec_shard_recorder.rb +143 -0
- package/rails/copy-overwrite/scripts/lisa_simplecov_collate.rb +51 -0
- package/rails/create-only/.github/workflows/ci.yml +4 -0
- package/scripts/two-channel-couplings.json +89 -7
- package/typescript/copy-overwrite/.prettierignore +1 -0
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// This file is managed by Lisa and IS replaced on each `lisa` run.
|
|
3
|
+
// Do not edit directly — durable changes belong upstream in Lisa.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* RSpec shard planner and verifier for `quality-rails.yml` `test_shards`.
|
|
7
|
+
*
|
|
8
|
+
* The reusable Rails workflow runs the suite as N parallel legs when a caller
|
|
9
|
+
* sets `test_shards` above 1. This file is the part of that which decides
|
|
10
|
+
* something, kept out of the workflow so it can be tested:
|
|
11
|
+
*
|
|
12
|
+
* plan Discover every spec file the way a bare `bundle exec rspec`
|
|
13
|
+
* would, weigh each one, and split them across N shards. Balanced
|
|
14
|
+
* by the per-file timings of a previous run when they exist, and by
|
|
15
|
+
* file size when they do not. The split is proven a partition —
|
|
16
|
+
* every file in exactly one shard — before it is written.
|
|
17
|
+
* files Write one shard's file list, one path per line.
|
|
18
|
+
* stage Copy a finished shard's SimpleCov `.resultset.json` beside its
|
|
19
|
+
* run record, so the aggregate can collate it.
|
|
20
|
+
* verify In the aggregate: prove from every shard's run record — written
|
|
21
|
+
* by `lisa_rspec_shard_recorder.rb` from RSpec's own
|
|
22
|
+
* `files_to_run` — that the shards loaded every planned file
|
|
23
|
+
* exactly once, and that the plan still matches the checkout. It
|
|
24
|
+
* then names the resultsets to collate.
|
|
25
|
+
* timings Fold every shard's per-example run times into per-file timings
|
|
26
|
+
* for the next run's plan.
|
|
27
|
+
*
|
|
28
|
+
* Node-only and dependency-free on purpose: it runs on the bare runner, in
|
|
29
|
+
* jobs that never install the project's gems, and never inside the scratch
|
|
30
|
+
* supervisor's payload.
|
|
31
|
+
*
|
|
32
|
+
* Usage:
|
|
33
|
+
* node scripts/lisa-rspec-shards.mjs plan --shards N --out plan.json [--timings t.json] [--root .]
|
|
34
|
+
* node scripts/lisa-rspec-shards.mjs files --plan plan.json --shard K --out files.txt
|
|
35
|
+
* node scripts/lisa-rspec-shards.mjs stage --record record.json --out DIR
|
|
36
|
+
* node scripts/lisa-rspec-shards.mjs verify --plan plan.json --shards-dir DIR --resultsets out.txt [--root .]
|
|
37
|
+
* node scripts/lisa-rspec-shards.mjs timings --shards-dir DIR --out timings.json
|
|
38
|
+
*/
|
|
39
|
+
import {
|
|
40
|
+
copyFileSync,
|
|
41
|
+
existsSync,
|
|
42
|
+
mkdirSync,
|
|
43
|
+
readFileSync,
|
|
44
|
+
readdirSync,
|
|
45
|
+
statSync,
|
|
46
|
+
writeFileSync,
|
|
47
|
+
} from "node:fs";
|
|
48
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
49
|
+
import { fileURLToPath } from "node:url";
|
|
50
|
+
|
|
51
|
+
/** Plan and record schema version. */
|
|
52
|
+
export const SCHEMA_VERSION = 1;
|
|
53
|
+
|
|
54
|
+
/** The bounds `test_shards` accepts, matching `unit_test_shards`. */
|
|
55
|
+
export const MIN_SHARDS = 1;
|
|
56
|
+
export const MAX_SHARDS = 20;
|
|
57
|
+
|
|
58
|
+
/** RSpec's default `--default-path`. */
|
|
59
|
+
const SPEC_DIR = "spec";
|
|
60
|
+
|
|
61
|
+
/** The run record each shard writes, named inside its shard directory. */
|
|
62
|
+
export const RECORD_FILE = "record.json";
|
|
63
|
+
|
|
64
|
+
/** The staged SimpleCov resultset inside a shard directory. */
|
|
65
|
+
export const RESULTSET_FILE = "resultset.json";
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* `.rspec` options that change which files a bare `rspec` loads. Sharding
|
|
69
|
+
* passes explicit file paths, which bypass every one of them, so a project
|
|
70
|
+
* that sets any would silently run a different set sharded than unsharded.
|
|
71
|
+
*/
|
|
72
|
+
const DISCOVERY_OPTIONS = [
|
|
73
|
+
"--pattern",
|
|
74
|
+
"-P",
|
|
75
|
+
"--exclude-pattern",
|
|
76
|
+
"--default-path",
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @typedef {object} PlanShard
|
|
81
|
+
* @property {number} index - 1-based shard number
|
|
82
|
+
* @property {string[]} files - Repo-relative spec paths, sorted
|
|
83
|
+
* @property {number} estimate - Summed weight of the shard's files
|
|
84
|
+
*/
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @typedef {object} Plan
|
|
88
|
+
* @property {number} version - Schema version
|
|
89
|
+
* @property {number} total - Shard count
|
|
90
|
+
* @property {"timings" | "size" | "mixed"} basis - What the weights came from
|
|
91
|
+
* @property {string[]} files - Every discovered spec file, sorted
|
|
92
|
+
* @property {PlanShard[]} shards - One entry per shard, in order
|
|
93
|
+
*/
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @typedef {object} ExampleTiming
|
|
97
|
+
* @property {string} file - Repo-relative spec path the example belongs to
|
|
98
|
+
* @property {number} run_time - Seconds
|
|
99
|
+
*/
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @typedef {object} ShardRecord
|
|
103
|
+
* @property {number} version - Schema version
|
|
104
|
+
* @property {string} shard - "K/N"
|
|
105
|
+
* @property {string[]} files_to_run - What RSpec itself loaded
|
|
106
|
+
* @property {ExampleTiming[]} examples - Per-example timings
|
|
107
|
+
* @property {{ active: boolean, coverage_path?: string }} simplecov - Coverage hand-off
|
|
108
|
+
*/
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Raised for a refusal whose message is the whole explanation.
|
|
112
|
+
*/
|
|
113
|
+
export class ShardError extends Error {}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Normalise a path to forward slashes, relative, without a leading `./`.
|
|
117
|
+
* @param {string} path - Path as RSpec or the filesystem spelled it
|
|
118
|
+
* @returns {string} Canonical repo-relative path
|
|
119
|
+
*/
|
|
120
|
+
export function canonicalPath(path) {
|
|
121
|
+
return path
|
|
122
|
+
.split(sep)
|
|
123
|
+
.join("/")
|
|
124
|
+
.replace(/^(\.\/)+/, "");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Refuse a project whose `.rspec` changes which files a bare `rspec` loads.
|
|
129
|
+
* @param {string} root - Project root
|
|
130
|
+
*/
|
|
131
|
+
export function refuseCustomDiscovery(root) {
|
|
132
|
+
for (const name of [".rspec", ".rspec-local"]) {
|
|
133
|
+
const file = join(root, name);
|
|
134
|
+
if (!existsSync(file)) continue;
|
|
135
|
+
const tokens = readFileSync(file, "utf8")
|
|
136
|
+
.split("\n")
|
|
137
|
+
.filter(line => !line.trim().startsWith("#"))
|
|
138
|
+
.flatMap(line => line.trim().split(/\s+/))
|
|
139
|
+
.map(token => token.split("=")[0]);
|
|
140
|
+
const found = DISCOVERY_OPTIONS.filter(option => tokens.includes(option));
|
|
141
|
+
if (found.length > 0) {
|
|
142
|
+
throw new ShardError(
|
|
143
|
+
`${name} sets ${found.join(", ")}, which changes which spec files a bare \`rspec\` loads. Sharding passes explicit file paths, which bypass ${found.length === 1 ? "that option" : "those options"}, so the sharded run would not be the suite the unsharded run is. Remove ${found.length === 1 ? "it" : "them"}, or leave test_shards at 1.`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Every spec file a bare `rspec` loads: `spec/**\/*_spec.rb`, skipping dot
|
|
151
|
+
* entries as Ruby's glob does. A symlinked directory is refused rather than
|
|
152
|
+
* guessed at, because RSpec's pattern follows exactly one level of them.
|
|
153
|
+
* @param {string} root - Project root
|
|
154
|
+
* @returns {string[]} Repo-relative spec paths, sorted
|
|
155
|
+
*/
|
|
156
|
+
export function discoverSpecFiles(root) {
|
|
157
|
+
const base = join(root, SPEC_DIR);
|
|
158
|
+
if (!existsSync(base)) return [];
|
|
159
|
+
/** @type {string[]} */
|
|
160
|
+
const found = [];
|
|
161
|
+
/** @param {string} dir - Directory to walk */
|
|
162
|
+
const walk = dir => {
|
|
163
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
164
|
+
if (entry.name.startsWith(".")) continue;
|
|
165
|
+
const full = join(dir, entry.name);
|
|
166
|
+
if (entry.isSymbolicLink()) {
|
|
167
|
+
if (statSync(full).isDirectory()) {
|
|
168
|
+
throw new ShardError(
|
|
169
|
+
`${canonicalPath(relative(root, full))} is a symlinked directory. RSpec's default pattern follows one level of symlinks, which this planner does not reproduce, so it refuses rather than risk dropping files. Leave test_shards at 1, or replace the symlink.`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
if (entry.name.endsWith("_spec.rb"))
|
|
173
|
+
found.push(canonicalPath(relative(root, full)));
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (entry.isDirectory()) walk(full);
|
|
177
|
+
else if (entry.isFile() && entry.name.endsWith("_spec.rb"))
|
|
178
|
+
found.push(canonicalPath(relative(root, full)));
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
walk(base);
|
|
182
|
+
return found.sort();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Read a timings file, tolerating its absence or a foreign shape.
|
|
187
|
+
* @param {string | undefined} path - Timings file path
|
|
188
|
+
* @returns {Record<string, number>} Seconds by repo-relative spec path
|
|
189
|
+
*/
|
|
190
|
+
export function readTimings(path) {
|
|
191
|
+
if (!path || !existsSync(path)) return {};
|
|
192
|
+
try {
|
|
193
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
194
|
+
if (parsed?.version !== SCHEMA_VERSION || typeof parsed.files !== "object")
|
|
195
|
+
return {};
|
|
196
|
+
/** @type {Record<string, number>} */
|
|
197
|
+
const out = {};
|
|
198
|
+
for (const [file, seconds] of Object.entries(parsed.files)) {
|
|
199
|
+
if (
|
|
200
|
+
typeof seconds === "number" &&
|
|
201
|
+
Number.isFinite(seconds) &&
|
|
202
|
+
seconds >= 0
|
|
203
|
+
)
|
|
204
|
+
out[file] = seconds;
|
|
205
|
+
}
|
|
206
|
+
return out;
|
|
207
|
+
} catch {
|
|
208
|
+
return {};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Weigh each file: its recorded seconds when known, otherwise its size scaled
|
|
214
|
+
* by the seconds-per-byte of the files that are known, otherwise its size.
|
|
215
|
+
* @param {string[]} files - Spec paths
|
|
216
|
+
* @param {Record<string, number>} sizes - Bytes by path
|
|
217
|
+
* @param {Record<string, number>} timings - Seconds by path
|
|
218
|
+
* @returns {{ weights: Record<string, number>, basis: Plan["basis"] }} Weights and their source
|
|
219
|
+
*/
|
|
220
|
+
export function weighFiles(files, sizes, timings) {
|
|
221
|
+
const known = files.filter(file => file in timings);
|
|
222
|
+
const knownBytes = known.reduce((sum, file) => sum + (sizes[file] ?? 0), 0);
|
|
223
|
+
const knownSeconds = known.reduce(
|
|
224
|
+
(sum, file) => sum + (timings[file] ?? 0),
|
|
225
|
+
0
|
|
226
|
+
);
|
|
227
|
+
const perByte =
|
|
228
|
+
known.length > 0 && knownBytes > 0 ? knownSeconds / knownBytes : 0;
|
|
229
|
+
/** @type {Record<string, number>} */
|
|
230
|
+
const weights = {};
|
|
231
|
+
for (const file of files) {
|
|
232
|
+
const size = Math.max(1, sizes[file] ?? 1);
|
|
233
|
+
const timed = timings[file];
|
|
234
|
+
if (timed !== undefined) weights[file] = timed;
|
|
235
|
+
else weights[file] = perByte > 0 ? size * perByte : size;
|
|
236
|
+
}
|
|
237
|
+
let basis = /** @type {Plan["basis"]} */ ("size");
|
|
238
|
+
if (known.length === files.length && files.length > 0) basis = "timings";
|
|
239
|
+
else if (known.length > 0) basis = "mixed";
|
|
240
|
+
return { weights, basis };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Longest-processing-time-first split: heaviest file to the lightest shard.
|
|
245
|
+
* Deterministic — ties break on path, then on the lower shard number.
|
|
246
|
+
* @param {string[]} files - Spec paths
|
|
247
|
+
* @param {Record<string, number>} weights - Weight by path
|
|
248
|
+
* @param {number} total - Shard count
|
|
249
|
+
* @returns {PlanShard[]} One entry per shard
|
|
250
|
+
*/
|
|
251
|
+
export function partition(files, weights, total) {
|
|
252
|
+
/** @type {PlanShard[]} */
|
|
253
|
+
const shards = Array.from({ length: total }, (_, i) => ({
|
|
254
|
+
index: i + 1,
|
|
255
|
+
files: [],
|
|
256
|
+
estimate: 0,
|
|
257
|
+
}));
|
|
258
|
+
const ordered = [...files].sort(
|
|
259
|
+
(a, b) =>
|
|
260
|
+
(weights[b] ?? 0) - (weights[a] ?? 0) || (a < b ? -1 : a > b ? 1 : 0)
|
|
261
|
+
);
|
|
262
|
+
for (const file of ordered) {
|
|
263
|
+
let target = shards[0];
|
|
264
|
+
for (const shard of shards) {
|
|
265
|
+
if (target === undefined || shard.estimate < target.estimate)
|
|
266
|
+
target = shard;
|
|
267
|
+
}
|
|
268
|
+
if (target === undefined) throw new ShardError("no shards to assign to");
|
|
269
|
+
target.files.push(file);
|
|
270
|
+
target.estimate += weights[file] ?? 0;
|
|
271
|
+
}
|
|
272
|
+
for (const shard of shards) shard.files.sort();
|
|
273
|
+
return shards;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Name each file and the shards it appeared in.
|
|
278
|
+
* @param {[string, number[]][]} overlap - File and shard numbers
|
|
279
|
+
* @returns {string} "file (shards 1, 2); ..."
|
|
280
|
+
*/
|
|
281
|
+
function describeOverlap(overlap) {
|
|
282
|
+
return overlap
|
|
283
|
+
.map(([file, at]) => {
|
|
284
|
+
const shards = at.join(", ");
|
|
285
|
+
return `${file} (shards ${shards})`;
|
|
286
|
+
})
|
|
287
|
+
.join("; ");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Prove a split is a partition of `files`: every file in exactly one shard,
|
|
292
|
+
* nothing else in any shard, and no shard empty (an empty file list makes a
|
|
293
|
+
* bare `rspec` run the WHOLE suite).
|
|
294
|
+
* @param {string[]} files - Every discovered spec path
|
|
295
|
+
* @param {{ index: number, files: string[] }[]} shards - The split
|
|
296
|
+
*/
|
|
297
|
+
export function assertPartition(files, shards) {
|
|
298
|
+
/** @type {Map<string, number[]>} */
|
|
299
|
+
const seen = new Map();
|
|
300
|
+
for (const shard of shards) {
|
|
301
|
+
if (shard.files.length === 0)
|
|
302
|
+
throw new ShardError(
|
|
303
|
+
`shard ${shard.index} has no spec files; a bare rspec would run the whole suite there.`
|
|
304
|
+
);
|
|
305
|
+
for (const file of shard.files)
|
|
306
|
+
seen.set(file, [...(seen.get(file) ?? []), shard.index]);
|
|
307
|
+
}
|
|
308
|
+
const expected = new Set(files);
|
|
309
|
+
const duplicated = [...seen].filter(([, at]) => at.length > 1);
|
|
310
|
+
const dropped = files.filter(file => !seen.has(file));
|
|
311
|
+
const foreign = [...seen.keys()].filter(file => !expected.has(file));
|
|
312
|
+
/** @type {string[]} */
|
|
313
|
+
const problems = [];
|
|
314
|
+
if (duplicated.length > 0)
|
|
315
|
+
problems.push(`in more than one shard: ${describeOverlap(duplicated)}`);
|
|
316
|
+
if (dropped.length > 0) problems.push(`in no shard: ${dropped.join(", ")}`);
|
|
317
|
+
if (foreign.length > 0)
|
|
318
|
+
problems.push(`not a discovered spec file: ${foreign.join(", ")}`);
|
|
319
|
+
if (problems.length > 0)
|
|
320
|
+
throw new ShardError(
|
|
321
|
+
`the shard split is not a partition of the suite — ${problems.join(" | ")}`
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Parse and bound-check a shard count.
|
|
327
|
+
* @param {string | number | undefined} value - Raw `test_shards`
|
|
328
|
+
* @returns {number} The count
|
|
329
|
+
*/
|
|
330
|
+
export function parseShardCount(value) {
|
|
331
|
+
const text = String(value ?? "").trim();
|
|
332
|
+
const count = /^\d+$/.test(text) ? Number(text) : Number.NaN;
|
|
333
|
+
if (!Number.isInteger(count) || count < MIN_SHARDS || count > MAX_SHARDS) {
|
|
334
|
+
throw new ShardError(
|
|
335
|
+
`test_shards is "${text}"; it must be a whole number from ${MIN_SHARDS} to ${MAX_SHARDS}. 1, the default, runs the suite in one unsharded job.`
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
return count;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Build a shard plan for a project.
|
|
343
|
+
* @param {object} options - Planner inputs
|
|
344
|
+
* @param {string} options.root - Project root
|
|
345
|
+
* @param {number} options.total - Shard count
|
|
346
|
+
* @param {Record<string, number>} [options.timings] - Seconds by path
|
|
347
|
+
* @returns {Plan} The proven plan
|
|
348
|
+
*/
|
|
349
|
+
export function buildPlan({ root, total, timings = {} }) {
|
|
350
|
+
refuseCustomDiscovery(root);
|
|
351
|
+
const files = discoverSpecFiles(root);
|
|
352
|
+
if (files.length === 0)
|
|
353
|
+
throw new ShardError(
|
|
354
|
+
`no spec files under ${SPEC_DIR}/ matched *_spec.rb, so there is nothing to shard.`
|
|
355
|
+
);
|
|
356
|
+
if (files.length < total)
|
|
357
|
+
throw new ShardError(
|
|
358
|
+
`test_shards is ${total} but the suite has only ${files.length} spec file(s). Lower test_shards to at most ${files.length}.`
|
|
359
|
+
);
|
|
360
|
+
/** @type {Record<string, number>} */
|
|
361
|
+
const sizes = {};
|
|
362
|
+
for (const file of files) sizes[file] = statSync(join(root, file)).size;
|
|
363
|
+
const { weights, basis } = weighFiles(files, sizes, timings);
|
|
364
|
+
const shards = partition(files, weights, total);
|
|
365
|
+
assertPartition(files, shards);
|
|
366
|
+
return { version: SCHEMA_VERSION, total, basis, files, shards };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Read and shape-check a plan file.
|
|
371
|
+
* @param {string} path - Plan path
|
|
372
|
+
* @returns {Plan} The plan
|
|
373
|
+
*/
|
|
374
|
+
export function readPlan(path) {
|
|
375
|
+
const plan = JSON.parse(readFileSync(path, "utf8"));
|
|
376
|
+
if (
|
|
377
|
+
plan?.version !== SCHEMA_VERSION ||
|
|
378
|
+
!Array.isArray(plan.shards) ||
|
|
379
|
+
!Array.isArray(plan.files)
|
|
380
|
+
)
|
|
381
|
+
throw new ShardError(
|
|
382
|
+
`${path} is not a version ${SCHEMA_VERSION} RSpec shard plan.`
|
|
383
|
+
);
|
|
384
|
+
return plan;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* One shard's planned files.
|
|
389
|
+
* @param {Plan} plan - The plan
|
|
390
|
+
* @param {number} index - 1-based shard number
|
|
391
|
+
* @returns {string[]} That shard's files
|
|
392
|
+
*/
|
|
393
|
+
export function shardFiles(plan, index) {
|
|
394
|
+
const shard = plan.shards.find(entry => entry.index === index);
|
|
395
|
+
if (!shard)
|
|
396
|
+
throw new ShardError(
|
|
397
|
+
`the plan has no shard ${index} (it has ${plan.total}).`
|
|
398
|
+
);
|
|
399
|
+
if (shard.files.length === 0)
|
|
400
|
+
throw new ShardError(
|
|
401
|
+
`shard ${index} has no spec files; refusing, because a bare rspec would run the whole suite.`
|
|
402
|
+
);
|
|
403
|
+
return shard.files;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Every shard directory's run record, keyed by shard number.
|
|
408
|
+
* @param {string} shardsDir - Directory holding one subdirectory per shard
|
|
409
|
+
* @returns {{ records: Map<number, { dir: string, record: ShardRecord }>, problems: string[] }} Records found, and what was wrong
|
|
410
|
+
*/
|
|
411
|
+
export function readShardRecords(shardsDir) {
|
|
412
|
+
/** @type {Map<number, { dir: string, record: ShardRecord }>} */
|
|
413
|
+
const records = new Map();
|
|
414
|
+
/** @type {string[]} */
|
|
415
|
+
const problems = [];
|
|
416
|
+
const dirs = existsSync(shardsDir)
|
|
417
|
+
? readdirSync(shardsDir, { withFileTypes: true }).filter(entry =>
|
|
418
|
+
entry.isDirectory()
|
|
419
|
+
)
|
|
420
|
+
: [];
|
|
421
|
+
for (const entry of dirs.sort((a, b) => (a.name < b.name ? -1 : 1))) {
|
|
422
|
+
const dir = join(shardsDir, entry.name);
|
|
423
|
+
const file = join(dir, RECORD_FILE);
|
|
424
|
+
if (!existsSync(file)) {
|
|
425
|
+
problems.push(
|
|
426
|
+
`${entry.name} has no ${RECORD_FILE}: RSpec did not finish there, or the shard ran without lisa_rspec_shard_recorder.rb.`
|
|
427
|
+
);
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
/** @type {ShardRecord} */
|
|
431
|
+
let record;
|
|
432
|
+
try {
|
|
433
|
+
record = JSON.parse(readFileSync(file, "utf8"));
|
|
434
|
+
} catch (error) {
|
|
435
|
+
problems.push(
|
|
436
|
+
`${entry.name}/${RECORD_FILE} is not JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
437
|
+
);
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
const index = Number(String(record?.shard ?? "").split("/")[0]);
|
|
441
|
+
if (
|
|
442
|
+
record?.version !== SCHEMA_VERSION ||
|
|
443
|
+
!Number.isInteger(index) ||
|
|
444
|
+
!Array.isArray(record.files_to_run)
|
|
445
|
+
) {
|
|
446
|
+
problems.push(
|
|
447
|
+
`${entry.name}/${RECORD_FILE} is not a version ${SCHEMA_VERSION} shard record.`
|
|
448
|
+
);
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
if (records.has(index)) {
|
|
452
|
+
problems.push(
|
|
453
|
+
`shard ${index} reported twice (${records.get(index)?.dir} and ${dir}).`
|
|
454
|
+
);
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
records.set(index, { dir, record });
|
|
458
|
+
}
|
|
459
|
+
return { records, problems };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* The aggregate's proof that the shards ran the suite exactly once.
|
|
464
|
+
* @param {object} options - Verifier inputs
|
|
465
|
+
* @param {Plan} options.plan - The plan the shards ran
|
|
466
|
+
* @param {Map<number, { dir: string, record: ShardRecord }>} options.records - Shard records
|
|
467
|
+
* @param {string[]} [options.recordProblems] - Problems found reading them
|
|
468
|
+
* @param {string[]} options.discovered - Spec files in the aggregate's checkout
|
|
469
|
+
* @returns {{ problems: string[], resultsets: string[], coverage: boolean }} Verdict and resultsets to collate
|
|
470
|
+
*/
|
|
471
|
+
export function verifyShards({
|
|
472
|
+
plan,
|
|
473
|
+
records,
|
|
474
|
+
recordProblems = [],
|
|
475
|
+
discovered,
|
|
476
|
+
}) {
|
|
477
|
+
const problems = [...recordProblems];
|
|
478
|
+
try {
|
|
479
|
+
assertPartition(discovered, plan.shards);
|
|
480
|
+
} catch (error) {
|
|
481
|
+
problems.push(
|
|
482
|
+
`the plan does not match this checkout: ${error instanceof Error ? error.message : String(error)}`
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
for (let index = 1; index <= plan.total; index++) {
|
|
486
|
+
if (!records.has(index))
|
|
487
|
+
problems.push(`shard ${index}/${plan.total} left no run record.`);
|
|
488
|
+
}
|
|
489
|
+
for (const index of records.keys()) {
|
|
490
|
+
if (index < 1 || index > plan.total)
|
|
491
|
+
problems.push(`a record names shard ${index}, outside 1-${plan.total}.`);
|
|
492
|
+
}
|
|
493
|
+
/** @type {Map<string, number[]>} */
|
|
494
|
+
const ranIn = new Map();
|
|
495
|
+
for (const [index, { record }] of records) {
|
|
496
|
+
const ran = new Set(record.files_to_run.map(canonicalPath));
|
|
497
|
+
for (const file of ran)
|
|
498
|
+
ranIn.set(file, [...(ranIn.get(file) ?? []), index]);
|
|
499
|
+
const planned = new Set(
|
|
500
|
+
plan.shards.find(shard => shard.index === index)?.files ?? []
|
|
501
|
+
);
|
|
502
|
+
const unplanned = [...ran].filter(file => !planned.has(file)).sort();
|
|
503
|
+
const skipped = [...planned].filter(file => !ran.has(file)).sort();
|
|
504
|
+
if (unplanned.length > 0)
|
|
505
|
+
problems.push(
|
|
506
|
+
`shard ${index} ran files it was not given: ${unplanned.join(", ")}`
|
|
507
|
+
);
|
|
508
|
+
if (skipped.length > 0)
|
|
509
|
+
problems.push(
|
|
510
|
+
`shard ${index} did not run files it was given: ${skipped.join(", ")}`
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
const twice = [...ranIn].filter(([, at]) => at.length > 1);
|
|
514
|
+
if (twice.length > 0)
|
|
515
|
+
problems.push(`ran in more than one shard: ${describeOverlap(twice)}`);
|
|
516
|
+
const never = discovered.filter(file => !ranIn.has(file));
|
|
517
|
+
if (never.length > 0 && records.size === plan.total)
|
|
518
|
+
problems.push(`ran in no shard: ${never.join(", ")}`);
|
|
519
|
+
|
|
520
|
+
const active = [...records.values()].filter(
|
|
521
|
+
({ record }) => record.simplecov?.active === true
|
|
522
|
+
);
|
|
523
|
+
/** @type {string[]} */
|
|
524
|
+
const resultsets = [];
|
|
525
|
+
if (active.length > 0 && active.length !== records.size) {
|
|
526
|
+
problems.push(
|
|
527
|
+
`SimpleCov measured ${active.length} of ${records.size} shards; merged coverage would be partial.`
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
for (const { dir, record } of active) {
|
|
531
|
+
const file = join(dir, RESULTSET_FILE);
|
|
532
|
+
if (existsSync(file)) resultsets.push(resolve(file));
|
|
533
|
+
else
|
|
534
|
+
problems.push(
|
|
535
|
+
`shard ${record.shard} measured coverage but uploaded no ${RESULTSET_FILE}.`
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
return {
|
|
539
|
+
problems,
|
|
540
|
+
resultsets: resultsets.sort(),
|
|
541
|
+
coverage: active.length > 0,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Fold every shard's example timings into per-file seconds.
|
|
547
|
+
* @param {Iterable<{ record: ShardRecord }>} records - Shard records
|
|
548
|
+
* @returns {{ version: number, files: Record<string, number> }} Timings document
|
|
549
|
+
*/
|
|
550
|
+
export function buildTimings(records) {
|
|
551
|
+
/** @type {Record<string, number>} */
|
|
552
|
+
const files = {};
|
|
553
|
+
for (const { record } of records) {
|
|
554
|
+
for (const file of record.files_to_run ?? [])
|
|
555
|
+
files[canonicalPath(file)] ??= 0;
|
|
556
|
+
for (const example of record.examples ?? []) {
|
|
557
|
+
if (
|
|
558
|
+
typeof example?.file !== "string" ||
|
|
559
|
+
typeof example.run_time !== "number"
|
|
560
|
+
)
|
|
561
|
+
continue;
|
|
562
|
+
const file = canonicalPath(example.file);
|
|
563
|
+
files[file] = (files[file] ?? 0) + Math.max(0, example.run_time);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
/** @type {Record<string, number>} */
|
|
567
|
+
const sorted = {};
|
|
568
|
+
for (const file of Object.keys(files).sort())
|
|
569
|
+
sorted[file] = Math.round((files[file] ?? 0) * 1000) / 1000;
|
|
570
|
+
return { version: SCHEMA_VERSION, files: sorted };
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Copy a shard's SimpleCov resultset beside its record.
|
|
575
|
+
* @param {string} recordPath - The shard's record
|
|
576
|
+
* @param {string} outDir - The shard's upload directory
|
|
577
|
+
* @returns {string | null} The staged resultset, or null when SimpleCov was not measuring
|
|
578
|
+
*/
|
|
579
|
+
export function stageResultset(recordPath, outDir) {
|
|
580
|
+
/** @type {ShardRecord} */
|
|
581
|
+
const record = JSON.parse(readFileSync(recordPath, "utf8"));
|
|
582
|
+
if (record.simplecov?.active !== true) return null;
|
|
583
|
+
const source = join(
|
|
584
|
+
record.simplecov.coverage_path ?? "coverage",
|
|
585
|
+
".resultset.json"
|
|
586
|
+
);
|
|
587
|
+
if (!existsSync(source))
|
|
588
|
+
throw new ShardError(
|
|
589
|
+
`SimpleCov was measuring but wrote no ${source}, so this shard's coverage cannot be collated.`
|
|
590
|
+
);
|
|
591
|
+
mkdirSync(outDir, { recursive: true });
|
|
592
|
+
const target = join(outDir, RESULTSET_FILE);
|
|
593
|
+
copyFileSync(source, target);
|
|
594
|
+
return target;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Parse `--flag value` pairs.
|
|
599
|
+
* @param {string[]} argv - Arguments after the subcommand
|
|
600
|
+
* @returns {Record<string, string>} Flags by name
|
|
601
|
+
*/
|
|
602
|
+
function parseFlags(argv) {
|
|
603
|
+
/** @type {Record<string, string>} */
|
|
604
|
+
const flags = {};
|
|
605
|
+
for (let i = 0; i < argv.length; i++) {
|
|
606
|
+
const arg = argv[i] ?? "";
|
|
607
|
+
if (!arg.startsWith("--"))
|
|
608
|
+
throw new ShardError(`unexpected argument "${arg}".`);
|
|
609
|
+
const value = argv[i + 1];
|
|
610
|
+
if (value === undefined) throw new ShardError(`${arg} needs a value.`);
|
|
611
|
+
flags[arg.slice(2)] = value;
|
|
612
|
+
i++;
|
|
613
|
+
}
|
|
614
|
+
return flags;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* A required flag.
|
|
619
|
+
* @param {Record<string, string>} flags - Parsed flags
|
|
620
|
+
* @param {string} name - Flag name
|
|
621
|
+
* @returns {string} Its value
|
|
622
|
+
*/
|
|
623
|
+
function required(flags, name) {
|
|
624
|
+
const value = flags[name];
|
|
625
|
+
if (!value) throw new ShardError(`--${name} is required.`);
|
|
626
|
+
return value;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Write a file, creating its directory.
|
|
631
|
+
* @param {string} path - Destination
|
|
632
|
+
* @param {string} body - Contents
|
|
633
|
+
*/
|
|
634
|
+
function writeOut(path, body) {
|
|
635
|
+
mkdirSync(dirname(resolve(path)), { recursive: true });
|
|
636
|
+
writeFileSync(path, body);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Run one subcommand.
|
|
641
|
+
* @param {string[]} argv - Process arguments after the script path
|
|
642
|
+
* @returns {number} Exit status
|
|
643
|
+
*/
|
|
644
|
+
export function main(argv) {
|
|
645
|
+
const [command, ...rest] = argv;
|
|
646
|
+
const flags = parseFlags(rest);
|
|
647
|
+
const root = resolve(flags.root ?? ".");
|
|
648
|
+
switch (command) {
|
|
649
|
+
case "plan": {
|
|
650
|
+
const total = parseShardCount(required(flags, "shards"));
|
|
651
|
+
const plan = buildPlan({
|
|
652
|
+
root,
|
|
653
|
+
total,
|
|
654
|
+
timings: readTimings(flags.timings),
|
|
655
|
+
});
|
|
656
|
+
writeOut(required(flags, "out"), `${JSON.stringify(plan, null, 2)}\n`);
|
|
657
|
+
console.log(
|
|
658
|
+
`Planned ${plan.files.length} spec files across ${plan.total} shards, balanced by ${plan.basis}.`
|
|
659
|
+
);
|
|
660
|
+
for (const shard of plan.shards)
|
|
661
|
+
console.log(
|
|
662
|
+
` shard ${shard.index}/${plan.total}: ${shard.files.length} files, estimate ${shard.estimate.toFixed(1)}`
|
|
663
|
+
);
|
|
664
|
+
return 0;
|
|
665
|
+
}
|
|
666
|
+
case "files": {
|
|
667
|
+
const plan = readPlan(required(flags, "plan"));
|
|
668
|
+
const files = shardFiles(plan, Number(required(flags, "shard")));
|
|
669
|
+
writeOut(required(flags, "out"), `${files.join("\n")}\n`);
|
|
670
|
+
console.log(
|
|
671
|
+
`Shard ${flags.shard}/${plan.total}: ${files.length} spec files.`
|
|
672
|
+
);
|
|
673
|
+
return 0;
|
|
674
|
+
}
|
|
675
|
+
case "stage": {
|
|
676
|
+
const staged = stageResultset(
|
|
677
|
+
required(flags, "record"),
|
|
678
|
+
required(flags, "out")
|
|
679
|
+
);
|
|
680
|
+
console.log(
|
|
681
|
+
staged
|
|
682
|
+
? `Staged ${staged}.`
|
|
683
|
+
: "SimpleCov was not measuring; no resultset to stage."
|
|
684
|
+
);
|
|
685
|
+
return 0;
|
|
686
|
+
}
|
|
687
|
+
case "verify": {
|
|
688
|
+
const plan = readPlan(required(flags, "plan"));
|
|
689
|
+
const { records, problems: recordProblems } = readShardRecords(
|
|
690
|
+
required(flags, "shards-dir")
|
|
691
|
+
);
|
|
692
|
+
refuseCustomDiscovery(root);
|
|
693
|
+
const verdict = verifyShards({
|
|
694
|
+
plan,
|
|
695
|
+
records,
|
|
696
|
+
recordProblems,
|
|
697
|
+
discovered: discoverSpecFiles(root),
|
|
698
|
+
});
|
|
699
|
+
writeOut(
|
|
700
|
+
required(flags, "resultsets"),
|
|
701
|
+
verdict.resultsets.map(file => `${file}\n`).join("")
|
|
702
|
+
);
|
|
703
|
+
if (verdict.problems.length > 0) {
|
|
704
|
+
for (const problem of verdict.problems)
|
|
705
|
+
console.log(
|
|
706
|
+
`::error title=RSpec shards did not run the suite exactly once::${problem}`
|
|
707
|
+
);
|
|
708
|
+
return 1;
|
|
709
|
+
}
|
|
710
|
+
console.log(
|
|
711
|
+
`Verified: ${plan.files.length} spec files ran exactly once across ${plan.total} shards.`
|
|
712
|
+
);
|
|
713
|
+
console.log(
|
|
714
|
+
verdict.coverage
|
|
715
|
+
? `${verdict.resultsets.length} coverage resultsets to collate.`
|
|
716
|
+
: "No shard measured SimpleCov coverage."
|
|
717
|
+
);
|
|
718
|
+
return 0;
|
|
719
|
+
}
|
|
720
|
+
case "timings": {
|
|
721
|
+
const { records } = readShardRecords(required(flags, "shards-dir"));
|
|
722
|
+
const timings = buildTimings(records.values());
|
|
723
|
+
writeOut(required(flags, "out"), `${JSON.stringify(timings, null, 2)}\n`);
|
|
724
|
+
console.log(
|
|
725
|
+
`Recorded timings for ${Object.keys(timings.files).length} spec files.`
|
|
726
|
+
);
|
|
727
|
+
return 0;
|
|
728
|
+
}
|
|
729
|
+
default:
|
|
730
|
+
throw new ShardError(
|
|
731
|
+
`unknown subcommand "${command ?? ""}"; expected plan, files, stage, verify or timings.`
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
if (
|
|
737
|
+
process.argv[1] &&
|
|
738
|
+
resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
739
|
+
) {
|
|
740
|
+
try {
|
|
741
|
+
process.exitCode = main(process.argv.slice(2));
|
|
742
|
+
} catch (error) {
|
|
743
|
+
if (!(error instanceof ShardError)) throw error;
|
|
744
|
+
console.log(`::error title=RSpec shard plan refused::${error.message}`);
|
|
745
|
+
process.exitCode = 1;
|
|
746
|
+
}
|
|
747
|
+
}
|