@event4u/agent-config 8.13.0 → 9.1.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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +183 -0
- package/MIGRATION.md +14 -0
- package/README.md +2 -2
- package/dist/agent-src/commands/agent-handoff.md +35 -0
- package/dist/agent-src/commands/agents/init.md +1 -0
- package/dist/agent-src/commands/agents/optimize.md +1 -0
- package/dist/agent-src/commands/agents/user/accept.md +3 -2
- package/dist/agent-src/commands/agents/user/init.md +5 -4
- package/dist/agent-src/commands/agents/user/update.md +3 -2
- package/dist/agent-src/commands/agents/user.md +1 -0
- package/dist/agent-src/commands/agents.md +1 -0
- package/dist/agent-src/commands/analytics/show.md +1 -0
- package/dist/agent-src/commands/analytics.md +1 -0
- package/dist/agent-src/commands/analyze/decision.md +1 -0
- package/dist/agent-src/commands/analyze/incident.md +1 -0
- package/dist/agent-src/commands/analyze/near-miss.md +1 -0
- package/dist/agent-src/commands/analyze/postmortem.md +1 -0
- package/dist/agent-src/commands/analyze/premortem.md +1 -0
- package/dist/agent-src/commands/analyze/reference-repo.md +1 -0
- package/dist/agent-src/commands/analyze.md +3 -2
- package/dist/agent-src/commands/brand/identity.md +6 -5
- package/dist/agent-src/commands/brand/review.md +6 -5
- package/dist/agent-src/commands/brand/strategy.md +6 -4
- package/dist/agent-src/commands/brand/tokens.md +7 -6
- package/dist/agent-src/commands/brand/voice.md +5 -4
- package/dist/agent-src/commands/brand.md +9 -7
- package/dist/agent-src/commands/bug/fix.md +1 -0
- package/dist/agent-src/commands/bug/investigate.md +1 -0
- package/dist/agent-src/commands/bug.md +2 -1
- package/dist/agent-src/commands/challenge-me/vision.md +1 -0
- package/dist/agent-src/commands/challenge-me/with-docs.md +1 -0
- package/dist/agent-src/commands/challenge-me.md +28 -6
- package/dist/agent-src/commands/chat-history.md +1 -0
- package/dist/agent-src/commands/check-current-md.md +1 -0
- package/dist/agent-src/commands/commit.md +1 -0
- package/dist/agent-src/commands/context/create.md +3 -2
- package/dist/agent-src/commands/context.md +1 -0
- package/dist/agent-src/commands/cost/profile.md +1 -0
- package/dist/agent-src/commands/cost.md +2 -1
- package/dist/agent-src/commands/council/analysis.md +1 -0
- package/dist/agent-src/commands/council/debate.md +1 -0
- package/dist/agent-src/commands/council/default.md +21 -20
- package/dist/agent-src/commands/council/design.md +1 -0
- package/dist/agent-src/commands/council/optimize.md +1 -0
- package/dist/agent-src/commands/council/pr.md +1 -0
- package/dist/agent-src/commands/council.md +1 -0
- package/dist/agent-src/commands/estimate-ticket.md +1 -0
- package/dist/agent-src/commands/explain-run.md +88 -0
- package/dist/agent-src/commands/feature/dev.md +1 -0
- package/dist/agent-src/commands/feature/explore.md +1 -0
- package/dist/agent-src/commands/feature/plan.md +1 -0
- package/dist/agent-src/commands/feature/refactor.md +1 -0
- package/dist/agent-src/commands/feature/roadmap.md +1 -0
- package/dist/agent-src/commands/feature.md +2 -1
- package/dist/agent-src/commands/fix/pr-comments.md +19 -0
- package/dist/agent-src/commands/fix.md +2 -1
- package/dist/agent-src/commands/ghostwriter/delete.md +1 -0
- package/dist/agent-src/commands/ghostwriter/fetch.md +1 -0
- package/dist/agent-src/commands/ghostwriter/show.md +1 -0
- package/dist/agent-src/commands/ghostwriter/write.md +1 -0
- package/dist/agent-src/commands/ghostwriter.md +1 -0
- package/dist/agent-src/commands/grill-me.md +17 -12
- package/dist/agent-src/commands/humanize.md +1 -0
- package/dist/agent-src/commands/image/analyse.md +2 -1
- package/dist/agent-src/commands/image/create.md +1 -0
- package/dist/agent-src/commands/image/verify.md +1 -0
- package/dist/agent-src/commands/image.md +16 -12
- package/dist/agent-src/commands/implement-ticket.md +1 -0
- package/dist/agent-src/commands/judge/on-diff.md +1 -0
- package/dist/agent-src/commands/judge/solo.md +1 -0
- package/dist/agent-src/commands/judge/steps.md +1 -0
- package/dist/agent-src/commands/judge.md +1 -0
- package/dist/agent-src/commands/knowledge/cross-repo.md +1 -0
- package/dist/agent-src/commands/knowledge/forget.md +1 -0
- package/dist/agent-src/commands/knowledge/ingest.md +1 -0
- package/dist/agent-src/commands/knowledge/list.md +10 -0
- package/dist/agent-src/commands/knowledge.md +1 -0
- package/dist/agent-src/commands/memory/learn-low-impact.md +1 -0
- package/dist/agent-src/commands/memory/mine-session.md +1 -0
- package/dist/agent-src/commands/memory/promote.md +3 -2
- package/dist/agent-src/commands/memory.md +1 -0
- package/dist/agent-src/commands/mission/upgrade.md +45 -24
- package/dist/agent-src/commands/mode.md +1 -0
- package/dist/agent-src/commands/module/create.md +1 -0
- package/dist/agent-src/commands/module/explore.md +1 -0
- package/dist/agent-src/commands/module.md +1 -0
- package/dist/agent-src/commands/optimize/agents-dir.md +1 -0
- package/dist/agent-src/commands/optimize/project.md +1 -0
- package/dist/agent-src/commands/optimize/prompt.md +1 -0
- package/dist/agent-src/commands/optimize.md +2 -1
- package/dist/agent-src/commands/orchestrate.md +1 -0
- package/dist/agent-src/commands/override.md +1 -0
- package/dist/agent-src/commands/package.md +2 -1
- package/dist/agent-src/commands/post-as/ghostwriter.md +1 -0
- package/dist/agent-src/commands/post-as/me.md +1 -0
- package/dist/agent-src/commands/post-as.md +1 -0
- package/dist/agent-src/commands/pr/create/description-only.md +139 -8
- package/dist/agent-src/commands/pr/create.md +35 -22
- package/dist/agent-src/commands/prediction-pool.md +7 -6
- package/dist/agent-src/commands/prepare-for-review.md +1 -0
- package/dist/agent-src/commands/profile/activate.md +1 -0
- package/dist/agent-src/commands/profile/deactivate.md +1 -0
- package/dist/agent-src/commands/profile.md +1 -0
- package/dist/agent-src/commands/project.md +2 -1
- package/dist/agent-src/commands/refine-ticket.md +7 -0
- package/dist/agent-src/commands/research/deep.md +1 -0
- package/dist/agent-src/commands/research.md +1 -0
- package/dist/agent-src/commands/review/routing.md +1 -0
- package/dist/agent-src/commands/review.md +2 -1
- package/dist/agent-src/commands/roadmap/ai-council.md +1 -0
- package/dist/agent-src/commands/roadmap/materialize.md +1 -0
- package/dist/agent-src/commands/roadmap/process-full.md +1 -0
- package/dist/agent-src/commands/roadmap/process-phase.md +1 -0
- package/dist/agent-src/commands/roadmap/process-step.md +1 -0
- package/dist/agent-src/commands/roadmap.md +1 -0
- package/dist/agent-src/commands/security-audit-config.md +2 -1
- package/dist/agent-src/commands/skill/preview.md +1 -0
- package/dist/agent-src/commands/skill.md +1 -0
- package/dist/agent-src/commands/skills/discover.md +1 -0
- package/dist/agent-src/commands/skills.md +1 -0
- package/dist/agent-src/commands/sync/agent-settings.md +1 -0
- package/dist/agent-src/commands/sync.md +2 -1
- package/dist/agent-src/commands/team/adversarial.md +3 -2
- package/dist/agent-src/commands/team/delegate.md +3 -2
- package/dist/agent-src/commands/team/review.md +3 -2
- package/dist/agent-src/commands/team/status.md +49 -18
- package/dist/agent-src/commands/team-knowledge.md +8 -6
- package/dist/agent-src/commands/team.md +21 -16
- package/dist/agent-src/commands/tests/e2e-heal.md +1 -0
- package/dist/agent-src/commands/tests/e2e-plan.md +1 -0
- package/dist/agent-src/commands/tests/execute.md +1 -0
- package/dist/agent-src/commands/tests.md +2 -1
- package/dist/agent-src/commands/update-form-request-messages.md +1 -0
- package/dist/agent-src/commands/upstream-contribute.md +1 -0
- package/dist/agent-src/commands/video/from-script.md +3 -2
- package/dist/agent-src/commands/video/from-song.md +22 -21
- package/dist/agent-src/commands/video/scene.md +1 -0
- package/dist/agent-src/commands/video/stitch.md +2 -1
- package/dist/agent-src/commands/video/storyboard.md +1 -0
- package/dist/agent-src/commands/video.md +1 -0
- package/dist/agent-src/commands/work.md +1 -0
- package/dist/agent-src/commands/worktree/cleanup.md +8 -7
- package/dist/agent-src/commands/worktree/create.md +1 -0
- package/dist/agent-src/commands/worktree/verify.md +1 -0
- package/dist/agent-src/commands/worktree.md +1 -0
- package/dist/agent-src/rules/active-remediation.md +12 -36
- package/dist/agent-src/rules/artifact-drafting-protocol.md +7 -64
- package/dist/agent-src/rules/context-hygiene.md +19 -113
- package/dist/agent-src/rules/design-fidelity.md +4 -60
- package/dist/agent-src/rules/domain-adoption-policy.md +10 -106
- package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +12 -60
- package/dist/agent-src/rules/improve-before-implement.md +4 -0
- package/dist/agent-src/rules/minimal-safe-diff.md +6 -86
- package/dist/agent-src/rules/size-enforcement.md +10 -0
- package/dist/agent-src/rules/tool-safety.md +17 -3
- package/dist/agent-src/skills/agent-security-review/SKILL.md +12 -1
- package/dist/agent-src/skills/ai-council/SKILL.md +11 -0
- package/dist/agent-src/skills/bug-analyzer/SKILL.md +24 -2
- package/dist/agent-src/skills/code-review/SKILL.md +105 -93
- package/dist/agent-src/skills/code-review/checklists/backend.md +50 -0
- package/dist/agent-src/skills/code-review/checklists/database.md +19 -0
- package/dist/agent-src/skills/code-review/checklists/dependency.md +18 -0
- package/dist/agent-src/skills/code-review/checklists/docs.md +14 -0
- package/dist/agent-src/skills/code-review/checklists/frontend.md +19 -0
- package/dist/agent-src/skills/code-review/checklists/infra.md +17 -0
- package/dist/agent-src/skills/code-review/checklists/producing-the-review.md +62 -0
- package/dist/agent-src/skills/code-review/checklists/test-only.md +13 -0
- package/dist/agent-src/skills/code-review/evals/evals.json +53 -15
- package/dist/agent-src/skills/condense-memory/SKILL.md +3 -2
- package/dist/agent-src/skills/conventional-commits-writing/evals/evals.json +15 -0
- package/dist/agent-src/skills/docker/SKILL.md +10 -0
- package/dist/agent-src/skills/docx-authoring/SKILL.md +102 -0
- package/dist/agent-src/skills/github-ci/evals/evals.json +25 -0
- package/dist/agent-src/skills/humanizer/SKILL.md +79 -3
- package/dist/agent-src/skills/humanizer/references/anti-aiisms.md +112 -0
- package/dist/agent-src/skills/humanizer/references/fixtures.md +70 -0
- package/dist/agent-src/skills/judge-bug-hunter/SKILL.md +7 -1
- package/dist/agent-src/skills/laravel/SKILL.md +10 -0
- package/dist/agent-src/skills/launch-readiness/SKILL.md +35 -0
- package/dist/agent-src/skills/llm-provider-knowledge/SKILL.md +146 -0
- package/dist/agent-src/skills/llm-provider-knowledge/evals/triggers.json +20 -0
- package/dist/agent-src/skills/merge-conflicts/SKILL.md +30 -0
- package/dist/agent-src/skills/pdf-tools/SKILL.md +101 -0
- package/dist/agent-src/skills/quality-tools/evals/evals.json +15 -0
- package/dist/agent-src/skills/security-audit/SKILL.md +29 -0
- package/dist/agent-src/skills/security-maturity-assessment/SKILL.md +90 -0
- package/dist/agent-src/skills/security-maturity-assessment/evals/triggers.json +52 -0
- package/dist/agent-src/skills/skill-writing/SKILL.md +79 -0
- package/dist/agent-src/skills/spreadsheet-authoring/SKILL.md +11 -3
- package/dist/agent-src/skills/subagent-orchestration/prompts/README.md +11 -0
- package/dist/agent-src/skills/subagent-orchestration/schemas/team-review-status.json +103 -0
- package/dist/agent-src/skills/terraform/SKILL.md +10 -0
- package/dist/agent-src/skills/test-driven-development/SKILL.md +31 -0
- package/dist/agent-src/skills/threat-modeling/SKILL.md +15 -0
- package/dist/agent-src/skills/token-optimizer/SKILL.md +10 -4
- package/dist/agent-src/templates/agent-settings.md +28 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/scripts/telemetry/aggregator.ts +0 -0
- package/dist/agent-src/templates/scripts/telemetry/engagement.ts +30 -0
- package/dist/agent-src/templates/scripts/telemetry/report_renderer.ts +22 -9
- package/dist/agent-src/templates/scripts/telemetry_record.ts +5 -0
- package/dist/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +4 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +314 -194
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +5 -5
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +23 -18
- package/dist/discovery/trust-report.md +8 -8
- package/dist/discovery/workspaces.json +18 -13
- package/dist/install/install.mjs +48 -33
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +4 -1
- package/dist/scripts/_lib/agent_settings.js.map +1 -1
- package/dist/scripts/condense.js +5 -0
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +10 -1
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/CLAIMS.md +9 -2
- package/docs/MIGRATION.md +20 -0
- package/docs/SKILL_CENSUS.md +8 -0
- package/docs/architecture.md +3 -3
- package/docs/benchmark.md +67 -2
- package/docs/catalog.md +2 -2
- package/docs/command-flows.md +1 -1
- package/docs/contracts/agents-layout.md +1 -1
- package/docs/contracts/ai-council-config.md +9 -2
- package/docs/contracts/ai-team-config.md +37 -4
- package/docs/contracts/consumer-bridge.md +55 -63
- package/docs/contracts/install-layout.md +1 -1
- package/docs/contracts/migrate-command.md +1 -1
- package/docs/contracts/prelaunch-diagnostics.md +151 -0
- package/docs/contracts/release-pr-gating.md +1 -0
- package/docs/customization.md +22 -1
- package/docs/decisions/ADR-020-global-only-consumer-scope.md +34 -0
- package/docs/decisions/ADR-049-configuration-trust-boundary.md +9 -1
- package/docs/featured-skills.md +2 -2
- package/docs/getting-started-by-role.md +1 -1
- package/docs/getting-started.md +17 -6
- package/docs/governance-advantage.md +1 -1
- package/docs/guidelines/agent-infra/active-remediation-mechanics.md +45 -0
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +28 -0
- package/docs/guidelines/agent-infra/api-cost-levers.md +126 -0
- package/docs/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +76 -0
- package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +90 -0
- package/docs/guidelines/agent-infra/domain-adoption-gates.md +112 -0
- package/docs/guidelines/agent-infra/framework-neutrality-patterns.md +66 -0
- package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +99 -0
- package/docs/guidelines/agent-infra/skill-quality-checklist.md +13 -0
- package/docs/guidelines/design-fidelity-mechanics.md +76 -0
- package/docs/installation.md +2 -1
- package/docs/maintainers/dev-mode.md +3 -2
- package/docs/parity/command-frontmatter.md +60 -0
- package/docs/proof.md +7 -6
- package/package.json +1 -1
- package/src/agent-src/templates/scripts/telemetry/aggregator.ts +0 -0
- package/src/agent-src/templates/scripts/telemetry/engagement.ts +30 -0
- package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +22 -9
- package/src/agent-src/templates/scripts/telemetry_record.ts +5 -0
- package/src/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +4 -1
- package/src/config/agent-settings.template.yml +73 -10
- package/src/config/gitignore-block.txt +16 -6
- package/src/config/prelaunch-areas.yml +36 -0
- package/src/scripts/_cli/cmd_conformance.ts +2 -2
- package/src/scripts/_cli/cmd_doctor.ts +280 -39
- package/src/scripts/_cli/cmd_refresh.ts +35 -118
- package/src/scripts/_lib/agent_settings.ts +4 -1
- package/src/scripts/_lib/claude_plugin.ts +77 -2
- package/src/scripts/_lib/orchestration_record.ts +5 -0
- package/src/scripts/_lib/tool_adapter_registry.ts +120 -0
- package/src/scripts/ai_council/clients.ts +68 -6
- package/src/scripts/ai_council/pricing.ts +62 -0
- package/src/scripts/ai_team/config.ts +73 -1
- package/src/scripts/ai_team/review_gate.ts +636 -0
- package/src/scripts/ai_team/team_dispatch.ts +612 -0
- package/src/scripts/bench_ab_task_runner.ts +11 -1
- package/src/scripts/bench_parity_count.ts +279 -0
- package/src/scripts/build_linear_digest.ts +3 -1
- package/src/scripts/check_bridge_derivation.ts +172 -0
- package/src/scripts/check_consumer_scope_flip.ts +179 -0
- package/src/scripts/check_council_config_location.ts +115 -15
- package/src/scripts/check_host_loadability.ts +81 -0
- package/src/scripts/condense.ts +5 -0
- package/src/scripts/council_cli.ts +19 -3
- package/src/scripts/hook_manifest.yaml +12 -1
- package/src/scripts/install.ts +44 -45
- package/src/scripts/lint_agents_layout.ts +10 -4
- package/src/scripts/lint_breaking_changes_index.ts +303 -0
- package/src/scripts/lint_handoffs.ts +42 -0
- package/src/scripts/lint_skill_descriptions.ts +228 -0
- package/src/scripts/lint_skill_descriptions_allowlist.json +1 -0
- package/src/scripts/lint_skill_scripts_readonly.ts +145 -0
- package/src/scripts/lint_skill_scripts_readonly_allowlist.json +8 -0
- package/src/scripts/lint_supported_tools_matrix.ts +35 -0
- package/src/scripts/mcp_server/tools.ts +1 -3
- package/src/scripts/optimize_skill_description.ts +144 -0
- package/src/scripts/orchestration_record.ts +2 -1
- package/src/scripts/prelaunch_diagnostics.ts +248 -0
- package/src/scripts/run_skill_evals.ts +70 -4
- package/src/scripts/schemas/agent-settings.schema.json +17 -0
- package/src/scripts/schemas/command.schema.json +7 -0
- package/src/scripts/schemas/evals.schema.json +105 -14
- package/src/scripts/schemas/prelaunch-report.schema.json +107 -0
- package/src/scripts/schemas/skill.schema.json +18 -1
- package/src/scripts/schemas/subagent.schema.json +6 -0
- package/src/scripts/self_review_gate.test.ts +11 -0
- package/src/scripts/self_review_gate.ts +7 -1
- package/src/scripts/skill_eval_coverage.ts +4 -1
- package/src/scripts/skill_linter.ts +43 -3
- package/src/scripts/smoke_host_loadability.sh +73 -0
- package/src/scripts/team_review_gate_hook.ts +115 -0
- package/src/scripts/trigger_eval_grandfather.json +1 -2
- package/src/scripts/utilization_report.ts +225 -0
- package/src/server/schemas/settings.ts +24 -1
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env -S node --import tsx
|
|
2
|
+
/**
|
|
3
|
+
* lint_skill_scripts_readonly — read-only-by-default script convention
|
|
4
|
+
* (ecosystem-harvest skill-quality-gates, Phase 3).
|
|
5
|
+
*
|
|
6
|
+
* A script shipped inside a skill (`src/skills/<skill>/scripts/**`) is
|
|
7
|
+
* side-effect-free by default: it inspects / prints, it does not mutate. Any
|
|
8
|
+
* mutation must be gated behind an explicit flag (`--writable` / `--apply` /
|
|
9
|
+
* `--write` / `--out` / `--fix` / `--emit`) named in the skill's SKILL.md, OR
|
|
10
|
+
* the script must be allowlisted with a rationale (a generator whose declared
|
|
11
|
+
* purpose IS to write into a caller-supplied path).
|
|
12
|
+
*
|
|
13
|
+
* The linter flags a script that contains a content-mutation primitive but
|
|
14
|
+
* neither references a write-gating flag nor is allowlisted. Directory creation
|
|
15
|
+
* (`mkdirSync`) and writes under a temp dir are not content mutation and do not
|
|
16
|
+
* trip the gate.
|
|
17
|
+
*
|
|
18
|
+
* Deterministic, static-config only. Allowlist:
|
|
19
|
+
* `lint_skill_scripts_readonly_allowlist.json`, capped at 20.
|
|
20
|
+
*
|
|
21
|
+
* Exit codes: 0 = clean (or only allowlisted), 1 = ungated write, 2 = over cap / usage.
|
|
22
|
+
*
|
|
23
|
+
* Usage: ./scripts-run src/scripts/lint_skill_scripts_readonly [--quiet] [--root <dir>]
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import * as fs from 'node:fs';
|
|
27
|
+
import * as path from 'node:path';
|
|
28
|
+
import process from 'node:process';
|
|
29
|
+
import { fileURLToPath } from 'node:url';
|
|
30
|
+
|
|
31
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
32
|
+
const REPO = path.resolve(path.dirname(_HERE), '..', '..');
|
|
33
|
+
const DEFAULT_SKILLS = path.join(REPO, 'src', 'skills');
|
|
34
|
+
const ALLOWLIST = path.join(path.dirname(_HERE), 'lint_skill_scripts_readonly_allowlist.json');
|
|
35
|
+
const ALLOWLIST_CAP = 20;
|
|
36
|
+
|
|
37
|
+
// Content-mutation primitives, language-scoped. mkdirSync is intentionally
|
|
38
|
+
// excluded (dir creation is not content mutation). Shell redirection is matched
|
|
39
|
+
// only in .sh files — `> "` / `=> "` is pervasive, harmless syntax in TS/JS.
|
|
40
|
+
const JS_WRITE_RE =
|
|
41
|
+
/\b(writeFileSync|writeFile\s*\(|appendFileSync|appendFile\s*\(|unlinkSync|unlink\s*\(|rmSync|rmdirSync|copyFileSync|renameSync)\b/;
|
|
42
|
+
const PY_WRITE_RE =
|
|
43
|
+
/\bopen\s*\([^)]*,\s*["'][wa]\+?b?["']|\bos\.(remove|unlink|rmdir)\b|\bshutil\.(rmtree|move|copy)\b|\bpathlib[^\n]*\.write_(text|bytes)\b|\.write_(text|bytes)\s*\(/;
|
|
44
|
+
const SH_WRITE_RE =
|
|
45
|
+
/(^|\s|;|&&|\|)(rm|rmdir|mv|cp|dd|truncate|tee)\s|>>?\s*[^&\s]|\b(DELETE\s+FROM|DROP\s+(TABLE|DATABASE)|TRUNCATE)\b/;
|
|
46
|
+
// A write-gating flag reference — mutation is behind an opt-in.
|
|
47
|
+
const GATE_RE = /--writable|--apply|--write|--out|--fix\b|--emit|--generate|--dry-run|dryRun|dry_run|parsed\.output|args\.output|opts\.output/;
|
|
48
|
+
const SCRIPT_EXT = new Set(['.ts', '.js', '.mjs', '.cjs', '.py', '.sh']);
|
|
49
|
+
|
|
50
|
+
function hasWritePrimitive(body: string, ext: string): boolean {
|
|
51
|
+
if (ext === '.py') return PY_WRITE_RE.test(body);
|
|
52
|
+
if (ext === '.sh') return SH_WRITE_RE.test(body);
|
|
53
|
+
return JS_WRITE_RE.test(body); // .ts/.js/.mjs/.cjs
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface Violation {
|
|
57
|
+
rel: string;
|
|
58
|
+
detail: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function loadAllowlist(): Set<string> {
|
|
62
|
+
if (!fs.existsSync(ALLOWLIST)) return new Set();
|
|
63
|
+
const data = JSON.parse(fs.readFileSync(ALLOWLIST, 'utf-8')) as {
|
|
64
|
+
entries?: Array<{ path: string; reason: string }>;
|
|
65
|
+
};
|
|
66
|
+
const entries = Array.isArray(data.entries) ? data.entries : [];
|
|
67
|
+
if (entries.length > ALLOWLIST_CAP) {
|
|
68
|
+
process.stderr.write(
|
|
69
|
+
`❌ lint_skill_scripts_readonly: allowlist has ${entries.length} entries (> ${ALLOWLIST_CAP}). ` +
|
|
70
|
+
`Per the autonomous-execution allowlist-growth antipattern, the convention is wrong, not the content.\n`,
|
|
71
|
+
);
|
|
72
|
+
process.exit(2);
|
|
73
|
+
}
|
|
74
|
+
const bad = entries.filter((e) => !e.reason || e.reason.trim() === '');
|
|
75
|
+
if (bad.length > 0) {
|
|
76
|
+
process.stderr.write(`❌ lint_skill_scripts_readonly: allowlist entries must carry a non-empty reason: ${bad.map((b) => b.path).join(', ')}\n`);
|
|
77
|
+
process.exit(2);
|
|
78
|
+
}
|
|
79
|
+
return new Set(entries.map((e) => e.path));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function* scriptFiles(root: string): Generator<string> {
|
|
83
|
+
if (!fs.existsSync(root)) return;
|
|
84
|
+
for (const skill of fs.readdirSync(root, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
85
|
+
if (!skill.isDirectory()) continue;
|
|
86
|
+
const scriptsDir = path.join(root, skill.name, 'scripts');
|
|
87
|
+
if (!fs.existsSync(scriptsDir)) continue;
|
|
88
|
+
const stack = [scriptsDir];
|
|
89
|
+
while (stack.length) {
|
|
90
|
+
const d = stack.pop() as string;
|
|
91
|
+
for (const e of fs.readdirSync(d, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
92
|
+
const full = path.join(d, e.name);
|
|
93
|
+
if (e.isDirectory()) stack.push(full);
|
|
94
|
+
else if (SCRIPT_EXT.has(path.extname(e.name))) yield full;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function parseArgs(argv: string[]): { quiet: boolean; root: string } {
|
|
101
|
+
const a = { quiet: false, root: DEFAULT_SKILLS };
|
|
102
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
103
|
+
if (argv[i] === '--quiet') a.quiet = true;
|
|
104
|
+
else if (argv[i] === '--root') a.root = argv[(i += 1)] ?? DEFAULT_SKILLS;
|
|
105
|
+
else {
|
|
106
|
+
process.stderr.write(`lint_skill_scripts_readonly: unrecognized argument: ${argv[i]}\n`);
|
|
107
|
+
process.exit(2);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return a;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function main(argv: string[]): number {
|
|
114
|
+
const args = parseArgs(argv);
|
|
115
|
+
const allow = loadAllowlist();
|
|
116
|
+
const violations: Violation[] = [];
|
|
117
|
+
let scanned = 0;
|
|
118
|
+
for (const f of scriptFiles(args.root)) {
|
|
119
|
+
scanned += 1;
|
|
120
|
+
const rel = path.relative(REPO, f);
|
|
121
|
+
// Compare against the src/skills-relative form for the allowlist key.
|
|
122
|
+
const relSkills = path.relative(args.root, f);
|
|
123
|
+
const body = fs.readFileSync(f, 'utf-8');
|
|
124
|
+
if (!hasWritePrimitive(body, path.extname(f))) continue;
|
|
125
|
+
if (GATE_RE.test(body)) continue;
|
|
126
|
+
if (allow.has(relSkills) || allow.has(rel)) continue;
|
|
127
|
+
violations.push({ rel, detail: 'contains a content-mutation primitive but no write-gating flag (--writable/--apply/--write/--out/--fix) and is not allowlisted' });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (violations.length > 0) {
|
|
131
|
+
process.stderr.write(`❌ lint_skill_scripts_readonly: ${violations.length} ungated write(s):\n`);
|
|
132
|
+
for (const v of violations) process.stderr.write(` ${v.rel} — ${v.detail}\n`);
|
|
133
|
+
return 1;
|
|
134
|
+
}
|
|
135
|
+
if (!args.quiet) {
|
|
136
|
+
process.stdout.write(`✅ lint_skill_scripts_readonly: ${scanned} skill script(s), all read-only or gated.\n`);
|
|
137
|
+
}
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (path.resolve(process.argv[1] ?? '') === path.resolve(_HERE)) {
|
|
142
|
+
process.exit(main(process.argv.slice(2)));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export { hasWritePrimitive, GATE_RE, main };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"entries": [
|
|
3
|
+
{
|
|
4
|
+
"path": "corpus-grounding/scripts/decision_engine.ts",
|
|
5
|
+
"reason": "Generator whose declared purpose IS to materialize the grounded design-system MASTER.md (and optional page-override files) into a caller-supplied output_dir; the caller (ground.ts) controls the path. Writing is the feature, not a side effect."
|
|
6
|
+
}
|
|
7
|
+
]
|
|
8
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* lint_supported_tools_matrix.ts — CI gate: the README `## Supported tools`
|
|
4
|
+
* matrix equals the canonical adapter registry (fail on drift in either
|
|
5
|
+
* direction), and every tool-tagged generator output root is claimed by the
|
|
6
|
+
* registry (road-to-ecosystem-harvest-skill-quality-gates Phase 4, Source AA).
|
|
7
|
+
*
|
|
8
|
+
* Exit codes: 0 — in sync · 1 — drift (each line names the drifted row).
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from 'node:fs';
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
|
|
14
|
+
import { lint_matrix } from './_lib/tool_adapter_registry.js';
|
|
15
|
+
|
|
16
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
17
|
+
const REPO_ROOT = path.join(path.dirname(_HERE), '..', '..');
|
|
18
|
+
|
|
19
|
+
export function run(readmePath: string): string[] {
|
|
20
|
+
const readme = fs.readFileSync(readmePath, 'utf-8');
|
|
21
|
+
return lint_matrix(readme);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const isMain = process.argv[1] !== undefined && path.resolve(process.argv[1]) === path.resolve(_HERE);
|
|
25
|
+
if (isMain) {
|
|
26
|
+
const idx = process.argv.indexOf('--readme');
|
|
27
|
+
const readmePath = idx !== -1 ? (process.argv[idx + 1] ?? '') : path.join(REPO_ROOT, 'README.md');
|
|
28
|
+
const errors = run(readmePath);
|
|
29
|
+
if (errors.length > 0) {
|
|
30
|
+
for (const e of errors) process.stderr.write(`❌ ${e}\n`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
process.stdout.write('✅ Supported-tools matrix ↔ adapter registry ↔ generator roots in sync\n');
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
@@ -775,9 +775,7 @@ async function _runDoctorLeg(consumerRoot: string): Promise<{
|
|
|
775
775
|
let tagDrift: Record<string, unknown>[] = [];
|
|
776
776
|
let checks: Record<string, unknown>[];
|
|
777
777
|
if (manifest === null) {
|
|
778
|
-
const bridgePresent =
|
|
779
|
-
path.join(projectRoot, doctor.BRIDGE_MARKER_RELATIVE),
|
|
780
|
-
);
|
|
778
|
+
const bridgePresent = doctor._is_global_only_consumer(projectRoot);
|
|
781
779
|
checks = doctor._run_checks_no_manifest(projectRoot, bridgePresent, null);
|
|
782
780
|
} else {
|
|
783
781
|
const [records, known] = doctor._collect_manifest_entries(projectRoot, manifest);
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* optimize_skill_description.ts — U1 packaged description-optimizer loop
|
|
3
|
+
* (road-to-ecosystem-harvest-skill-authoring-rigor).
|
|
4
|
+
*
|
|
5
|
+
* Chains the suite's existing primitives into ONE authoring workflow:
|
|
6
|
+
* candidate descriptions × the skill's `evals/triggers.json` queries →
|
|
7
|
+
* per-candidate trigger-rate, measured on a HELD-OUT split so the winning
|
|
8
|
+
* description is picked on queries it was never tuned against (avoids
|
|
9
|
+
* overfitting to the training queries).
|
|
10
|
+
*
|
|
11
|
+
* Split: queries are partitioned deterministically (stable hash of the query
|
|
12
|
+
* text, even→train / odd→test) — reproducible without storing a seed.
|
|
13
|
+
*
|
|
14
|
+
* Judges:
|
|
15
|
+
* default — deterministic proxy: does the query share ≥ 2 informative
|
|
16
|
+
* tokens (or one rare token) with the description? Free, fast,
|
|
17
|
+
* good for iteration.
|
|
18
|
+
* --live — model judge (claude-haiku): "would this description fire for
|
|
19
|
+
* this query? YES/NO" — the real signal, ~$0.001/query·candidate.
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
* npx tsx src/scripts/optimize_skill_description.ts --skill fe-design \
|
|
23
|
+
* [--candidate "alt description …"]... [--live] [--max-usd 1]
|
|
24
|
+
*
|
|
25
|
+
* Output: per-candidate train/test trigger-accuracy table; the pick is the
|
|
26
|
+
* best TEST accuracy. Exit 0 always (authoring tool, not a gate).
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import * as fs from 'node:fs';
|
|
30
|
+
import * as path from 'node:path';
|
|
31
|
+
|
|
32
|
+
export interface TriggerQuery {
|
|
33
|
+
q: string;
|
|
34
|
+
trigger: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Deterministic stable split — even hash → train, odd → test. */
|
|
38
|
+
export function split_queries(queries: TriggerQuery[]): { train: TriggerQuery[]; test: TriggerQuery[] } {
|
|
39
|
+
const train: TriggerQuery[] = [];
|
|
40
|
+
const test: TriggerQuery[] = [];
|
|
41
|
+
for (const query of queries) {
|
|
42
|
+
let h = 0;
|
|
43
|
+
for (const ch of query.q) h = (h * 31 + ch.codePointAt(0)!) >>> 0;
|
|
44
|
+
(h % 2 === 0 ? train : test).push(query);
|
|
45
|
+
}
|
|
46
|
+
return { train, test };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const STOP = new Set(['the', 'a', 'an', 'for', 'to', 'of', 'and', 'or', 'in', 'on', 'with', 'when', 'use', 'via', 'this', 'that', 'my', 'our', 'me', 'we', 'is', 'are', 'it', 'before', 'after']);
|
|
50
|
+
|
|
51
|
+
export function tokens(s: string): Set<string> {
|
|
52
|
+
return new Set(
|
|
53
|
+
s.toLowerCase().split(/[^a-z0-9]+/u).filter((t) => t.length > 2 && !STOP.has(t)),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Deterministic proxy judge: informative-token overlap between query and description. */
|
|
58
|
+
export function proxy_fires(description: string, query: string): boolean {
|
|
59
|
+
const d = tokens(description);
|
|
60
|
+
const q = tokens(query);
|
|
61
|
+
let overlap = 0;
|
|
62
|
+
for (const t of q) if (d.has(t)) overlap += 1;
|
|
63
|
+
return overlap >= 2;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function accuracy(
|
|
67
|
+
fires: (desc: string, q: string) => boolean,
|
|
68
|
+
desc: string,
|
|
69
|
+
queries: TriggerQuery[],
|
|
70
|
+
): number {
|
|
71
|
+
if (queries.length === 0) return 0;
|
|
72
|
+
let ok = 0;
|
|
73
|
+
for (const query of queries) {
|
|
74
|
+
if (fires(desc, query.q) === query.trigger) ok += 1;
|
|
75
|
+
}
|
|
76
|
+
return ok / queries.length;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function live_judge_factory(maxUsd: number): Promise<(desc: string, q: string) => boolean> {
|
|
80
|
+
const c = await import('./ai_council/clients.js');
|
|
81
|
+
const client = new c.AnthropicClient({ api_key: c.load_anthropic_key(), model: 'claude-haiku-4-5-20251001' });
|
|
82
|
+
let spent = 0;
|
|
83
|
+
return (desc: string, q: string): boolean => {
|
|
84
|
+
if (spent >= maxUsd) throw new Error(`live-judge budget $${maxUsd} exhausted`);
|
|
85
|
+
const resp = client.ask(
|
|
86
|
+
'You decide whether an agent skill would activate. Answer exactly YES or NO.',
|
|
87
|
+
`Skill description: "${desc}"\nUser query: "${q}"\nWould this skill fire for this query?`,
|
|
88
|
+
8,
|
|
89
|
+
);
|
|
90
|
+
if (resp.error) throw new Error(resp.error);
|
|
91
|
+
spent += ((resp.input_tokens / 1e6) * 0.25) + ((resp.output_tokens / 1e6) * 1.25);
|
|
92
|
+
return /YES/i.test(resp.text);
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function main(argv: string[]): Promise<number> {
|
|
97
|
+
const skillIdx = argv.indexOf('--skill');
|
|
98
|
+
if (skillIdx === -1) {
|
|
99
|
+
process.stderr.write('usage: --skill <id> [--candidate "…"]... [--live] [--max-usd 1]\n');
|
|
100
|
+
return 2;
|
|
101
|
+
}
|
|
102
|
+
const skill = argv[skillIdx + 1] ?? '';
|
|
103
|
+
const live = argv.includes('--live');
|
|
104
|
+
const mu = argv.indexOf('--max-usd');
|
|
105
|
+
const maxUsd = mu !== -1 ? Number(argv[mu + 1]) : 1;
|
|
106
|
+
const candidates: string[] = [];
|
|
107
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
108
|
+
if (argv[i] === '--candidate') candidates.push(argv[i + 1] ?? '');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const skillMd = fs.readFileSync(path.join('src/skills', skill, 'SKILL.md'), 'utf-8');
|
|
112
|
+
const current = (skillMd.match(/^description:\s*["']?(.+?)["']?\s*$/m) ?? [])[1] ?? '';
|
|
113
|
+
const triggers = JSON.parse(
|
|
114
|
+
fs.readFileSync(path.join('src/skills', skill, 'evals', 'triggers.json'), 'utf-8'),
|
|
115
|
+
) as { queries: TriggerQuery[] };
|
|
116
|
+
const { train, test } = split_queries(triggers.queries);
|
|
117
|
+
process.stdout.write(`skill ${skill}: ${triggers.queries.length} queries → train ${train.length} / held-out ${test.length}\n\n`);
|
|
118
|
+
|
|
119
|
+
const judge = live ? await live_judge_factory(maxUsd) : proxy_fires;
|
|
120
|
+
const all = [current, ...candidates];
|
|
121
|
+
process.stdout.write('| # | description (first 70 chars) | train | TEST (held-out) |\n|---|---|---:|---:|\n');
|
|
122
|
+
let best = 0;
|
|
123
|
+
let bestAcc = -1;
|
|
124
|
+
all.forEach((desc, i) => {
|
|
125
|
+
const tr = accuracy(judge, desc, train);
|
|
126
|
+
const te = accuracy(judge, desc, test);
|
|
127
|
+
process.stdout.write(`| ${i === 0 ? 'current' : i} | ${desc.slice(0, 70)} | ${(tr * 100).toFixed(0)}% | ${(te * 100).toFixed(0)}% |\n`);
|
|
128
|
+
if (te > bestAcc) { bestAcc = te; best = i; }
|
|
129
|
+
});
|
|
130
|
+
process.stdout.write(`\npick (best held-out): ${best === 0 ? 'current description' : `candidate ${best}`} (${(bestAcc * 100).toFixed(0)}% test)\n`);
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const isMain = process.argv[1] !== undefined
|
|
135
|
+
&& path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname);
|
|
136
|
+
if (isMain) {
|
|
137
|
+
main(process.argv.slice(2)).then(
|
|
138
|
+
(code) => process.exit(code),
|
|
139
|
+
(err) => {
|
|
140
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* ./scripts-run src/scripts/orchestration_record \
|
|
11
11
|
* --spawn-count 1 --token-delta -72000 --provenance measured \
|
|
12
12
|
* --tier-chosen lite --tier-source inferred --task-class read-only-fanout \
|
|
13
|
-
* [--tiers sonnet,opus] [--wall-clock-ms 18500] [--dispatch-outcome DONE] \
|
|
13
|
+
* [--tiers sonnet,opus] [--agent-combo implementer,judge] [--wall-clock-ms 18500] [--dispatch-outcome DONE] \
|
|
14
14
|
* [--first-pass-success true|false] [--escalated true|false] \
|
|
15
15
|
* [--dir <audit-dir>] [--dry-run]
|
|
16
16
|
*
|
|
@@ -109,6 +109,7 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
109
109
|
token_delta: int(flags, 'token-delta') ?? NaN,
|
|
110
110
|
token_delta_provenance: str(flags, 'provenance') as Provenance | undefined,
|
|
111
111
|
tiers: str(flags, 'tiers')?.split(',').map((s) => s.trim()).filter(Boolean),
|
|
112
|
+
agent_combo: str(flags, 'agent-combo')?.split(',').map((s) => s.trim()).filter(Boolean),
|
|
112
113
|
tier_chosen: str(flags, 'tier-chosen') as TierChosen | undefined,
|
|
113
114
|
tier_source: str(flags, 'tier-source') as TierSource | undefined,
|
|
114
115
|
task_class: str(flags, 'task-class'),
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* prelaunch_diagnostics.ts — validate / verdict / diff for pre-launch
|
|
4
|
+
* diagnostic reports (road-to-ecosystem-harvest-prelaunch-diagnostics,
|
|
5
|
+
* Source L). Contract: docs/contracts/prelaunch-diagnostics.md.
|
|
6
|
+
*
|
|
7
|
+
* Read-only by design: reads report JSON (+ optional suppression YAML),
|
|
8
|
+
* writes nothing — the consumer-side fix loop stays approval-gated.
|
|
9
|
+
*
|
|
10
|
+
* CLI:
|
|
11
|
+
* validate <report.json> [--suppressions <file>]
|
|
12
|
+
* diff <baseline.json> <current.json> [--ci] [--suppressions <file>]
|
|
13
|
+
*
|
|
14
|
+
* Exit codes: 0 clean · 1 validation error / --ci gate tripped · 2 usage.
|
|
15
|
+
*
|
|
16
|
+
* NO numeric score anywhere — council-locked (contract § 9).
|
|
17
|
+
*/
|
|
18
|
+
import * as fs from 'node:fs';
|
|
19
|
+
import * as path from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
|
|
22
|
+
import { parse as parseYaml } from 'yaml';
|
|
23
|
+
|
|
24
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
25
|
+
const REPO_ROOT = path.join(path.dirname(_HERE), '..', '..');
|
|
26
|
+
const AREAS_FILE = path.join(REPO_ROOT, 'src', 'config', 'prelaunch-areas.yml');
|
|
27
|
+
|
|
28
|
+
export interface AreaDef {
|
|
29
|
+
code: string;
|
|
30
|
+
launch_gate: boolean;
|
|
31
|
+
description?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface AreaState {
|
|
35
|
+
state: 'pass' | 'finding' | 'unknown' | 'not-applicable';
|
|
36
|
+
evidence?: string;
|
|
37
|
+
reason?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface Finding {
|
|
41
|
+
id: string;
|
|
42
|
+
area: string;
|
|
43
|
+
severity: 'P0' | 'P1' | 'P2' | 'P3';
|
|
44
|
+
title: string;
|
|
45
|
+
evidence?: string;
|
|
46
|
+
status: 'open' | 'fixed' | 'accepted-risk' | 'deferred-with-reason' | 'suppressed-with-evidence' | 'not-applicable';
|
|
47
|
+
reason?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface Report {
|
|
51
|
+
schema_version: string;
|
|
52
|
+
project: string;
|
|
53
|
+
run_at: string;
|
|
54
|
+
areas: Record<string, AreaState>;
|
|
55
|
+
findings: Finding[];
|
|
56
|
+
questions?: string[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface Suppression {
|
|
60
|
+
id: string;
|
|
61
|
+
reason: string;
|
|
62
|
+
evidence: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const ID_RE = /^AC-([A-Z][A-Z0-9]{1,7})-([0-9]{3})$/;
|
|
66
|
+
const REASON_REQUIRED = new Set(['accepted-risk', 'deferred-with-reason', 'suppressed-with-evidence', 'not-applicable']);
|
|
67
|
+
|
|
68
|
+
export function load_areas(file: string = AREAS_FILE): Record<string, AreaDef> {
|
|
69
|
+
const doc = parseYaml(fs.readFileSync(file, 'utf-8')) as { areas?: Record<string, AreaDef> };
|
|
70
|
+
return doc.areas ?? {};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function load_suppressions(file: string): Suppression[] {
|
|
74
|
+
if (!fs.existsSync(file)) return [];
|
|
75
|
+
const doc = parseYaml(fs.readFileSync(file, 'utf-8')) as { suppressions?: Suppression[] };
|
|
76
|
+
return doc.suppressions ?? [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Epistemics + structure validation beyond the JSON schema shape:
|
|
81
|
+
* pass-needs-evidence, N/A-needs-reason, ID grammar + area agreement,
|
|
82
|
+
* unique IDs, status-reason coupling, suppression receipts.
|
|
83
|
+
*/
|
|
84
|
+
export function validate_report(
|
|
85
|
+
report: Report,
|
|
86
|
+
areas: Record<string, AreaDef>,
|
|
87
|
+
suppressions: Suppression[] = [],
|
|
88
|
+
): string[] {
|
|
89
|
+
const errors: string[] = [];
|
|
90
|
+
const codeByArea = new Map(Object.entries(areas).map(([k, v]) => [k, v.code]));
|
|
91
|
+
|
|
92
|
+
for (const [areaKey, st] of Object.entries(report.areas ?? {})) {
|
|
93
|
+
if (!codeByArea.has(areaKey)) {
|
|
94
|
+
errors.push(`areas.${areaKey}: unknown area (not in prelaunch-areas.yml — the vocabulary is fixed)`);
|
|
95
|
+
}
|
|
96
|
+
if (st.state === 'pass' && !st.evidence) {
|
|
97
|
+
errors.push(`areas.${areaKey}: state 'pass' without evidence — a pass is a cited diagnosis, not an assertion`);
|
|
98
|
+
}
|
|
99
|
+
if (st.state === 'not-applicable' && !st.reason) {
|
|
100
|
+
errors.push(`areas.${areaKey}: state 'not-applicable' without a reason`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const seen = new Set<string>();
|
|
105
|
+
const suppressionById = new Map(suppressions.map((s) => [s.id, s]));
|
|
106
|
+
for (const f of report.findings ?? []) {
|
|
107
|
+
const m = ID_RE.exec(f.id);
|
|
108
|
+
if (!m) {
|
|
109
|
+
errors.push(`${f.id}: malformed finding ID (grammar: AC-<AREA>-NNN)`);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (seen.has(f.id)) errors.push(`${f.id}: duplicate finding ID — IDs are immutable and never re-assigned`);
|
|
113
|
+
seen.add(f.id);
|
|
114
|
+
const expected = codeByArea.get(f.area);
|
|
115
|
+
if (expected === undefined) {
|
|
116
|
+
errors.push(`${f.id}: unknown area '${f.area}'`);
|
|
117
|
+
} else if (m[1] !== expected) {
|
|
118
|
+
errors.push(`${f.id}: ID area code '${m[1]}' does not match area '${f.area}' (code ${expected})`);
|
|
119
|
+
}
|
|
120
|
+
if (REASON_REQUIRED.has(f.status) && !f.reason) {
|
|
121
|
+
errors.push(`${f.id}: status '${f.status}' requires a reason`);
|
|
122
|
+
}
|
|
123
|
+
if (f.status === 'suppressed-with-evidence') {
|
|
124
|
+
const s = suppressionById.get(f.id);
|
|
125
|
+
if (!s) errors.push(`${f.id}: suppressed-with-evidence without a matching suppression entry`);
|
|
126
|
+
else if (!s.reason || !s.evidence) errors.push(`${f.id}: suppression entry must carry both reason and evidence`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return errors;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The launch verdict. READY IS THE RESIDUAL STATE, NEVER THE DEFAULT:
|
|
134
|
+
* any open P0 refuses ready regardless of every other area (suppression
|
|
135
|
+
* does not rescue a P0 — contract § 5); any open P1 or a launch-gate
|
|
136
|
+
* area not at pass/not-applicable refuses ready too.
|
|
137
|
+
*/
|
|
138
|
+
export function verdict(report: Report, areas: Record<string, AreaDef>): { ready: boolean; blockers: string[] } {
|
|
139
|
+
const blockers: string[] = [];
|
|
140
|
+
for (const f of report.findings ?? []) {
|
|
141
|
+
if (f.severity === 'P0' && f.status !== 'fixed' && f.status !== 'not-applicable') {
|
|
142
|
+
blockers.push(`${f.id} (P0, ${f.status}): open P0 caps the verdict regardless of every other area`);
|
|
143
|
+
} else if (f.severity === 'P1' && f.status === 'open') {
|
|
144
|
+
blockers.push(`${f.id} (P1, open): fix before general rollout`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
for (const [key, def] of Object.entries(areas)) {
|
|
148
|
+
if (!def.launch_gate) continue;
|
|
149
|
+
const st = report.areas?.[key]?.state ?? 'unknown';
|
|
150
|
+
if (st !== 'pass' && st !== 'not-applicable') {
|
|
151
|
+
blockers.push(`area ${key} (launch-gate): state '${st}' — Unknown is never a Pass`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return { ready: blockers.length === 0, blockers };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface DiffResult {
|
|
158
|
+
new_findings: Finding[];
|
|
159
|
+
resolved: Finding[];
|
|
160
|
+
regressed_areas: string[];
|
|
161
|
+
ci_trips: string[];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Diff two reports BY ID (titles are mutable metadata, never the key). */
|
|
165
|
+
export function diff_reports(
|
|
166
|
+
baseline: Report,
|
|
167
|
+
current: Report,
|
|
168
|
+
areas: Record<string, AreaDef>,
|
|
169
|
+
suppressions: Suppression[] = [],
|
|
170
|
+
): DiffResult {
|
|
171
|
+
const suppressed = new Set(suppressions.map((s) => s.id));
|
|
172
|
+
const baseIds = new Set((baseline.findings ?? []).map((f) => f.id));
|
|
173
|
+
const baseById = new Map((baseline.findings ?? []).map((f) => [f.id, f]));
|
|
174
|
+
|
|
175
|
+
const new_findings = (current.findings ?? []).filter((f) => !baseIds.has(f.id));
|
|
176
|
+
const resolved = (current.findings ?? []).filter(
|
|
177
|
+
(f) => f.status === 'fixed' && baseById.get(f.id)?.status === 'open',
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const regressed_areas: string[] = [];
|
|
181
|
+
for (const [key, def] of Object.entries(areas)) {
|
|
182
|
+
if (!def.launch_gate) continue;
|
|
183
|
+
const before = baseline.areas?.[key]?.state;
|
|
184
|
+
const after = current.areas?.[key]?.state ?? 'unknown';
|
|
185
|
+
if (before === 'pass' && after !== 'pass' && after !== 'not-applicable') {
|
|
186
|
+
regressed_areas.push(key);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const ci_trips: string[] = [];
|
|
191
|
+
for (const f of new_findings) {
|
|
192
|
+
if ((f.severity === 'P0' || f.severity === 'P1') && f.status === 'open' && !suppressed.has(f.id)) {
|
|
193
|
+
ci_trips.push(`new open ${f.severity}: ${f.id} — ${f.title}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
for (const a of regressed_areas) {
|
|
197
|
+
ci_trips.push(`launch-gate area '${a}' regressed from pass (Pass→Finding flip)`);
|
|
198
|
+
}
|
|
199
|
+
return { new_findings, resolved, regressed_areas, ci_trips };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function _readReport(p: string): Report {
|
|
203
|
+
return JSON.parse(fs.readFileSync(p, 'utf-8')) as Report;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function _renderDiff(d: DiffResult, suppressions: Suppression[]): string {
|
|
207
|
+
const lines: string[] = [];
|
|
208
|
+
lines.push(`new: ${d.new_findings.length} · resolved: ${d.resolved.length} · regressed launch-gate areas: ${d.regressed_areas.length}`);
|
|
209
|
+
for (const f of d.new_findings) lines.push(` + ${f.id} [${f.severity}/${f.status}] ${f.title}`);
|
|
210
|
+
for (const f of d.resolved) lines.push(` ✓ ${f.id} resolved`);
|
|
211
|
+
for (const a of d.regressed_areas) lines.push(` ↓ area ${a} regressed from pass`);
|
|
212
|
+
if (suppressions.length > 0) {
|
|
213
|
+
lines.push('<details><summary>suppressed findings (evidence-backed, excluded from CI triggers)</summary>');
|
|
214
|
+
for (const s of suppressions) lines.push(` ~ ${s.id} — ${s.reason} (evidence: ${s.evidence})`);
|
|
215
|
+
lines.push('</details>');
|
|
216
|
+
}
|
|
217
|
+
return lines.join('\n');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const isMain = process.argv[1] !== undefined && path.resolve(process.argv[1]) === path.resolve(_HERE);
|
|
221
|
+
if (isMain) {
|
|
222
|
+
const argv = process.argv.slice(2);
|
|
223
|
+
const cmd = argv[0];
|
|
224
|
+
const sIdx = argv.indexOf('--suppressions');
|
|
225
|
+
const suppressions = sIdx !== -1 ? load_suppressions(argv[sIdx + 1] ?? '') : [];
|
|
226
|
+
const areas = load_areas();
|
|
227
|
+
|
|
228
|
+
if (cmd === 'validate' && argv[1]) {
|
|
229
|
+
const report = _readReport(argv[1]);
|
|
230
|
+
const errors = validate_report(report, areas, suppressions);
|
|
231
|
+
for (const e of errors) process.stderr.write(`❌ ${e}\n`);
|
|
232
|
+
const v = verdict(report, areas);
|
|
233
|
+
process.stdout.write(v.ready ? '✅ verdict: ready\n' : `⚠️ verdict: NOT ready (${v.blockers.length} blocker(s))\n`);
|
|
234
|
+
for (const b of v.blockers) process.stdout.write(` - ${b}\n`);
|
|
235
|
+
process.exit(errors.length > 0 ? 1 : 0);
|
|
236
|
+
} else if (cmd === 'diff' && argv[1] && argv[2]) {
|
|
237
|
+
const d = diff_reports(_readReport(argv[1]), _readReport(argv[2]), areas, suppressions);
|
|
238
|
+
process.stdout.write(_renderDiff(d, suppressions) + '\n');
|
|
239
|
+
if (argv.includes('--ci') && d.ci_trips.length > 0) {
|
|
240
|
+
for (const t of d.ci_trips) process.stderr.write(`❌ ${t}\n`);
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
process.exit(0);
|
|
244
|
+
} else {
|
|
245
|
+
process.stderr.write('usage: prelaunch_diagnostics validate <report.json> | diff <baseline.json> <current.json> [--ci] [--suppressions <file>]\n');
|
|
246
|
+
process.exit(2);
|
|
247
|
+
}
|
|
248
|
+
}
|